1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import * as firebaserules from "@distilled.cloud/gcp/firebaserules_v1";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import FirebaseRulesBindingsHost, {
11 DenyAll,
12 Live,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "FirebaseRulesBindings");
18
19let baseUrl: string;
20let rulesetName: string;
21let releaseName: string;
22let hostAccount: string;
23
24/** Roles the host's service account holds on the project, by condition. */
25const hostProjectRoles = Effect.gen(function* () {
26 const { project } = yield* GcpEnvironment.current;
27 const policy = yield* resourcemanager.getIamPolicyProjects({
28 resource: `projects/${project}`,
29 body: { options: { requestedPolicyVersion: 3 } },
30 });
31 return (policy.bindings ?? [])
32 .filter((binding) =>
33 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
34 )
35 .map((binding) => ({
36 role: binding.role,
37 condition: binding.condition?.expression,
38 }));
39});
40
41describe.skipIf(!dockerAvailable)(
42 "FirebaseRules Bindings",
43 {
44 tags: [
45 "provider:gcp",
46 "provider:gcp:firebaserules",
47 "provider:gcp:run",
48 "live",
49 ],
50 },
51 () => {
52 beforeAll(
53 Effect.gen(function* () {
54 yield* sharedStack.destroy();
55 const out = yield* sharedStack.deploy(
56 Effect.gen(function* () {
57 const host = yield* FirebaseRulesBindingsHost;
58 const ruleset = yield* DenyAll;
59 const release = yield* Live;
60 return {
61 uri: host.uri,
62 serviceAccount: host.serviceAccount,
63 ruleset: ruleset.name,
64 release: release.name,
65 };
66 }),
67 );
68 baseUrl = out.uri!;
69 hostAccount = out.serviceAccount!;
70 rulesetName = out.ruleset;
71 releaseName = out.release;
72 }),
73 { timeout: 900_000 },
74 );
75
76 afterAll(sharedStack.destroy(), { timeout: 600_000 });
77
78 describe("TestRuleset", () => {
79 test.provider(
80 "evaluates the deployed ruleset as the host's service account",
81 (_stack) =>
82 Effect.gen(function* () {
83 const out = yield* expectProbe<firebaserules.TestRulesetResponse>(
84 baseUrl,
85 "testRuleset",
86 );
87 expect(out.issues ?? []).toEqual([]);
88 // deny-all: the DENY expectation holds, the ALLOW one fails.
89 expect(
90 (out.testResults ?? []).map((result) => result.state),
91 ).toEqual(["SUCCESS", "FAILURE"]);
92
93 // Firebase Rules has no resource-level IAM: project grant.
94 const roles = yield* hostProjectRoles;
95 expect(roles).toEqual([
96 { role: "roles/firebaserules.admin", condition: undefined },
97 ]);
98 }),
99 {
100 tags: ["provider:gcp", "provider:gcp:firebaserules", "live"],
101 timeout: 600_000,
102 },
103 );
104 });
105
106 describe("GetReleaseExecutable", () => {
107 test.provider(
108 "reads the release's compiled executable as the host's service account",
109 (_stack) =>
110 Effect.gen(function* () {
111 const out =
112 yield* expectProbe<firebaserules.GetReleaseExecutableResponse>(
113 baseUrl,
114 "getReleaseExecutable",
115 );
116 expect(out.rulesetName).toEqual(rulesetName);
117 expect((out.executable ?? "").length).toBeGreaterThan(0);
118
119 const direct = yield* firebaserules.getExecutableProjectsReleases({
120 name: releaseName,
121 });
122 expect(out.rulesetName).toEqual(direct.rulesetName);
123 expect(out.executableVersion).toEqual(direct.executableVersion);
124
125 const roles = yield* hostProjectRoles;
126 expect(roles).toEqual([
127 { role: "roles/firebaserules.admin", condition: undefined },
128 ]);
129 }),
130 {
131 tags: ["provider:gcp", "provider:gcp:firebaserules", "live"],
132 timeout: 600_000,
133 },
134 );
135 });
136 },
137);