Some checks failed
CI / Portal Lint (pull_request) Failing after 33s
CI / Portal Type Check (pull_request) Successful in 57s
CI / Portal Build (pull_request) Failing after 33s
CI / Orchestrator Type Check (pull_request) Failing after 5s
CI / Orchestrator Build (pull_request) Failing after 5s
CI / Orchestrator Test (pull_request) Failing after 5s
CI / Contracts Compile (pull_request) Failing after 12s
CI / Contracts Test (pull_request) Failing after 7s
Code Quality / SonarQube Analysis (pull_request) Failing after 20s
Code Quality / Code Quality Checks (pull_request) Failing after 5s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 4s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 4s
Closes gap-analysis v2 §8.1 / §8.4 / §8.6 and §10.1 / §10.2. - assertProductionEnv() in config/env.ts fails-fast in NODE_ENV=production when SESSION_SECRET / EVENT_BUS_HMAC_SECRET / CHAIN_138_RPC_URL / NOTARY_REGISTRY_ADDRESS / ORCHESTRATOR_PRIVATE_KEY / DATABASE_URL is missing or uses the dev placeholder. Catches the silent-degrade-to-mock failure mode that would turn the Ledger Anchor back into a lie. - New EVENT_BUS_HMAC_SECRET env added to the schema. - .github/workflows/ci.yml rewritten: portal jobs target repo root (not the removed webapp/ gitlink), orchestrator type-check + test job added, contracts jobs kept as-is. - 7 unit tests for assertProductionEnv; full suite 87/87 green.
127 lines
5.0 KiB
TypeScript
127 lines
5.0 KiB
TypeScript
/**
|
|
* Tests for `assertProductionEnv` — arch §13 + gap-analysis v2 §8.1 / §8.4.
|
|
*
|
|
* These tests exercise the boot-time env assertion in isolation: they
|
|
* snapshot `process.env`, stub `process.exit`, flip envs, call the
|
|
* assertion, and restore.
|
|
*/
|
|
|
|
import { assertProductionEnv } from "../../src/config/env";
|
|
|
|
describe("assertProductionEnv", () => {
|
|
const savedEnv = { ...process.env };
|
|
let exitSpy: jest.SpyInstance;
|
|
let errorSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...savedEnv };
|
|
exitSpy = jest
|
|
.spyOn(process, "exit")
|
|
.mockImplementation(((code?: number | string | null) => {
|
|
throw new Error(`process.exit(${code})`);
|
|
}) as never);
|
|
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
exitSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
process.env = { ...savedEnv };
|
|
});
|
|
|
|
it("does nothing when NODE_ENV is not production", () => {
|
|
process.env.NODE_ENV = "development";
|
|
expect(() => assertProductionEnv()).not.toThrow();
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails fast when SESSION_SECRET is missing in production", () => {
|
|
process.env.NODE_ENV = "production";
|
|
delete process.env.SESSION_SECRET;
|
|
process.env.EVENT_BUS_HMAC_SECRET = "x".repeat(40);
|
|
process.env.CHAIN_138_RPC_URL = "https://rpc.example.com";
|
|
process.env.NOTARY_REGISTRY_ADDRESS =
|
|
"0x" + "a".repeat(40);
|
|
process.env.ORCHESTRATOR_PRIVATE_KEY = "0x" + "b".repeat(64);
|
|
process.env.DATABASE_URL = "postgres://localhost/db";
|
|
|
|
expect(() => assertProductionEnv()).toThrow("process.exit(1)");
|
|
const output = errorSpy.mock.calls.flat().join(" ");
|
|
expect(output).toMatch(/SESSION_SECRET/);
|
|
});
|
|
|
|
it("fails fast when SESSION_SECRET is the dev placeholder", () => {
|
|
process.env.NODE_ENV = "production";
|
|
process.env.SESSION_SECRET =
|
|
"dev-secret-change-in-production-min-32-chars";
|
|
process.env.EVENT_BUS_HMAC_SECRET = "x".repeat(40);
|
|
process.env.CHAIN_138_RPC_URL = "https://rpc.example.com";
|
|
process.env.NOTARY_REGISTRY_ADDRESS = "0x" + "a".repeat(40);
|
|
process.env.ORCHESTRATOR_PRIVATE_KEY = "0x" + "b".repeat(64);
|
|
process.env.DATABASE_URL = "postgres://localhost/db";
|
|
|
|
expect(() => assertProductionEnv()).toThrow("process.exit(1)");
|
|
expect(errorSpy.mock.calls.flat().join(" ")).toMatch(
|
|
/SESSION_SECRET.*dev placeholder/,
|
|
);
|
|
});
|
|
|
|
it("fails fast when NotaryRegistry envs are absent", () => {
|
|
process.env.NODE_ENV = "production";
|
|
process.env.SESSION_SECRET = "s".repeat(40);
|
|
process.env.EVENT_BUS_HMAC_SECRET = "x".repeat(40);
|
|
process.env.DATABASE_URL = "postgres://localhost/db";
|
|
delete process.env.CHAIN_138_RPC_URL;
|
|
delete process.env.NOTARY_REGISTRY_ADDRESS;
|
|
delete process.env.ORCHESTRATOR_PRIVATE_KEY;
|
|
|
|
expect(() => assertProductionEnv()).toThrow("process.exit(1)");
|
|
const output = errorSpy.mock.calls.flat().join(" ");
|
|
expect(output).toMatch(/NotaryRegistry/);
|
|
expect(output).toMatch(/CHAIN_138_RPC_URL/);
|
|
expect(output).toMatch(/NOTARY_REGISTRY_ADDRESS/);
|
|
expect(output).toMatch(/ORCHESTRATOR_PRIVATE_KEY/);
|
|
});
|
|
|
|
it("fails fast when EVENT_BUS_HMAC_SECRET falls back to dev placeholder", () => {
|
|
process.env.NODE_ENV = "production";
|
|
process.env.SESSION_SECRET = "s".repeat(40);
|
|
delete process.env.EVENT_BUS_HMAC_SECRET;
|
|
process.env.CHAIN_138_RPC_URL = "https://rpc.example.com";
|
|
process.env.NOTARY_REGISTRY_ADDRESS = "0x" + "a".repeat(40);
|
|
process.env.ORCHESTRATOR_PRIVATE_KEY = "0x" + "b".repeat(64);
|
|
process.env.DATABASE_URL = "postgres://localhost/db";
|
|
|
|
// eventSecret falls back to SESSION_SECRET which is valid, so this
|
|
// path should *succeed* — SESSION_SECRET is a legitimate source of
|
|
// signing material per the getSigningSecret fallback chain.
|
|
expect(() => assertProductionEnv()).not.toThrow();
|
|
});
|
|
|
|
it("passes when all envs are set to real values in production", () => {
|
|
process.env.NODE_ENV = "production";
|
|
process.env.SESSION_SECRET = "s".repeat(40);
|
|
process.env.EVENT_BUS_HMAC_SECRET = "e".repeat(40);
|
|
process.env.CHAIN_138_RPC_URL = "https://rpc.example.com";
|
|
process.env.NOTARY_REGISTRY_ADDRESS = "0x" + "a".repeat(40);
|
|
process.env.ORCHESTRATOR_PRIVATE_KEY = "0x" + "b".repeat(64);
|
|
process.env.DATABASE_URL = "postgres://localhost/db";
|
|
|
|
expect(() => assertProductionEnv()).not.toThrow();
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports DATABASE_URL missing in production", () => {
|
|
process.env.NODE_ENV = "production";
|
|
process.env.SESSION_SECRET = "s".repeat(40);
|
|
process.env.EVENT_BUS_HMAC_SECRET = "e".repeat(40);
|
|
process.env.CHAIN_138_RPC_URL = "https://rpc.example.com";
|
|
process.env.NOTARY_REGISTRY_ADDRESS = "0x" + "a".repeat(40);
|
|
process.env.ORCHESTRATOR_PRIVATE_KEY = "0x" + "b".repeat(64);
|
|
delete process.env.DATABASE_URL;
|
|
|
|
expect(() => assertProductionEnv()).toThrow("process.exit(1)");
|
|
expect(errorSpy.mock.calls.flat().join(" ")).toMatch(/DATABASE_URL/);
|
|
});
|
|
});
|