Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 52 additions & 8 deletions src/compare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ import { compareReqHeader } from "./requestHeader";
import { compareResBody } from "./responseBody";
import { compareResHeader } from "./responseHeader";
import { baseMockDetails } from "../results/index";
import { Config, DEFAULT_CONFIG } from "../utils/config";
import { ARRAY_SEPARATOR } from "../utils/queryParams";

export class Comparator {
#ajvCoerce: Ajv;
#ajvNocoerce: Ajv;
#config: Config;
#oas: OpenAPIV2.Document | OpenAPIV3.Document;
#router?: Router.Instance<Router.HTTPVersion.V1>;

constructor(oas: OpenAPIV2.Document | OpenAPIV3.Document) {
this.#config = new Map(DEFAULT_CONFIG);
this.#oas = oas;

const ajvOptions = {
Expand All @@ -46,7 +49,12 @@ export class Comparator {
async *compare(pact: Pact): AsyncGenerator<Result> {
if (!this.#router) {
await parseOas(this.#oas);
this.#router = setupRouter(this.#oas);
for (const [key, value] of Object.entries(this.#oas.info)) {
if (key.startsWith("x-opc-config-")) {
this.#config.set(key.substring(13), value);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuan-pham I got distracted and didn't get around to committing this.

See https://swagger.io/docs/specification/v3_0/openapi-extensions/

this.#router = setupRouter(this.#oas, this.#config);
}

const parsedPact = parsePact(pact);
Expand All @@ -63,7 +71,7 @@ export class Comparator {
const { method, path, query } = interaction.request;
let pathWithLeadingSlash = path.startsWith("/") ? path : `/${path}`;

if (process.env.QUIRKS) {
if (this.#config.get("no-percent-encoding")) {
pathWithLeadingSlash = pathWithLeadingSlash.replaceAll("%", "%25");
}

Expand Down Expand Up @@ -106,19 +114,55 @@ export class Comparator {
}

const results = Array.from(
compareReqPath(this.#ajvCoerce, route, interaction, index),
compareReqPath(
this.#ajvCoerce,
route,
interaction,
index,
this.#config,
),
);

if (results.length) {
yield* results;
continue;
}

yield* compareReqHeader(this.#ajvCoerce, route, interaction, index);
yield* compareReqQuery(this.#ajvCoerce, route, interaction, index);
yield* compareReqBody(this.#ajvNocoerce, route, interaction, index);
yield* compareResHeader(this.#ajvCoerce, route, interaction, index);
yield* compareResBody(this.#ajvNocoerce, route, interaction, index);
yield* compareReqHeader(
this.#ajvCoerce,
route,
interaction,
index,
this.#config,
);
yield* compareReqQuery(
this.#ajvCoerce,
route,
interaction,
index,
this.#config,
);
yield* compareReqBody(
this.#ajvNocoerce,
route,
interaction,
index,
this.#config,
);
yield* compareResHeader(
this.#ajvCoerce,
route,
interaction,
index,
this.#config,
);
yield* compareResBody(
this.#ajvNocoerce,
route,
interaction,
index,
this.#config,
);
}
}
}
39 changes: 27 additions & 12 deletions src/compare/requestBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { OpenAPIV2 } from "openapi-types";
import type Router from "find-my-way";
import { get } from "lodash-es";
import qs from "qs";
import querystring from "node:querystring";
import multipart from "parse-multipart-data";

import type { Interaction } from "../documents/pact";
Expand All @@ -15,21 +16,27 @@ import {
formatSchemaPath,
} from "../results/index";
import { minimumSchema, transformRequestSchema } from "../transform/index";
import type { Config } from "../utils/config";
import { isValidRequest } from "../utils/interaction";
import { dereferenceOas, splitPath } from "../utils/schema";
import { getValidateFunction } from "../utils/validation";
import { findMatchingType, getByContentType } from "./utils/content";

const parseBody = (body: unknown, contentType: string) => {
const parseBody = (
body: unknown,
contentType: string,
legacyParser: boolean,
) => {
if (
contentType.includes("application/x-www-form-urlencoded") &&
typeof body === "string"
) {
return qs.parse(body as string, {
allowDots: true,
comma: true,
depth: process.env.QUIRKS ? 0 : undefined,
});
return legacyParser
? querystring.parse(body as string)
: qs.parse(body as string, {
allowDots: true,
comma: true,
});
}

if (contentType.includes("multipart/form-data") && typeof body === "string") {
Expand All @@ -54,13 +61,16 @@ const parseBody = (body: unknown, contentType: string) => {
return body;
};

const canValidate = (contentType: string): boolean => {
const canValidate = (
contentType: string,
disableMultipartFormdata: boolean,
): boolean => {
return !!findMatchingType(
contentType,
[
"application/json",
"application/x-www-form-urlencoded",
process.env.QUIRKS ? "" : "multipart/form-data",
disableMultipartFormdata ? "" : "multipart/form-data",
].filter(Boolean),
);
};
Expand All @@ -72,6 +82,7 @@ export function* compareReqBody(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
config: Config,
): Iterable<Result> {
const { method, oas, operation, path } = route.store;
const { body } = interaction.request;
Expand Down Expand Up @@ -108,13 +119,17 @@ export function* compareReqBody(

if (
schema &&
canValidate(contentType) &&
canValidate(contentType, config.get("disable-multipart-formdata")!) &&
isValidRequest(interaction) &&
(process.env.QUIRKS
(config.get("no-validate-request-body-unless-application-json")
? !!findMatchingType("application/json", availableRequestContentTypes)
: true)
) {
const value = parseBody(body, requestContentType);
const value = parseBody(
body,
requestContentType,
config.get("legacy-parser")!,
);
const schemaId = `[root].paths.${path}.${method}.requestBody.content.${contentType}`;
const validate = getValidateFunction(ajv, schemaId, () =>
transformRequestSchema(minimumSchema(schema, oas)),
Expand Down Expand Up @@ -147,7 +162,7 @@ export function* compareReqBody(
!!body &&
!schema &&
isValidRequest(interaction) &&
(process.env.QUIRKS
(config.get("no-validate-request-body-unless-application-json")
? !!findMatchingType("application/json", availableRequestContentTypes) ||
availableRequestContentTypes.length === 0
: true)
Expand Down
10 changes: 7 additions & 3 deletions src/compare/requestHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
formatSchemaPath,
} from "../results/index";
import { minimumSchema } from "../transform/index";
import type { Config } from "../utils/config";
import { isValidRequest } from "../utils/interaction";
import { isQuirky } from "../utils/quirks";
import { isSimpleSchema } from "../utils/quirks";
import { dereferenceOas, splitPath } from "../utils/schema";
import { getValidateFunction } from "../utils/validation";
import { findMatchingType, standardHttpRequestHeaders } from "./utils/content";
Expand All @@ -25,6 +26,7 @@ export function* compareReqHeader(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
config: Config,
): Iterable<Result> {
const { method, oas, operation, path, securitySchemes } = route.store;
const { body } = interaction.request;
Expand Down Expand Up @@ -250,7 +252,7 @@ export function* compareReqHeader(
break;
}

if (process.env.QUIRKS) {
if (config.get("no-authorization-schema")) {
isValid = requestHeaders.get("authorization") !== null;
}

Expand Down Expand Up @@ -317,7 +319,9 @@ export function* compareReqHeader(
if (value !== null && schema && isValidRequest(interaction)) {
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
const validate = getValidateFunction(ajv, schemaId, () =>
process.env.QUIRKS && value && isQuirky(schema)
config.get("no-validate-complex-parameters") &&
isSimpleSchema(schema) &&
value
? {}
: minimumSchema(schema, oas),
);
Expand Down
8 changes: 6 additions & 2 deletions src/compare/requestPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type { Interaction } from "../documents/pact";
import type { Result } from "../results/index";
import { baseMockDetails } from "../results/index";
import { minimumSchema } from "../transform/index";
import type { Config } from "../utils/config";
import { dereferenceOas } from "../utils/schema";
import { isQuirky } from "../utils/quirks";
import { isSimpleSchema } from "../utils/quirks";
import { getValidateFunction } from "../utils/validation";
import { cleanPathParameter } from "./utils/parameters";
import { parseValue } from "./utils/parse";
Expand All @@ -18,6 +19,7 @@ export function* compareReqPath(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
config: Config,
): Iterable<Result> {
const { method, oas, operation, path } = route.store;

Expand All @@ -44,7 +46,9 @@ export function* compareReqPath(
if (schema) {
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
const validate = getValidateFunction(ajv, schemaId, () =>
process.env.QUIRKS && value && isQuirky(schema)
config.get("no-validate-complex-parameters") &&
isSimpleSchema(schema) &&
value
? {}
: minimumSchema(schema, oas),
);
Expand Down
14 changes: 9 additions & 5 deletions src/compare/requestQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
formatSchemaPath,
} from "../results/index";
import { minimumSchema } from "../transform/index";
import type { Config } from "../utils/config";
import { isValidRequest } from "../utils/interaction";
import { ARRAY_SEPARATOR } from "../utils/queryParams";
import { isQuirky } from "../utils/quirks";
import { isSimpleSchema } from "../utils/quirks";
import { dereferenceOas, splitPath } from "../utils/schema";
import { getValidateFunction } from "../utils/validation";

Expand All @@ -25,17 +26,18 @@ export function* compareReqQuery(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
config: Config,
): Iterable<Result> {
const { method, oas, operation, path, securitySchemes } = route.store;

const searchParamsParsed = process.env.QUIRKS
const searchParamsParsed = config.get("legacy-parser")
? querystring.parse(route.searchParams as unknown as string)
: qs.parse(route.searchParams, {
allowDots: true,
comma: true,
});

const searchParamsUnparsed = process.env.QUIRKS
const searchParamsUnparsed = config.get("legacy-parser")
? querystring.parse(route.searchParams as unknown as string)
: qs.parse(route.searchParams, {
allowDots: false,
Expand Down Expand Up @@ -66,7 +68,9 @@ export function* compareReqQuery(
) {
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
const validate = getValidateFunction(ajv, schemaId, () =>
process.env.QUIRKS && value && isQuirky(schema)
config.get("no-validate-complex-parameters") &&
isSimpleSchema(schema) &&
value
? {}
: minimumSchema(schema, oas),
);
Expand All @@ -76,7 +80,7 @@ export function* compareReqQuery(
? value.split(ARRAY_SEPARATOR)
: value;

if (process.env.QUIRKS && value === "[object Object]") {
if (config.get("cast-objects-in-pact") && value === "[object Object]") {
convertedValue = {};
}

Expand Down
7 changes: 6 additions & 1 deletion src/compare/responseBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
formatInstancePath,
formatSchemaPath,
} from "../results/index";
import type { Config } from "../utils/config";
import { minimumSchema, transformResponseSchema } from "../transform/index";
import { dereferenceOas, splitPath } from "../utils/schema";
import { getValidateFunction } from "../utils/validation";
Expand All @@ -28,6 +29,7 @@ export function* compareResBody(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
config: Config,
): Iterable<Result> {
const { method, oas, operation, path } = route.store;
const { body, status } = interaction.response;
Expand Down Expand Up @@ -119,7 +121,10 @@ export function* compareResBody(
if (value && canValidate(contentType) && schema) {
const schemaId = `[root].paths.${path}.${method}.responses.${status}.content.${contentType}`;
const validate = getValidateFunction(ajv, schemaId, () =>
transformResponseSchema(minimumSchema(schema, oas)),
transformResponseSchema(
minimumSchema(schema, oas),
config.get("no-transform-non-nullable-response-schema")!,
),
);
if (!validate(value)) {
for (const error of validate.errors!) {
Expand Down
2 changes: 2 additions & 0 deletions src/compare/responseHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
formatSchemaPath,
} from "../results/index";
import { minimumSchema } from "../transform/index";
import type { Config } from "../utils/config";
import { dereferenceOas, splitPath } from "../utils/schema";
import { getValidateFunction } from "../utils/validation";
import { findMatchingType, standardHttpResponseHeaders } from "./utils/content";
Expand All @@ -21,6 +22,7 @@ export function* compareResHeader(
route: Router.FindResult<Router.HTTPVersion.V1>,
interaction: Interaction,
index: number,
_config: Config,
): Iterable<Result> {
const { method, oas, operation, path } = route.store;

Expand Down
6 changes: 4 additions & 2 deletions src/compare/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import addFormats from "ajv-formats";
import Router, { HTTPMethod } from "find-my-way";
import { uniqWith } from "lodash-es";
import { cleanPathParameter } from "./utils/parameters";
import type { Config } from "../utils/config";
import { dereferenceOas } from "../utils/schema";

export function setupAjv(options: Options): Ajv {
Expand All @@ -29,10 +30,11 @@ const SUPPORTED_METHODS = [

export function setupRouter(
oas: OpenAPIV2.Document | OpenAPIV3.Document,
config: Config,
): Router.Instance<Router.HTTPVersion.V1> {
const router = Router({
ignoreDuplicateSlashes: process.env.QUIRKS ? true : false,
ignoreTrailingSlash: process.env.QUIRKS ? true : false,
ignoreDuplicateSlashes: config.get("ignore-duplicate-slashes"),
ignoreTrailingSlash: config.get("ignore-trailing-slash"),
querystringParser: (s: string): string => s, // don't parse query in router
});
for (const oasPath in oas.paths) {
Expand Down
Loading