Initial Commit
This commit is contained in:
136
openspec/specs/testing-integration-typesense/spec.md
Normal file
136
openspec/specs/testing-integration-typesense/spec.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# testing-integration-typesense
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/testing-integration-typesense` is the Typesense `ServiceFixture` for the genesis integration-test infrastructure. It plugs into `@suaveplan/testing-integration`'s `TestEnvironment` so an integration suite can stand up a Typesense instance (or attach to a shared one in CI) with a single `createIntegrationSetup({ services: [new TypesenseFixture()] })` call. Two operating modes: env-bridge passthrough (read `TYPESENSE_URL` + `TYPESENSE_API_KEY` from the environment, no container started) for CI / shared-infra; container mode (Testcontainers-driven Docker container) for local dev. JSON `{ ok: true }` health probe with injectable `fetch`. Random per-run API key in container mode so concurrent suites don't trample each other.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: TypesenseFixture implements ServiceFixture<TypesenseConfig>
|
||||
|
||||
The package SHALL export `TypesenseFixture` with `name === "typesense"`. `TypesenseConfig` SHALL be `{ url: string; apiKey: string }`.
|
||||
|
||||
#### Scenario: name property is "typesense"
|
||||
|
||||
- **WHEN** an instance's `.name` is read
|
||||
- **THEN** the value is the literal `"typesense"`
|
||||
|
||||
**Verified by:** `src/fixture/typesense-fixture.test.ts::"name property returns typesense"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Env-bridge passthrough mode (2 vars, both required)
|
||||
|
||||
When BOTH `TYPESENSE_URL` AND `TYPESENSE_API_KEY` are set and non-empty, `start()` MUST return them directly without starting a container. A partial set (only one of the two) MUST fall through to container mode.
|
||||
|
||||
#### Scenario: returns config from both env vars
|
||||
|
||||
- **WHEN** both env vars are set
|
||||
- **THEN** `start()` returns `{ url, apiKey }` without invoking testcontainers
|
||||
|
||||
#### Scenario: stop is a no-op in passthrough mode
|
||||
|
||||
- **WHEN** `stop()` follows passthrough start
|
||||
- **THEN** no container is touched
|
||||
|
||||
#### Scenario: partial passthrough falls through to container mode
|
||||
|
||||
- **WHEN** only `TYPESENSE_URL` is set
|
||||
- **THEN** `start()` boots a container instead of returning passthrough config
|
||||
|
||||
**Verified by:** `src/fixture/typesense-fixture.test.ts::"passthrough mode: returns config from both env vars"` AND `src/fixture/typesense-fixture.test.ts::"passthrough mode: stop is a no-op"` AND `src/fixture/typesense-fixture.test.ts::"partial passthrough (only URL set) falls through to container mode"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Container mode
|
||||
|
||||
When env vars are partial or unset, `start()` MUST start a Typesense 27 container via the injected `containerFactory` (or `GenericContainer`), pass `--data-dir=/data --api-key=<generated> --enable-cors`, expose `TYPESENSE_CONTAINER_PORT`, and return `{ url: "http://localhost:<mappedPort>", apiKey: <generated> }`.
|
||||
|
||||
#### Scenario: container mode happy path returns correct config
|
||||
|
||||
- **WHEN** `start()` completes container boot
|
||||
- **THEN** `result.url` and `result.apiKey` are populated
|
||||
|
||||
#### Scenario: stop calls container.stop()
|
||||
|
||||
- **WHEN** `stop()` follows `start()` in container mode
|
||||
- **THEN** the container's `stop()` is invoked exactly once
|
||||
|
||||
#### Scenario: factory falls back to dynamic GenericContainer import
|
||||
|
||||
- **WHEN** no `containerFactory` is injected
|
||||
- **THEN** the fixture dynamically imports testcontainers and uses `GenericContainer`
|
||||
|
||||
**Verified by:** `src/fixture/typesense-fixture.test.ts::"container mode happy path: returns correct config"` AND `src/fixture/typesense-fixture.test.ts::"container mode: stop calls container.stop()"` AND `src/fixture/typesense-fixture.test.ts::"resolveContainerFactory: falls back to GenericContainer import"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: JSON `{ ok: true }` health probe
|
||||
|
||||
In container mode, `healthCheck()` MUST `GET http://localhost:<mappedPort>/health` via the injected `fetchFn` (default `globalThis.fetch`), parse the JSON body, and return `true` only when `data.ok === true`. Non-200 responses, parse failures, and `fetchFn` throws MUST return `false`. Without a started container, `healthCheck()` MUST return `false`.
|
||||
|
||||
#### Scenario: returns true when JSON ok=true
|
||||
|
||||
- **WHEN** the response body is `{ "ok": true }`
|
||||
- **THEN** `healthCheck()` resolves to `true`
|
||||
|
||||
#### Scenario: returns false when JSON ok=false
|
||||
|
||||
- **WHEN** the response body is `{ "ok": false }`
|
||||
- **THEN** `healthCheck()` resolves to `false`
|
||||
|
||||
#### Scenario: returns false on fetch throw
|
||||
|
||||
- **WHEN** the injected fetch throws
|
||||
- **THEN** `healthCheck()` resolves to `false`
|
||||
|
||||
#### Scenario: returns false on non-200 HTTP response
|
||||
|
||||
- **WHEN** the response has `ok: false`
|
||||
- **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/typesense-fixture.test.ts::"healthCheck returns true when JSON { ok: true }"` AND `src/fixture/typesense-fixture.test.ts::"healthCheck returns false when JSON { ok: false }"` AND `src/fixture/typesense-fixture.test.ts::"healthCheck returns false on fetch throw"` AND `src/fixture/typesense-fixture.test.ts::"healthCheck returns false on non-200 HTTP response"` AND `src/fixture/typesense-fixture.test.ts::"healthCheck returns false when no container"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Random per-run API key
|
||||
|
||||
In container mode, `start()` MUST generate the API key via `crypto.randomUUID()` and use it as both the container start arg and the returned config field.
|
||||
|
||||
#### Scenario: generated apiKey matches UUID format
|
||||
|
||||
- **WHEN** `start()` runs in container mode
|
||||
- **THEN** `result.apiKey` matches the UUID v4 regex
|
||||
|
||||
**Verified by:** `src/fixture/typesense-fixture.test.ts::"generated apiKey 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/typesense-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/typesense-fixture.test.ts::"name property returns typesense"`
|
||||
Reference in New Issue
Block a user