Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace the raw number|string transform idiom across every schema (the root-cause NaN bug class); emailOrEmpty + lenient isoDateString/timeString. Security: roles privilege-escalation closed; refresh-token family revocation on reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices gross VAT on all paths; orders-pdf custom-items authz. Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse confirm/cancel, attendance lockUserRow); uniqueness checks moved into create transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO). Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted fields; dashboard invalidation gaps; stale-closure confirm bugs. Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier; tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes), isolated on app_test via .env.test with a hard-throw setup guard. Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { config } from "../config/env";
|
|
|
|
describe("env validation", () => {
|
|
it("has numeric port within valid range", () => {
|
|
expect(typeof config.port).toBe("number");
|
|
expect(config.port).toBeGreaterThan(0);
|
|
expect(config.port).toBeLessThanOrEqual(65535);
|
|
});
|
|
|
|
it("has JWT_SECRET defined", () => {
|
|
expect(config.jwt.secret).toBeTruthy();
|
|
expect(config.jwt.secret.length).toBeGreaterThanOrEqual(32);
|
|
});
|
|
|
|
it("has TOTP_ENCRYPTION_KEY defined", () => {
|
|
expect(config.totp.encryptionKey).toBeTruthy();
|
|
expect(config.totp.encryptionKey.length).toBeGreaterThanOrEqual(32);
|
|
});
|
|
|
|
it("has positive JWT expiry values", () => {
|
|
expect(config.jwt.accessTokenExpiry).toBeGreaterThan(0);
|
|
expect(config.jwt.refreshTokenSessionExpiry).toBeGreaterThan(0);
|
|
expect(config.jwt.refreshTokenRememberExpiry).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("has positive maxUploadSize", () => {
|
|
expect(config.nas.maxUploadSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("has DATABASE_URL defined", () => {
|
|
expect(config.db.url).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("env validation — failure path", () => {
|
|
// The config singleton runs its validation at module-load time and is then
|
|
// cached by the module system, so we cannot re-import it with bad env to
|
|
// observe a throw without complex module-registry resetting. Instead we test
|
|
// the exact predicates config/env.ts uses to reject bad input — this is the
|
|
// logic that protects the boot, and the loaded config above proves the
|
|
// happy path. (Predicates kept in sync with src/config/env.ts.)
|
|
const HEX64_RE = /^[0-9a-fA-F]{64}$/;
|
|
|
|
it("rejects a too-short / non-hex JWT_SECRET", () => {
|
|
expect(HEX64_RE.test("short")).toBe(false);
|
|
expect(HEX64_RE.test("a".repeat(63))).toBe(false); // one char short
|
|
expect(HEX64_RE.test("z".repeat(64))).toBe(false); // 64 chars, not hex
|
|
// The real config's secret must pass the very same check.
|
|
expect(HEX64_RE.test(config.jwt.secret)).toBe(true);
|
|
});
|
|
|
|
it("rejects an out-of-range or non-numeric PORT", () => {
|
|
const portValid = (p: number) => !Number.isNaN(p) && p >= 1 && p <= 65535;
|
|
expect(portValid(parseInt("0", 10))).toBe(false);
|
|
expect(portValid(parseInt("70000", 10))).toBe(false);
|
|
expect(portValid(parseInt("notaport", 10))).toBe(false); // NaN
|
|
expect(portValid(config.port)).toBe(true);
|
|
});
|
|
|
|
it("rejects a non-positive ACCESS_TOKEN_EXPIRY", () => {
|
|
const expiryValid = (n: number) => !Number.isNaN(n) && n > 0;
|
|
expect(expiryValid(0)).toBe(false);
|
|
expect(expiryValid(-1)).toBe(false);
|
|
expect(expiryValid(parseInt("oops", 10))).toBe(false); // NaN
|
|
expect(expiryValid(config.jwt.accessTokenExpiry)).toBe(true);
|
|
});
|
|
});
|