@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/cli.cjs CHANGED
@@ -4177,7 +4177,7 @@ function useColor() {
4177
4177
 
4178
4178
  const program = new Command();
4179
4179
 
4180
- var version = "2.0.1";
4180
+ var version = "2.2.0";
4181
4181
  var packageJson = {
4182
4182
  version: version};
4183
4183
 
@@ -10808,14 +10808,199 @@ function* compareMessageHeaders(ajv, message, content, interactionContext, metad
10808
10808
  }
10809
10809
  }
10810
10810
 
10811
- const canValidate$2 = (contentType) => {
10811
+ const PARAMETER_SEPARATOR = ";";
10812
+ const WILDCARD = "*";
10813
+ const TYPE_SUBTYPE_SEPARATOR = "/";
10814
+ const EXTENSION_SEPARATOR = "+";
10815
+ const quality = (a) => {
10816
+ const match = /.*;q=([01].?\d*)/.exec(a);
10817
+ return match ? parseFloat(match[1]) : 1.0;
10818
+ };
10819
+ const byQuality = (a, b) => {
10820
+ return quality(b) - quality(a);
10821
+ };
10822
+ const ignoreCasing = (t) => t.toLowerCase();
10823
+ const ignoreWhitespace = (t) => t.replace(/\s+/g, "");
10824
+ const ignoreParameters = (t) => t.split(PARAMETER_SEPARATOR)[0];
10825
+ const removeQuality = (t) => t.replace(/;q=[01].?\d*/, "");
10826
+ const type$a = (t) => t.split(TYPE_SUBTYPE_SEPARATOR)[0];
10827
+ const subtype = (t) => {
10828
+ const [_maintype, subtype] = t.split(TYPE_SUBTYPE_SEPARATOR);
10829
+ return subtype?.split(EXTENSION_SEPARATOR).pop();
10830
+ };
10831
+ function findMatchingType(requestType, responseTypes) {
10832
+ // exact match
10833
+ let accept = requestType
10834
+ .split(",")
10835
+ .map(ignoreWhitespace)
10836
+ .map(ignoreCasing)
10837
+ .sort(byQuality)
10838
+ .map(removeQuality);
10839
+ let available = responseTypes.map(ignoreWhitespace).map(ignoreCasing);
10840
+ for (const a of accept) {
10841
+ const matchExactly = (t) => t === a;
10842
+ const index = available.findIndex(matchExactly);
10843
+ if (index >= 0) {
10844
+ return responseTypes[index];
10845
+ }
10846
+ }
10847
+ // ignore additional parameters
10848
+ accept = accept.map(ignoreParameters);
10849
+ available = available.map(ignoreParameters);
10850
+ for (const a of accept) {
10851
+ const matchExactly = (t) => t === a;
10852
+ const index = available.findIndex(matchExactly);
10853
+ if (index >= 0) {
10854
+ return responseTypes[index];
10855
+ }
10856
+ }
10857
+ // ignore vendor extensions
10858
+ for (const a of accept) {
10859
+ const matchTypesAndSubtypes = (t) => type$a(t) === type$a(a) && subtype(t) === subtype(a);
10860
+ const index = available.findIndex(matchTypesAndSubtypes);
10861
+ if (index >= 0) {
10862
+ return responseTypes[index];
10863
+ }
10864
+ }
10865
+ // wildcards in responseTypes
10866
+ for (const a of accept) {
10867
+ const matchSubtype = (t) => subtype(t) === WILDCARD && type$a(t) === type$a(a);
10868
+ const index = available.findIndex(matchSubtype);
10869
+ if (index >= 0) {
10870
+ return responseTypes[index];
10871
+ }
10872
+ }
10873
+ if (available.includes(`${WILDCARD}/${WILDCARD}`)) {
10874
+ return `${WILDCARD}/${WILDCARD}`;
10875
+ }
10876
+ // wildcards in requestTypes
10877
+ for (const a of accept) {
10878
+ const matchSubtype = (t) => subtype(a) === WILDCARD && type$a(t) === type$a(a);
10879
+ const index = available.findIndex(matchSubtype);
10880
+ if (index >= 0) {
10881
+ return responseTypes[index];
10882
+ }
10883
+ }
10884
+ if (accept.includes(`${WILDCARD}/${WILDCARD}`)) {
10885
+ return responseTypes[0];
10886
+ }
10887
+ return undefined;
10888
+ }
10889
+ const getByContentType = (object = {}, contentType) => {
10890
+ const key = findMatchingType(contentType, Object.keys(object));
10891
+ return object[key];
10892
+ };
10893
+ const standardHttpRequestHeaders = [
10894
+ "accept",
10895
+ "accept-charset",
10896
+ "accept-datetime",
10897
+ "accept-encoding",
10898
+ "accept-language",
10899
+ "authorization",
10900
+ "cache-control",
10901
+ "connection",
10902
+ "content-length",
10903
+ "content-md5",
10904
+ "content-type",
10905
+ "cookie",
10906
+ "date",
10907
+ "expect",
10908
+ "forwarded",
10909
+ "from",
10910
+ "host",
10911
+ "if-match",
10912
+ "if-modified-since",
10913
+ "if-none-match",
10914
+ "if-range",
10915
+ "if-unmodified-since",
10916
+ "max-forwards",
10917
+ "origin",
10918
+ "pragma",
10919
+ "proxy-authorization",
10920
+ "range",
10921
+ "referer",
10922
+ "te",
10923
+ "upgrade",
10924
+ "user-agent",
10925
+ "via",
10926
+ "warning",
10927
+ ];
10928
+ const standardHttpResponseHeaders = [
10929
+ "access-control-allow-origin",
10930
+ "accept-patch",
10931
+ "accept-ranges",
10932
+ "age",
10933
+ "allow",
10934
+ "alt-svc",
10935
+ "cache-control",
10936
+ "connection",
10937
+ "content-disposition",
10938
+ "content-encoding",
10939
+ "content-language",
10940
+ "content-length",
10941
+ "content-location",
10942
+ "content-md5",
10943
+ "content-range",
10944
+ "date",
10945
+ "etag",
10946
+ "expires",
10947
+ "last-modified",
10948
+ "link",
10949
+ "location",
10950
+ "p3p",
10951
+ "pragma",
10952
+ "proxy-authenticate",
10953
+ "public-key-pins",
10954
+ "refresh",
10955
+ "retry-after",
10956
+ "server",
10957
+ "set-cookie",
10958
+ "status",
10959
+ "strict-transport-security",
10960
+ "trailer",
10961
+ "transfer-encoding",
10962
+ "tsv",
10963
+ "upgrade",
10964
+ "vary",
10965
+ "via",
10966
+ "warning",
10967
+ "www-authenticate",
10968
+ "x-frame-options",
10969
+ ];
10970
+
10971
+ const VALIDATABLE_CONTENT_TYPES = [
10972
+ "application/json",
10973
+ "application/x-www-form-urlencoded",
10974
+ "multipart/form-data",
10975
+ ];
10976
+ const bodyValidationStatus = (contentType, body, validatableTypes = [...VALIDATABLE_CONTENT_TYPES]) => {
10812
10977
  if (typeof contentType !== "string")
10813
- return false;
10814
- const normalized = contentType.split(";", 1)[0].trim().toLowerCase();
10815
- return normalized === "application/json" || normalized.endsWith("+json");
10978
+ return "skip";
10979
+ if (findMatchingType(contentType, validatableTypes))
10980
+ return "validate";
10981
+ return body !== undefined ? "warn" : "skip";
10816
10982
  };
10983
+
10817
10984
  function* compareMessagePayload(ajv, message, content, interactionContext, contentLocation, messagePath, direction) {
10818
- if (!canValidate$2(content.contentType))
10985
+ const status = bodyValidationStatus(content.contentType, content.payload);
10986
+ if (status === "warn") {
10987
+ yield {
10988
+ code: "message.payload.unvalidatable",
10989
+ message: `Body with content type '${content.contentType}' is not supported by the spec comparator`,
10990
+ mockDetails: {
10991
+ ...baseMockDetails(interactionContext),
10992
+ location: contentLocation,
10993
+ value: content.payload,
10994
+ },
10995
+ specDetails: {
10996
+ location: messagePath,
10997
+ value: undefined,
10998
+ },
10999
+ type: "warning",
11000
+ };
11001
+ return;
11002
+ }
11003
+ if (status === "skip")
10819
11004
  return;
10820
11005
  if (!message.payload) {
10821
11006
  if (content.payload !== undefined) {
@@ -11278,16 +11463,16 @@ function requireMultipart () {
11278
11463
  var multipartExports = requireMultipart();
11279
11464
  var multipart = /*@__PURE__*/getDefaultExportFromCjs(multipartExports);
11280
11465
 
11281
- var type$a;
11466
+ var type$9;
11282
11467
  var hasRequiredType;
11283
11468
 
11284
11469
  function requireType () {
11285
- if (hasRequiredType) return type$a;
11470
+ if (hasRequiredType) return type$9;
11286
11471
  hasRequiredType = 1;
11287
11472
 
11288
11473
  /** @type {import('./type')} */
11289
- type$a = TypeError;
11290
- return type$a;
11474
+ type$9 = TypeError;
11475
+ return type$9;
11291
11476
  }
11292
11477
 
11293
11478
  var util_inspect;
@@ -14409,166 +14594,6 @@ var qs = /*@__PURE__*/getDefaultExportFromCjs(libExports);
14409
14594
 
14410
14595
  const isValidRequest = (interaction) => interaction.response.status < 400;
14411
14596
 
14412
- const PARAMETER_SEPARATOR = ";";
14413
- const WILDCARD = "*";
14414
- const TYPE_SUBTYPE_SEPARATOR = "/";
14415
- const EXTENSION_SEPARATOR = "+";
14416
- const quality = (a) => {
14417
- const match = /.*;q=([01].?\d*)/.exec(a);
14418
- return match ? parseFloat(match[1]) : 1.0;
14419
- };
14420
- const byQuality = (a, b) => {
14421
- return quality(b) - quality(a);
14422
- };
14423
- const ignoreCasing = (t) => t.toLowerCase();
14424
- const ignoreWhitespace = (t) => t.replace(/\s+/g, "");
14425
- const ignoreParameters = (t) => t.split(PARAMETER_SEPARATOR)[0];
14426
- const removeQuality = (t) => t.replace(/;q=[01].?\d*/, "");
14427
- const type$9 = (t) => t.split(TYPE_SUBTYPE_SEPARATOR)[0];
14428
- const subtype = (t) => {
14429
- const [_maintype, subtype] = t.split(TYPE_SUBTYPE_SEPARATOR);
14430
- return subtype?.split(EXTENSION_SEPARATOR).pop();
14431
- };
14432
- function findMatchingType(requestType, responseTypes) {
14433
- // exact match
14434
- let accept = requestType
14435
- .split(",")
14436
- .map(ignoreWhitespace)
14437
- .map(ignoreCasing)
14438
- .sort(byQuality)
14439
- .map(removeQuality);
14440
- let available = responseTypes.map(ignoreWhitespace).map(ignoreCasing);
14441
- for (const a of accept) {
14442
- const matchExactly = (t) => t === a;
14443
- const index = available.findIndex(matchExactly);
14444
- if (index >= 0) {
14445
- return responseTypes[index];
14446
- }
14447
- }
14448
- // ignore additional parameters
14449
- accept = accept.map(ignoreParameters);
14450
- available = available.map(ignoreParameters);
14451
- for (const a of accept) {
14452
- const matchExactly = (t) => t === a;
14453
- const index = available.findIndex(matchExactly);
14454
- if (index >= 0) {
14455
- return responseTypes[index];
14456
- }
14457
- }
14458
- // ignore vendor extensions
14459
- for (const a of accept) {
14460
- const matchTypesAndSubtypes = (t) => type$9(t) === type$9(a) && subtype(t) === subtype(a);
14461
- const index = available.findIndex(matchTypesAndSubtypes);
14462
- if (index >= 0) {
14463
- return responseTypes[index];
14464
- }
14465
- }
14466
- // wildcards in responseTypes
14467
- for (const a of accept) {
14468
- const matchSubtype = (t) => subtype(t) === WILDCARD && type$9(t) === type$9(a);
14469
- const index = available.findIndex(matchSubtype);
14470
- if (index >= 0) {
14471
- return responseTypes[index];
14472
- }
14473
- }
14474
- if (available.includes(`${WILDCARD}/${WILDCARD}`)) {
14475
- return `${WILDCARD}/${WILDCARD}`;
14476
- }
14477
- // wildcards in requestTypes
14478
- for (const a of accept) {
14479
- const matchSubtype = (t) => subtype(a) === WILDCARD && type$9(t) === type$9(a);
14480
- const index = available.findIndex(matchSubtype);
14481
- if (index >= 0) {
14482
- return responseTypes[index];
14483
- }
14484
- }
14485
- if (accept.includes(`${WILDCARD}/${WILDCARD}`)) {
14486
- return responseTypes[0];
14487
- }
14488
- return undefined;
14489
- }
14490
- const getByContentType = (object = {}, contentType) => {
14491
- const key = findMatchingType(contentType, Object.keys(object));
14492
- return object[key];
14493
- };
14494
- const standardHttpRequestHeaders = [
14495
- "accept",
14496
- "accept-charset",
14497
- "accept-datetime",
14498
- "accept-encoding",
14499
- "accept-language",
14500
- "authorization",
14501
- "cache-control",
14502
- "connection",
14503
- "content-length",
14504
- "content-md5",
14505
- "content-type",
14506
- "cookie",
14507
- "date",
14508
- "expect",
14509
- "forwarded",
14510
- "from",
14511
- "host",
14512
- "if-match",
14513
- "if-modified-since",
14514
- "if-none-match",
14515
- "if-range",
14516
- "if-unmodified-since",
14517
- "max-forwards",
14518
- "origin",
14519
- "pragma",
14520
- "proxy-authorization",
14521
- "range",
14522
- "referer",
14523
- "te",
14524
- "upgrade",
14525
- "user-agent",
14526
- "via",
14527
- "warning",
14528
- ];
14529
- const standardHttpResponseHeaders = [
14530
- "access-control-allow-origin",
14531
- "accept-patch",
14532
- "accept-ranges",
14533
- "age",
14534
- "allow",
14535
- "alt-svc",
14536
- "cache-control",
14537
- "connection",
14538
- "content-disposition",
14539
- "content-encoding",
14540
- "content-language",
14541
- "content-length",
14542
- "content-location",
14543
- "content-md5",
14544
- "content-range",
14545
- "date",
14546
- "etag",
14547
- "expires",
14548
- "last-modified",
14549
- "link",
14550
- "location",
14551
- "p3p",
14552
- "pragma",
14553
- "proxy-authenticate",
14554
- "public-key-pins",
14555
- "refresh",
14556
- "retry-after",
14557
- "server",
14558
- "set-cookie",
14559
- "status",
14560
- "strict-transport-security",
14561
- "trailer",
14562
- "transfer-encoding",
14563
- "tsv",
14564
- "upgrade",
14565
- "vary",
14566
- "via",
14567
- "warning",
14568
- "www-authenticate",
14569
- "x-frame-options",
14570
- ];
14571
-
14572
14597
  const parseBody = (body, contentType, legacyParser) => {
14573
14598
  if (contentType.includes("application/x-www-form-urlencoded") &&
14574
14599
  typeof body === "string") {
@@ -14592,13 +14617,6 @@ const parseBody = (body, contentType, legacyParser) => {
14592
14617
  }
14593
14618
  return body;
14594
14619
  };
14595
- const canValidate$1 = (contentType, disableMultipartFormdata) => {
14596
- return !!findMatchingType(contentType, [
14597
- "application/json",
14598
- "application/x-www-form-urlencoded",
14599
- disableMultipartFormdata ? "" : "multipart/form-data",
14600
- ].filter(Boolean));
14601
- };
14602
14620
  const DEFAULT_CONTENT_TYPE$1 = "application/json";
14603
14621
  function* compareReqBody(ajv, route, interaction, index, config) {
14604
14622
  const { method, oas, operation, path } = route.store;
@@ -14618,8 +14636,30 @@ function* compareReqBody(ajv, route, interaction, index, config) {
14618
14636
  if (!required && body === undefined) {
14619
14637
  return;
14620
14638
  }
14639
+ const bodyStatus = bodyValidationStatus(contentType, body, VALIDATABLE_CONTENT_TYPES.filter((t) => t !== "multipart/form-data" ||
14640
+ !config.get("disable-multipart-formdata")));
14641
+ if (bodyStatus === "skip")
14642
+ return;
14643
+ if (bodyStatus === "warn" && isValidRequest(interaction)) {
14644
+ yield {
14645
+ code: "request.body.unvalidatable",
14646
+ message: `Body with content type '${contentType}' is not supported by the spec comparator`,
14647
+ mockDetails: {
14648
+ ...baseMockDetails(interaction),
14649
+ location: `[root].interactions[${index}].request.body`,
14650
+ value: body,
14651
+ },
14652
+ specDetails: {
14653
+ location: `[root].paths.${path}.${method}.requestBody.content`,
14654
+ pathMethod: method,
14655
+ pathName: path,
14656
+ value: get$1(operation, "requestBody.content"),
14657
+ },
14658
+ type: "warning",
14659
+ };
14660
+ return;
14661
+ }
14621
14662
  if (schema &&
14622
- canValidate$1(contentType, config.get("disable-multipart-formdata")) &&
14623
14663
  isValidRequest(interaction) &&
14624
14664
  (config.get("no-validate-request-body-unless-application-json")
14625
14665
  ? !!findMatchingType("application/json", availableRequestContentTypes)
@@ -15258,9 +15298,6 @@ function* compareReqSecurity(ajv, route, interaction, index, config) {
15258
15298
 
15259
15299
  const patternedStatus = (status) => `${Math.floor(status / 100)}XX`;
15260
15300
 
15261
- const canValidate = (contentType = "") => {
15262
- return !!findMatchingType(contentType, ["application/json"]);
15263
- };
15264
15301
  const DEFAULT_CONTENT_TYPE = "application/json";
15265
15302
  function* compareResBody(ajv, route, interaction, index, config) {
15266
15303
  const { method, oas, operation, path } = route.store;
@@ -15315,14 +15352,15 @@ function* compareResBody(ajv, route, interaction, index, config) {
15315
15352
  type: "warning",
15316
15353
  };
15317
15354
  }
15318
- if (value && canValidate(contentType) && !schema) {
15355
+ const bodyStatus = bodyValidationStatus(contentType, value);
15356
+ if (bodyStatus === "warn") {
15319
15357
  yield {
15320
- code: "response.body.unknown",
15321
- message: "No matching schema found for response body",
15358
+ code: "response.body.unvalidatable",
15359
+ message: `Body with content type '${contentType}' is not supported by the spec comparator`,
15322
15360
  mockDetails: {
15323
15361
  ...baseMockDetails(interaction),
15324
15362
  location: `[root].interactions[${index}].response.body`,
15325
- value: value,
15363
+ value,
15326
15364
  },
15327
15365
  specDetails: {
15328
15366
  location: `[root].paths.${path}.${method}.responses.${status}.content`,
@@ -15330,32 +15368,52 @@ function* compareResBody(ajv, route, interaction, index, config) {
15330
15368
  pathName: path,
15331
15369
  value: response.content,
15332
15370
  },
15333
- type: "error",
15371
+ type: "warning",
15334
15372
  };
15335
15373
  }
15336
- if (value && canValidate(contentType) && schema) {
15337
- const schemaId = `[root].paths.${path}.${method}.responses.${status}.content.${contentType}`;
15338
- const validate = getValidateFunction(ajv, schemaId, () => transformReceivedSchema(minimumSchema(schema, oas), config.get("no-transform-non-nullable-response-schema")));
15339
- if (!validate(value)) {
15340
- for (const error of validate.errors) {
15341
- yield {
15342
- code: "response.body.incompatible",
15343
- message: `Response body is incompatible with the response body schema in the spec file: ${formatMessage(error)}`,
15344
- mockDetails: {
15345
- ...baseMockDetails(interaction),
15346
- location: `[root].interactions[${index}].response.body${formatInstancePath(error)}`,
15347
- value: error.instancePath
15348
- ? get$1(value, splitPath(error.instancePath))
15349
- : value,
15350
- },
15351
- specDetails: {
15352
- location: `${schemaId}.schema.${formatSchemaPath(error)}`,
15353
- pathMethod: method,
15354
- pathName: path,
15355
- value: get$1(validate.schema, splitPath(error.schemaPath)),
15356
- },
15357
- type: "error",
15358
- };
15374
+ else if (bodyStatus === "validate") {
15375
+ if (value && !schema) {
15376
+ yield {
15377
+ code: "response.body.unknown",
15378
+ message: "No matching schema found for response body",
15379
+ mockDetails: {
15380
+ ...baseMockDetails(interaction),
15381
+ location: `[root].interactions[${index}].response.body`,
15382
+ value: value,
15383
+ },
15384
+ specDetails: {
15385
+ location: `[root].paths.${path}.${method}.responses.${status}.content`,
15386
+ pathMethod: method,
15387
+ pathName: path,
15388
+ value: response.content,
15389
+ },
15390
+ type: "error",
15391
+ };
15392
+ }
15393
+ if (value && schema) {
15394
+ const schemaId = `[root].paths.${path}.${method}.responses.${status}.content.${contentType}`;
15395
+ const validate = getValidateFunction(ajv, schemaId, () => transformReceivedSchema(minimumSchema(schema, oas), config.get("no-transform-non-nullable-response-schema")));
15396
+ if (!validate(value)) {
15397
+ for (const error of validate.errors) {
15398
+ yield {
15399
+ code: "response.body.incompatible",
15400
+ message: `Response body is incompatible with the response body schema in the spec file: ${formatMessage(error)}`,
15401
+ mockDetails: {
15402
+ ...baseMockDetails(interaction),
15403
+ location: `[root].interactions[${index}].response.body${formatInstancePath(error)}`,
15404
+ value: error.instancePath
15405
+ ? get$1(value, splitPath(error.instancePath))
15406
+ : value,
15407
+ },
15408
+ specDetails: {
15409
+ location: `${schemaId}.schema.${formatSchemaPath(error)}`,
15410
+ pathMethod: method,
15411
+ pathName: path,
15412
+ value: get$1(validate.schema, splitPath(error.schemaPath)),
15413
+ },
15414
+ type: "error",
15415
+ };
15416
+ }
15359
15417
  }
15360
15418
  }
15361
15419
  }
@@ -15551,11 +15609,17 @@ function* compareHttpInteraction(ajvCoerce, ajvNocoerce, router, interaction, in
15551
15609
  return;
15552
15610
  }
15553
15611
  yield* compareReqSecurity(ajvCoerce, route, interaction, index, config);
15554
- yield* compareReqHeader(ajvCoerce, route, interaction, index, config);
15612
+ const reqHeaderResults = Array.from(compareReqHeader(ajvCoerce, route, interaction, index, config));
15613
+ yield* reqHeaderResults;
15555
15614
  yield* compareReqQuery(ajvCoerce, route, interaction, index, config);
15556
- yield* compareReqBody(ajvNocoerce, route, interaction, index, config);
15557
- yield* compareResHeader(ajvCoerce, route, interaction, index);
15558
- yield* compareResBody(ajvNocoerce, route, interaction, index, config);
15615
+ if (!reqHeaderResults.some((r) => r.code === "request.content-type.incompatible")) {
15616
+ yield* compareReqBody(ajvNocoerce, route, interaction, index, config);
15617
+ }
15618
+ const resHeaderResults = Array.from(compareResHeader(ajvCoerce, route, interaction, index));
15619
+ yield* resHeaderResults;
15620
+ if (!resHeaderResults.some((r) => r.code === "response.content-type.incompatible")) {
15621
+ yield* compareResBody(ajvNocoerce, route, interaction, index, config);
15622
+ }
15559
15623
  }
15560
15624
 
15561
15625
  const isSwagger2 = (oas) => Object.hasOwn(oas, "swagger") &&