@pactflow/openapi-pact-comparator 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4252,14 +4252,199 @@ function* compareMessageHeaders(ajv, message, content, interactionContext, metad
4252
4252
  }
4253
4253
  }
4254
4254
 
4255
- const canValidate$2 = (contentType) => {
4255
+ const PARAMETER_SEPARATOR = ";";
4256
+ const WILDCARD = "*";
4257
+ const TYPE_SUBTYPE_SEPARATOR = "/";
4258
+ const EXTENSION_SEPARATOR = "+";
4259
+ const quality = (a) => {
4260
+ const match = /.*;q=([01].?\d*)/.exec(a);
4261
+ return match ? parseFloat(match[1]) : 1.0;
4262
+ };
4263
+ const byQuality = (a, b) => {
4264
+ return quality(b) - quality(a);
4265
+ };
4266
+ const ignoreCasing = (t) => t.toLowerCase();
4267
+ const ignoreWhitespace = (t) => t.replace(/\s+/g, "");
4268
+ const ignoreParameters = (t) => t.split(PARAMETER_SEPARATOR)[0];
4269
+ const removeQuality = (t) => t.replace(/;q=[01].?\d*/, "");
4270
+ const type$a = (t) => t.split(TYPE_SUBTYPE_SEPARATOR)[0];
4271
+ const subtype = (t) => {
4272
+ const [_maintype, subtype] = t.split(TYPE_SUBTYPE_SEPARATOR);
4273
+ return subtype?.split(EXTENSION_SEPARATOR).pop();
4274
+ };
4275
+ function findMatchingType(requestType, responseTypes) {
4276
+ // exact match
4277
+ let accept = requestType
4278
+ .split(",")
4279
+ .map(ignoreWhitespace)
4280
+ .map(ignoreCasing)
4281
+ .sort(byQuality)
4282
+ .map(removeQuality);
4283
+ let available = responseTypes.map(ignoreWhitespace).map(ignoreCasing);
4284
+ for (const a of accept) {
4285
+ const matchExactly = (t) => t === a;
4286
+ const index = available.findIndex(matchExactly);
4287
+ if (index >= 0) {
4288
+ return responseTypes[index];
4289
+ }
4290
+ }
4291
+ // ignore additional parameters
4292
+ accept = accept.map(ignoreParameters);
4293
+ available = available.map(ignoreParameters);
4294
+ for (const a of accept) {
4295
+ const matchExactly = (t) => t === a;
4296
+ const index = available.findIndex(matchExactly);
4297
+ if (index >= 0) {
4298
+ return responseTypes[index];
4299
+ }
4300
+ }
4301
+ // ignore vendor extensions
4302
+ for (const a of accept) {
4303
+ const matchTypesAndSubtypes = (t) => type$a(t) === type$a(a) && subtype(t) === subtype(a);
4304
+ const index = available.findIndex(matchTypesAndSubtypes);
4305
+ if (index >= 0) {
4306
+ return responseTypes[index];
4307
+ }
4308
+ }
4309
+ // wildcards in responseTypes
4310
+ for (const a of accept) {
4311
+ const matchSubtype = (t) => subtype(t) === WILDCARD && type$a(t) === type$a(a);
4312
+ const index = available.findIndex(matchSubtype);
4313
+ if (index >= 0) {
4314
+ return responseTypes[index];
4315
+ }
4316
+ }
4317
+ if (available.includes(`${WILDCARD}/${WILDCARD}`)) {
4318
+ return `${WILDCARD}/${WILDCARD}`;
4319
+ }
4320
+ // wildcards in requestTypes
4321
+ for (const a of accept) {
4322
+ const matchSubtype = (t) => subtype(a) === WILDCARD && type$a(t) === type$a(a);
4323
+ const index = available.findIndex(matchSubtype);
4324
+ if (index >= 0) {
4325
+ return responseTypes[index];
4326
+ }
4327
+ }
4328
+ if (accept.includes(`${WILDCARD}/${WILDCARD}`)) {
4329
+ return responseTypes[0];
4330
+ }
4331
+ return undefined;
4332
+ }
4333
+ const getByContentType = (object = {}, contentType) => {
4334
+ const key = findMatchingType(contentType, Object.keys(object));
4335
+ return object[key];
4336
+ };
4337
+ const standardHttpRequestHeaders = [
4338
+ "accept",
4339
+ "accept-charset",
4340
+ "accept-datetime",
4341
+ "accept-encoding",
4342
+ "accept-language",
4343
+ "authorization",
4344
+ "cache-control",
4345
+ "connection",
4346
+ "content-length",
4347
+ "content-md5",
4348
+ "content-type",
4349
+ "cookie",
4350
+ "date",
4351
+ "expect",
4352
+ "forwarded",
4353
+ "from",
4354
+ "host",
4355
+ "if-match",
4356
+ "if-modified-since",
4357
+ "if-none-match",
4358
+ "if-range",
4359
+ "if-unmodified-since",
4360
+ "max-forwards",
4361
+ "origin",
4362
+ "pragma",
4363
+ "proxy-authorization",
4364
+ "range",
4365
+ "referer",
4366
+ "te",
4367
+ "upgrade",
4368
+ "user-agent",
4369
+ "via",
4370
+ "warning",
4371
+ ];
4372
+ const standardHttpResponseHeaders = [
4373
+ "access-control-allow-origin",
4374
+ "accept-patch",
4375
+ "accept-ranges",
4376
+ "age",
4377
+ "allow",
4378
+ "alt-svc",
4379
+ "cache-control",
4380
+ "connection",
4381
+ "content-disposition",
4382
+ "content-encoding",
4383
+ "content-language",
4384
+ "content-length",
4385
+ "content-location",
4386
+ "content-md5",
4387
+ "content-range",
4388
+ "date",
4389
+ "etag",
4390
+ "expires",
4391
+ "last-modified",
4392
+ "link",
4393
+ "location",
4394
+ "p3p",
4395
+ "pragma",
4396
+ "proxy-authenticate",
4397
+ "public-key-pins",
4398
+ "refresh",
4399
+ "retry-after",
4400
+ "server",
4401
+ "set-cookie",
4402
+ "status",
4403
+ "strict-transport-security",
4404
+ "trailer",
4405
+ "transfer-encoding",
4406
+ "tsv",
4407
+ "upgrade",
4408
+ "vary",
4409
+ "via",
4410
+ "warning",
4411
+ "www-authenticate",
4412
+ "x-frame-options",
4413
+ ];
4414
+
4415
+ const VALIDATABLE_CONTENT_TYPES = [
4416
+ "application/json",
4417
+ "application/x-www-form-urlencoded",
4418
+ "multipart/form-data",
4419
+ ];
4420
+ const bodyValidationStatus = (contentType, body, validatableTypes = [...VALIDATABLE_CONTENT_TYPES]) => {
4256
4421
  if (typeof contentType !== "string")
4257
- return false;
4258
- const normalized = contentType.split(";", 1)[0].trim().toLowerCase();
4259
- return normalized === "application/json" || normalized.endsWith("+json");
4422
+ return "skip";
4423
+ if (findMatchingType(contentType, validatableTypes))
4424
+ return "validate";
4425
+ return body !== undefined ? "warn" : "skip";
4260
4426
  };
4427
+
4261
4428
  function* compareMessagePayload(ajv, message, content, interactionContext, contentLocation, messagePath, direction) {
4262
- if (!canValidate$2(content.contentType))
4429
+ const status = bodyValidationStatus(content.contentType, content.payload);
4430
+ if (status === "warn") {
4431
+ yield {
4432
+ code: "message.payload.unvalidatable",
4433
+ message: `Body with content type '${content.contentType}' is not supported by the spec comparator`,
4434
+ mockDetails: {
4435
+ ...baseMockDetails(interactionContext),
4436
+ location: contentLocation,
4437
+ value: content.payload,
4438
+ },
4439
+ specDetails: {
4440
+ location: messagePath,
4441
+ value: undefined,
4442
+ },
4443
+ type: "warning",
4444
+ };
4445
+ return;
4446
+ }
4447
+ if (status === "skip")
4263
4448
  return;
4264
4449
  if (!message.payload) {
4265
4450
  if (content.payload !== undefined) {
@@ -4722,16 +4907,16 @@ function requireMultipart () {
4722
4907
  var multipartExports = requireMultipart();
4723
4908
  var multipart = /*@__PURE__*/getDefaultExportFromCjs(multipartExports);
4724
4909
 
4725
- var type$a;
4910
+ var type$9;
4726
4911
  var hasRequiredType;
4727
4912
 
4728
4913
  function requireType () {
4729
- if (hasRequiredType) return type$a;
4914
+ if (hasRequiredType) return type$9;
4730
4915
  hasRequiredType = 1;
4731
4916
 
4732
4917
  /** @type {import('./type')} */
4733
- type$a = TypeError;
4734
- return type$a;
4918
+ type$9 = TypeError;
4919
+ return type$9;
4735
4920
  }
4736
4921
 
4737
4922
  var util_inspect;
@@ -7853,166 +8038,6 @@ var qs = /*@__PURE__*/getDefaultExportFromCjs(libExports);
7853
8038
 
7854
8039
  const isValidRequest = (interaction) => interaction.response.status < 400;
7855
8040
 
7856
- const PARAMETER_SEPARATOR = ";";
7857
- const WILDCARD = "*";
7858
- const TYPE_SUBTYPE_SEPARATOR = "/";
7859
- const EXTENSION_SEPARATOR = "+";
7860
- const quality = (a) => {
7861
- const match = /.*;q=([01].?\d*)/.exec(a);
7862
- return match ? parseFloat(match[1]) : 1.0;
7863
- };
7864
- const byQuality = (a, b) => {
7865
- return quality(b) - quality(a);
7866
- };
7867
- const ignoreCasing = (t) => t.toLowerCase();
7868
- const ignoreWhitespace = (t) => t.replace(/\s+/g, "");
7869
- const ignoreParameters = (t) => t.split(PARAMETER_SEPARATOR)[0];
7870
- const removeQuality = (t) => t.replace(/;q=[01].?\d*/, "");
7871
- const type$9 = (t) => t.split(TYPE_SUBTYPE_SEPARATOR)[0];
7872
- const subtype = (t) => {
7873
- const [_maintype, subtype] = t.split(TYPE_SUBTYPE_SEPARATOR);
7874
- return subtype?.split(EXTENSION_SEPARATOR).pop();
7875
- };
7876
- function findMatchingType(requestType, responseTypes) {
7877
- // exact match
7878
- let accept = requestType
7879
- .split(",")
7880
- .map(ignoreWhitespace)
7881
- .map(ignoreCasing)
7882
- .sort(byQuality)
7883
- .map(removeQuality);
7884
- let available = responseTypes.map(ignoreWhitespace).map(ignoreCasing);
7885
- for (const a of accept) {
7886
- const matchExactly = (t) => t === a;
7887
- const index = available.findIndex(matchExactly);
7888
- if (index >= 0) {
7889
- return responseTypes[index];
7890
- }
7891
- }
7892
- // ignore additional parameters
7893
- accept = accept.map(ignoreParameters);
7894
- available = available.map(ignoreParameters);
7895
- for (const a of accept) {
7896
- const matchExactly = (t) => t === a;
7897
- const index = available.findIndex(matchExactly);
7898
- if (index >= 0) {
7899
- return responseTypes[index];
7900
- }
7901
- }
7902
- // ignore vendor extensions
7903
- for (const a of accept) {
7904
- const matchTypesAndSubtypes = (t) => type$9(t) === type$9(a) && subtype(t) === subtype(a);
7905
- const index = available.findIndex(matchTypesAndSubtypes);
7906
- if (index >= 0) {
7907
- return responseTypes[index];
7908
- }
7909
- }
7910
- // wildcards in responseTypes
7911
- for (const a of accept) {
7912
- const matchSubtype = (t) => subtype(t) === WILDCARD && type$9(t) === type$9(a);
7913
- const index = available.findIndex(matchSubtype);
7914
- if (index >= 0) {
7915
- return responseTypes[index];
7916
- }
7917
- }
7918
- if (available.includes(`${WILDCARD}/${WILDCARD}`)) {
7919
- return `${WILDCARD}/${WILDCARD}`;
7920
- }
7921
- // wildcards in requestTypes
7922
- for (const a of accept) {
7923
- const matchSubtype = (t) => subtype(a) === WILDCARD && type$9(t) === type$9(a);
7924
- const index = available.findIndex(matchSubtype);
7925
- if (index >= 0) {
7926
- return responseTypes[index];
7927
- }
7928
- }
7929
- if (accept.includes(`${WILDCARD}/${WILDCARD}`)) {
7930
- return responseTypes[0];
7931
- }
7932
- return undefined;
7933
- }
7934
- const getByContentType = (object = {}, contentType) => {
7935
- const key = findMatchingType(contentType, Object.keys(object));
7936
- return object[key];
7937
- };
7938
- const standardHttpRequestHeaders = [
7939
- "accept",
7940
- "accept-charset",
7941
- "accept-datetime",
7942
- "accept-encoding",
7943
- "accept-language",
7944
- "authorization",
7945
- "cache-control",
7946
- "connection",
7947
- "content-length",
7948
- "content-md5",
7949
- "content-type",
7950
- "cookie",
7951
- "date",
7952
- "expect",
7953
- "forwarded",
7954
- "from",
7955
- "host",
7956
- "if-match",
7957
- "if-modified-since",
7958
- "if-none-match",
7959
- "if-range",
7960
- "if-unmodified-since",
7961
- "max-forwards",
7962
- "origin",
7963
- "pragma",
7964
- "proxy-authorization",
7965
- "range",
7966
- "referer",
7967
- "te",
7968
- "upgrade",
7969
- "user-agent",
7970
- "via",
7971
- "warning",
7972
- ];
7973
- const standardHttpResponseHeaders = [
7974
- "access-control-allow-origin",
7975
- "accept-patch",
7976
- "accept-ranges",
7977
- "age",
7978
- "allow",
7979
- "alt-svc",
7980
- "cache-control",
7981
- "connection",
7982
- "content-disposition",
7983
- "content-encoding",
7984
- "content-language",
7985
- "content-length",
7986
- "content-location",
7987
- "content-md5",
7988
- "content-range",
7989
- "date",
7990
- "etag",
7991
- "expires",
7992
- "last-modified",
7993
- "link",
7994
- "location",
7995
- "p3p",
7996
- "pragma",
7997
- "proxy-authenticate",
7998
- "public-key-pins",
7999
- "refresh",
8000
- "retry-after",
8001
- "server",
8002
- "set-cookie",
8003
- "status",
8004
- "strict-transport-security",
8005
- "trailer",
8006
- "transfer-encoding",
8007
- "tsv",
8008
- "upgrade",
8009
- "vary",
8010
- "via",
8011
- "warning",
8012
- "www-authenticate",
8013
- "x-frame-options",
8014
- ];
8015
-
8016
8041
  const parseBody = (body, contentType, legacyParser) => {
8017
8042
  if (contentType.includes("application/x-www-form-urlencoded") &&
8018
8043
  typeof body === "string") {
@@ -8036,13 +8061,6 @@ const parseBody = (body, contentType, legacyParser) => {
8036
8061
  }
8037
8062
  return body;
8038
8063
  };
8039
- const canValidate$1 = (contentType, disableMultipartFormdata) => {
8040
- return !!findMatchingType(contentType, [
8041
- "application/json",
8042
- "application/x-www-form-urlencoded",
8043
- disableMultipartFormdata ? "" : "multipart/form-data",
8044
- ].filter(Boolean));
8045
- };
8046
8064
  const DEFAULT_CONTENT_TYPE$1 = "application/json";
8047
8065
  function* compareReqBody(ajv, route, interaction, index, config) {
8048
8066
  const { method, oas, operation, path } = route.store;
@@ -8062,8 +8080,30 @@ function* compareReqBody(ajv, route, interaction, index, config) {
8062
8080
  if (!required && body === undefined) {
8063
8081
  return;
8064
8082
  }
8083
+ const bodyStatus = bodyValidationStatus(contentType, body, VALIDATABLE_CONTENT_TYPES.filter((t) => t !== "multipart/form-data" ||
8084
+ !config.get("disable-multipart-formdata")));
8085
+ if (bodyStatus === "skip")
8086
+ return;
8087
+ if (bodyStatus === "warn" && isValidRequest(interaction)) {
8088
+ yield {
8089
+ code: "request.body.unvalidatable",
8090
+ message: `Body with content type '${contentType}' is not supported by the spec comparator`,
8091
+ mockDetails: {
8092
+ ...baseMockDetails(interaction),
8093
+ location: `[root].interactions[${index}].request.body`,
8094
+ value: body,
8095
+ },
8096
+ specDetails: {
8097
+ location: `[root].paths.${path}.${method}.requestBody.content`,
8098
+ pathMethod: method,
8099
+ pathName: path,
8100
+ value: get$1(operation, "requestBody.content"),
8101
+ },
8102
+ type: "warning",
8103
+ };
8104
+ return;
8105
+ }
8065
8106
  if (schema &&
8066
- canValidate$1(contentType, config.get("disable-multipart-formdata")) &&
8067
8107
  isValidRequest(interaction) &&
8068
8108
  (config.get("no-validate-request-body-unless-application-json")
8069
8109
  ? !!findMatchingType("application/json", availableRequestContentTypes)
@@ -8702,9 +8742,6 @@ function* compareReqSecurity(ajv, route, interaction, index, config) {
8702
8742
 
8703
8743
  const patternedStatus = (status) => `${Math.floor(status / 100)}XX`;
8704
8744
 
8705
- const canValidate = (contentType = "") => {
8706
- return !!findMatchingType(contentType, ["application/json"]);
8707
- };
8708
8745
  const DEFAULT_CONTENT_TYPE = "application/json";
8709
8746
  function* compareResBody(ajv, route, interaction, index, config) {
8710
8747
  const { method, oas, operation, path } = route.store;
@@ -8759,14 +8796,15 @@ function* compareResBody(ajv, route, interaction, index, config) {
8759
8796
  type: "warning",
8760
8797
  };
8761
8798
  }
8762
- if (value && canValidate(contentType) && !schema) {
8799
+ const bodyStatus = bodyValidationStatus(contentType, value);
8800
+ if (bodyStatus === "warn") {
8763
8801
  yield {
8764
- code: "response.body.unknown",
8765
- message: "No matching schema found for response body",
8802
+ code: "response.body.unvalidatable",
8803
+ message: `Body with content type '${contentType}' is not supported by the spec comparator`,
8766
8804
  mockDetails: {
8767
8805
  ...baseMockDetails(interaction),
8768
8806
  location: `[root].interactions[${index}].response.body`,
8769
- value: value,
8807
+ value,
8770
8808
  },
8771
8809
  specDetails: {
8772
8810
  location: `[root].paths.${path}.${method}.responses.${status}.content`,
@@ -8774,32 +8812,52 @@ function* compareResBody(ajv, route, interaction, index, config) {
8774
8812
  pathName: path,
8775
8813
  value: response.content,
8776
8814
  },
8777
- type: "error",
8815
+ type: "warning",
8778
8816
  };
8779
8817
  }
8780
- if (value && canValidate(contentType) && schema) {
8781
- const schemaId = `[root].paths.${path}.${method}.responses.${status}.content.${contentType}`;
8782
- const validate = getValidateFunction(ajv, schemaId, () => transformReceivedSchema(minimumSchema(schema, oas), config.get("no-transform-non-nullable-response-schema")));
8783
- if (!validate(value)) {
8784
- for (const error of validate.errors) {
8785
- yield {
8786
- code: "response.body.incompatible",
8787
- message: `Response body is incompatible with the response body schema in the spec file: ${formatMessage(error)}`,
8788
- mockDetails: {
8789
- ...baseMockDetails(interaction),
8790
- location: `[root].interactions[${index}].response.body${formatInstancePath(error)}`,
8791
- value: error.instancePath
8792
- ? get$1(value, splitPath(error.instancePath))
8793
- : value,
8794
- },
8795
- specDetails: {
8796
- location: `${schemaId}.schema.${formatSchemaPath(error)}`,
8797
- pathMethod: method,
8798
- pathName: path,
8799
- value: get$1(validate.schema, splitPath(error.schemaPath)),
8800
- },
8801
- type: "error",
8802
- };
8818
+ else if (bodyStatus === "validate") {
8819
+ if (value && !schema) {
8820
+ yield {
8821
+ code: "response.body.unknown",
8822
+ message: "No matching schema found for response body",
8823
+ mockDetails: {
8824
+ ...baseMockDetails(interaction),
8825
+ location: `[root].interactions[${index}].response.body`,
8826
+ value: value,
8827
+ },
8828
+ specDetails: {
8829
+ location: `[root].paths.${path}.${method}.responses.${status}.content`,
8830
+ pathMethod: method,
8831
+ pathName: path,
8832
+ value: response.content,
8833
+ },
8834
+ type: "error",
8835
+ };
8836
+ }
8837
+ if (value && schema) {
8838
+ const schemaId = `[root].paths.${path}.${method}.responses.${status}.content.${contentType}`;
8839
+ const validate = getValidateFunction(ajv, schemaId, () => transformReceivedSchema(minimumSchema(schema, oas), config.get("no-transform-non-nullable-response-schema")));
8840
+ if (!validate(value)) {
8841
+ for (const error of validate.errors) {
8842
+ yield {
8843
+ code: "response.body.incompatible",
8844
+ message: `Response body is incompatible with the response body schema in the spec file: ${formatMessage(error)}`,
8845
+ mockDetails: {
8846
+ ...baseMockDetails(interaction),
8847
+ location: `[root].interactions[${index}].response.body${formatInstancePath(error)}`,
8848
+ value: error.instancePath
8849
+ ? get$1(value, splitPath(error.instancePath))
8850
+ : value,
8851
+ },
8852
+ specDetails: {
8853
+ location: `${schemaId}.schema.${formatSchemaPath(error)}`,
8854
+ pathMethod: method,
8855
+ pathName: path,
8856
+ value: get$1(validate.schema, splitPath(error.schemaPath)),
8857
+ },
8858
+ type: "error",
8859
+ };
8860
+ }
8803
8861
  }
8804
8862
  }
8805
8863
  }
@@ -8995,11 +9053,17 @@ function* compareHttpInteraction(ajvCoerce, ajvNocoerce, router, interaction, in
8995
9053
  return;
8996
9054
  }
8997
9055
  yield* compareReqSecurity(ajvCoerce, route, interaction, index, config);
8998
- yield* compareReqHeader(ajvCoerce, route, interaction, index, config);
9056
+ const reqHeaderResults = Array.from(compareReqHeader(ajvCoerce, route, interaction, index, config));
9057
+ yield* reqHeaderResults;
8999
9058
  yield* compareReqQuery(ajvCoerce, route, interaction, index, config);
9000
- yield* compareReqBody(ajvNocoerce, route, interaction, index, config);
9001
- yield* compareResHeader(ajvCoerce, route, interaction, index);
9002
- yield* compareResBody(ajvNocoerce, route, interaction, index, config);
9059
+ if (!reqHeaderResults.some((r) => r.code === "request.content-type.incompatible")) {
9060
+ yield* compareReqBody(ajvNocoerce, route, interaction, index, config);
9061
+ }
9062
+ const resHeaderResults = Array.from(compareResHeader(ajvCoerce, route, interaction, index));
9063
+ yield* resHeaderResults;
9064
+ if (!resHeaderResults.some((r) => r.code === "response.content-type.incompatible")) {
9065
+ yield* compareResBody(ajvNocoerce, route, interaction, index, config);
9066
+ }
9003
9067
  }
9004
9068
 
9005
9069
  const isSwagger2 = (oas) => Object.hasOwn(oas, "swagger") &&