This commit is contained in:
jackbeeby
2024-12-05 18:55:18 +11:00
commit 09db3a54c3
291 changed files with 102282 additions and 0 deletions

269
src/__tests__/polly.ts Normal file
View File

@@ -0,0 +1,269 @@
import path from "path";
import NodeHttpAdapter from "@pollyjs/adapter-node-http";
import FetchAdapter from "@pollyjs/adapter-fetch";
import { Polly, type Headers, type PollyConfig } from "@pollyjs/core";
import FSPersister from "@pollyjs/persister-fs";
import { afterEach, beforeEach, expect } from "vitest";
import merge from "lodash-es/merge";
import omit from "lodash-es/omit";
import omitDeep from "omit-deep-lodash";
import { tryJsonParse, tryIgnore } from "../lib/utils";
import { testEnv } from "@/__tests__/test-env.mjs";
import { env } from "@/lib/env.mjs";
declare module "vitest" {
export interface TestContext {
polly?: Polly;
}
}
export const omitPathsFromJson = (paths: string[]) => (input: string) => {
return JSON.stringify(omit(JSON.parse(input) as object, paths));
};
export const omitPathsFromHeaders = (paths: string[]) => (headers: Headers) => {
return omit(headers, paths);
};
const HEADERS_BLACKLIST = new Set([
"authorization-bearer",
"authorization",
"saleor-signature",
"set-cookie",
"x-api-key",
"x-stripe-client-user-agent",
]);
const VARIABLES_BLACKLIST = new Set([
"csrfToken",
"email",
"newEmail",
"newPassword",
"oldPassword",
"password",
"redirectUrl",
"refreshToken",
"token",
"authorisationToken",
]);
const removeBlacklistedVariables = (
obj: {} | undefined | string | null,
): {} | undefined | string | null => {
if (!obj || typeof obj === "string") {
return obj;
}
if ("client_secret" in obj) {
obj.client_secret = "pi_FAKE_CLIENT_SECRET";
}
return omitDeep(obj, ...VARIABLES_BLACKLIST);
};
/**
* This interface is incomplete
*/
interface PollyRecording {
response: {
content?: {
mimeType: string;
text: string;
};
cookies: string[];
headers: Array<{ value: string; name: string }>;
};
request: {
postData?: {
text: string;
};
cookies: string[];
headers: Array<{ value: string; name: string }>;
};
}
const responseIsJson = (recording: PollyRecording) => {
return recording.response.content?.mimeType.includes("application/json");
};
const requestIsJson = (recording: PollyRecording) => {
return recording.request.headers.some(
({ name, value }) =>
name.toLowerCase() === "content-type" && value.includes("application/json"),
);
};
export const setupRecording = (config?: PollyConfig) => {
Polly.on("create", (polly) => {
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
polly.server
.any()
// Hide sensitive data in headers or in body
.on("beforePersist", (_req, recording: PollyRecording) => {
recording.response.cookies = [];
recording.response.headers = recording.response.headers.filter(
(el: Record<string, string>) => !HEADERS_BLACKLIST.has(el.name),
);
recording.request.headers = recording.request.headers.filter(
(el: Record<string, string>) => !HEADERS_BLACKLIST.has(el.name),
);
if (recording.request.postData?.text) {
const requestJson = tryJsonParse(recording.request.postData.text);
const filteredRequestJson = removeBlacklistedVariables(requestJson);
recording.request.postData.text =
typeof filteredRequestJson === "string"
? filteredRequestJson
: JSON.stringify(filteredRequestJson);
}
if (recording.response.content?.text) {
const responseJson = tryJsonParse(recording.response.content.text);
const filteredResponseJson = removeBlacklistedVariables(responseJson);
recording.response.content.text =
typeof filteredResponseJson === "string"
? filteredResponseJson
: JSON.stringify(filteredResponseJson);
}
})
// make JSON response and requests more readable
// https://github.com/Netflix/pollyjs/issues/322
.on("beforePersist", (_req, recording: PollyRecording) => {
if (responseIsJson(recording)) {
tryIgnore(
() =>
(recording.response.content!.text = JSON.parse(
recording.response.content!.text,
) as string),
);
}
if (requestIsJson(recording)) {
tryIgnore(
() =>
(recording.request.postData!.text = JSON.parse(
recording.request.postData!.text,
) as string),
);
}
})
.on("beforeReplay", (_req, recording: PollyRecording) => {
if (responseIsJson(recording)) {
tryIgnore(
() =>
(recording.response.content!.text = JSON.stringify(recording.response.content!.text)),
);
}
if (requestIsJson(recording)) {
tryIgnore(
() =>
(recording.request.postData!.text = JSON.stringify(recording.request.postData!.text)),
);
}
});
/* eslint-enable */
});
const defaultConfig = {
...getRecordingSettings(),
adapters: [FetchAdapter, NodeHttpAdapter],
persister: FSPersister,
adapterOptions: {
fetch: {
context: globalThis,
},
},
persisterOptions: {
fs: {},
keepUnusedRequests: false,
},
flushRequestsOnStop: true,
matchRequestsBy: {
url: {
protocol: true,
username: true,
password: true,
hostname: true,
port: true,
pathname: true,
query: true,
hash: false,
},
body: true,
order: false,
method: true,
headers: {
exclude: [
"date",
"idempotency-key",
"original-request",
"request-id",
"content-length",
"x-stripe-client-user-agent",
],
},
},
};
beforeEach(async (ctx) => {
const { currentTestName } = expect.getState();
if (!currentTestName) {
throw new Error("This function must be run inside a test case!");
}
const recordingsRoot = path.dirname(expect.getState().testPath || "");
const recordingsDirectory = path.join(recordingsRoot, "__recordings__");
const [, ...names] = currentTestName.split(" > ");
const polly = new Polly(
names.join("/"),
merge(
defaultConfig,
{
persisterOptions: {
fs: {
recordingsDir: recordingsDirectory,
},
},
},
config,
),
);
ctx.polly = polly;
});
afterEach((ctx) => ctx.polly?.flush());
afterEach((ctx) => ctx.polly?.stop());
};
const getRecordingSettings = (): Pick<
PollyConfig,
"mode" | "recordIfMissing" | "recordFailedRequests"
> => {
// use replay mode by default, override if POLLY_MODE env variable is passed
const mode = env.CI ? "replay" : testEnv.POLLY_MODE;
if (mode === "record") {
return {
mode: "record",
recordIfMissing: true,
recordFailedRequests: true,
};
}
if (mode === "record_missing") {
return {
mode: "replay",
recordIfMissing: true,
recordFailedRequests: true,
};
}
return {
mode: "replay",
recordIfMissing: false,
recordFailedRequests: false,
};
};