Skip to content

Commit

Permalink
Refactor the form matching to be cleaner.
Browse files Browse the repository at this point in the history
  • Loading branch information
sangaline committed Jan 14, 2024
1 parent 1459e98 commit 1129dfb
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 4 deletions.
130 changes: 130 additions & 0 deletions test/utils/matchFormPayloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { isEqual } from "lodash";
import makeSynchronous from "make-synchronous";
import { type Definition as NockDefinition } from "nock";
import * as multipart from "parse-multipart-data";

type MultipartInput = {
filename?: string;
name?: string;
type: string;
data: Buffer;
};

const boundaryRegex = /----WebKitFormBoundary................/;
const parseTarball = makeSynchronous(
async (buffer: Buffer): Promise<{ [path: string]: Buffer }> =>
new Promise((resolve) => {
const files: { [path: string]: Buffer } = {};
import("tar").then((tar) => {
const parseStream = new tar.Parse({
// @ts-expect-error - The `ondone` callback is missing from the `ParseOptions` type.
ondone: () => {
resolve(files);
},
onentry: (entry) => {
entry.on("data", (chunk) => {
files[entry.path] = Buffer.concat([
files[entry.path] ?? Buffer.alloc(0),
chunk,
]);
});
},
});
parseStream.end(buffer);
});
}),
);

const partSorter = (a: MultipartInput, b: MultipartInput): number => {
// First sort by name with nameless entries last.
if (a.name != null || b.name != null) {
if (a.name == null) return 1;
if (b.name == null) return -1;
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
}

// If names are equal, sort by data.
return a.data.compare(b.data);
};

export function matchFormPayloads(scope: NockDefinition) {
// @ts-expect-error - Types are wrong.
scope.filteringRequestBody = (
body: string | null,
recordedBody: string | null,
) => {
if (typeof body !== "string" || typeof recordedBody !== "string") {
return body;
}

// Find the boundaries for the multipart form bodies.
const isText = boundaryRegex.test(body);
const bodyBuffer = Buffer.from(body, isText ? "utf-8" : "hex");
const bodyText = bodyBuffer.toString("utf-8");
const boundaryMatch = boundaryRegex.exec(bodyText);
if (!boundaryMatch) {
return body;
}
const boundary = boundaryMatch[0];
const recordedBodyBuffer = Buffer.from(
recordedBody,
isText ? "utf-8" : "hex",
);
const recordedBodyText = recordedBodyBuffer.toString("utf-8");
const recordedBoundaryMatch = boundaryRegex.exec(recordedBodyText);
if (!recordedBoundaryMatch) {
return body;
}
const recordedBoundary = recordedBoundaryMatch[0];

// Parse the form data and get it into a normalized format by sorting the parts.
const parts: MultipartInput[] = multipart.parse(bodyBuffer, boundary);
parts.sort(partSorter);
const recordedParts: MultipartInput[] = multipart.parse(
recordedBodyBuffer,
recordedBoundary,
);
recordedParts.sort(partSorter);

// Check that the form inputs are equivalent.
if (parts.length !== recordedParts.length) {
return body;
}
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const recordedPart = recordedParts[i];

// Check that all the metadata matches.
if (
!part ||
!recordedPart ||
part.filename !== recordedPart.filename ||
part.name !== recordedPart.name ||
part.type !== recordedPart.type
) {
return body;
}

// Check that tarballs contain the same data.
if (
part &&
part.filename &&
/\.(tar|tar\.gz|tgz)$/i.test(part.filename)
) {
const tarball = parseTarball(part.data);
const recordedTarball = parseTarball(part.data);
if (!isEqual(tarball, recordedTarball)) {
return body;
}
} else {
if (part.data.compare(recordedPart.data) !== 0) {
return body;
}
}
}

// If we made it this far, it's a match, so pretend the body is whatever we recorded.
return recordedBody;
};
}
4 changes: 2 additions & 2 deletions test/utils/useNock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import process from "process";
import testWithoutContext, { type ExecutionContext, type TestFn } from "ava";
import nock, { back as nockBack, type BackMode } from "nock";

import { patchFormBoundaries } from "test/utils/patchFormBoundaries";
import { matchFormPayloads } from "test/utils/matchFormPayloads";

// Add the context to the test function type.
type Context = {
Expand All @@ -25,7 +25,7 @@ export const useNock = async () => {
nock.enableNetConnect("sindri.app");
nockBack.setMode((process.env.NOCK_BACK_MODE ?? "lockdown") as BackMode);
const { nockDone } = await nockBack(fixtureFilename, {
before: patchFormBoundaries,
before: matchFormPayloads,
});
t.context.nockDone = nockDone;
});
Expand Down
4 changes: 2 additions & 2 deletions test/utils/usePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Proxy } from "http-mitm-proxy";
import puppeteer, { type Browser, type Page } from "puppeteer";

import sindriLibrary from "lib";
import { patchFormBoundaries } from "test/utils/patchFormBoundaries";
import { matchFormPayloads } from "test/utils/matchFormPayloads";

// The `sindri` library is injected in `withPage.ts`, but this tells TypeScript what the type is.
type SindriLibrary = typeof sindriLibrary;
Expand Down Expand Up @@ -110,7 +110,7 @@ export const usePage = async ({
nock.enableNetConnect("sindri.app");
nockBack.setMode((process.env.NOCK_BACK_MODE ?? "lockdown") as BackMode);
const { nockDone } = await nockBack(fixtureFilename, {
before: patchFormBoundaries,
before: matchFormPayloads,
});
t.context.nockDone = nockDone;
});
Expand Down

0 comments on commit 1129dfb

Please sign in to comment.