fix(ci): harden test determinism and dependencies

This commit is contained in:
Marc-André Bombeck committed 2026-07-21 00:36:40 +02:00
1 parent 9adeb8fc19
commit 3ce6726646
13 files changed
+354 -75

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { getPrismaClient } from "./setup";
const TEST_ENCRYPTION_KEY =
"0000000000000000000000000000000000000000000000000000000000000000";
const TEST_HMAC_KEY = "integration-test-hmac-key-32-bytes-minimum";
const TEST_SESSION_SECRET = "integration-test-session-secret-32-bytes";
describe("integration worker environment", () => {
it(
"replaces ambient database and encryption configuration with test values",
async () => {
expect(process.env.DATABASE_URL).toContain("healthlog_test");
expect(process.env.DATABASE_URL).not.toContain("ambient_database");
expect(process.env.ENCRYPTION_KEY).toBe(TEST_ENCRYPTION_KEY);
expect(process.env.ENCRYPTION_KEYS).toBeUndefined();
expect(process.env.ENCRYPTION_ACTIVE_KEY_ID).toBeUndefined();
expect(process.env.API_TOKEN_HMAC_KEY).toBe(TEST_HMAC_KEY);
expect(process.env.SESSION_SECRET).toBe(TEST_SESSION_SECRET);
const rows = await getPrismaClient().$queryRaw<
Array<{ database_name: string }>
>`
SELECT current_database() AS database_name
`;
expect(rows).toEqual([{ database_name: "healthlog_test" }]);
},
);
});
+15
View File
@@ -0,0 +1,15 @@
import { inject } from "vitest";
declare module "vitest" {
export interface ProvidedContext {
integrationDatabaseUrl: string;
}
}
process.env.DATABASE_URL = inject("integrationDatabaseUrl");
process.env.ENCRYPTION_KEY =
"0000000000000000000000000000000000000000000000000000000000000000";
delete process.env.ENCRYPTION_KEYS;
delete process.env.ENCRYPTION_ACTIVE_KEY_ID;
process.env.API_TOKEN_HMAC_KEY = "integration-test-hmac-key-32-bytes-minimum";
process.env.SESSION_SECRET = "integration-test-session-secret-32-bytes";
+6 -4
View File
@@ -18,6 +18,7 @@ import { execSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { TestProject } from "vitest/node";
import {
PostgreSqlContainer,
type StartedPostgreSqlContainer,
@@ -29,7 +30,9 @@ const PROJECT_ROOT = resolve(__dirname, "..", "..");
let container: StartedPostgreSqlContainer | null = null;
export default async function setup(): Promise<() => Promise<void>> {
export default async function setup(
project: TestProject,
): Promise<() => Promise<void>> {
container = await new PostgreSqlContainer("postgres:16-alpine")
.withDatabase("healthlog_test")
.withUsername("healthlog")
@@ -38,10 +41,9 @@ export default async function setup(): Promise<() => Promise<void>> {
const url = container.getConnectionUri() + "?schema=public&pgbouncer=false";
process.env.DATABASE_URL = url;
project.provide("integrationDatabaseUrl", url);
// Provided so child processes (e.g. Prisma's CLI) inherit the URL.
// Vitest also forwards process.env to test workers, so the
// application Prisma singleton sees this URL when it loads.
// The explicit child environment keeps Prisma CLI on the same container.
execSync("pnpm db:migrate:deploy", {
cwd: PROJECT_ROOT,
stdio: "inherit",
+2 -30
View File
@@ -1,39 +1,11 @@
/**
* Per-test helpers for the integration suite. The Postgres testcontainer
* is started ONCE in `global-setup.ts` and torn down at the end of the
* run, so test files only need:
*
* import { getPrismaClient, truncateAllTables } from "./setup";
*
* beforeEach(async () => { await truncateAllTables(getPrismaClient()); });
*
* No beforeAll(startTestDb) / afterAll(stopTestDb) is needed — the
* container is alive whenever the test file is loaded, and Vitest's
* worker inherits `process.env.DATABASE_URL` from globalSetup so the
* application's Prisma singleton (`src/lib/db.ts`) connects to the
* testcontainer transparently.
* is started once in global-setup.ts and bridged into each worker by
* environment-setup.ts before application modules load.
*/
import { prisma } from "@/lib/db";
import type { PrismaClient } from "@/generated/prisma/client";
// v1.23 — every integration test imports this module. The free-text health-note
// columns (mood + measurement) are now AES-256-GCM at rest, so any test that
// writes/reads a note needs an encryption key. Crypto reads the key lazily on
// first `encrypt()`, so a `??=` default here covers every test without touching
// each file (individual tests that set their own key still win — `??=`).
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
// v1.30.32 — session cookies now carry a CSPRNG secret whose HMAC is what
// lands in `sessions.token_hash`, so `createSession` needs this key exactly
// like token issuance already did. It is a Core-required variable in
// `scripts/env-manifest.json` (the app refuses to boot without it), so
// defaulting it here matches production rather than papering over a gap.
// Same `??=` treatment as the encryption key above: files that set their own
// still win.
process.env.API_TOKEN_HMAC_KEY ??=
"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
/**
* The application's Prisma singleton. Tests use this so any code
* imported via `await import("@/lib/...")` shares the exact same client