1import { expect } from "alchemy-test";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as HttpBody from "effect/http/HttpBody";
6import * as HttpClient from "effect/http/HttpClient";
7import * as HttpClientRequest from "effect/http/HttpClientRequest";
8
9class UnexpectedStatus extends Data.TaggedError("UnexpectedStatus")<{
10 message: string;
11}> {}
12
13// Bounded: rides out cold starts and (live) API-token propagation, which can
14// take several seconds before R2 accepts the token-derived S3 credentials.
15const retry = Schedule.max([Schedule.spaced("2 seconds"), Schedule.recurs(20)]);
16
17const expectStatus = (
18 request: HttpClientRequest.HttpClientRequest,
19 status: number,
20) =>
21 Effect.gen(function* () {
22 const client = yield* HttpClient.HttpClient;
23 const res = yield* client.execute(request);
24 const body = yield* res.text;
25 if (res.status !== status) {
26 return yield* new UnexpectedStatus({
27 message: `${request.method} ${request.url} → ${res.status}: ${body.slice(0, 500)}`,
28 });
29 }
30 return body;
31 });
32
33const until = <A, R>(effect: Effect.Effect<A, unknown, R>) =>
34 effect.pipe(Effect.retry({ schedule: retry }), Effect.orDie);
35
36const json = <T>(url: string) =>
37 until(
38 expectStatus(HttpClientRequest.get(url), 200).pipe(
39 Effect.map((body) => JSON.parse(body) as T),
40 ),
41 );
42
43/**
44 * Drive a deployed presign fixture (`routes.ts`) end to end: upload through
45 * a presigned PUT URL as an unauthenticated client, read it back through
46 * the Worker's native binding and through a presigned GET URL, and check
47 * that a pinned Content-Type is enforced.
48 */
49export const presignRoundTrip = (workerUrl: string, key: string) =>
50 Effect.gen(function* () {
51 const { url: putUrl } = yield* json<{ url: string }>(
52 `${workerUrl}/presign-put?key=${encodeURIComponent(key)}&contentType=text/plain`,
53 );
54
55 yield* until(
56 expectStatus(
57 HttpClientRequest.put(putUrl).pipe(
58 HttpClientRequest.setBody(
59 HttpBody.text("uploaded via presigned url", "text/plain"),
60 ),
61 ),
62 200,
63 ),
64 );
65
66 const read = yield* json<{ value: string | null; contentType: string }>(
67 `${workerUrl}/read?key=${encodeURIComponent(key)}`,
68 );
69 expect(read.value).toBe("uploaded via presigned url");
70 expect(read.contentType).toBe("text/plain");
71
72 // The signed Content-Type is enforced
73 const client = yield* HttpClient.HttpClient;
74 const wrongType = yield* client.execute(
75 HttpClientRequest.put(putUrl).pipe(
76 HttpClientRequest.setBody(HttpBody.text("{}", "application/json")),
77 ),
78 );
79 expect(wrongType.status).toBe(403);
80
81 const { url: getUrl } = yield* json<{ url: string }>(
82 `${workerUrl}/presign-get?key=${encodeURIComponent(key)}&contentType=application/octet-stream`,
83 );
84 const downloaded = yield* client.get(getUrl);
85 expect(downloaded.status).toBe(200);
86 expect(downloaded.headers["content-type"]).toBe("application/octet-stream");
87 expect(yield* downloaded.text).toBe("uploaded via presigned url");
88
89 return { putUrl, getUrl };
90 });