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