Skip to content

Commit d5b5d1a

Browse files
committed
feat: support fine-grained quirks mode
1 parent 15d083d commit d5b5d1a

File tree

8 files changed

+65
-20
lines changed

8 files changed

+65
-20
lines changed

src/compare/requestBody.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { OpenAPIV2 } from "openapi-types";
44
import type Router from "find-my-way";
55
import { get } from "lodash-es";
66
import qs from "qs";
7+
import querystring from "node:querystring";
78
import multipart from "parse-multipart-data";
89

910
import type { Interaction } from "../documents/pact";
@@ -15,6 +16,7 @@ import {
1516
formatSchemaPath,
1617
} from "../results/index";
1718
import { minimumSchema, transformRequestSchema } from "../transform/index";
19+
import { config } from "../utils/config";
1820
import { isValidRequest } from "../utils/interaction";
1921
import { dereferenceOas, splitPath } from "../utils/schema";
2022
import { getValidateFunction } from "../utils/validation";
@@ -25,11 +27,12 @@ const parseBody = (body: unknown, contentType: string) => {
2527
contentType.includes("application/x-www-form-urlencoded") &&
2628
typeof body === "string"
2729
) {
28-
return qs.parse(body as string, {
29-
allowDots: true,
30-
comma: true,
31-
depth: process.env.QUIRKS ? 0 : undefined,
32-
});
30+
return config.get("legacyParser")
31+
? querystring.parse(body as string)
32+
: qs.parse(body as string, {
33+
allowDots: true,
34+
comma: true,
35+
});
3336
}
3437

3538
if (contentType.includes("multipart/form-data") && typeof body === "string") {
@@ -60,7 +63,7 @@ const canValidate = (contentType: string): boolean => {
6063
[
6164
"application/json",
6265
"application/x-www-form-urlencoded",
63-
process.env.QUIRKS ? "" : "multipart/form-data",
66+
config.get("disableMultipartFormdata") ? "" : "multipart/form-data",
6467
].filter(Boolean),
6568
);
6669
};
@@ -110,7 +113,7 @@ export function* compareReqBody(
110113
schema &&
111114
canValidate(contentType) &&
112115
isValidRequest(interaction) &&
113-
(process.env.QUIRKS
116+
(config.get("noValidateRequestBodyUnlessApplicationJson")
114117
? !!findMatchingType("application/json", availableRequestContentTypes)
115118
: true)
116119
) {
@@ -147,7 +150,7 @@ export function* compareReqBody(
147150
!!body &&
148151
!schema &&
149152
isValidRequest(interaction) &&
150-
(process.env.QUIRKS
153+
(config.get("noValidateRequestBodyUnlessApplicationJson")
151154
? !!findMatchingType("application/json", availableRequestContentTypes) ||
152155
availableRequestContentTypes.length === 0
153156
: true)

src/compare/requestHeader.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
} from "../results/index";
1515
import { minimumSchema } from "../transform/index";
1616
import { isValidRequest } from "../utils/interaction";
17-
import { isQuirky } from "../utils/quirks";
17+
import { config } from "../utils/config";
18+
import { isSimpleSchema } from "../utils/quirks";
1819
import { dereferenceOas, splitPath } from "../utils/schema";
1920
import { getValidateFunction } from "../utils/validation";
2021
import { findMatchingType, standardHttpRequestHeaders } from "./utils/content";
@@ -317,7 +318,9 @@ export function* compareReqHeader(
317318
if (value !== null && schema && isValidRequest(interaction)) {
318319
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
319320
const validate = getValidateFunction(ajv, schemaId, () =>
320-
process.env.QUIRKS && value && isQuirky(schema)
321+
config.get("noValidateComplexParameters") &&
322+
isSimpleSchema(schema) &&
323+
value
321324
? {}
322325
: minimumSchema(schema, oas),
323326
);

src/compare/requestPath.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type { Interaction } from "../documents/pact";
77
import type { Result } from "../results/index";
88
import { baseMockDetails } from "../results/index";
99
import { minimumSchema } from "../transform/index";
10+
import { config } from "../utils/config";
1011
import { dereferenceOas } from "../utils/schema";
11-
import { isQuirky } from "../utils/quirks";
12+
import { isSimpleSchema } from "../utils/quirks";
1213
import { getValidateFunction } from "../utils/validation";
1314
import { cleanPathParameter } from "./utils/parameters";
1415
import { parseValue } from "./utils/parse";
@@ -44,7 +45,9 @@ export function* compareReqPath(
4445
if (schema) {
4546
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
4647
const validate = getValidateFunction(ajv, schemaId, () =>
47-
process.env.QUIRKS && value && isQuirky(schema)
48+
config.get("noValidateComplexParameters") &&
49+
isSimpleSchema(schema) &&
50+
value
4851
? {}
4952
: minimumSchema(schema, oas),
5053
);

src/compare/requestQuery.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
formatSchemaPath,
1515
} from "../results/index";
1616
import { minimumSchema } from "../transform/index";
17+
import { config } from "../utils/config";
1718
import { isValidRequest } from "../utils/interaction";
1819
import { ARRAY_SEPARATOR } from "../utils/queryParams";
19-
import { isQuirky } from "../utils/quirks";
20+
import { isSimpleSchema } from "../utils/quirks";
2021
import { dereferenceOas, splitPath } from "../utils/schema";
2122
import { getValidateFunction } from "../utils/validation";
2223

@@ -28,14 +29,14 @@ export function* compareReqQuery(
2829
): Iterable<Result> {
2930
const { method, oas, operation, path, securitySchemes } = route.store;
3031

31-
const searchParamsParsed = process.env.QUIRKS
32+
const searchParamsParsed = config.get("legacyParser")
3233
? querystring.parse(route.searchParams as unknown as string)
3334
: qs.parse(route.searchParams, {
3435
allowDots: true,
3536
comma: true,
3637
});
3738

38-
const searchParamsUnparsed = process.env.QUIRKS
39+
const searchParamsUnparsed = config.get("legacyParser")
3940
? querystring.parse(route.searchParams as unknown as string)
4041
: qs.parse(route.searchParams, {
4142
allowDots: false,
@@ -66,7 +67,9 @@ export function* compareReqQuery(
6667
) {
6768
const schemaId = `[root].paths.${path}.${method}.parameters[${parameterIndex}]`;
6869
const validate = getValidateFunction(ajv, schemaId, () =>
69-
process.env.QUIRKS && value && isQuirky(schema)
70+
config.get("noValidateComplexParameters") &&
71+
isSimpleSchema(schema) &&
72+
value
7073
? {}
7174
: minimumSchema(schema, oas),
7275
);

src/compare/setup.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Router, { HTTPMethod } from "find-my-way";
55
import { uniqWith } from "lodash-es";
66
import { cleanPathParameter } from "./utils/parameters";
77
import { dereferenceOas } from "../utils/schema";
8+
import { config } from "../utils/config";
89

910
export function setupAjv(options: Options): Ajv {
1011
const ajv = new Ajv(options);
@@ -31,8 +32,8 @@ export function setupRouter(
3132
oas: OpenAPIV2.Document | OpenAPIV3.Document,
3233
): Router.Instance<Router.HTTPVersion.V1> {
3334
const router = Router({
34-
ignoreDuplicateSlashes: process.env.QUIRKS ? true : false,
35-
ignoreTrailingSlash: process.env.QUIRKS ? true : false,
35+
ignoreDuplicateSlashes: config.get("duplicateSlashes"),
36+
ignoreTrailingSlash: config.get("trailingSlash"),
3637
querystringParser: (s: string): string => s, // don't parse query in router
3738
});
3839
for (const oasPath in oas.paths) {

src/transform/responseSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SchemaObject } from "ajv";
22
import { each, get } from "lodash-es";
3+
import { config } from "../utils/config";
34
import {
45
splitPath,
56
traverseWithDereferencing as traverse,
@@ -17,7 +18,7 @@ export const transformResponseSchema = (schema: SchemaObject): SchemaObject => {
1718
!s.anyOf &&
1819
s.type &&
1920
s.type === "object" &&
20-
(process.env.QUIRKS ? !s.nullable : true)
21+
(config.get("noTransformNonNullableResponseSchema") ? !s.nullable : true)
2122
) {
2223
s.additionalProperties = false;
2324
}

src/utils/config.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const quirks = !!process.env.QUIRKS;
2+
3+
export const config = new Map([
4+
// SMV ignores duplicate slashes in OAS
5+
["ignoreDuplicateSlashes", quirks],
6+
7+
// SMV ignores trailing slashes in OAS
8+
["ignoreTrailingSlash", quirks],
9+
10+
// SMV had a bug whereby nullable response schemas were not transformed
11+
// correctly. It was missing a transformation to set additionalProperties to
12+
// false. This flag reproduces the bug.
13+
["noTransformNonNullableResponseSchema", quirks],
14+
15+
// SMV used node:querystring to parse query strings and
16+
// application/x-www-form-urlencoded form bodies. This had a limitation that
17+
// nested objects/arrays are not parsed correctly
18+
["legacyParser", quirks],
19+
20+
// SMV didn't support multipart/form-data
21+
["disableMultipartFormdata", quirks],
22+
23+
// SMV had a bug whereby request bodies of *any* content-type are not
24+
// validated unless application/json is in the list of supported request
25+
// content types
26+
["noValidateRequestBodyUnlessApplicationJson", quirks],
27+
28+
// SMV only validated schemas of arrays and objects in path, headers, and
29+
// query params
30+
["noValidateComplexParameters", quirks],
31+
]);

src/utils/quirks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import type { SchemaObject } from "ajv";
22

3-
export const isQuirky = (s?: SchemaObject): boolean =>
3+
export const isSimpleSchema = (s?: SchemaObject): boolean =>
44
s === undefined || s.type === "array" || s.type === "object";

0 commit comments

Comments
 (0)