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.getProjectsReleases({ 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 "getProjectsReleases on a missing release 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.getProjectsReleases({
55 name: `projects/${project}/releases/alchemy-missing-xxxx`,
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, update, replace, and delete a firebaserules release",
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 const ruleset = yield* GCP.FirebaseRules.Ruleset("Firestore", {
78 source: {
79 files: [{ name: "firestore.rules", content: denyRules }],
80 },
81 });
82 const release = yield* GCP.FirebaseRules.Release("Live", {
83 rulesetName: ruleset.name,
84 });
85 return { ruleset, release };
86 }),
87 );
88
89 expect(created.release.releaseId.startsWith("alc-")).toEqual(true);
90 expect(created.release.name).toEqual(
91 `projects/${project}/releases/${created.release.releaseId}`,
92 );
93 expect(created.release.rulesetName).toEqual(created.ruleset.name);
94 expect(created.release.project).toEqual(project);
95
96 const fetched = yield* firebaserules.getProjectsReleases({
97 name: created.release.name,
98 });
99 expect(fetched.name).toEqual(created.release.name);
100 expect(fetched.rulesetName).toEqual(created.ruleset.name);
101
102 const updated = yield* stack.deploy(
103 Effect.gen(function* () {
104 const ruleset = yield* GCP.FirebaseRules.Ruleset("Firestore", {
105 source: {
106 files: [{ name: "firestore.rules", content: denyRules }],
107 },
108 });
109 const next = yield* GCP.FirebaseRules.Ruleset("FirestoreNext", {
110 source: {
111 files: [{ name: "firestore.rules", content: authRules }],
112 },
113 });
114 const release = yield* GCP.FirebaseRules.Release("Live", {
115 releaseId: created.release.releaseId,
116 rulesetName: next.name,
117 });
118 return { ruleset, next, release };
119 }),
120 );
121
122 expect(updated.release.name).toEqual(created.release.name);
123 expect(updated.release.rulesetName).toEqual(updated.next.name);
124 expect(updated.release.rulesetName).not.toEqual(created.ruleset.name);
125
126 const fetchedUpdate = yield* firebaserules.getProjectsReleases({
127 name: created.release.name,
128 });
129 expect(fetchedUpdate.rulesetName).toEqual(updated.next.name);
130
131 const nextReleaseId = `${created.release.releaseId}-v2`;
132
133 const replaced = yield* stack.deploy(
134 Effect.gen(function* () {
135 const next = yield* GCP.FirebaseRules.Ruleset("FirestoreNext", {
136 source: {
137 files: [{ name: "firestore.rules", content: authRules }],
138 },
139 });
140 const release = yield* GCP.FirebaseRules.Release("Live", {
141 releaseId: nextReleaseId,
142 rulesetName: next.name,
143 });
144 return { next, release };
145 }),
146 );
147
148 expect(replaced.release.releaseId).toEqual(nextReleaseId);
149 expect(replaced.release.name).not.toEqual(created.release.name);
150
151 const previousGone = yield* waitUntilGone(created.release.name);
152 expect(previousGone).toEqual("gone");
153
154 yield* stack.destroy();
155
156 const gone = yield* waitUntilGone(replaced.release.name);
157 expect(gone).toEqual("gone");
158 }).pipe(logLevel),
159 {
160 tags: ["provider:gcp", "provider:gcp:firebaserules", "live"],
161 timeout: 90_000,
162 },
163);