From 200b9b398150d278b5f95b6a3aac3dcecda98de6 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:09:35 +0330 Subject: [PATCH] Import tests: backend/tests/e2e/setup Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- backend/tests/e2e/setup.ts | 161 +++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 backend/tests/e2e/setup.ts diff --git a/backend/tests/e2e/setup.ts b/backend/tests/e2e/setup.ts new file mode 100644 index 0000000..afc6a4b --- /dev/null +++ b/backend/tests/e2e/setup.ts @@ -0,0 +1,161 @@ +import type { ApiResponse } from "../../../src/api/contracts/common.js"; + +export const API_URL = process.env.API_URL || "http://localhost:3001"; + +const cookieJars = new Map>(); + +export function getCookieJar(sessionId = "default"): Map { + if (!cookieJars.has(sessionId)) { + cookieJars.set(sessionId, new Map()); + } + return cookieJars.get(sessionId)!; +} + +export function clearCookieJar(sessionId = "default") { + const jar = cookieJars.get(sessionId); + if (jar) { + jar.clear(); + } +} + +export type FetchOptions = RequestInit & { + sessionId?: string; +}; + +export type ApiResponseWithStatus = ApiResponse & { + $status: number; + $statusText: string; + $headers: Headers; +}; + +export async function apiFetch( + endpoint: string, + options: FetchOptions = {}, +): Promise> { + const { sessionId = "default", ...fetchArgs } = options; + const url = endpoint.startsWith("http") ? endpoint : `${API_URL}${endpoint}`; + + // Prepare headers + const headersInit: Record = { + "Content-Type": "application/json", + ...(fetchArgs.headers as Record), + }; + + // Inject cookies from the specific session's jar + const jar = getCookieJar(sessionId); + if (jar.size > 0) { + const cookiePairs: string[] = []; + for (const [key, value] of jar.entries()) { + cookiePairs.push(`${key}=${value}`); + } + headersInit["Cookie"] = cookiePairs.join("; "); + } + + const finalOptions: RequestInit = { + ...fetchArgs, + headers: headersInit, + }; + + if (process.env.DEBUG_E2E) { + console.log(`[E2E Request] ${options.method || "GET"} ${url}`, { + headers: headersInit, + body: fetchArgs.body, + }); + } + + let response: Response; + try { + response = await fetch(url, finalOptions); + } catch (err: any) { + if (process.env.DEBUG_E2E) { + console.log(`[E2E Request Failed] ${options.method || "GET"} ${url} Error:`, err.message); + } + throw err; + } + + // Extract and save cookies + const setCookieHeader = + typeof response.headers.getSetCookie === "function" + ? response.headers.getSetCookie() + : response.headers.get("set-cookie"); + + if (setCookieHeader) { + const cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader]; + for (const cookieStr of cookies) { + const parts = cookieStr.split(/,(?=[^;]*=)/); + for (const part of parts) { + const cleanPart = part.trim(); + if (!cleanPart) continue; + const [cookieKV] = cleanPart.split(";"); + const [key, ...valParts] = cookieKV.split("="); + if (key) { + jar.set(key.trim(), valParts.join("=").trim()); + } + } + } + } + + const bodyText = await response.text(); + + if (process.env.DEBUG_E2E) { + console.log(`[E2E Response] ${response.status} ${response.statusText} ${url}`, { + body: bodyText, + }); + } + + let json: any; + try { + json = JSON.parse(bodyText); + } catch (_parseErr) { + throw new Error( + `Response body is not valid JSON. Status: ${response.status}. Body: ${bodyText}`, + ); + } + + // Envelope validation + if (json === null || typeof json !== "object") { + throw new Error(`Response envelope is not an object: ${bodyText}`); + } + + if (!("data" in json) || !("meta" in json) || !("error" in json)) { + throw new Error( + `Response envelope is missing required keys {data, meta, error}. Body: ${bodyText}`, + ); + } + + if (json.meta === null || typeof json.meta !== "object") { + throw new Error(`Response envelope 'meta' must be a non-null object. Body: ${bodyText}`); + } + + if (typeof json.meta.requestId !== "string" || !json.meta.requestId) { + throw new Error( + `Response envelope 'meta.requestId' must be a non-empty string. Body: ${bodyText}`, + ); + } + + if (json.error !== null) { + if (typeof json.error !== "object") { + throw new Error(`Response envelope 'error' must be null or an object. Body: ${bodyText}`); + } + if (typeof json.error.code !== "string") { + throw new Error(`Response envelope 'error.code' must be a string. Body: ${bodyText}`); + } + if (typeof json.error.message !== "string") { + throw new Error(`Response envelope 'error.message' must be a string. Body: ${bodyText}`); + } + if (json.data !== null) { + throw new Error( + `Response envelope cannot have both 'data' and 'error' populated. Body: ${bodyText}`, + ); + } + } + + const result = json as ApiResponse; + Object.defineProperties(result, { + $status: { value: response.status, enumerable: false }, + $statusText: { value: response.statusText, enumerable: false }, + $headers: { value: response.headers, enumerable: false }, + }); + + return result as ApiResponseWithStatus; +}