Skip to content

Commit 41b2545

Browse files
committed
test(functions-deploy): e2e preserveExternalChanges invoker preservation
Adds an end-to-end test that: 1. Deploys with preserveExternalChanges=true 2. Adds an external `user:` member to the v2scheduled function's Cloud Run `roles/run.invoker` binding via the real IAM API 3. Redeploys (flag still on) — asserts the member is still present 4. Redeploys with flag off — asserts the member is removed This is the scenario from issue #6549 end-to-end. Requires a real GCP test project to run (`npm run test:functions-deploy`); unit + fabricator tests from earlier commits cover the decoding/gating paths for CI speed.
1 parent 55dfcaa commit 41b2545

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

scripts/functions-deploy-tests/tests.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as cli from "./cli";
99
import * as proto from "../../src/gcp/proto";
1010
import * as tasks from "../../src/gcp/cloudtasks";
1111
import * as scheduler from "../../src/gcp/cloudscheduler";
12+
import * as run from "../../src/gcp/run";
1213
import { Endpoint } from "../../src/deploy/functions/backend";
1314
import { requireAuth } from "../../src/requireAuth";
1415

@@ -352,6 +353,77 @@ describe("firebase deploy", function (this) {
352353
}
353354
});
354355

356+
it("preserves externally-added Cloud Run invoker members when preserveExternalChanges is true", async () => {
357+
// Regression test for firebase/firebase-tools#6549.
358+
// Covers the scheduled-v2 path which is what the original issue reports.
359+
const TEST_INVOKER = `user:test-preserve-${RUN_ID}@example.com`;
360+
361+
// 1. Deploy with the flag set.
362+
const optsOn: Opts = {
363+
v1Opts: { preserveExternalChanges: true },
364+
v2Opts: { preserveExternalChanges: true },
365+
v1TqOpts: {},
366+
v2TqOpts: {},
367+
v1IdpOpts: {},
368+
v2IdpOpts: {},
369+
v1ScheduleOpts: {},
370+
v2ScheduleOpts: { schedule: "every 30 minutes" },
371+
};
372+
const firstDeploy = await setOptsAndDeploy(optsOn);
373+
expect(firstDeploy.stdout, "first deploy").to.match(/Deploy complete!/);
374+
375+
const endpoints = await listFns(RUN_ID);
376+
const scheduled = Object.values(endpoints).find(
377+
(e) => e.platform === "gcfv2" && "scheduleTrigger" in e,
378+
);
379+
expect(scheduled, "v2scheduled endpoint").to.not.be.undefined;
380+
381+
// 2. Add an external invoker member directly to the Cloud Run service.
382+
const runServiceName = `projects/${FIREBASE_PROJECT}/locations/${scheduled!.region}/services/${scheduled!.runServiceId}`;
383+
const beforePolicy = await run.getIamPolicy(runServiceName);
384+
const preservedBinding = beforePolicy.bindings?.find(
385+
(b) => b.role === "roles/run.invoker" && !b.condition,
386+
);
387+
const preservedMembers = [...(preservedBinding?.members || []), TEST_INVOKER];
388+
await run.setIamPolicy(runServiceName, {
389+
bindings: [
390+
...(beforePolicy.bindings || []).filter((b) => b.role !== "roles/run.invoker" || b.condition),
391+
{ role: "roles/run.invoker", members: preservedMembers },
392+
],
393+
etag: beforePolicy.etag || "",
394+
version: 3,
395+
});
396+
397+
// 3. Redeploy with the flag still set — the external invoker must survive.
398+
const secondDeploy = await setOptsAndDeploy(optsOn);
399+
expect(secondDeploy.stdout, "second deploy").to.match(/Deploy complete!|No changes detected/);
400+
401+
const afterOnPolicy = await run.getIamPolicy(runServiceName);
402+
const afterOnBinding = afterOnPolicy.bindings?.find(
403+
(b) => b.role === "roles/run.invoker" && !b.condition,
404+
);
405+
expect(afterOnBinding?.members, "invoker members after preserve-on redeploy").to.include(
406+
TEST_INVOKER,
407+
);
408+
409+
// 4. Flip the flag off and redeploy — the external invoker should now be removed.
410+
const optsOff: Opts = {
411+
...optsOn,
412+
v1Opts: { preserveExternalChanges: false },
413+
v2Opts: { preserveExternalChanges: false },
414+
};
415+
const thirdDeploy = await setOptsAndDeploy(optsOff);
416+
expect(thirdDeploy.stdout, "third deploy").to.match(/Deploy complete!|No changes detected/);
417+
418+
const afterOffPolicy = await run.getIamPolicy(runServiceName);
419+
const afterOffBinding = afterOffPolicy.bindings?.find(
420+
(b) => b.role === "roles/run.invoker" && !b.condition,
421+
);
422+
expect(afterOffBinding?.members, "invoker members after preserve-off redeploy").to.not.include(
423+
TEST_INVOKER,
424+
);
425+
});
426+
355427
// BUGBUG: Setting options to null SHOULD restore their values to default, but this isn't correctly implemented in
356428
// the CLI.
357429
it.skip("restores default values when unspecified and preserveExternalChanges is not set", async () => {

0 commit comments

Comments
 (0)