Files
CurrenciCombo/orchestrator/tests/unit/env.test.ts
Devin d6d74f2267
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
Add boot-time env assertions + fix ci.yml for post-webapp layout
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.
2026-04-22 18:06:08 +00:00

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/);
});
});