Files
suaveplan/openspec/specs/testing-integration-surrealdb/spec.md
2026-05-27 23:29:53 +01:00

142 lines
6.7 KiB
Markdown

# testing-integration-surrealdb
## Purpose
SurrealDB v3.0.4 `ServiceFixture` for integration tests. Provides env-bridge passthrough (`SURREALDB_URL`, `SURREALDB_USERNAME`, `SURREALDB_PASSWORD`), HTTP health checking on `/health`, a random password per run, and an injectable container factory. Enables real SurrealDB integration tests without mocks. Depends on `@suaveplan/testing-integration` and `testcontainers`.
## Requirements
### Requirement: SurrealDBFixture implements ServiceFixture<SurrealDBConfig>
The package SHALL export `SurrealDBFixture` with `name === "surrealdb"`. `SurrealDBConfig` SHALL be `{ url: string; namespace: string; database: string; username: string; password: string }`.
#### Scenario: name property is "surrealdb"
- **WHEN** an instance's `.name` is read
- **THEN** the value is the literal `"surrealdb"`
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"name property returns surrealdb"`
---
### Requirement: Env-bridge passthrough mode (3 vars)
When `SURREALDB_URL`, `SURREALDB_USERNAME`, AND `SURREALDB_PASSWORD` are all set and non-empty, `start()` MUST return the config directly without starting a container. Namespace/database fall back to `SURREALDB_DEFAULT_NAMESPACE`/`SURREALDB_DEFAULT_DATABASE` unless overridden via options. In passthrough mode `stop()` MUST be a no-op and `healthCheck()` MUST return `false` (no container to probe).
#### Scenario: returns config from env vars
- **WHEN** all three env vars are set
- **THEN** `start()` returns the config without invoking testcontainers
#### Scenario: stop is a no-op in passthrough mode
- **WHEN** the fixture started in passthrough mode and `stop()` is called
- **THEN** no container is touched
#### Scenario: healthCheck returns false in passthrough mode
- **WHEN** `healthCheck()` is called after a passthrough start
- **THEN** the result is `false`
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"passthrough mode: returns config from env vars"` AND `src/fixture/surrealdb-fixture.test.ts::"passthrough mode: stop is a no-op"` AND `src/fixture/surrealdb-fixture.test.ts::"passthrough mode: healthCheck returns false when no container"`
---
### Requirement: Container mode
When env vars are partially or unset, `start()` MUST start a SurrealDB v3 container via the injected `containerFactory` (or `GenericContainer`), pass `start --auth --user <root> --pass <generated>` as the command, expose `SURREALDB_CONTAINER_PORT`, and return `{ url: "ws://localhost:<port>", namespace, database, username: SURREALDB_DEFAULT_USERNAME, password: <generated> }`. Image MUST be overridable. Namespace/database MUST be overridable via constructor options.
#### Scenario: container mode happy path returns correct config
- **WHEN** `start()` completes container boot
- **THEN** `result.url`, `namespace`, `database`, `username`, `password` are populated
#### Scenario: stop calls container.stop()
- **WHEN** `stop()` follows `start()` in container mode
- **THEN** the container's `stop()` is invoked exactly once
#### Scenario: image override: factory called with custom image
- **WHEN** the fixture is constructed with `image: "x"`
- **THEN** the container factory is invoked with `"x"`
#### Scenario: namespace and database overrides reflected in config
- **WHEN** the fixture is constructed with custom namespace/database
- **THEN** the returned config uses those overrides
#### Scenario: factory falls back to dynamic GenericContainer import
- **WHEN** no `containerFactory` is injected
- **THEN** the fixture dynamically imports testcontainers and uses `GenericContainer`
#### Scenario: start() stops container if getMappedPort throws
- **WHEN** `getMappedPort()` throws after the container has booted
- **THEN** `start()` calls `container.stop()` and clears the reference before re-throwing
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"container mode happy path: returns correct config"` AND `src/fixture/surrealdb-fixture.test.ts::"container mode: stop calls container.stop()"` AND `src/fixture/surrealdb-fixture.test.ts::"image override: factory called with custom image"` AND `src/fixture/surrealdb-fixture.test.ts::"namespace and database overrides reflected in config"` AND `src/fixture/surrealdb-fixture.test.ts::"resolveContainerFactory: returns factory from dynamic import when no containerFactory"` AND `src/fixture/surrealdb-fixture.test.ts::"start(): stops container and clears reference when getMappedPort throws"`
---
### Requirement: HTTP /health probe
In container mode, `healthCheck()` MUST `GET http://localhost:<mappedPort>/health` via the injected `fetchFn` (default `globalThis.fetch`) and return `res.ok`. Network errors MUST be swallowed and return `false`. Without a started container, `healthCheck()` MUST return `false`.
#### Scenario: returns true when fetchFn returns ok
- **WHEN** the injected fetch returns `{ ok: true }`
- **THEN** `healthCheck()` resolves to `true`
#### Scenario: returns false when fetchFn throws
- **WHEN** the injected fetch throws
- **THEN** `healthCheck()` resolves to `false`
#### Scenario: returns false when no container
- **WHEN** `healthCheck()` is called before `start()`
- **THEN** the result is `false`
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"container mode: healthCheck returns true when fetchFn returns ok"` AND `src/fixture/surrealdb-fixture.test.ts::"container mode: healthCheck returns false when fetchFn throws"` AND `src/fixture/surrealdb-fixture.test.ts::"healthCheck returns false when no container"`
---
### Requirement: Random per-run password
In container mode, `start()` MUST generate the SurrealDB root password via `crypto.randomUUID()` and use it as both the container start arg and the returned config field. Password reuse across runs MUST NOT occur.
#### Scenario: generated password matches UUID format
- **WHEN** `start()` runs in container mode
- **THEN** `result.password` matches the UUID v4 regex
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"generated password matches UUID format"`
---
### Requirement: stop() is idempotent
`stop()` MUST be safe to call when no container has been started.
#### Scenario: stop without start is a no-op
- **WHEN** `stop()` is called on a freshly-constructed fixture
- **THEN** no error is thrown
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"stop when no container is a no-op"`
---
### Requirement: testing-integration is a peer dependency
The package's `package.json` MUST list `@suaveplan/testing-integration` and `testcontainers` in `peerDependencies`, NOT in `dependencies`.
#### Scenario: variant does not bundle the core or testcontainers
- **WHEN** the published `package.json` is inspected
- **THEN** `dependencies` does not contain either symbol
**Verified by:** `src/fixture/surrealdb-fixture.test.ts::"name property returns surrealdb"`