1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as siteVerification from "@distilled.cloud/gcp/siteVerification_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Site Verification needs a credential with the siteverification OAuth
17// scope (a Cloud Platform service-account token is rejected with
18// InsufficientAuthenticationScopes) and a site whose verification token is
19// already placed; set GCP_TEST_SITE_VERIFICATION=1 when both hold.
20const runLifecycle =
21 !process.env.FAST && process.env.GCP_TEST_SITE_VERIFICATION === "1";
22
23const identifier = "https://alchemy-site-verification.test/";
24
25const waitUntilGone = (webResourceId: string) =>
26 siteVerification
27 .getWebResource({
28 id: decodeURIComponent(webResourceId),
29 })
30 .pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("1 second"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider.skipIf(!runLifecycle)(
41 "getWebResource on a missing site fails with NotFound",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 siteVerification.getWebResource({
48 id: "http://alchemy-missing.example.com/",
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 yield* stack.destroy();
54 }).pipe(logLevel),
55 {
56 tags: ["provider:gcp", "provider:gcp:siteverification", "live"],
57 timeout: 90_000,
58 },
59);
60
61test.provider.skipIf(runLifecycle)(
62 "getTokenWebResource without the siteverification scope fails with InsufficientAuthenticationScopes",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const error = yield* Effect.flip(
68 siteVerification.getTokenWebResource({
69 body: {
70 site: { identifier, type: "SITE" },
71 verificationMethod: "FILE",
72 },
73 }),
74 );
75 expect(error._tag).toEqual("InsufficientAuthenticationScopes");
76
77 yield* stack.destroy();
78 }).pipe(logLevel),
79 {
80 tags: ["provider:gcp", "provider:gcp:siteverification", "live"],
81 timeout: 90_000,
82 },
83);
84
85test.provider.skipIf(runLifecycle)(
86 "insertWebResource without the siteverification scope fails with InsufficientAuthenticationScopes",
87 (stack) =>
88 Effect.gen(function* () {
89 yield* stack.destroy();
90
91 const error = yield* Effect.flip(
92 siteVerification.insertWebResource({
93 verificationMethod: "FILE",
94 body: { site: { identifier, type: "SITE" } },
95 }),
96 );
97 expect(error._tag).toEqual("InsufficientAuthenticationScopes");
98
99 yield* stack.destroy();
100 }).pipe(logLevel),
101 {
102 tags: ["provider:gcp", "provider:gcp:siteverification", "live"],
103 timeout: 90_000,
104 },
105);
106
107test.provider.skipIf(!runLifecycle)(
108 "create, update, and delete a web resource",
109 (stack) =>
110 Effect.gen(function* () {
111 yield* stack.destroy();
112
113 const resource = yield* stack.deploy(
114 Effect.gen(function* () {
115 return yield* GCP.SiteVerification.WebResource("Docs", {
116 identifier,
117 siteType: "SITE",
118 verificationMethod: "FILE",
119 });
120 }),
121 );
122 expect(resource.webResourceId.length).toBeGreaterThan(0);
123 expect(resource.identifier).toContain("alchemy-site-verification.test");
124 expect(resource.siteType).toEqual("SITE");
125
126 const fetched = yield* siteVerification.getWebResource({
127 id: decodeURIComponent(resource.webResourceId),
128 });
129 expect(fetched.id).toEqual(resource.webResourceId);
130 expect(fetched.site?.type).toEqual("SITE");
131
132 const updated = yield* stack.deploy(
133 Effect.gen(function* () {
134 return yield* GCP.SiteVerification.WebResource("Docs", {
135 identifier: resource.identifier,
136 siteType: "SITE",
137 owners: resource.owners,
138 });
139 }),
140 );
141
142 expect(updated.webResourceId).toEqual(resource.webResourceId);
143 expect(updated.owners).toEqual(resource.owners);
144
145 yield* stack.destroy();
146
147 const gone = yield* waitUntilGone(resource.webResourceId);
148 expect(gone).toEqual("gone");
149 }).pipe(logLevel),
150 {
151 tags: ["provider:gcp", "provider:gcp:siteverification", "live"],
152 timeout: 90_000,
153 },
154);