Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.0 KiB
@suaveplan/testing-integration-step-ca - smallstep/step-ca testcontainers fixture for @suaveplan/testing-integration — real PKI for HTTPS/WebRTC integration tests
Summary
A ServiceFixture for smallstep/step-ca that gives any integration test suite a real, ephemeral certificate authority — root cert, JWK provisioner, and an issueCert() helper that mints leaf certificates on demand.
Documentation tree
- parse-pem-blocks —
parsePemBlocksis a small, dependency-free PEM splitter used byStepCaFixture.issueCert()to separate a leaf certif… - step-ca-fixture —
StepCaFixturemanages the full lifecycle of asmallstep/step-cacontainer for integration tests that need a real PK…
Purpose
WebRTC, secure SMTPS/IMAPS, HTTPS edges (Caddy/nginx), and other integration scenarios refuse non-secure contexts. Without a real PKI chain you end up either rolling per-suite self-signed certs or sprinkling rejectUnauthorized: false everywhere. This fixture replaces both with a single await fixture.start() that returns a config with a trusted root cert, a fingerprint, and an issueCert() method backed by a JWK provisioner inside the container.
Features
ServiceFixture<StepCaConfig>implementation matching the@suaveplan/testing-integrationcontract — drops intoTestEnvironmentalongsidemailpit,garage-s3,postgres,redis.- 5-var env-bridge passthrough mode (
STEPCA_URL,STEPCA_ROOT_CERT_PEM,STEPCA_ROOT_CERT_PATH,STEPCA_FINGERPRINT,STEPCA_PROVISIONER_PASSWORD); partial sets (4 of 5) fall through to container mode. - Bake-init shell entrypoint runs
step ca init, creates an EC P-256 JWK provisioner, merges it intoca.json, captures the root fingerprint, andexecsstep-ca— all in one container start. - Host-side root-cert tmpfile (
rootCertPath) automatically materialised, available forNODE_EXTRA_CA_CERTS, Playwright--ignore-certificate-errors-spki-list, or volume-mounting into sibling containers. issueCert({ commonName, sans?, ttl? })shellsstep ca certificateagainst the JWK provisioner; returns{ certPem, chainPem, keyPem, serial }.- Injectable
containerFactory,execFn, andfetchFnfor unit-testability of the fixture itself (100% Istanbul coverage on touched modules). - HTTPS health probe with TLS verification disabled (root not yet trusted on the host).
Basic example
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture();
const config = await ca.start();
// Issue a leaf cert for our test server
const leaf = await ca.issueCert({
commonName: "host.test",
sans: ["host.test", "localhost"],
ttl: "24h",
});
// Use the root in a Node TLS context
import { createSecureContext } from "node:tls";
const trust = createSecureContext({ ca: config.rootCertPem });
await ca.stop();
Advanced example — Playwright + secure context
import { TestEnvironment } from "@suaveplan/testing-integration";
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
import { chromium } from "playwright";
const ca = new StepCaFixture();
const env = new TestEnvironment([ca]);
await env.startAll();
const cfg = ca.start ? await ca.start() : null;
// Mount the root into a sibling container or load it into the test process:
process.env.NODE_EXTRA_CA_CERTS = cfg!.rootCertPath;
// Issue an HTTPS cert for our app server
const { certPem, keyPem } = await ca.issueCert({
commonName: "app.test",
sans: ["app.test"],
ttl: "1h",
});
// Launch Playwright with the root trusted (Chromium will follow the system
// store; for tests, --ignore-certificate-errors-spki-list works too)
const browser = await chromium.launch({
args: [`--ignore-certificate-errors-spki-list=${cfg!.fingerprint}`],
});
await env.stopAll();
API reference
class StepCaFixture implements ServiceFixture<StepCaConfig>
new StepCaFixture(options?: StepCaFixtureOptions)
Options:
image?: string— override the Docker image. Defaults toSTEPCA_IMAGE(a pinnedsmallstep/step-catag).caName?: string— CA display name. Defaults to"Suaveplan Test CA".dnsNames?: string— comma-separated DNS SANs for the CA. Defaults to"localhost,step-ca".provisionerName?: string— JWK provisioner name. Defaults to"api".healthTimeoutMs?: number/healthPollIntervalMs?: number— override health-poll timing.containerFactory?,execFn?,fetchFn?— injection points for unit-testing the fixture itself.
start(): Promise<StepCaConfig>
Returns { caUrl, rootCertPem, rootCertPath, fingerprint, provisionerName, provisionerPassword }. In container mode, also materialises rootCertPath to a host-side tmpfile.
stop(): Promise<void>
Stops the container (idempotent) and unlinks the host-side root-cert tmpfile (best-effort).
healthCheck(): Promise<boolean>
GET https://localhost:<mappedPort>/health with TLS verification disabled. Returns false when no container has been started or on any network error.
issueCert(opts: IssueCertOptions): Promise<IssuedCert>
Issues a leaf via the JWK provisioner. Throws INTEGRATION.STEP_CA.ISSUE_CERT_FAILED on empty commonName, non-zero step exit, or read failures; throws INTEGRATION.STEP_CA.PARSE_PEM_FAILED on malformed PEM or invalid step certificate inspect JSON.
parsePemBlocks(text: string): { leaf: string; chain: string }
Splits a PEM payload into the first -----BEGIN CERTIFICATE-----…-----END CERTIFICATE----- block (leaf) and the concatenation of remaining blocks (chain). Throws on empty input, non-PEM input, or unterminated blocks.
buildStepCaInitScript(args)
Builds the bake-init shell script used as the container entrypoint. Exported for inspection in tests; not normally consumed directly.
Implementation notes
- The container runs all init steps as part of its entrypoint (
sh -c "<initScript>") andexecsstep-caso health passes only after init has completed. The fixture then readsroot_ca.crtandsecrets/fingerprintviacontainer.exec. caName,dnsNames, andprovisionerNameare validated against^[A-Za-z0-9 _.-]{1,128}$before being interpolated into the entrypoint script. Special characters are JSON-escaped viaJSON.stringifyso a malformed override cannot inject shell metacharacters.- The default health-fetch uses
node:https.requestwithrejectUnauthorized: false. The host trust store does not yet contain the test root, so TLS verification on the health probe must be off; this is the fixture's only HTTPS read of an untrusted endpoint. parsePemBlocksis a pure helper with no dependency onstep-caand is reused byissueCert()for splitting leaf-from-chain.- Coverage gate: 100% Istanbul on every non-
*.types.ts, non-barrel module. Container/fetchdefaults are excluded via/* istanbul ignore next */because they're only exercisable in the integration smoke test.