Import tests: backend/tests/e2e/setup
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<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>;
|
||||
}
|
||||
Reference in New Issue
Block a user