Skip to content

feat: Add validation for function prefixes and source/prefix pairs #8911

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@
}
]
},
"prefix": {
"type": "string"
},
"runtime": {
"enum": [
"nodejs18",
Expand Down
42 changes: 42 additions & 0 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,48 @@ describe("FunctionsEmulator", function () {
}).timeout(TIMEOUT_MED);
});

it("should support multiple codebases with the same source and apply prefixes", async () => {
const backend1: EmulatableBackend = {
...TEST_BACKEND,
codebase: "one",
prefix: "prefix-one",
};
const backend2: EmulatableBackend = {
...TEST_BACKEND,
codebase: "two",
prefix: "prefix-two",
};

emu = new FunctionsEmulator({
projectId: TEST_PROJECT_ID,
projectDir: MODULE_ROOT,
emulatableBackends: [backend1, backend2],
verbosity: "QUIET",
debugPort: false,
});

await writeSource(() => {
return {
functionId: require("firebase-functions").https.onRequest(
(req: express.Request, res: express.Response) => {
res.json({ path: req.path });
},
),
};
});

await emu.start();
await emu.connect();

await supertest(emu.createHubServer())
.get(`/${TEST_PROJECT_ID}/us-central1/prefix-one-functionId`)
.expect(200);

await supertest(emu.createHubServer())
.get(`/${TEST_PROJECT_ID}/us-central1/prefix-two-functionId`)
.expect(200);
});

describe("user-defined environment variables", () => {
let cleanup: (() => Promise<void>) | undefined;

Expand Down
75 changes: 67 additions & 8 deletions src/deploy/functions/prepare.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { expect } from "chai";

import * as backend from "./backend";
import * as sinon from "sinon";
import * as build from "./build";
import * as prepare from "./prepare";
import * as runtimes from "./runtimes";
import { RuntimeDelegate } from "./runtimes";
import { RUNTIMES } from "./runtimes/supported";
import { FirebaseError } from "../../error";
import { Options } from "../../options";
import { ValidatedConfig } from "../../functions/projectConfig";
import * as backend from "./backend";
import * as ensureApiEnabled from "../../ensureApiEnabled";
import * as serviceusage from "../../gcp/serviceusage";
import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../functions/events/v1";
import * as sinon from "sinon";
import * as prompt from "../../prompt";
import { FirebaseError } from "../../error";

describe("prepare", () => {
const ENDPOINT_BASE: Omit<backend.Endpoint, "httpsTrigger"> = {
Expand All @@ -16,14 +21,68 @@ describe("prepare", () => {
region: "region",
project: "project",
entryPoint: "entry",
runtime: "nodejs16",
runtime: "nodejs22",
};

const ENDPOINT: backend.Endpoint = {
...ENDPOINT_BASE,
httpsTrigger: {},
};

describe("loadCodebases", () => {
let sandbox: sinon.SinonSandbox;
let runtimeDelegateStub: RuntimeDelegate;
let discoverBuildStub: sinon.SinonStub;

beforeEach(() => {
sandbox = sinon.createSandbox();
discoverBuildStub = sandbox.stub();
runtimeDelegateStub = {
language: "nodejs",
runtime: "nodejs22",
bin: "node",
validate: sandbox.stub().resolves(),
build: sandbox.stub().resolves(),
watch: sandbox.stub().resolves(() => Promise.resolve()),
discoverBuild: discoverBuildStub,
};
discoverBuildStub.resolves(
build.of({
test: {
platform: "gcfv2",
entryPoint: "test",
project: "project",
runtime: "nodejs22",
httpsTrigger: {},
},
}),
);
sandbox.stub(runtimes, "getRuntimeDelegate").resolves(runtimeDelegateStub);
});

afterEach(() => {
sandbox.restore();
});

it("should apply the prefix to the function name", async () => {
const config: ValidatedConfig = [
{ source: "source", codebase: "codebase", prefix: "my-prefix", runtime: "nodejs22" },
];
const options = {
config: {
path: (p: string) => p,
},
projectId: "project",
} as unknown as Options;
const firebaseConfig = { projectId: "project" };
const runtimeConfig = {};

const builds = await prepare.loadCodebases(config, options, firebaseConfig, runtimeConfig);

expect(Object.keys(builds.codebase.endpoints)).to.deep.equal(["my-prefix-test"]);
});
});

describe("inferDetailsFromExisting", () => {
it("merges env vars if .env is not used", () => {
const oldE = {
Expand Down Expand Up @@ -304,7 +363,7 @@ describe("prepare", () => {
region: "us-central1",
project: "project",
entryPoint: "entry",
runtime: "nodejs16",
runtime: "nodejs22",
httpsTrigger: {},
};

Expand All @@ -314,7 +373,7 @@ describe("prepare", () => {
region: "us-central1",
project: "project",
entryPoint: "entry",
runtime: "nodejs16",
runtime: "nodejs22",
callableTrigger: {
genkitAction: "action",
},
Expand All @@ -333,7 +392,7 @@ describe("prepare", () => {
region: "us-central1",
project: "project",
entryPoint: "entry",
runtime: "nodejs16",
runtime: "nodejs22",
callableTrigger: {
genkitAction: "action",
},
Expand Down
10 changes: 9 additions & 1 deletion src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,21 @@ export async function loadCodebases(
"functions",
`Loading and analyzing source code for codebase ${codebase} to determine what to deploy`,
);
wantBuilds[codebase] = await runtimeDelegate.discoverBuild(runtimeConfig, {
const build = await runtimeDelegate.discoverBuild(runtimeConfig, {
...firebaseEnvs,
// Quota project is required when using GCP's Client-based APIs
// Some GCP client SDKs, like Vertex AI, requires appropriate quota project setup
// in order for .init() calls to succeed.
GOOGLE_CLOUD_QUOTA_PROJECT: projectId,
});
if (codebaseConfig.prefix) {
const newEndpoints: Record<string, build.Endpoint> = {};
for (const id of Object.keys(build.endpoints)) {
newEndpoints[`${codebaseConfig.prefix}-${id}`] = build.endpoints[id];
}
build.endpoints = newEndpoints;
}
wantBuilds[codebase] = build;
wantBuilds[codebase].runtime = codebaseConfig.runtime;
}
return wantBuilds;
Expand Down
1 change: 1 addition & 0 deletions src/emulator/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ export async function startAll(
functionsDir,
runtime,
codebase: cfg.codebase,
prefix: cfg.prefix,
env: {
...options.extDevEnv,
},
Expand Down
8 changes: 8 additions & 0 deletions src/emulator/functionsEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
env: Record<string, string>;
secretEnv: backend.SecretEnvVar[];
codebase: string;
prefix?: string;
predefinedTriggers?: ParsedTriggerDefinition[];
runtime?: Runtime;
bin?: string;
Expand Down Expand Up @@ -563,6 +564,13 @@
);
await this.loadDynamicExtensionBackends();
}
if (emulatableBackend.prefix) {
const newEndpoints: Record<string, backend.Endpoint> = {};
for (const id of Object.keys(discoveredBuild.endpoints)) {
newEndpoints[`${emulatableBackend.prefix}-${id}`] = discoveredBuild.endpoints[id];

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (22)

Type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (20)

Type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (22)

Type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_integration (20)

Type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (20)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (22)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (20)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_integration (20)

Type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.

Check failure on line 570 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (22)

Type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint' is not assignable to type 'import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint'.
}
discoveredBuild.endpoints = newEndpoints;

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (22)

Type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (20)

Type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_unit (22)

Type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_integration (20)

Type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (20)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (22)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (20)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / vscode_integration (20)

Type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/Users/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.

Check failure on line 572 in src/emulator/functionsEmulator.ts

View workflow job for this annotation

GitHub Actions / check-json-schema (22)

Type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/backend").Endpoint>' is not assignable to type 'Record<string, import("/home/runner/work/firebase-tools/firebase-tools/src/deploy/functions/build").Endpoint>'.
}
const resolution = await resolveBackend({
build: discoveredBuild,
firebaseConfig: JSON.parse(firebaseConfig),
Expand Down
1 change: 1 addition & 0 deletions src/firebaseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export type FunctionConfig = {
ignore?: string[];
runtime?: ActiveRuntime;
codebase?: string;
prefix?: string;
} & Deployable;

export type FunctionsConfig = FunctionConfig | FunctionConfig[];
Expand Down
90 changes: 79 additions & 11 deletions src/functions/projectConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,43 @@ describe("projectConfig", () => {
);
});

it("fails validation given config w/ duplicate source", () => {
expect(() =>
projectConfig.validate([TEST_CONFIG_0, { ...TEST_CONFIG_0, codebase: "unique-codebase" }]),
).to.throw(FirebaseError, /source must be unique/);
it("passes validation for multi-instance config with same source", () => {
const config: projectConfig.NormalizedConfig = [
{ source: "foo", codebase: "bar" },
{ source: "foo", codebase: "baz", prefix: "prefix-two" },
];
expect(projectConfig.validate(config)).to.deep.equal(config);
});

it("passes validation for multi-instance config with one missing codebase", () => {
const config: projectConfig.NormalizedConfig = [
{ source: "foo", codebase: "bar", prefix: "bar-prefix" },
{ source: "foo" },
];
const expected = [
{ source: "foo", codebase: "bar", prefix: "bar-prefix" },
{ source: "foo", codebase: "default" },
];
expect(projectConfig.validate(config)).to.deep.equal(expected);
});

it("fails validation for multi-instance config with missing codebase and a default codebase", () => {
const config: projectConfig.NormalizedConfig = [
{ source: "foo", codebase: "default" },
{ source: "foo" },
];
expect(() => projectConfig.validate(config)).to.throw(
FirebaseError,
/functions.codebase must be unique but 'default' was used more than once./,
);
});

it("fails validation for multi-instance config with multiple missing codebases", () => {
const config: projectConfig.NormalizedConfig = [{ source: "foo" }, { source: "foo" }];
expect(() => projectConfig.validate(config)).to.throw(
FirebaseError,
/functions.codebase must be unique but 'default' was used more than once./,
);
});

it("fails validation given codebase name with capital letters", () => {
Expand All @@ -72,6 +105,48 @@ describe("projectConfig", () => {
]),
).to.throw(FirebaseError, /Invalid codebase name/);
});

it("fails validation given prefix with invalid characters", () => {
expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "abc.efg" }])).to.throw(
FirebaseError,
/Invalid prefix/,
);
});

it("fails validation given prefix with capital letters", () => {
expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "ABC" }])).to.throw(
FirebaseError,
/Invalid prefix/,
);
});

it("fails validation given a duplicate source/prefix pair", () => {
const config: projectConfig.NormalizedConfig = [
{ source: "foo", codebase: "bar", prefix: "a" },
{ source: "foo", codebase: "baz", prefix: "a" },
];
expect(() => projectConfig.validate(config)).to.throw(
FirebaseError,
/More than one functions config specifies the same source directory \('foo'\) and prefix \('a'\)/,
);
});

it("fails validation for multi-instance config with same source and no prefixes", () => {
const config: projectConfig.NormalizedConfig = [
{ source: "foo", codebase: "bar" },
{ source: "foo", codebase: "baz" },
];
expect(() => projectConfig.validate(config)).to.throw(
FirebaseError,
/More than one functions config specifies the same source directory \('foo'\) and prefix \(''\)/,
);
});

it("should allow a single function in an array to have a default codebase", () => {
const config: projectConfig.NormalizedConfig = [{ source: "foo" }];
const expected = [{ source: "foo", codebase: "default" }];
expect(projectConfig.validate(config)).to.deep.equal(expected);
});
});

describe("normalizeAndValidate", () => {
Expand Down Expand Up @@ -104,13 +179,6 @@ describe("projectConfig", () => {
);
});

it("fails validation given config w/ duplicate source", () => {
expect(() => projectConfig.normalizeAndValidate([TEST_CONFIG_0, TEST_CONFIG_0])).to.throw(
FirebaseError,
/functions.source must be unique/,
);
});

it("fails validation given config w/ duplicate codebase", () => {
expect(() =>
projectConfig.normalizeAndValidate([
Expand Down
Loading
Loading