1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as firebaserules from "@distilled.cloud/gcp/firebaserules_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const denyRules = `rules_version = '2';
18service cloud.firestore {
19 match /databases/{database}/documents {
20 match /{document=**} {
21 allow read, write: if false;
22 }
23 }
24}`;
25
26const authRules = `rules_version = '2';
27service cloud.firestore {
28 match /databases/{database}/documents {
29 match /{document=**} {
30 allow read, write: if request.auth != null;
31 }
32 }
33}`;
34
35const waitUntilGone = (name: string) =>
36 firebaserules.getProjectsRulesets({ name }).pipe(
37 Effect.as("found" as const),
38 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
39 Effect.repeat({
40 schedule: Schedule.spaced("1 second"),
41 until: (status) => status === "gone",
42 times: 10,
43 }),
44 );
45
46test.provider(
47 "getProjectsRulesets on a missing ruleset fails with a typed tag",
48 (stack) =>
49 Effect.gen(function* () {
50 const { project } = yield* GcpEnvironment.current;
51 yield* stack.destroy();
52
53 const error = yield* Effect.flip(
54 firebaserules.getProjectsRulesets({
55 name: `projects/${project}/rulesets/00000000-0000-0000-0000-000000000000`,
56 }),
57 );
58 expect(error._tag).toEqual("NotFound");
59
60 yield* stack.destroy();
61 }).pipe(logLevel),
62 {
63 tags: ["provider:gcp", "provider:gcp:firebaserules", "live"],
64 timeout: 90_000,
65 },
66);
67
68test.provider(
69 "create, replace, and delete a firebaserules ruleset",
70 (stack) =>
71 Effect.gen(function* () {
72 const { project } = yield* GcpEnvironment.current;
73 yield* stack.destroy();
74
75 const created = yield* stack.deploy(
76 Effect.gen(function* () {
77 return yield* GCP.FirebaseRules.Ruleset("Firestore", {
78 source: {
79 files: [{ name: "firestore.rules", content: denyRules }],
80 },
81 });
82 }),
83 );
84
85 expect(created.name).toEqual(
86 `projects/${project}/rulesets/${created.rulesetId}`,
87 );
88 expect(created.rulesetId).toEqual(expect.any(String));
89 expect(created.project).toEqual(project);
90 expect(created.source.files[0]?.name).toEqual("firestore.rules");
91 expect(created.source.files[0]?.content).toEqual(denyRules);
92 expect(created.source.files[0]?.content).not.toContain("[alchemy ");
93
94 const fetched = yield* firebaserules.getProjectsRulesets({
95 name: created.name,
96 });
97 expect(fetched.name).toEqual(created.name);
98 expect(fetched.source?.files?.[0]?.content).toContain("alchemy-id=");
99 expect(fetched.source?.files?.[0]?.content).toContain("if false");
100
101 const replaced = yield* stack.deploy(
102 Effect.gen(function* () {
103 return yield* GCP.FirebaseRules.Ruleset("Firestore", {
104 source: {
105 files: [{ name: "firestore.rules", content: authRules }],
106 },
107 });
108 }),
109 );
110
111 expect(replaced.name).not.toEqual(created.name);
112 expect(replaced.source.files[0]?.content).toEqual(authRules);
113
114 const fetchedReplacement = yield* firebaserules.getProjectsRulesets({
115 name: replaced.name,
116 });
117 expect(fetchedReplacement.source?.files?.[0]?.content).toContain(
118 "request.auth != null",
119 );
120
121 const previousGone = yield* waitUntilGone(created.name);
122 expect(previousGone).toEqual("gone");
123
124 yield* stack.destroy();
125
126 const gone = yield* waitUntilGone(replaced.name);
127 expect(gone).toEqual("gone");
128 }).pipe(logLevel),
129 {
130 tags: ["provider:gcp", "provider:gcp:firebaserules", "live"],
131 timeout: 90_000,
132 },
133);