Files
HealthLog/tests/integration/environment-setup.ts
T
Marc-André Bombeck b09a6d2df8 test: fail a test whose code caught and logged a missing mock export
A module mock that leaves out an export the code under test reaches throws
at the call, and where that code catches and logs (a best-effort tail, a
fail-soft read) the test went on green while the path it meant to cover
never ran. The Apple Health import tests mocked the rollup module without
its fold window, so the import tail's rollup fold and cache invalidation
never ran in any of them; four route test files mocked the logging context
or the rollups without what the measurement tail reaches.

Both setup files now watch the console and fail the test in which such a
line was logged. The ECG import test keeps the real rollup module and
stubs only the fold, and the four unit mocks gain what they lacked.
2026-10-22 23:15:43 +02:00

105 lines
4.0 KiB
TypeScript

import { afterEach, beforeEach, inject, vi } from "vitest";
import { quarantineEntryFor } from "../quarantine/quarantine";
import { watchSwallowedMockErrors } from "../support/swallowed-mock-errors";
import { prepareWorkerDatabase } from "./worker-database";
// Each worker runs against its own clone of the migrated template (see
// `worker-database.ts`). The URL is final before any application module is
// imported, so the Prisma singleton is built against the clone.
process.env.DATABASE_URL = await prepareWorkerDatabase(
inject("integrationDatabaseUrl"),
inject("integrationDatabasePlan"),
);
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";
/**
* Leave no background work in flight when an integration test ends — the same
* contract the unit setup keeps, for the same reason: detached background work
* logs on its own handle, and a console write that reaches vitest after the
* worker has begun closing its RPC channel fails the run with
* `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending`.
* This suite has its own config and its own setup file, so the unit hook does
* not cover it.
*
* The import is deferred: this file's whole job is to bridge DATABASE_URL into
* the worker BEFORE any application module reads it at import time, and a
* static import here would pull part of the application tree in first.
*/
/** A swallowed "missing mock export" error fails its test (see the module). */
const checkSwallowedMockErrors = watchSwallowedMockErrors();
afterEach(async () => {
const { settleBackgroundTasks } =
await import("@/lib/logging/background-tasks");
await settleBackgroundTasks();
checkSwallowedMockErrors();
});
/** The flaky-test quarantine, as in the unit setup (`vitest.setup.ts`). */
beforeEach((context) => {
const entry = quarantineEntryFor(context.task);
if (entry) {
context.skip(`quarantined until ${entry.expires} (${entry.issue})`);
}
});
/**
* Route handlers read cookies and headers through `next/headers`, which only
* works inside a Next.js request scope. Every file gets the same stand-in,
* backed by the shared jars in `mock-next-headers.ts`, and the same no-op for
* the boot-time compatibility probe. A file that needs different behaviour
* still declares its own `vi.mock`, which takes precedence over this one.
*/
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
has: (name: string) => headerJar.has(name.toLowerCase()),
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
/**
* The same pin the unit setup applies (see `vitest.setup.ts`): the Fitbit
* suites run before the Web API turndown, whatever the real date, and a test
* about the turndown flips `isFitbitRetired` itself. The factory imports the
* module lazily, so nothing of the application loads before DATABASE_URL is
* bridged above.
*/
vi.mock("@/lib/fitbit/retirement", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/fitbit/retirement")>();
const isFitbitRetired = vi.fn((_now?: Date) => false);
return {
...actual,
isFitbitRetired,
fitbitRetirement: vi.fn((now?: Date) => ({
...actual.fitbitRetirement(now),
ended: isFitbitRetired(now),
})),
};
});