1import { describe, expect, test } from "bun:test";
2import * as Effect from "effect/Effect";
3import * as HttpServerResponse from "effect/http/HttpServerResponse";
4import {
5 corsHeaders,
6 MAX_BYTES,
7 objectKey,
8 parseUpload,
9 serializeUploadRow,
10 UUID,
11} from "../src/policy.ts";
12
13describe("upload response serialization", () => {
14 for (const actualBytes of [null, 0n, 35n, 9007199254740993n]) {
15 test(`encodes Postgres bigint metadata with actual bytes ${actualBytes}`, () =>
16 Effect.runPromise(
17 Effect.gen(function* () {
18 const row = yield* Effect.sync(() =>
19 serializeUploadRow({
20 id: "10000000-0000-4000-8000-000000000001",
21 filename: "report.txt",
22 object_key: "incoming/owner/file-id",
23 content_type: "text/plain",
24 expected_bytes: 35n,
25 actual_bytes: actualBytes,
26 status: actualBytes === null ? "awaiting_upload" : "ready",
27 created_at: Date.parse("2026-09-17T00:00:00.000Z"),
28 }),
29 );
30 const response = yield* HttpServerResponse.json([row]);
31 expect(response.status).toBe(200);
32 expect(row.expected_bytes).toBe("35");
33 expect(row.actual_bytes).toBe(actualBytes?.toString() ?? null);
34 expect(row.created_at).toBe("2026-09-17T00:00:00.000Z");
35 }),
36 ));
37 }
38});
39
40describe("upload request policy", () => {
41 test("accepts valid metadata at the size limit", () => {
42 expect(
43 parseUpload({
44 filename: "report.txt",
45 contentType: "text/plain",
46 size: MAX_BYTES,
47 }),
48 ).toEqual({
49 filename: "report.txt",
50 contentType: "text/plain",
51 size: MAX_BYTES,
52 });
53 });
54 test("rejects empty, oversized, fractional and nonnumeric sizes", () => {
55 for (const size of [0, -1, MAX_BYTES + 1, 0.5, "1", null, Infinity]) {
56 expect(
57 parseUpload({
58 filename: "report.txt",
59 contentType: "text/plain",
60 size,
61 }),
62 ).toBeUndefined();
63 }
64 });
65 test("rejects malformed metadata", () => {
66 for (const value of [
67 null,
68 [],
69 "file",
70 {},
71 { filename: "x", size: 1 },
72 { filename: "\r\n", contentType: "text/plain", size: 1 },
73 { filename: "x", contentType: "text/plain\r\nx-header: bad", size: 1 },
74 ]) {
75 expect(parseUpload(value)).toBeUndefined();
76 }
77 });
78 test("encodes owner path segments and never uses the filename in keys", () => {
79 expect(objectKey("user/../../other", "file-id")).toBe(
80 "incoming/user%2F..%2F..%2Fother/file-id",
81 );
82 });
83 test("normalizes deployed URLs with trailing slashes", () => {
84 expect(
85 corsHeaders("https://app.example", "https://app.example/")?.[
86 "access-control-allow-origin"
87 ],
88 ).toBe("https://app.example");
89 expect(corsHeaders("null", "not-a-url")).toBeUndefined();
90 });
91 test("allows explicitly configured wildcard CORS without cookies", () => {
92 const headers = corsHeaders("https://app.example", "*");
93 expect(headers?.["access-control-allow-origin"]).toBe("*");
94 expect(headers?.["access-control-allow-credentials"]).toBeUndefined();
95 });
96 test("rejects unexpected origins", () => {
97 expect(
98 corsHeaders("https://attacker.example", "https://app.example"),
99 ).toBeUndefined();
100 expect(corsHeaders("https://app.example", undefined)).toBeUndefined();
101 });
102 test("permits the configured browser origin without credentials", () => {
103 const headers = corsHeaders("https://app.example", "https://app.example");
104 expect(headers?.["access-control-allow-origin"]).toBe(
105 "https://app.example",
106 );
107 expect(headers?.["access-control-allow-credentials"]).toBeUndefined();
108 expect(headers?.vary).toBe("Origin");
109 });
110 test("permits nonbrowser requests but does not manufacture an origin", () => {
111 expect(
112 corsHeaders(null, "https://app.example")?.["access-control-allow-origin"],
113 ).toBeUndefined();
114 });
115 test("validates download IDs before the uuid query", () => {
116 expect(UUID.test("10000000-0000-4000-8000-000000000001")).toBe(true);
117 expect(UUID.test("../another-user")).toBe(false);
118 });
119});