1import * as AWS from "@/AWS";
2import * as Cloudflare from "@/Cloudflare";
3import { bucketStorageClient, type BucketAttributes } from "@/Neon/Bucket";
4import { providers } from "@/Neon/Providers";
5import { storageBodyBytes } from "@/Neon/Object";
6import * as Test from "@/Test/Alchemy";
7import * as SDK from "@distilled.cloud/neon";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Schedule from "effect/Schedule";
12import * as HttpClient from "effect/http/HttpClient";
13import { StorageBucket } from "./fixtures/StorageResources.ts";
14import StorageHttpWorker from "./fixtures/StorageHttpWorker.ts";
15import StorageHttpLambda from "./fixtures/StorageHttpLambda.ts";
16
17const { test: workerTest } = Test.make({
18 providers: Layer.mergeAll(providers(), Cloudflare.providers()),
19});
20const { test: lambdaTest } = Test.make({
21 providers: Layer.mergeAll(providers(), AWS.providers()),
22});
23
24const verify = Effect.fn(function* (url: string, bucket: BucketAttributes) {
25 const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
26 const response = yield* http
27 .get(`${url}/write`)
28 .pipe(Effect.retry({ schedule: Schedule.spaced("3 seconds"), times: 8 }));
29 expect(response.status).toBe(200);
30 expect(yield* (yield* http.get(url)).json).toEqual({
31 size: 18,
32 hasAccountKey: false,
33 });
34 const client = yield* bucketStorageClient(bucket);
35 const bytes = yield* storageBodyBytes(
36 (yield* client.get("external.txt"))?.Body,
37 );
38 expect(yield* Effect.sync(() => new TextDecoder().decode(bytes))).toBe(
39 "external roundtrip",
40 );
41 const scope = { project_id: bucket.projectId, branch_id: bucket.branchId };
42 const credentials = (yield* SDK.listCredentials(scope)).credentials.filter(
43 (credential) =>
44 credential.branch_id === bucket.branchId && !credential.revoked_at,
45 );
46 expect(
47 credentials
48 .map((credential) => credential.scopes)
49 .sort((a, b) => a.length - b.length),
50 ).toEqual([
51 ["storage:read"],
52 ["storage:read", "storage:write"],
53 ["storage:read", "storage:write"],
54 ]);
55 return credentials
56 .filter((credential) => credential.token_id !== bucket.credential.tokenId)
57 .map((credential) => credential.token_id);
58});
59
60workerTest.provider(
61 "Worker HTTP storage uses scoped credentials and revokes them when unbound",
62 (stack) =>
63 Effect.gen(function* () {
64 yield* stack.destroy();
65 const { worker, bucket } = yield* stack.deploy(
66 Effect.gen(function* () {
67 return {
68 worker: yield* StorageHttpWorker,
69 bucket: yield* StorageBucket,
70 };
71 }),
72 );
73 const tokens = yield* verify(worker.url!, bucket);
74 yield* stack.deploy(StorageBucket);
75 const remaining = (yield* SDK.listCredentials({
76 project_id: bucket.projectId,
77 branch_id: bucket.branchId,
78 })).credentials;
79 expect(
80 remaining.some(
81 (credential) =>
82 tokens.includes(credential.token_id) && !credential.revoked_at,
83 ),
84 ).toBe(false);
85 yield* stack.destroy();
86 yield* stack.destroy();
87 }),
88 {
89 tags: [
90 "provider:cloudflare",
91 "provider:cloudflare:worker",
92 "provider:neon",
93 "provider:neon:branch",
94 "provider:neon:bucket",
95 "provider:neon:object",
96 "provider:neon:project",
97 "live",
98 ],
99 timeout: 120_000,
100 },
101);
102
103lambdaTest.provider(
104 "Lambda HTTP storage uses namespaced non-account secrets",
105 (stack) =>
106 Effect.gen(function* () {
107 yield* stack.destroy();
108 const { fn, bucket } = yield* stack.deploy(
109 Effect.gen(function* () {
110 return { fn: yield* StorageHttpLambda, bucket: yield* StorageBucket };
111 }),
112 );
113 const tokens = yield* verify(fn.functionUrl!, bucket);
114 yield* stack.deploy(StorageBucket);
115 const remaining = (yield* SDK.listCredentials({
116 project_id: bucket.projectId,
117 branch_id: bucket.branchId,
118 })).credentials;
119 expect(
120 remaining.some(
121 (credential) =>
122 tokens.includes(credential.token_id) && !credential.revoked_at,
123 ),
124 ).toBe(false);
125 yield* stack.destroy();
126 yield* stack.destroy();
127 }),
128 {
129 tags: [
130 "provider:aws",
131 "provider:aws:lambda",
132 "provider:neon",
133 "provider:neon:branch",
134 "provider:neon:bucket",
135 "provider:neon:object",
136 "provider:neon:project",
137 "live",
138 ],
139 timeout: 240_000,
140 },
141);