Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kits/storage-resize-images/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
- fix: the "Off (No filtering)" content-filter option now stores `OFF` instead of `False`, which the resolver rejected, so every storage event threw `Invalid HarmBlockThreshold: False`. A `.env` written by an earlier deploy keeps the stored value on upgrade: if it carries `CONTENT_FILTER_LEVEL=False`, edit it to `OFF` (or re-select the option).
- fix: the "original" image-type option now stores `false` instead of `False`. The resize path treated `False` as a target format, so each resized file was uploaded as `<name>_<size>.False` with no content type and reported as a success. A stored `IMAGE_TYPE=["False"]` likewise needs editing to `["false"]`.
- fix: restore the `us-central1` content-filter fallback. `checkImageContent` threw `FUNCTION_REGION is required for Vertex AI filtering.` when no region was available; the extension fell back to `us-central1`. The Vertex AI call now uses the function's region when known and `us-central1` otherwise, matching the extension. Normal CLI deploys were unaffected (the Firebase CLI sets `FUNCTION_REGION` on deployed functions); the throw was reachable for library consumers, emulator runs, and hand-rolled environments.
- Initial release of kit, see README for differences between the legacy extension and this kit
20 changes: 2 additions & 18 deletions kits/storage-resize-images/src/config.ts
Comment thread
IzaakGough marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,6 @@ import {
type ResizeImagesConfig,
} from "./export-config";

const IMAGE_TYPE_OPTIONS = [
"jpeg",
"webp",
"png",
"tiff",
"gif",
"avif",
"false",
] as const;
const MEMORY_OPTIONS = [512, 1024, 2048, 4096, 8192] as const;
const CONTENT_FILTER_OPTIONS = [
"OFF",
"BLOCK_ONLY_HIGH",
"BLOCK_MEDIUM_AND_ABOVE",
"BLOCK_LOW_AND_ABOVE",
] as const;
const ABSOLUTE_PATH_LIST_VALIDATION = {
validationRegex: /^(?:(\/[^\s\/\,]+)+(\,(\/[^\s\/\,]+)+)*|)$/,
validationErrorMessage:
Expand Down Expand Up @@ -166,7 +150,7 @@ const params = {
tiff: "tiff",
gif: "gif",
avif: "avif",
original: "False",
original: "false",
Comment thread
IzaakGough marked this conversation as resolved.
}),
}),
outputOptions: defineString("OUTPUT_OPTIONS", {
Expand Down Expand Up @@ -232,7 +216,7 @@ const params = {

default: "OFF",
input: select({
"Off (No filtering)": "False",
"Off (No filtering)": "OFF",
Comment thread
IzaakGough marked this conversation as resolved.
"Low strictness (Block only high severity content)": "BLOCK_ONLY_HIGH",
"Medium strictness (Block medium and high severity content)":
"BLOCK_MEDIUM_AND_ABOVE",
Expand Down
110 changes: 110 additions & 0 deletions kits/storage-resize-images/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@

import { afterEach, beforeEach, describe, expect, test } from "vitest";

import type {
ListParam,
MultiSelectInput,
SelectInput,
StringParam,
} from "firebase-functions/params";
import type { ContentFilterLevel } from "../src/export-config";

const ENV_KEYS = [
"IMG_BUCKET",
"IMG_SIZES",
Expand Down Expand Up @@ -214,6 +222,108 @@ describe("configFromEnv", () => {
});
});

/**
* The select is the only source of CONTENT_FILTER_LEVEL values at deploy
* time, so every option it offers must be a value the resolver accepts.
*/
describe("CONTENT_FILTER_LEVEL select", () => {
const baseConfig = {
bucket: "extensions-testing.appspot.com",
sizes: "200x200",
} as const;

async function selectOptions() {
await import("../src/config");
const { declaredParams } = await import("firebase-functions/params");
const param = declaredParams.find(
(declared) => declared.name === "CONTENT_FILTER_LEVEL"
) as StringParam | undefined;
const input = param?.options.input as SelectInput<string> | undefined;
return input?.select.options ?? [];
}

test("offers OFF and the three block thresholds", async () => {
expect(await selectOptions()).toEqual([
{ label: "Off (No filtering)", value: "OFF" },
{
label: "Low strictness (Block only high severity content)",
value: "BLOCK_ONLY_HIGH",
},
{
label: "Medium strictness (Block medium and high severity content)",
value: "BLOCK_MEDIUM_AND_ABOVE",
},
{
label: "High strictness (Block low, medium, and high severity content)",
value: "BLOCK_LOW_AND_ABOVE",
},
]);
});

test("every option resolves, and OFF disables filtering", async () => {
const { resolveResizeImagesConfig } = await import("../src/export-config");
for (const { value } of await selectOptions()) {
const resolved = resolveResizeImagesConfig({
...baseConfig,
contentFilterLevel: value as ContentFilterLevel,
});
if (value === "OFF") {
expect(resolved.contentFilterLevel).toBeNull();
} else {
expect(resolved.contentFilterLevel).toBe(value);
}
}
});

test('the retired "False" option value is rejected', async () => {
const { resolveResizeImagesConfig } = await import("../src/export-config");
expect(() =>
resolveResizeImagesConfig({
...baseConfig,
contentFilterLevel: "False" as never,
})
).toThrow("Invalid HarmBlockThreshold: False");
});
Comment thread
cabljac marked this conversation as resolved.
});

/**
* The same audit for IMAGE_TYPE: every multiSelect value must be one the
* resize path accepts - `"false"` for keeping the original format, or a key
* of `SUPPORTED_IMAGE_CONTENT_TYPE_MAP` so the output content type resolves.
*/
describe("IMAGE_TYPE multiSelect", () => {
async function multiSelectOptions() {
await import("../src/config");
const { declaredParams } = await import("firebase-functions/params");
const param = declaredParams.find(
(declared) => declared.name === "IMAGE_TYPE"
) as ListParam | undefined;
const input = param?.options.input as MultiSelectInput | undefined;
return input?.multiSelect.options ?? [];
}

test('offers the six conversion formats and "false" for the original type', async () => {
expect(await multiSelectOptions()).toEqual([
{ label: "jpeg", value: "jpeg" },
{ label: "webp", value: "webp" },
{ label: "png", value: "png" },
{ label: "tiff", value: "tiff" },
{ label: "gif", value: "gif" },
{ label: "avif", value: "avif" },
{ label: "original", value: "false" },
]);
});

test("every conversion value maps to an output content type", async () => {
const { SUPPORTED_IMAGE_CONTENT_TYPE_MAP } = await import("../src/global");
for (const { value } of await multiSelectOptions()) {
if (value !== "false") {
expect(SUPPORTED_IMAGE_CONTENT_TYPE_MAP).toHaveProperty(value);
}
}
});
});

/**
* The extension enforces path-list shape at install time via the
* `extension.yaml` param regex. The kit has no install step, so the same
Expand Down
Loading