Skip to content
Source
Sign inSign up

StorageBinding.test.ts

160 lines5.8 KB

1import { bucketStorageClient } from "@alchemy.run/neon/Bucket";
2import { Function as NeonFunction } from "@alchemy.run/neon/Function";
3import { providers } from "@alchemy.run/neon/Providers";
4import { expect } from "@alchemy.run/test";
5import * as Test from "@alchemy.run/test/Alchemy";
6import * as SDK from "@distilled.cloud/neon";
7import * as Effect from "effect/Effect";
8import * as HttpBody from "effect/http/HttpBody";
9import * as HttpClient from "effect/http/HttpClient";
10import * as Path from "effect/Path";
11import * as Redacted from "effect/Redacted";
12import * as Schedule from "effect/Schedule";
13import StorageFunction from "./fixtures/StorageFunction.ts";
14import { StorageBucket, StorageSettings } from "./fixtures/StorageResources.ts";
15​
16const { test } = Test.make({ providers: providers() });
17​
18test.provider(
19 "injected Function credentials preserve typed object DX without extra customer credentials",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23 const { fn, bucket } = yield* stack.deploy(
24 Effect.gen(function* () {
25 const bucket = yield* StorageBucket;
26 yield* StorageSettings;
27 const fn = yield* StorageFunction;
28 return { fn, bucket };
29 }),
30 );
31 const credentials = yield* SDK.listCredentials({
32 project_id: bucket.projectId,
33 branch_id: bucket.branchId,
34 });
35 expect(
36 credentials.credentials
37 .filter(
38 (credential) =>
39 credential.principal_type === "user" &&
40 credential.branch_id === bucket.branchId &&
41 !credential.revoked_at,
42 )
43 .map((credential) => credential.name),
44 ).toHaveLength(1);
45 const http = yield* HttpClient.HttpClient;
46 const initial = yield* HttpClient.filterStatusOk(http)
47 .get(fn.url)
48 .pipe(Effect.retry({ schedule: Schedule.spaced("1 second"), times: 8 }));
49 expect(initial.status).toBe(200);
50 expect(yield* initial.json).toEqual({ theme: "system", pageSize: 25 });
51 const upload = (yield* (yield* http.get(`${fn.url}/presign`)).json) as {
52 url: string;
53 };
54 expect(
55 (yield* http.put(upload.url, {
56 body: HttpBody.text("effect presign", "text/plain"),
57 })).status,
58 ).toBe(200);
59 const download = (yield* (yield* http.get(`${fn.url}/download`)).json) as { url: string };
60 expect(yield* (yield* http.get(download.url)).text).toBe("effect presign");
61 const written = yield* http.get(`${fn.url}/write`);
62 if (written.status !== 200)
63 yield* Effect.log("Storage write failed", {
64 tag: written.headers["x-storage-error"] ?? "unclassified",
65 });
66 expect(written.status).toBe(200);
67 yield* Effect.forEach(
68 Array.from({ length: 8 }, (_, index) => index),
69 () =>
70 Effect.gen(function* () {
71 const repeated = yield* http.get(`${fn.url}/write`);
72 if (repeated.status !== 200)
73 yield* Effect.log("Storage write failed", {
74 tag: repeated.headers["x-storage-error"] ?? "unclassified",
75 });
76 expect(repeated.status).toBe(200);
77 }),
78 );
79 expect(yield* (yield* http.get(fn.url)).json).toEqual({
80 theme: "dark",
81 pageSize: 50,
82 });
83 expect(yield* (yield* http.get(`${fn.url}/invalid`)).text).toBe("rejected");
84 const invalidRead = yield* http.get(fn.url);
85 expect(invalidRead.status).toBe(500);
86 expect(yield* invalidRead.text).toBe("Storage request failed");
87 yield* stack.destroy();
88 expect(
89 yield* SDK.getProject({ project_id: bucket.projectId }).pipe(
90 Effect.as(false),
91 Effect.catchTag("NotFound", () => Effect.succeed(true)),
92 ),
93 ).toBe(true);
94 yield* stack.destroy();
95 }),
96 {
97 tags: [
98 "provider:neon",
99 "provider:neon:branch",
100 "provider:neon:bucket",
101 "provider:neon:function",
102 "provider:neon:object",
103 "provider:neon:project",
104 "live",
105 ],
106 timeout: 120_000,
107 },
108);
109​
110test.provider(
111 "native injected S3 credentials support authenticated and presigned roundtrips",
112 (stack) =>
113 Effect.gen(function* () {
114 yield* stack.destroy();
115 const { fn, bucket } = yield* stack.deploy(
116 Effect.gen(function* () {
117 const bucket = yield* StorageBucket;
118 const fn = yield* NeonFunction("StorageNative", {
119 branch: bucket.Props.branch!,
120 main: (yield* Path.Path).join(import.meta.dirname, "fixtures", "StorageNative.ts"),
121 env: {
122 BUCKET_NAME: bucket.bucketName,
123 APP_TOKEN: Redacted.make("neon-storage-fixture-token"),
124 },
125 });
126 return { fn, bucket };
127 }),
128 );
129 const http = yield* HttpClient.HttpClient;
130 const headers = { authorization: "Bearer neon-storage-fixture-token" };
131 expect((yield* http.get(fn.url)).status).toBe(401);
132 const presign = yield* HttpClient.filterStatusOk(http)
133 .get(`${fn.url}/presign`, { headers })
134 .pipe(Effect.retry({ schedule: Schedule.spaced("1 second"), times: 8 }));
135 const { url } = (yield* presign.json) as { url: string };
136 const upload = yield* http.put(url, {
137 body: HttpBody.text("native roundtrip"),
138 });
139 expect(upload.status).toBe(200);
140 const download = yield* http.get(fn.url, { headers });
141 expect(yield* download.text).toBe("native roundtrip");
142 expect((yield* (yield* bucketStorageClient(bucket)).head("native.txt"))?.ContentLength).toBe(
143 16,
144 );
145 yield* stack.destroy();
146 yield* stack.destroy();
147 }),
148 {
149 tags: [
150 "provider:neon",
151 "provider:neon:branch",
152 "provider:neon:bucket",
153 "provider:neon:function",
154 "provider:neon:object",
155 "provider:neon:project",
156 "live",
157 ],
158 timeout: 120_000,
159 },
160);