test(delete-user-data): add kit test suite - #2949
Conversation
There was a problem hiding this comment.
Code Review
This pull request renames the package to @firebase-function-kits/delete-user-data, integrates vitest for testing, and introduces a comprehensive suite of unit tests covering configuration resolution, handlers, helpers, recursive deletion, and Pub/Sub batch operations. The review feedback highlights two opportunities to prevent test pollution: restoring modified process.env variables in runBatchPubSubDeletions.test.ts and restoring the mocked console.warn spy in recursiveDelete.test.ts using try...finally blocks.
| test("falls back to the bare topic name without a project id", async () => { | ||
| const { ctx, published } = makeCtx({ projectId: undefined }); | ||
| delete process.env.GOOGLE_CLOUD_PROJECT; | ||
| delete process.env.PROJECT_ID; | ||
|
|
||
| await runBatchPubSubDeletions( | ||
| { firestorePaths: ["users/uid1"] }, | ||
| "uid1", | ||
| ctx | ||
| ); | ||
|
|
||
| expect(published[0].topic).toBe("kit-inst-deletion"); | ||
| }); |
There was a problem hiding this comment.
Modifying process.env globally in a test without restoring it can lead to test pollution and flaky tests, as other tests running in the same process might unexpectedly rely on or be affected by these environment variables. Use a try...finally block to safely restore the original environment variables after the test runs.
test("falls back to the bare topic name without a project id", async () => {
const oldGoogleCloudProject = process.env.GOOGLE_CLOUD_PROJECT;
const oldProjectId = process.env.PROJECT_ID;
delete process.env.GOOGLE_CLOUD_PROJECT;
delete process.env.PROJECT_ID;
try {
const { ctx, published } = makeCtx({ projectId: undefined });
await runBatchPubSubDeletions(
{ firestorePaths: ["users/uid1"] },
"uid1",
ctx
);
expect(published[0].topic).toBe("kit-inst-deletion");
} finally {
if (oldGoogleCloudProject === undefined) {
delete process.env.GOOGLE_CLOUD_PROJECT;
} else {
process.env.GOOGLE_CLOUD_PROJECT = oldGoogleCloudProject;
}
if (oldProjectId === undefined) {
delete process.env.PROJECT_ID;
} else {
process.env.PROJECT_ID = oldProjectId;
}
}
});| test("retries a failed write up to three attempts", async () => { | ||
| const { db, bulkWriter } = fakeFirestore(); | ||
| vi.spyOn(console, "warn").mockImplementation(() => undefined); | ||
|
|
||
| await recursiveDelete("documents/doc1", db); | ||
|
|
||
| const [onError] = bulkWriter.onWriteError.mock.calls[0]; | ||
| const error = (failedAttempts: number) => ({ | ||
| failedAttempts, | ||
| documentRef: { path: "documents/doc1" }, | ||
| }); | ||
|
|
||
| expect(onError(error(1))).toBe(true); | ||
| expect(onError(error(2))).toBe(true); | ||
| expect(onError(error(3))).toBe(false); | ||
| }); |
There was a problem hiding this comment.
Spying on console.warn and mocking its implementation globally without restoring it can pollute other tests by silencing warnings. Wrap the test logic in a try...finally block and call mockRestore() on the spy to ensure console.warn is always restored to its original behavior.
test("retries a failed write up to three attempts", async () => {
const { db, bulkWriter } = fakeFirestore();
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
try {
await recursiveDelete("documents/doc1", db);
const [onError] = bulkWriter.onWriteError.mock.calls[0];
const error = (failedAttempts: number) => ({
failedAttempts,
documentRef: { path: "documents/doc1" },
});
expect(onError(error(1))).toBe(true);
expect(onError(error(2))).toBe(true);
expect(onError(error(3))).toBe(false);
} finally {
consoleWarnSpy.mockRestore();
}
});Ports the extension's coverage to the kit and wires up `pnpm test` with vitest. The extension suite runs against the Firestore, Auth and Pub/Sub emulators; the kit takes its clients from an injected context, so the same behaviours are covered with test doubles instead.
Ports the extension's emulator suite. A small app under tests/emulator/app loads the built kit as a functions codebase, so the auth and Pub/Sub triggers run for real and the discovery and deletion loop is exercised end to end. The app carries its own package.json because the Functions emulator rejects the kit's `engines: ">=22"` and wants an exact major. Its .env sets every param so the emulator never prompts and caches a stale .env.local. `pnpm test` still runs only the unit tests; `pnpm test:emulator` runs these.
7536b48 to
166026d
Compare
The
delete-user-datakit had no tests, andtestwas a placeholder that echoes a warning.Adds two suites.
pnpm testruns 54 unit tests over the handlers, helpers, publisher and custom search function, using test doubles for the injected clients.pnpm test:emulatorruns 16 tests against the Firestore, Auth, Pub/Sub and Database emulators, porting the extension's emulator suite: recursive deletes,hasValidUserPath, the discovery and deletion loop end to end, the search depth limit, foreign paths being refused, and deleting an auth user clearing its Firestore data.The emulator suite loads the built kit through a small app under
tests/emulator/app, which carries its ownpackage.jsonbecause the Functions emulator rejects the kit'sengines: ">=22"and wants an exact major. Its.envsets every param so the emulator never prompts and caches a stale.env.local, andkits/.gitignoregains one negation so that file can be committed.Both suites pass. I mutated the source to check they bite: the ownership check and the search depth limit each fail a test when removed, though depth needs both of its redundant guards removed since either alone still stops the recursion.
One thing to decide: the emulator
.envhas to setSELECTED_DATABASE_INSTANCE, because without itgetContextthrowsCan't determine Firebase Database URLand every invocation dies. That reproduces on a live deploy too, and the extension avoids it by callingadmin.database()lazily inside the RTDB path rather than eagerly per invocation. Worth its own PR.