Files
Dorod-Sky/skyvern-ts/client/tests/mock-server/withJson.ts
2025-10-23 20:14:59 -06:00

159 lines
5.5 KiB
TypeScript

import { type HttpResponseResolver, passthrough } from "msw";
import { fromJson, toJson } from "../../src/core/json";
/**
* Creates a request matcher that validates if the request JSON body exactly matches the expected object
* @param expectedBody - The exact body object to match against
* @param resolver - Response resolver to execute if body matches
*/
export function withJson(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver {
return async (args) => {
const { request } = args;
let clonedRequest: Request;
let bodyText: string | undefined;
let actualBody: unknown;
try {
clonedRequest = request.clone();
bodyText = await clonedRequest.text();
if (bodyText === "") {
console.error("Request body is empty, expected a JSON object.");
return passthrough();
}
actualBody = fromJson(bodyText);
} catch (error) {
console.error(`Error processing request body:\n\tError: ${error}\n\tBody: ${bodyText}`);
return passthrough();
}
const mismatches = findMismatches(actualBody, expectedBody);
if (Object.keys(mismatches).filter((key) => !key.startsWith("pagination.")).length > 0) {
console.error("JSON body mismatch:", toJson(mismatches, undefined, 2));
return passthrough();
}
return resolver(args);
};
}
function findMismatches(actual: any, expected: any): Record<string, { actual: any; expected: any }> {
const mismatches: Record<string, { actual: any; expected: any }> = {};
if (typeof actual !== typeof expected) {
if (areEquivalent(actual, expected)) {
return {};
}
return { value: { actual, expected } };
}
if (typeof actual !== "object" || actual === null || expected === null) {
if (actual !== expected) {
if (areEquivalent(actual, expected)) {
return {};
}
return { value: { actual, expected } };
}
return {};
}
if (Array.isArray(actual) && Array.isArray(expected)) {
if (actual.length !== expected.length) {
return { length: { actual: actual.length, expected: expected.length } };
}
const arrayMismatches: Record<string, { actual: any; expected: any }> = {};
for (let i = 0; i < actual.length; i++) {
const itemMismatches = findMismatches(actual[i], expected[i]);
if (Object.keys(itemMismatches).length > 0) {
for (const [mismatchKey, mismatchValue] of Object.entries(itemMismatches)) {
arrayMismatches[`[${i}]${mismatchKey === "value" ? "" : `.${mismatchKey}`}`] = mismatchValue;
}
}
}
return arrayMismatches;
}
const actualKeys = Object.keys(actual);
const expectedKeys = Object.keys(expected);
const allKeys = new Set([...actualKeys, ...expectedKeys]);
for (const key of allKeys) {
if (!expectedKeys.includes(key)) {
if (actual[key] === undefined) {
continue; // Skip undefined values in actual
}
mismatches[key] = { actual: actual[key], expected: undefined };
} else if (!actualKeys.includes(key)) {
if (expected[key] === undefined) {
continue; // Skip undefined values in expected
}
mismatches[key] = { actual: undefined, expected: expected[key] };
} else if (
typeof actual[key] === "object" &&
actual[key] !== null &&
typeof expected[key] === "object" &&
expected[key] !== null
) {
const nestedMismatches = findMismatches(actual[key], expected[key]);
if (Object.keys(nestedMismatches).length > 0) {
for (const [nestedKey, nestedValue] of Object.entries(nestedMismatches)) {
mismatches[`${key}${nestedKey === "value" ? "" : `.${nestedKey}`}`] = nestedValue;
}
}
} else if (actual[key] !== expected[key]) {
if (areEquivalent(actual[key], expected[key])) {
continue;
}
mismatches[key] = { actual: actual[key], expected: expected[key] };
}
}
return mismatches;
}
function areEquivalent(actual: unknown, expected: unknown): boolean {
if (actual === expected) {
return true;
}
if (isEquivalentBigInt(actual, expected)) {
return true;
}
if (isEquivalentDatetime(actual, expected)) {
return true;
}
return false;
}
function isEquivalentBigInt(actual: unknown, expected: unknown) {
if (typeof actual === "number") {
actual = BigInt(actual);
}
if (typeof expected === "number") {
expected = BigInt(expected);
}
if (typeof actual === "bigint" && typeof expected === "bigint") {
return actual === expected;
}
return false;
}
function isEquivalentDatetime(str1: unknown, str2: unknown): boolean {
if (typeof str1 !== "string" || typeof str2 !== "string") {
return false;
}
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
if (!isoDatePattern.test(str1) || !isoDatePattern.test(str2)) {
return false;
}
try {
const date1 = new Date(str1).getTime();
const date2 = new Date(str2).getTime();
return date1 === date2;
} catch {
return false;
}
}