Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# StepCaFixture
## Purpose
`StepCaFixture` manages the full lifecycle of a `smallstep/step-ca` container for integration tests that need a real PKI chain — WebRTC, secure SMTPS/IMAPS, HTTPS edges (Caddy/nginx), and any scenario that refuses non-secure contexts. Implementing the `ServiceFixture<StepCaConfig>` contract from `@suaveplan/testing-integration`, it can be composed into a `TestEnvironment` alongside Mailpit, Garage S3, Postgres, Redis, and other fixtures, with parallel startup and reverse-order teardown.
## Features
- 5-var env-bridge passthrough mode (all of `STEPCA_URL`, `STEPCA_ROOT_CERT_PEM`, `STEPCA_ROOT_CERT_PATH`, `STEPCA_FINGERPRINT`, `STEPCA_PROVISIONER_PASSWORD`); a partial set (4 of 5) falls through to container mode.
- Bake-init shell entrypoint runs `step ca init`, generates an EC P-256 JWK provisioner, merges it into `ca.json`, captures the root fingerprint, and `exec`s `step-ca` — all in one container start.
- Host-side root-cert tmpfile (`rootCertPath`) materialised after health passes; suitable for `NODE_EXTRA_CA_CERTS`, Playwright trust args, or volume-mounting into sibling containers.
- HTTPS health probe with TLS verification disabled (root is not yet trusted on the host).
- `issueCert({ commonName, sans?, ttl? })` shells `step ca certificate` against the JWK provisioner; returns leaf + chain + key + serial.
- Injectable `containerFactory`, `execFn`, and `fetchFn` so the fixture itself is unit-testable at 100% Istanbul coverage.
## Usage: basic example
```ts
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture();
const cfg = await ca.start();
console.log(cfg.fingerprint); // 64-hex SHA-256 of the root
console.log(cfg.rootCertPath); // host-side tmpfile path
const { certPem } = await ca.issueCert({ commonName: "host.test" });
await ca.stop();
```
## Usage: advanced example
```ts
import { TestEnvironment } from "@suaveplan/testing-integration";
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture({
caName: "Edge Test CA",
dnsNames: "localhost,step-ca,edge.test",
provisionerName: "api",
healthTimeoutMs: 30_000,
});
const env = new TestEnvironment([ca]);
await env.startAll();
const cfg = await ca.start();
process.env.NODE_EXTRA_CA_CERTS = cfg.rootCertPath;
const leaf = await ca.issueCert({
commonName: "edge.test",
sans: ["edge.test", "edge.local"],
ttl: "24h",
});
await env.stopAll();
```
## API reference
### `new StepCaFixture(options?: StepCaFixtureOptions)`
`StepCaFixtureOptions` accepts `image`, `caName`, `dnsNames`, `provisionerName`, `healthTimeoutMs`, `healthPollIntervalMs`, plus the injectable test seams `containerFactory`, `execFn`, `fetchFn`.
### `start(): Promise<StepCaConfig>`
In env-bridge mode: returns the env-supplied config without starting a container. In container mode: starts the container with the bake-init entrypoint, polls `/health` until ok, reads `root_ca.crt` + `secrets/fingerprint` via exec, materialises the host-side tmpfile, and returns `{ caUrl, rootCertPem, rootCertPath, fingerprint, provisionerName, provisionerPassword }`. Throws on container failure; if `waitForHealth()` rejects the container is stopped and the reference cleared before the error propagates.
### `stop(): Promise<void>`
Idempotent. Removes the host-side root-cert tmpfile (best-effort) and stops the container exactly once.
### `healthCheck(): Promise<boolean>`
`GET https://localhost:<mappedPort>/health` via the injected `fetchFn` (default uses `node:https.request` with `rejectUnauthorized: false`). Returns `false` when no container is running, when the response is not `ok`, or on any network error.
### `issueCert(opts: IssueCertOptions): Promise<IssuedCert>`
Empty `commonName` → throws `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` without invoking exec. Non-zero `step` exit → `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` with stderr (or stdout fallback) attached as `cause`. Malformed PEM or `step certificate inspect` JSON failure → `INTEGRATION.STEP_CA.PARSE_PEM_FAILED`.
## Implementation notes
- The container runs init+serve in a single entrypoint (`sh -c "<initScript>"`); the script `exec`s `step-ca` so the running PID has the daemon, not the shell. Health passing therefore implies init has fully completed.
- `caName`, `dnsNames`, `provisionerName` are validated against `^[A-Za-z0-9 _.-]{1,128}$` and JSON-escaped via `JSON.stringify` before interpolation into the entrypoint script. This blocks shell-metacharacter injection at the caller boundary.
- The provisioner password is generated with `crypto.randomBytes(24).toString("hex")` (192 bits of entropy).
- `parsePemBlocks` (sibling helper) is the only PEM-aware code; `issueCert` delegates leaf/chain splitting to it.
- The `defaultHealthFetch` and `container.exec` fallback paths are marked `/* istanbul ignore next */` — they're only exercisable in the integration smoke test (gated by `INTEGRATION_DOCKER=1`), not unit tests.