Initial Commit
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
/** Default Docker image for SurrealDB. */
|
||||
export const SURREALDB_IMAGE = "surrealdb/surrealdb:v3.0.4";
|
||||
|
||||
/** Container port exposed by SurrealDB (HTTP + WebSocket). */
|
||||
export const SURREALDB_CONTAINER_PORT = 8000;
|
||||
|
||||
/** Default namespace for test databases. */
|
||||
export const SURREALDB_DEFAULT_NAMESPACE = "test";
|
||||
|
||||
/** Default database name for test databases. */
|
||||
export const SURREALDB_DEFAULT_DATABASE = "test";
|
||||
|
||||
/** Default username. */
|
||||
export const SURREALDB_DEFAULT_USERNAME = "root";
|
||||
|
||||
/** Health check poll interval in milliseconds. */
|
||||
export const SURREALDB_HEALTH_POLL_INTERVAL_MS = 1_000;
|
||||
|
||||
/** Maximum time to wait for SurrealDB to become healthy. */
|
||||
export const SURREALDB_HEALTH_TIMEOUT_MS = 60_000;
|
||||
|
||||
/**
|
||||
* Log line testcontainers should wait for before treating the container as
|
||||
* ready. SurrealDB v3 emits this once at end of startup. Used in place of the
|
||||
* default `Wait.forListeningPorts()` strategy (which hangs under Bun per
|
||||
* oven-sh/bun#21342).
|
||||
*/
|
||||
export const SURREALDB_READY_LOG = "Started web server";
|
||||
|
||||
/** Number of `SURREALDB_READY_LOG` occurrences to wait for during a single startup. */
|
||||
export const SURREALDB_READY_LOG_TIMES = 1;
|
||||
|
||||
/** Env vars checked for passthrough mode — ALL must be set. */
|
||||
export const SURREALDB_ENV_VARS = [
|
||||
"SURREALDB_URL",
|
||||
"SURREALDB_USERNAME",
|
||||
"SURREALDB_PASSWORD",
|
||||
] as const;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { SurrealDBFixture } from "./surrealdb-fixture.js";
|
||||
export type {
|
||||
SurrealDBConfig,
|
||||
SurrealDBFixtureOptions,
|
||||
} from "./surrealdb-fixture.types.js";
|
||||
@@ -0,0 +1,112 @@
|
||||
# SurrealDBFixture
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`SurrealDBFixture` is an integration test fixture that manages the full lifecycle of a SurrealDB v3 container using testcontainers. It implements the `ServiceFixture<SurrealDBConfig>` interface from `@suaveplan/testing-integration`, enabling consistent startup, health checking, and teardown as part of a `TestEnvironment` or standalone usage.
|
||||
|
||||
The fixture supports an env-bridge passthrough mode so developer workstations with live SurrealDB instances can skip container startup entirely — just set the three required environment variables and tests run against the real service without Docker overhead.
|
||||
|
||||
## Features
|
||||
|
||||
- **Env-bridge passthrough**: when `SURREALDB_URL`, `SURREALDB_USERNAME`, and `SURREALDB_PASSWORD` are all set and non-empty, container startup is bypassed and env values are returned directly.
|
||||
- **HTTP health check**: polls `GET /health` on SurrealDB's HTTP port (same port as WebSocket) to verify the service is ready before returning from `start()`.
|
||||
- **Random password per run**: generates a new `crypto.randomUUID()` password each time a container starts, preventing credential reuse across test suites.
|
||||
- **Injectable container factory**: the `containerFactory` option allows unit tests to swap in a fake container without starting Docker, achieving 100% Istanbul coverage without integration dependencies.
|
||||
- **Configurable namespace and database**: override defaults via options to isolate test suites at the logical database level.
|
||||
- **Idempotent stop**: calling `stop()` on an already-stopped or passthrough fixture is safe and does not throw.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
const config = await fixture.start();
|
||||
|
||||
// config.url → "ws://localhost:<randomPort>"
|
||||
// config.username → "root"
|
||||
// config.password → auto-generated UUID
|
||||
// config.namespace → "test"
|
||||
// config.database → "test"
|
||||
|
||||
await fixture.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
|
||||
import Surreal from "surrealdb";
|
||||
|
||||
const fixture = new SurrealDBFixture({
|
||||
image: "surrealdb/surrealdb:v3.0.4",
|
||||
namespace: "myapp",
|
||||
database: "integration",
|
||||
});
|
||||
|
||||
const config = await fixture.start();
|
||||
const db = new Surreal();
|
||||
await db.connect(config.url);
|
||||
await db.use({ namespace: config.namespace, database: config.database });
|
||||
await db.signin({ username: config.username, password: config.password });
|
||||
|
||||
// run tests…
|
||||
|
||||
await db.close();
|
||||
await fixture.stop();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SurrealDBFixture`
|
||||
|
||||
Implements `ServiceFixture<SurrealDBConfig>`.
|
||||
|
||||
#### `constructor(options?: SurrealDBFixtureOptions)`
|
||||
|
||||
Creates a new fixture instance. All options are optional.
|
||||
|
||||
#### `name: string`
|
||||
|
||||
Always `"surrealdb"`. Used by `TestEnvironment` as the registry key.
|
||||
|
||||
#### `start(): Promise<SurrealDBConfig>`
|
||||
|
||||
Starts the service. Returns the runtime configuration on success. If all three env vars are set, returns passthrough config immediately. Otherwise starts a testcontainers container and waits for the HTTP health endpoint.
|
||||
|
||||
#### `stop(): Promise<void>`
|
||||
|
||||
Stops the container. No-op in passthrough mode or when `start()` was never called.
|
||||
|
||||
#### `healthCheck(): Promise<boolean>`
|
||||
|
||||
Returns `true` when `GET http://localhost:<port>/health` returns an OK response. Returns `false` if no container is active or the request fails.
|
||||
|
||||
### `interface SurrealDBConfig`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `url` | `string` | WebSocket URL: `ws://localhost:<port>` |
|
||||
| `namespace` | `string` | SurrealDB namespace |
|
||||
| `database` | `string` | SurrealDB database name |
|
||||
| `username` | `string` | Auth username (always `root` in container mode) |
|
||||
| `password` | `string` | Auth password (random UUID in container mode) |
|
||||
|
||||
### `interface SurrealDBFixtureOptions`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `image?` | `string` | Docker image override. Defaults to `surrealdb/surrealdb:v3.0.4`. |
|
||||
| `namespace?` | `string` | Namespace override. Defaults to `"test"`. |
|
||||
| `database?` | `string` | Database override. Defaults to `"test"`. |
|
||||
| `containerFactory?` | `(image: string) => GenericContainer` | Injectable factory for unit testing. When provided, replaces the real `GenericContainer` constructor. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Why HTTP health over WebSocket**: the `/health` HTTP endpoint is simpler to poll and does not require a SurrealDB client package in this fixture. WebSocket handshakes are stateful and require protocol negotiation; plain HTTP is sufficient for readiness signalling.
|
||||
- **Why random password per run**: reusing a static password across test runs creates a security surface for tests running in CI with shared Docker networks. UUID-generation via `crypto.randomUUID()` (the Web Crypto API, universally available in modern Bun and Node.js) is both fast and cryptographically random.
|
||||
- **Dynamic import of testcontainers**: `GenericContainer` is dynamically imported inside `start()` to keep the module side-effect-free and to allow the `containerFactory` injection path to bypass the import entirely for unit tests that provide their own factory.
|
||||
- **Port mapping**: testcontainers allocates an ephemeral host port for each `withExposedPorts()` declaration. `getMappedPort(8000)` retrieves the actual host port after the container starts.
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { SurrealDBFixture } from "./surrealdb-fixture.js";
|
||||
|
||||
function makeContainerFake(mappedPort = 9999) {
|
||||
const stopped: string[] = [];
|
||||
const fakeStarted = {
|
||||
getMappedPort: (_port: number) => mappedPort,
|
||||
stop: async () => {
|
||||
stopped.push("stopped");
|
||||
},
|
||||
};
|
||||
const fakeContainer = {
|
||||
withCommand: function () {
|
||||
return this;
|
||||
},
|
||||
withExposedPorts: function () {
|
||||
return this;
|
||||
},
|
||||
withWaitStrategy: function () {
|
||||
return this;
|
||||
},
|
||||
start: async () => fakeStarted,
|
||||
};
|
||||
return {
|
||||
fakeContainer,
|
||||
fakeStarted,
|
||||
stopped,
|
||||
factory: (_img: string) => fakeContainer as never,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SurrealDBFixture", () => {
|
||||
const ENV_KEYS = [
|
||||
"SURREALDB_URL",
|
||||
"SURREALDB_USERNAME",
|
||||
"SURREALDB_PASSWORD",
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of ENV_KEYS) delete process.env[key];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) delete process.env[key];
|
||||
});
|
||||
|
||||
it("name property returns surrealdb", { timeout: 5000 }, () => {
|
||||
const fixture = new SurrealDBFixture();
|
||||
expect(fixture.name).toBe("surrealdb");
|
||||
});
|
||||
|
||||
it("passthrough mode: returns config from env vars", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
process.env["SURREALDB_URL"] = "ws://localhost:8000";
|
||||
process.env["SURREALDB_USERNAME"] = "admin";
|
||||
process.env["SURREALDB_PASSWORD"] = "secret";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
const config = await fixture.start();
|
||||
|
||||
expect(config.url).toBe("ws://localhost:8000");
|
||||
expect(config.username).toBe("admin");
|
||||
expect(config.password).toBe("secret");
|
||||
expect(config.namespace).toBe("test");
|
||||
expect(config.database).toBe("test");
|
||||
});
|
||||
|
||||
it("passthrough mode: stop is a no-op", { timeout: 30000 }, async () => {
|
||||
process.env["SURREALDB_URL"] = "ws://localhost:8000";
|
||||
process.env["SURREALDB_USERNAME"] = "admin";
|
||||
process.env["SURREALDB_PASSWORD"] = "secret";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
await fixture.start();
|
||||
// Should not throw
|
||||
await fixture.stop();
|
||||
});
|
||||
|
||||
it("passthrough mode: healthCheck returns false when no container", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
process.env["SURREALDB_URL"] = "ws://localhost:8000";
|
||||
process.env["SURREALDB_USERNAME"] = "admin";
|
||||
process.env["SURREALDB_PASSWORD"] = "secret";
|
||||
|
||||
const fixture = new SurrealDBFixture();
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("container mode happy path: returns correct config", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory, fakeStarted } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({ containerFactory: factory });
|
||||
const config = await fixture.start();
|
||||
|
||||
expect(config.url).toBe("ws://localhost:9999");
|
||||
expect(config.username).toBe("root");
|
||||
expect(config.namespace).toBe("test");
|
||||
expect(config.database).toBe("test");
|
||||
expect(config.password).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||
);
|
||||
expect(fakeStarted.getMappedPort(8000)).toBe(9999);
|
||||
});
|
||||
|
||||
it("container mode: stop calls container.stop()", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory, stopped } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({ containerFactory: factory });
|
||||
await fixture.start();
|
||||
await fixture.stop();
|
||||
expect(stopped).toContain("stopped");
|
||||
});
|
||||
|
||||
it("container mode: healthCheck returns true when fetchFn returns ok", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({
|
||||
containerFactory: factory,
|
||||
fetchFn: async () => ({ ok: true }) as Response,
|
||||
});
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it("container mode: healthCheck returns false when fetchFn throws", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({
|
||||
containerFactory: factory,
|
||||
fetchFn: async () => {
|
||||
throw new Error("Connection refused");
|
||||
},
|
||||
});
|
||||
await fixture.start();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("healthCheck returns false when no container", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const fixture = new SurrealDBFixture();
|
||||
const healthy = await fixture.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
|
||||
it("image override: factory called with custom image", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const calledWith: string[] = [];
|
||||
const { fakeContainer } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({
|
||||
image: "custom/surrealdb:latest",
|
||||
containerFactory: (img) => {
|
||||
calledWith.push(img);
|
||||
return fakeContainer as never;
|
||||
},
|
||||
});
|
||||
await fixture.start();
|
||||
expect(calledWith[0]).toBe("custom/surrealdb:latest");
|
||||
});
|
||||
|
||||
it("namespace and database overrides reflected in config", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({
|
||||
namespace: "ns1",
|
||||
database: "db1",
|
||||
containerFactory: factory,
|
||||
});
|
||||
const config = await fixture.start();
|
||||
expect(config.namespace).toBe("ns1");
|
||||
expect(config.database).toBe("db1");
|
||||
});
|
||||
|
||||
it("generated password matches UUID format", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const { factory } = makeContainerFake(9999);
|
||||
const fixture = new SurrealDBFixture({ containerFactory: factory });
|
||||
const config = await fixture.start();
|
||||
expect(config.password).toMatch(/^[0-9a-f-]{36}$/);
|
||||
});
|
||||
|
||||
it("stop when no container is a no-op", { timeout: 30000 }, async () => {
|
||||
const fixture = new SurrealDBFixture();
|
||||
await expect(fixture.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("start(): stops container and clears reference when getMappedPort throws", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const stopped: string[] = [];
|
||||
const fakeStarted = {
|
||||
getMappedPort: (_port: number) => {
|
||||
throw new Error("port mapping failed");
|
||||
},
|
||||
stop: async () => {
|
||||
stopped.push("stopped");
|
||||
},
|
||||
};
|
||||
const fakeContainer = {
|
||||
withCommand: function () {
|
||||
return this;
|
||||
},
|
||||
withExposedPorts: function () {
|
||||
return this;
|
||||
},
|
||||
withWaitStrategy: function () {
|
||||
return this;
|
||||
},
|
||||
start: async () => fakeStarted,
|
||||
};
|
||||
|
||||
const fixture = new SurrealDBFixture({
|
||||
containerFactory: (_img: string) => fakeContainer as never,
|
||||
});
|
||||
|
||||
await expect(fixture.start()).rejects.toThrow("port mapping failed");
|
||||
expect(stopped).toContain("stopped");
|
||||
});
|
||||
|
||||
it("resolveContainerFactory: returns factory from dynamic import when no containerFactory", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
// Access protected method via subclass
|
||||
class Exposed extends SurrealDBFixture {
|
||||
resolveFactory() {
|
||||
return this.resolveContainerFactory();
|
||||
}
|
||||
}
|
||||
// No containerFactory provided — exercises the dynamic import branch
|
||||
const exposed = new Exposed();
|
||||
const resolved = await exposed.resolveFactory();
|
||||
expect(typeof resolved).toBe("function");
|
||||
// Invoke the returned factory to exercise the inner arrow function.
|
||||
// new GenericContainer(img) constructs the object; it does not start Docker.
|
||||
const container = resolved("surrealdb/surrealdb:v3.0.4");
|
||||
expect(container).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* SurrealDBFixture — testcontainers-backed ServiceFixture for SurrealDB v3.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
applyWaitStrategy,
|
||||
type ServiceFixture,
|
||||
} from "@suaveplan/testing-integration";
|
||||
import type { GenericContainer, StartedTestContainer } from "testcontainers";
|
||||
import {
|
||||
SURREALDB_CONTAINER_PORT,
|
||||
SURREALDB_DEFAULT_DATABASE,
|
||||
SURREALDB_DEFAULT_NAMESPACE,
|
||||
SURREALDB_DEFAULT_USERNAME,
|
||||
SURREALDB_ENV_VARS,
|
||||
SURREALDB_IMAGE,
|
||||
SURREALDB_READY_LOG,
|
||||
SURREALDB_READY_LOG_TIMES,
|
||||
} from "../constants/index.js";
|
||||
import type {
|
||||
SurrealDBConfig,
|
||||
SurrealDBFixtureOptions,
|
||||
} from "./surrealdb-fixture.types.js";
|
||||
|
||||
/**
|
||||
* Manages the full lifecycle of a SurrealDB v3 container for integration tests.
|
||||
*
|
||||
* Supports env-bridge passthrough mode: when `SURREALDB_URL`, `SURREALDB_USERNAME`,
|
||||
* and `SURREALDB_PASSWORD` are all set, the fixture returns them directly without
|
||||
* starting a container.
|
||||
*
|
||||
* Uses a random password generated via `crypto.randomUUID()` each run to avoid
|
||||
* credential reuse across test suites.
|
||||
*/
|
||||
export class SurrealDBFixture implements ServiceFixture<SurrealDBConfig> {
|
||||
readonly name = "surrealdb";
|
||||
|
||||
protected readonly options: SurrealDBFixtureOptions;
|
||||
protected container: StartedTestContainer | null = null;
|
||||
protected config: SurrealDBConfig | null = null;
|
||||
protected passthrough = false;
|
||||
|
||||
constructor(options: SurrealDBFixtureOptions = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the container factory. When `containerFactory` is provided via
|
||||
* options, it is returned immediately. Otherwise performs a dynamic import
|
||||
* of testcontainers and returns a factory wrapping `GenericContainer`.
|
||||
*
|
||||
* Protected so subclasses and test doubles can override this seam.
|
||||
*/
|
||||
protected async resolveContainerFactory(): Promise<
|
||||
(image: string) => GenericContainer
|
||||
> {
|
||||
if (this.options.containerFactory) {
|
||||
return this.options.containerFactory;
|
||||
}
|
||||
const { GenericContainer } = await import("testcontainers");
|
||||
return (img: string) => new GenericContainer(img);
|
||||
}
|
||||
|
||||
async start(): Promise<SurrealDBConfig> {
|
||||
// Env-bridge passthrough mode — ALL three vars must be set and non-empty
|
||||
const [url, username, password] = SURREALDB_ENV_VARS.map(
|
||||
(v) => process.env[v] ?? ""
|
||||
);
|
||||
if (url && username && password) {
|
||||
this.passthrough = true;
|
||||
this.config = {
|
||||
url,
|
||||
namespace:
|
||||
this.options.namespace ?? SURREALDB_DEFAULT_NAMESPACE,
|
||||
database: this.options.database ?? SURREALDB_DEFAULT_DATABASE,
|
||||
username,
|
||||
password,
|
||||
};
|
||||
return this.config;
|
||||
}
|
||||
|
||||
const generatedPassword = crypto.randomUUID();
|
||||
const image = this.options.image ?? SURREALDB_IMAGE;
|
||||
const factory = await this.resolveContainerFactory();
|
||||
const builder = factory(image)
|
||||
// Flag set ported from ../suaveplan/packages/universal/surrealdb/
|
||||
// docker-compose.test.yml — proven against v3.0.x. v3 dropped
|
||||
// `--auth` (always on; `--unauthenticated` to disable) and made
|
||||
// capabilities deny-by-default, so `--allow-all` is required for
|
||||
// tests that exercise funcs/net/scripting/guests. Trailing `memory`
|
||||
// selects the in-memory datastore explicitly.
|
||||
.withCommand([
|
||||
"start",
|
||||
"--log=info",
|
||||
`--user=${SURREALDB_DEFAULT_USERNAME}`,
|
||||
`--pass=${generatedPassword}`,
|
||||
"--allow-all",
|
||||
"memory",
|
||||
])
|
||||
.withExposedPorts(SURREALDB_CONTAINER_PORT);
|
||||
const raw = await applyWaitStrategy(builder, {
|
||||
readyLog: SURREALDB_READY_LOG,
|
||||
times: SURREALDB_READY_LOG_TIMES,
|
||||
});
|
||||
|
||||
this.container = await raw.start();
|
||||
|
||||
try {
|
||||
const port = this.container.getMappedPort(SURREALDB_CONTAINER_PORT);
|
||||
|
||||
this.config = {
|
||||
url: `ws://localhost:${port}`,
|
||||
namespace:
|
||||
this.options.namespace ?? SURREALDB_DEFAULT_NAMESPACE,
|
||||
database: this.options.database ?? SURREALDB_DEFAULT_DATABASE,
|
||||
username: SURREALDB_DEFAULT_USERNAME,
|
||||
password: generatedPassword,
|
||||
};
|
||||
return this.config;
|
||||
} catch (err) {
|
||||
await this.container.stop();
|
||||
this.container = null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.passthrough || !this.container) return;
|
||||
await this.container.stop();
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
if (!this.container) return false;
|
||||
const port = this.container.getMappedPort(SURREALDB_CONTAINER_PORT);
|
||||
try {
|
||||
const fetchFn =
|
||||
this.options.fetchFn ??
|
||||
/* istanbul ignore next */ globalThis.fetch;
|
||||
const res = await fetchFn(`http://localhost:${port}/health`);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { GenericContainer } from "testcontainers";
|
||||
|
||||
export interface SurrealDBConfig extends Record<string, unknown> {
|
||||
/** WebSocket URL: "ws://localhost:<port>" */
|
||||
url: string;
|
||||
namespace: string;
|
||||
database: string;
|
||||
username: string;
|
||||
/** Generated per run via crypto.randomUUID() */
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SurrealDBFixtureOptions {
|
||||
/** Override the Docker image. Defaults to SURREALDB_IMAGE constant. */
|
||||
image?: string;
|
||||
/** Override namespace. Defaults to "test". */
|
||||
namespace?: string;
|
||||
/** Override database. Defaults to "test". */
|
||||
database?: string;
|
||||
/**
|
||||
* Injectable GenericContainer constructor for unit testing.
|
||||
* When provided, the fixture uses this instead of importing from testcontainers.
|
||||
* This is the primary seam for unit tests — pass a fake container factory.
|
||||
*/
|
||||
containerFactory?: (image: string) => GenericContainer;
|
||||
/**
|
||||
* Injectable fetch function for unit testing healthCheck().
|
||||
* When provided, replaces globalThis.fetch. Mirrors the pattern used by
|
||||
* MailpitFixture, TypesenseFixture, and GarageS3Fixture.
|
||||
*/
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./constants/index.js";
|
||||
export { SurrealDBFixture } from "./fixture/surrealdb-fixture.js";
|
||||
export type {
|
||||
SurrealDBConfig,
|
||||
SurrealDBFixtureOptions,
|
||||
} from "./fixture/surrealdb-fixture.types.js";
|
||||
Reference in New Issue
Block a user