Files
arsam 200b9b3981 Import tests: backend/tests/e2e/setup
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:09:35 +03:30

162 lines
4.7 KiB
TypeScript

import type { ApiResponse } from "../../../src/api/contracts/common.js";
export const API_URL = process.env.API_URL || "http://localhost:3001";
const cookieJars = new Map<string, Map<string, string>>();
export function getCookieJar(sessionId = "default"): Map<string, string> {
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<T> = ApiResponse<T> & {
$status: number;
$statusText: string;
$headers: Headers;
};
export async function apiFetch<T = any>(
endpoint: string,
options: FetchOptions = {},
): Promise<ApiResponseWithStatus<T>> {
const { sessionId = "default", ...fetchArgs } = options;
const url = endpoint.startsWith("http") ? endpoint : `${API_URL}${endpoint}`;
// Prepare headers
const headersInit: Record<string, string> = {
"Content-Type": "application/json",
...(fetchArgs.headers as Record<string, string>),
};
// 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<T>;
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<T>;
}