Add boot-time env assertions + fix ci.yml for post-webapp layout
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
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.
This commit is contained in:
126
orchestrator/tests/unit/env.test.ts
Normal file
126
orchestrator/tests/unit/env.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user