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