Skip to content
File

Blob: tests/worker/helpers/request.ts

typescript81 lines
1import { exports } from "cloudflare:workers";
2import { D1_BOOKMARK_HEADER } from "@/shared/bookmark";
3import { bearerFor } from "@tests/worker/helpers/auth";
4 
5type JsonBody = Record<string, unknown> | unknown[] | null;
6type WorkerRequestRedirect = "error" | "follow" | "manual";
7 
8export const LOOPBACK_ORIGIN = "http://127.0.0.1";
9export const PROD_ORIGIN = "https://bland.test";
10 
11export interface ApiRequestOptions {
12 method?: string;
13 body?: JsonBody | FormData | ArrayBuffer | Blob;
14 headers?: Record<string, string>;
15 userId?: string;
16 shareToken?: string;
17 origin?: string;
18 bookmark?: string;
19 cookie?: string;
20 search?: Record<string, string>;
21 // Set to "manual" to observe 3xx responses directly. workerd's default
22 // Worker dispatch follows redirects internally.
23 redirect?: WorkerRequestRedirect;
24}
25 
26/**
27 * Dispatches a request at the worker's default-export fetch. Defaults to
28 * `LOOPBACK_ORIGIN`, which `src/worker/http.ts#isLocalRequestUrl` recognizes
29 * and which triggers the rate-limit and turnstile middleware bypasses plus
30 * the router's 401->403 rewrite. Pass `origin: PROD_ORIGIN` when asserting
31 * 401 (not 403) or any behavior gated on a non-loopback hostname.
32 */
33export async function apiRequest(path: string, opts: ApiRequestOptions = {}): Promise<Response> {
34 const origin = opts.origin ?? LOOPBACK_ORIGIN;
35 const url = new URL(path, origin);
36 if (opts.shareToken) {
37 url.searchParams.set("share", opts.shareToken);
38 }
39 if (opts.search) {
40 for (const [k, v] of Object.entries(opts.search)) {
41 url.searchParams.set(k, v);
42 }
43 }
44 
45 const headers = new Headers(opts.headers ?? {});
46 if (opts.userId && !headers.has("authorization")) {
47 headers.set("authorization", await bearerFor(opts.userId));
48 }
49 if (opts.bookmark) {
50 headers.set(D1_BOOKMARK_HEADER, opts.bookmark);
51 }
52 if (opts.cookie) {
53 headers.set("cookie", opts.cookie);
54 }
55 
56 let body: BodyInit | undefined;
57 if (opts.body !== undefined && opts.body !== null) {
58 if (opts.body instanceof FormData || opts.body instanceof ArrayBuffer || opts.body instanceof Blob) {
59 body = opts.body;
60 } else {
61 if (!headers.has("content-type")) headers.set("content-type", "application/json");
62 body = JSON.stringify(opts.body);
63 }
64 }
65 
66 const method = opts.method ?? (body ? "POST" : "GET");
67 const init: RequestInit = { method, headers, body };
68 if (opts.redirect) init.redirect = opts.redirect;
69 
70 return exports.default.fetch(new Request(url.toString(), init));
71}
72 
73export async function expectJson<T>(res: Response): Promise<T> {
74 const text = await res.text();
75 try {
76 return JSON.parse(text) as T;
77 } catch {
78 throw new Error(`expected JSON, got: ${text}`);
79 }
80}