Skip to content

test(storage-resize-images): add kit test suite - #2952

Draft
IzaakGough wants to merge 2 commits into
kitsfrom
kits-storage-resize-images-tests
Draft

test(storage-resize-images): add kit test suite#2952
IzaakGough wants to merge 2 commits into
kitsfrom
kits-storage-resize-images-tests

Conversation

@IzaakGough

@IzaakGough IzaakGough commented Aug 18, 2026

Copy link
Copy Markdown

The storage-resize-images kit had no tests, and test was a placeholder that echoes a warning.

Adds two suites. pnpm test runs 101 unit tests covering path matching, shouldResize, image conversion, resize, the retry queue, the content filter including its schema-refusal cases, and the resize handler's filter and placeholder routing. pnpm test:emulator runs the extension's 4 e2e tests against the Storage and Functions emulators, with the real trigger writing the files the tests assert on.

Two deliberate differences from the extension's unit tests. Its config test snapshots env vars, which does not apply now that config comes from Firebase params, so it is replaced with tests over configFromEnv, validatePathListsFromEnv and resolveResizeImagesConfig. Image assertions use sharp().metadata() rather than adding image-type and image-size.

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. kits/.gitignore gains one negation so the app's .env can be committed.

Both suites pass. I mutated the source to check they bite: seven mutations each fail at least one unit test, and forcing shouldResize to false fails all four e2e tests.

Not ported: the extension's vulnerability.test.ts and content-filter.live.test.ts, which need a real project and billing.

@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 adds a comprehensive suite of unit tests using Vitest for the @firebase-function-kits/storage-resize-images package, covering configuration, content filtering, file operations, image resizing, and utility functions. Feedback on the new tests identifies a critical path traversal detection bug in countNegativeTraversals that is currently codified by the tests, recommending a robust path-splitting implementation instead. Additionally, it is recommended to use Vitest's fake timers in the content filter retry tests to prevent real-time sleeping and speed up the test suite.

Comment on lines +69 to +75
test("is non-zero for a path that walks up a directory", () => {
// Matches do not overlap, so consecutive hops share a slash and count
// once. Callers only branch on zero versus non-zero.
expect(countNegativeTraversals("images/../config.json")).toBe(1);
expect(countNegativeTraversals("images/../../config.json")).toBe(1);
expect(countNegativeTraversals("images/thumbs")).toBe(0);
});

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.

security-high high

The countNegativeTraversals function in src/util.ts has a critical bug: it uses the regex /\/\.\.\//g to count negative traversals. This regex only matches literal /../ patterns, which means it completely fails to detect directory traversals in several common and dangerous cases:

  1. Leading traversals (e.g., ../images or ../ or .. at the start of the path) return 0.
  2. Trailing traversals (e.g., images/.. at the end of the path) return 0.
  3. Single parent directory traversals (e.g., ..) return 0.
  4. Consecutive traversals (e.g., images/../../config.json) return 1 instead of 2 because matches do not overlap.

This test currently codifies the buggy behavior by expecting images/../../config.json to return 1.

To fix this, we should update the test to expect the correct counts and add missing edge cases. Additionally, the implementation of countNegativeTraversals in src/util.ts should be updated to split the path and count the .. segments accurately:

export function countNegativeTraversals(filePath: string): number {
  return filePath.split(/[/\]/).filter(segment => segment === "..").length;
}
  test("is non-zero for a path that walks up a directory", () => {
    expect(countNegativeTraversals("images/../config.json")).toBe(1);
    expect(countNegativeTraversals("images/../../config.json")).toBe(2);
    expect(countNegativeTraversals("../images")).toBe(1);
    expect(countNegativeTraversals("..")).toBe(1);
    expect(countNegativeTraversals("images/..")).toBe(1);
    expect(countNegativeTraversals("images/thumbs")).toBe(0);
  });

Comment on lines +263 to +279
test("retries and rethrows errors that are not schema refusals", async () => {
generate.mockRejectedValue(new Error("ECONNRESET"));

await expect(
checkImageContent(
imagePath,
"BLOCK_LOW_AND_ABOVE",
"prompt",
"image/png",
LOCATION,
3
)
).rejects.toThrow("ECONNRESET");

expect(generate).toHaveBeenCalledTimes(3);
expect(log.contentFilterBlocked).not.toHaveBeenCalled();
}, 30000);

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

This test currently sleeps in real-time for over 3 seconds because it executes the actual retry logic with exponential backoff (sleep(backoffTime)) on a real PQueue instance. This significantly slows down the unit test suite and can lead to flaky test runs in CI environments.

We can use Vitest's fake timers (vi.useFakeTimers()) and vi.runAllTimersAsync() to fast-forward the timers instantly, making the test run in milliseconds without any real-time waiting.

  test("retries and rethrows errors that are not schema refusals", async () => {
    vi.useFakeTimers();
    generate.mockRejectedValue(new Error("ECONNRESET"));

    const checkPromise = checkImageContent(
      imagePath,
      "BLOCK_LOW_AND_ABOVE",
      "prompt",
      "image/png",
      LOCATION,
      3
    );

    await vi.runAllTimersAsync();

    await expect(checkPromise).rejects.toThrow("ECONNRESET");

    expect(generate).toHaveBeenCalledTimes(3);
    expect(log.contentFilterBlocked).not.toHaveBeenCalled();
    vi.useRealTimers();
  });

Ports the extension's unit coverage to the kit and wires up `pnpm test` with
vitest. Filters, path handling, image conversion, resize, the content filter
and the resize handler carry over. The extension's config snapshot test does
not apply now that config comes from Firebase params, so it is replaced with
tests over `configFromEnv` and `resolveResizeImagesConfig`.

Adds coverage the extension only had behind an emulator or a live project:
the failed-image path guard against directory traversal, and the delete
original file modes.
Ports the extension's e2e suite. A small app under tests/emulator/app loads
the built kit as a functions codebase, so the Storage trigger runs for real
against the emulator and the tests assert on the files it writes.

The app carries its own package.json because the Functions emulator rejects
the kit's `engines: ">=22"` and wants an exact major.

`pnpm test` still runs only the unit tests; `pnpm test:emulator` runs these.
@IzaakGough
IzaakGough force-pushed the kits-storage-resize-images-tests branch from c96e127 to 234137f Compare August 18, 2026 22:31
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