Path: blob/master/webhooks/stream-parkpow-webhook-worker/test/index.spec.ts
1094 views
import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test";1import { describe, expect, it } from "vitest";2import worker from "../src/index";34const requestPayload = JSON.stringify({ foo: "bar" });5const testEnv = {6PARKPOW_ENDPOINT: "http://example.com/parkpow-webhook",7PARKPOW_TOKEN: "parkpow-token-456",8STREAM_TOKEN: "stream-token-123",9};1011async function executeWorkerRequest(request: Request, env: typeof testEnv = testEnv) {12const ctx = createExecutionContext();13const response = await worker.fetch(request, env, ctx);14await waitOnExecutionContext(ctx);15return response;16}1718async function mockFetchAndExecute(19request: Request,20mockResponse: Response,21env: typeof testEnv = testEnv,22): Promise<{ response: Response; capturedRequest?: Request }> {23const ctx = createExecutionContext();24const originalFetch = globalThis.fetch;25let capturedRequest: Request | undefined;2627globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {28capturedRequest = new Request(input as string, init);29return mockResponse;30};3132const response = await worker.fetch(request, env, ctx);33await waitOnExecutionContext(ctx);34globalThis.fetch = originalFetch;3536return { response, capturedRequest };37}3839function createRequest(40method: string,41headers: Record<string, string>,42body?: BodyInit | Uint8Array,43): Request {44return new Request("http://example.com/", {45method,46headers,47body: body as BodyInit,48});49}5051function createAuthorizedPostRequest(body?: BodyInit): Request {52return createRequest(53"POST",54{55"Content-Type": "application/json",56Authorization: `Token ${testEnv.STREAM_TOKEN}`,57},58body,59);60}6162describe("Stream to ParkPow webhook worker", () => {63it("should reject non-POST requests", async () => {64const request = createRequest("GET", {});65const response = await executeWorkerRequest(request);6667expect(response.status).toBe(401);68expect(await response.text()).toBe("Unauthorized");69});7071it("should forward POST request with correct incoming Authorization header and multipart/form-data body with mock image as-is", async () => {72const boundary = "37dd8504ce9387f0e07b86f03a25ab0a"; // pragma: allowlist secret73const imageBuffer = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);74const multipartBody = createMultipartBody(boundary, imageBuffer);7576const request = createRequest(77"POST",78{79"Content-Type": `multipart/form-data; boundary=${boundary}`,80Authorization: `Token ${testEnv.STREAM_TOKEN}`,81},82multipartBody,83);8485const mockResponse = new Response(JSON.stringify({ success: true }), {86status: 200,87headers: { "Content-Type": "application/json" },88});8990const { response, capturedRequest } = await mockFetchAndExecute(91request,92mockResponse,93);9495expect(response.status).toBe(200);96expect(capturedRequest).toBeDefined();97if (capturedRequest) {98expect(capturedRequest.headers.get("Authorization")).toBe(99`Token ${testEnv.PARKPOW_TOKEN}`,100);101expect(capturedRequest.headers.get("Content-Type")).toBe(102`multipart/form-data; boundary=${boundary}`,103);104const forwardedBody = new Uint8Array(await capturedRequest.arrayBuffer());105expect(forwardedBody.length).toBe(multipartBody.length);106expect(forwardedBody.every((v, i) => v === multipartBody[i])).toBe(true);107}108});109110it("should reject POST request with missing Authorization header", async () => {111const request = createRequest(112"POST",113{ "Content-Type": "application/json" },114requestPayload,115);116const response = await executeWorkerRequest(request);117118expect(response.status).toBe(401);119expect(await response.text()).toBe("Unauthorized");120});121122it("should reject POST request with invalid Authorization header", async () => {123const request = createRequest(124"POST",125{126"Content-Type": "application/json",127Authorization: "Token wrong-token",128},129requestPayload,130);131const response = await executeWorkerRequest(request);132133expect(response.status).toBe(401);134expect(await response.text()).toBe("Unauthorized");135});136137it("should handle fetch errors gracefully", async () => {138const request = createAuthorizedPostRequest(requestPayload);139140const ctx = createExecutionContext();141const originalFetch = globalThis.fetch;142globalThis.fetch = async () => {143throw new Error("Network error");144};145146const response = await worker.fetch(request, testEnv, ctx);147await waitOnExecutionContext(ctx);148globalThis.fetch = originalFetch;149150expect(response.status).toBe(502);151expect(await response.text()).toBe("Network error");152});153154it("should preserve ParkPow response status and headers", async () => {155const request = createAuthorizedPostRequest(requestPayload);156const mockResponse = new Response(JSON.stringify({ error: "Bad request" }), {157status: 400,158statusText: "Bad Request",159headers: { "X-Custom-Header": "test-value" },160});161162const { response } = await mockFetchAndExecute(request, mockResponse);163164expect(response.status).toBe(400);165expect(response.statusText).toBe("Bad Request");166expect(response.headers.get("X-Custom-Header")).toBe("test-value");167});168169it("should handle missing PARKPOW_TOKEN", async () => {170const request = createAuthorizedPostRequest(requestPayload);171const envWithoutToken = {172...testEnv,173PARKPOW_TOKEN: "",174};175176const mockResponse = new Response(JSON.stringify({ success: true }), {177status: 200,178});179180const { response, capturedRequest } = await mockFetchAndExecute(181request,182mockResponse,183envWithoutToken,184);185186expect(response.status).toBe(400);187expect(capturedRequest).toBeUndefined();188});189190it("should handle ParkPow 401 Unauthorized response", async () => {191const request = createAuthorizedPostRequest(requestPayload);192const mockResponse = new Response(JSON.stringify({ detail: "Invalid token" }), {193status: 401,194statusText: "Unauthorized",195});196197const { response } = await mockFetchAndExecute(request, mockResponse);198199expect(response.status).toBe(401);200});201202it("should handle ParkPow 403 Forbidden response", async () => {203const request = createAuthorizedPostRequest(requestPayload);204const mockResponse = new Response(205JSON.stringify({ detail: "Token not authorized for this resource" }),206{207status: 403,208statusText: "Forbidden",209},210);211212const { response } = await mockFetchAndExecute(request, mockResponse);213214expect(response.status).toBe(403);215});216});217218function createMultipartBody(boundary: string, imageBuffer: Uint8Array): Uint8Array {219const encoder = new TextEncoder();220const CRLF = "\r\n";221const bodyParts: (string | Uint8Array)[] = [222`--${boundary}${CRLF}` +223`Content-Disposition: form-data; name="json"${CRLF}` +224`Content-Type: application/json${CRLF}${CRLF}` +225`{"foo":"bar"}${CRLF}` +226`--${boundary}${CRLF}` +227`Content-Disposition: form-data; name="upload"; filename="taxi.jpg"${CRLF}` +228`Content-Type: image/jpeg${CRLF}${CRLF}`,229imageBuffer,230`${CRLF}--${boundary}--${CRLF}`,231];232233const encodedParts: Uint8Array[] = bodyParts.map((part) =>234typeof part === "string" ? encoder.encode(part) : part,235);236237const totalLength = encodedParts.reduce(238(acc: number, curr: Uint8Array) => acc + curr.length,2390,240);241const result = new Uint8Array(totalLength);242let offset = 0;243for (const arr of encodedParts) {244result.set(arr, offset);245offset += arr.length;246}247return result;248}249250251