1import * as Cloudflare from "@/Cloudflare/index.ts";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment.ts";
3import * as Test from "@/Test/Alchemy";
4import * as r2 from "@distilled.cloud/cloudflare/r2";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Stream from "effect/Stream";
9import * as pathe from "pathe";
10import PresignLocalWorker, {
11 PresignLocalBucket,
12} from "./fixtures/presign/local-worker.ts";
13import PresignRemoteWorker, {
14 PresignRemoteBucket,
15} from "./fixtures/presign/remote-worker.ts";
16import { presignRoundTrip } from "./fixtures/presign/roundtrip.ts";
17
18// `dev: true` runs local providers behind the RPC sidecar proxy by default,
19// matching the process topology of the real `alchemy dev` command.
20const { test } = Test.make({
21 providers: Cloudflare.providers(),
22 dev: true,
23});
24
25const logLevel = Effect.provideService(
26 MinimumLogLevel,
27 process.env.DEBUG ? "Debug" : "Info",
28);
29
30/**
31 * Under `alchemy dev` a locally-emulated bucket is served on the Worker's
32 * local S3 endpoint with no extra configuration: the same
33 * `PresignPutObject`/`PresignGetObject` code mints URLs that point at the dev
34 * server, an unauthenticated client uploads through them, and the Worker's
35 * native binding reads the object back from the same local simulator.
36 */
37test.provider(
38 "presigned URLs round-trip against a locally-emulated bucket",
39 (stack) =>
40 Effect.gen(function* () {
41 yield* stack.destroy();
42
43 const deployed = yield* stack.deploy(
44 Effect.gen(function* () {
45 const bucket = yield* PresignLocalBucket;
46 const worker = yield* PresignLocalWorker;
47 return { bucket, worker };
48 }),
49 );
50
51 // `dev:` name + localhost URL — proof no cloud call ran
52 expect(deployed.bucket.bucketName).toMatch(/^dev:/);
53 expect(deployed.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
54
55 const { putUrl, getUrl } = yield* presignRoundTrip(
56 deployed.worker.url!,
57 "uploads/local file.txt",
58 );
59 const prefix = `${deployed.worker.url}/cdn-cgi/local/r2/s3/${encodeURIComponent(deployed.bucket.bucketName)}/`;
60 expect(putUrl.startsWith(prefix)).toBe(true);
61 expect(getUrl.startsWith(prefix)).toBe(true);
62
63 yield* stack.destroy();
64 }).pipe(logLevel),
65 {
66 tags: [
67 "provider:cloudflare",
68 "provider:cloudflare:r2",
69 "provider:cloudflare:worker",
70 "local",
71 ],
72 timeout: 120_000,
73 },
74);
75
76/**
77 * Async (non-Effect) Workers get the same per-mode wiring through
78 * `Cloudflare.R2.S3Credentials` on `env`: in dev it resolves to the Worker's
79 * local S3 endpoint and local credentials, and `aws4fetch` presigns against
80 * it.
81 */
82test.provider(
83 "async Worker presigns with S3Credentials against a locally-emulated bucket",
84 (stack) =>
85 Effect.gen(function* () {
86 yield* stack.destroy();
87
88 const deployed = yield* stack.deploy(
89 Effect.gen(function* () {
90 // The declaration form, as in async Worker modules
91 const Bucket = Cloudflare.R2.Bucket("PresignAsyncBucket", {
92 forceDestroy: true,
93 });
94 const worker = yield* Cloudflare.Worker("PresignAsyncWorker", {
95 main: pathe.resolve(
96 import.meta.dirname,
97 "fixtures/presign/async-worker.ts",
98 ),
99 env: {
100 BUCKET: Bucket,
101 BUCKET_S3: Cloudflare.R2.S3Credentials(Bucket, {
102 access: "read-write",
103 }),
104 },
105 });
106 const bucket = yield* Bucket;
107 return { bucket, worker };
108 }),
109 );
110
111 expect(deployed.bucket.bucketName).toMatch(/^dev:/);
112 expect(deployed.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
113
114 const { putUrl } = yield* presignRoundTrip(
115 deployed.worker.url!,
116 "uploads/async.txt",
117 );
118 expect(
119 putUrl.startsWith(`${deployed.worker.url}/cdn-cgi/local/r2/s3/`),
120 ).toBe(true);
121
122 yield* stack.destroy();
123 }).pipe(logLevel),
124 {
125 tags: [
126 "provider:cloudflare",
127 "provider:cloudflare:r2",
128 "provider:cloudflare:worker",
129 "local",
130 ],
131 timeout: 120_000,
132 },
133);
134
135/**
136 * `Alchemy.remote()` keeps the bucket on real Cloudflare during dev: the
137 * presign binding mints a scoped API token and signs URLs for R2's S3
138 * endpoint, the upload lands in the real bucket (verified out-of-band), and
139 * destroy removes it.
140 */
141test.provider(
142 "Alchemy.remote() bucket presigns against real R2 in dev",
143 (stack) =>
144 Effect.gen(function* () {
145 yield* stack.destroy();
146
147 const deployed = yield* stack.deploy(
148 Effect.gen(function* () {
149 const bucket = yield* PresignRemoteBucket;
150 const worker = yield* PresignRemoteWorker;
151 return { bucket, worker };
152 }),
153 );
154
155 expect(deployed.bucket.bucketName).not.toMatch(/^dev:/);
156
157 const { putUrl } = yield* presignRoundTrip(
158 deployed.worker.url!,
159 "uploads/remote.txt",
160 );
161 expect(new URL(putUrl).hostname).toMatch(/\.r2\.cloudflarestorage\.com$/);
162
163 // Out-of-band: the upload is in the real bucket
164 const { accountId } = yield* yield* CloudflareEnvironment;
165 const object = yield* r2.getObject({
166 accountId,
167 bucketName: deployed.bucket.bucketName,
168 objectName: "uploads/remote.txt",
169 });
170 const text = yield* object.body.pipe(Stream.decodeText, Stream.mkString);
171 expect(text).toBe("uploaded via presigned url");
172
173 yield* stack.destroy();
174
175 const gone = yield* r2
176 .getBucket({ accountId, bucketName: deployed.bucket.bucketName })
177 .pipe(
178 Effect.as(false),
179 Effect.catchTag("NoSuchBucket", () => Effect.succeed(true)),
180 );
181 expect(gone).toBe(true);
182 }).pipe(logLevel),
183 {
184 tags: [
185 "provider:cloudflare",
186 "provider:cloudflare:r2",
187 "provider:cloudflare:worker",
188 "live",
189 ],
190 timeout: 180_000,
191 },
192);