# Tasks — testing-integration-step-ca ## 1. Package scaffolding - [x] 1.1 Create `packages/dev/testing-integration-step-ca/` with the standard layout (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`, `src/index.ts`, `src/fixture/`). - [x] 1.2 `package.json`: name `@suaveplan/testing-integration-step-ca`, version `0.1.0`, `peerDependencies` for `@suaveplan/testing-integration` and `testcontainers` (NOT `dependencies`), `dependencies` for `@suaveplan/error` and `@suaveplan/types` only. - [x] 1.3 Subpath exports: `.`, `./fixture`, `./constants`. - [x] 1.4 Add the package to `CLAUDE.md` `Packages` section, then run `bun scripts/folder-reorg/generate-manifest.ts`. ## 2. Image + constants - [x] 2.1 Pin `smallstep/step-ca` to a specific tag + digest in `src/constants/index.ts` as `STEPCA_IMAGE`. - [x] 2.2 Define `STEPCA_HEALTH_TIMEOUT_MS = 60_000`, `STEPCA_HEALTH_POLL_INTERVAL_MS = 1000`. - [x] 2.3 Define `STEPCA_ENV_VARS = ['STEPCA_URL','STEPCA_ROOT_CERT_PEM','STEPCA_ROOT_CERT_PATH','STEPCA_FINGERPRINT','STEPCA_PROVISIONER_PASSWORD'] as const`. - [x] 2.4 Define `STEPCA_DEFAULT_PROVISIONER = 'api'`, `STEPCA_DEFAULT_DNS = 'localhost,step-ca'`, `STEPCA_INTERNAL_PORT = 8443`. ## 3. Error codes - [x] 3.1 Register codes via `@suaveplan/error`'s `defineCodes` with prefix `INTEGRATION.STEP_CA.*`: `INIT_FAILED`, `JWK_PROVISIONER_FAILED`, `ROOT_READ_FAILED`, `FINGERPRINT_FAILED`, `ISSUE_CERT_FAILED`, `PARSE_PEM_FAILED`, `HEALTH_TIMEOUT`. ## 4. Fixture core (`src/fixture/step-ca-fixture.ts`) - [x] 4.1 Implement `StepCaFixture implements ServiceFixture` with `name === 'step-ca'`. - [x] 4.2 Constructor options: `{ image?; caName?; dnsNames?; provisionerName?; healthTimeoutMs?; healthPollIntervalMs?; containerFactory?; execFn?; fetchFn? }`. - [x] 4.3 `start()` env-bridge: when ALL FIVE `STEPCA_ENV_VARS` are set+non-empty → return passthrough config; partial (4 of 5) → fall through to container mode. - [x] 4.4 Container mode: start container via factory (default: dynamic `GenericContainer` import), expose internal `8443` → host port, set environment variables required by step-ca. - [x] 4.5 Init sequence (steps 1–6 from proposal §What Changes/Container mode); each step wrapped in a try/catch that throws the matching `INTEGRATION.STEP_CA.*` error with the underlying stderr attached. - [x] 4.6 Materialize `rootCertPem` to a host-side tmpfile (`fs.mkdtempSync` + `tmp/.crt`), set `rootCertPath` accordingly. - [x] 4.7 If `waitForHealth()` rejects, stop the container exactly once and clear the reference before re-throwing. ## 5. Health probe - [x] 5.1 `healthCheck()` returns `false` when no container started. - [x] 5.2 Container mode: `GET https://localhost:/health` via injected `fetchFn` (default `globalThis.fetch`) with TLS verification disabled (use a per-call `https.Agent` or `Bun`-equivalent that the injected fetch can apply). Return `res.ok`. Network errors → `false`. - [x] 5.3 `waitForHealth()` delegates to `new HealthPoller({ timeoutMs, intervalMs }).poll(this)` with `STEPCA_HEALTH_TIMEOUT_MS` / `STEPCA_HEALTH_POLL_INTERVAL_MS` defaults. - [x] 5.4 Timeout rejection MUST be a `TestEnvironmentError` whose message contains `Health check timed out for fixture "step-ca"`. ## 6. `issueCert()` helper - [x] 6.1 Signature `issueCert({ commonName, sans?, ttl? }) → Promise<{ certPem; keyPem; chainPem; serial }>`. - [x] 6.2 Shells `step ca certificate --provisioner --provisioner-password-file --not-after= --san --san ...` inside the container via injected `execFn`. - [x] 6.3 Reads cert + key from container, splits leaf + chain via PEM block parser (own implementation; no external dep), extracts serial via `step certificate inspect --format json`. - [x] 6.4 Throws `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` on non-zero exit; `INTEGRATION.STEP_CA.PARSE_PEM_FAILED` on malformed PEM. - [x] 6.5 Verifies `commonName` is non-empty before shelling — empty → `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` (no shell invocation). ## 7. Tests (100% Istanbul, per AGENTS.md §7) - [x] 7.1 `src/fixture/step-ca-fixture.test.ts` — passthrough mode (full + partial), container mode happy path, init sequence command order, image override, `containerFactory` injection, `execFn` injection, `fetchFn` injection, health-check truth table, `waitForHealth` timeout + cleanup, `stop()` idempotency. - [x] 7.2 `src/fixture/step-ca-fixture-issue-cert.test.ts` — issueCert happy path (with mocked `execFn`), SAN passthrough, TTL passthrough, malformed PEM rejection, empty commonName rejection, non-zero exit rejection. - [x] 7.3 `src/fixture/step-ca-fixture-parse.test.ts` — PEM block splitter (leaf + chain), serial extraction, edge cases (empty input, missing END line, multiple chain certs). - [x] 7.4 Integration-level smoke test `src/fixture/step-ca-fixture.integration.test.ts` (gated by `INTEGRATION_DOCKER=1`) that actually boots the container, issues a cert, and verifies the leaf chains to the root using Node's `tls.createSecureContext({ ca: rootCertPem })`. ## 8. Pre-publish gate - [x] 8.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration-step-ca` green. - [x] 8.2 `bunx turbo test:coverage --filter=@suaveplan/testing-integration-step-ca` reports 100% on touched modules. - [x] 8.3 Spec-conformance test file (`src/fixture/step-ca-fixture.spec-conformance.test.ts`) asserts every "Verified by" anchor in `specs/testing-integration-step-ca/spec.md` resolves to a real test. ## 9. Documentation - [x] 9.1 Root `README.md` for the package: purpose, basic example (`new StepCaFixture()` + `issueCert`), advanced example (using the root cert with `NODE_EXTRA_CA_CERTS` and Playwright `--ignore-certificate-errors-spki-list`), API reference, implementation notes, ≥200 words. - [x] 9.2 Co-located `.md` for each implementation file per `CLAUDE.md` Documentation rules. ## 10. Archive - [x] 10.1 On ship: fold the spec delta into `openspec/specs/testing-integration-step-ca/spec.md` and move this folder to `openspec/archive/-testing-integration-step-ca/`.