Skip to content

test(delete-user-data): add kit test suite - #2949

Draft
IzaakGough wants to merge 2 commits into
kitsfrom
kits-delete-user-data-tests
Draft

test(delete-user-data): add kit test suite#2949
IzaakGough wants to merge 2 commits into
kitsfrom
kits-delete-user-data-tests

Conversation

@IzaakGough

@IzaakGough IzaakGough commented Aug 18, 2026

Copy link
Copy Markdown

The delete-user-data kit had no tests, and test was a placeholder that echoes a warning.

Adds two suites. pnpm test runs 54 unit tests over the handlers, helpers, publisher and custom search function, using test doubles for the injected clients. pnpm test:emulator runs 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 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, and kits/.gitignore gains 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 .env has to set SELECTED_DATABASE_INSTANCE, because without it getContext throws Can't determine Firebase Database URL and every invocation dies. That reproduces on a live deploy too, and the extension avoids it by calling admin.database() lazily inside the RTDB path rather than eagerly per invocation. Worth its own PR.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +98 to +110
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");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment on lines +77 to +92
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);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants