1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import { Credentials } from "@distilled.cloud/gcp/Credentials";
4import * as storage from "@distilled.cloud/gcp/storage_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12
13const { test } = Test.make({ providers: GCP.providers() });
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20const ENTITY = "user-alchemy-gcp-testing-83661@appspot.gserviceaccount.com";
21const OBJECT_NAME = "hello.txt";
22
23const waitUntilGone = (bucketName: string, object: string, entity: string) =>
24 storage.getObjectAccessControls({ bucket: bucketName, object, entity }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("1 second"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34const uploadObject = (bucketName: string, object: string, body: string) =>
35 Effect.gen(function* () {
36 const creds = yield* yield* Credentials;
37 const client = yield* HttpClient.HttpClient;
38 const bytes = yield* Effect.sync(() => new TextEncoder().encode(body));
39 const url =
40 `https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucketName)}/o` +
41 `?uploadType=media&name=${encodeURIComponent(object)}`;
42 const response = yield* client.execute(
43 HttpClientRequest.post(url).pipe(
44 HttpClientRequest.setHeader(
45 "Authorization",
46 `Bearer ${Redacted.value(creds.accessToken)}`,
47 ),
48 HttpClientRequest.bodyUint8Array(bytes, "text/plain"),
49 ),
50 );
51 if (response.status < 200 || response.status >= 300) {
52 const text = yield* response.text.pipe(
53 Effect.catch(() => Effect.succeed("")),
54 );
55 return yield* Effect.fail(
56 new Error(`object upload failed: ${response.status} ${text}`),
57 );
58 }
59 });
60
61test.provider(
62 "create, update, and delete an object ACL",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const bucket = yield* stack.deploy(
68 Effect.gen(function* () {
69 return yield* GCP.Storage.Bucket("Assets", {
70 location: "US-CENTRAL1",
71 forceDestroy: true,
72 });
73 }),
74 );
75
76 yield* uploadObject(bucket.bucketName, OBJECT_NAME, "hello");
77
78 const created = yield* stack.deploy(
79 Effect.gen(function* () {
80 const next = yield* GCP.Storage.Bucket("Assets", {
81 bucketName: bucket.bucketName,
82 location: "US-CENTRAL1",
83 forceDestroy: true,
84 });
85 return yield* GCP.Storage.ObjectAccessControl("Reader", {
86 bucketName: next.bucketName,
87 object: OBJECT_NAME,
88 entity: ENTITY,
89 role: "READER",
90 });
91 }),
92 );
93
94 expect(created.bucketName).toEqual(bucket.bucketName);
95 expect(created.object).toEqual(OBJECT_NAME);
96 expect(created.entity).toEqual(ENTITY);
97 expect(created.role).toEqual("READER");
98
99 const fetched = yield* storage.getObjectAccessControls({
100 bucket: created.bucketName,
101 object: OBJECT_NAME,
102 entity: ENTITY,
103 });
104 expect(fetched.entity).toEqual(ENTITY);
105 expect(fetched.role).toEqual("READER");
106
107 const updated = yield* stack.deploy(
108 Effect.gen(function* () {
109 const next = yield* GCP.Storage.Bucket("Assets", {
110 bucketName: created.bucketName,
111 location: "US-CENTRAL1",
112 forceDestroy: true,
113 });
114 return yield* GCP.Storage.ObjectAccessControl("Reader", {
115 bucketName: next.bucketName,
116 object: OBJECT_NAME,
117 entity: ENTITY,
118 role: "OWNER",
119 });
120 }),
121 );
122
123 expect(updated.bucketName).toEqual(created.bucketName);
124 expect(updated.entity).toEqual(ENTITY);
125 expect(updated.role).toEqual("OWNER");
126
127 const refetched = yield* storage.getObjectAccessControls({
128 bucket: updated.bucketName,
129 object: OBJECT_NAME,
130 entity: ENTITY,
131 });
132 expect(refetched.role).toEqual("OWNER");
133
134 yield* stack.destroy();
135
136 const gone = yield* waitUntilGone(
137 created.bucketName,
138 OBJECT_NAME,
139 ENTITY,
140 );
141 expect(gone).toEqual("gone");
142 }).pipe(logLevel),
143 { tags: ["provider:gcp", "provider:gcp:storage", "live"], timeout: 90_000 },
144);