test(storage-resize-images): add kit test suite - #2952
Conversation
There was a problem hiding this comment.
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.
| 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); | ||
| }); |
There was a problem hiding this comment.
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:
- Leading traversals (e.g., ../images or ../ or .. at the start of the path) return 0.
- Trailing traversals (e.g., images/.. at the end of the path) return 0.
- Single parent directory traversals (e.g., ..) return 0.
- 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);
});| 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); |
There was a problem hiding this comment.
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.
c96e127 to
234137f
Compare
The
storage-resize-imageskit had no tests, andtestwas a placeholder that echoes a warning.Adds two suites.
pnpm testruns 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:emulatorruns 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,validatePathListsFromEnvandresolveResizeImagesConfig. Image assertions usesharp().metadata()rather than addingimage-typeandimage-size.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.kits/.gitignoregains one negation so the app's.envcan be committed.Both suites pass. I mutated the source to check they bite: seven mutations each fail at least one unit test, and forcing
shouldResizeto false fails all four e2e tests.Not ported: the extension's
vulnerability.test.tsandcontent-filter.live.test.ts, which need a real project and billing.