Skip to content

Commit 7f243f7

Browse files
authored
fix: handle content encoding in content-types in OAS (#40)
1 parent 2e1da2a commit 7f243f7

File tree

8 files changed

+66
-23
lines changed

8 files changed

+66
-23
lines changed

src/__tests__/fixtures/response-content-negotiation/baseline.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"mockDetails": {
66
"interactionDescription": "should error on unknown request accept",
77
"interactionState": "[none]",
8-
"location": "[root].interactions[1].request.headers.accept",
8+
"location": "[root].interactions[2].request.headers.accept",
99
"value": "application/xxx"
1010
},
1111
"specDetails": {
@@ -29,7 +29,7 @@
2929
"mockDetails": {
3030
"interactionDescription": "should error on incompatible request accept",
3131
"interactionState": "[none]",
32-
"location": "[root].interactions[2].request.headers.accept",
32+
"location": "[root].interactions[3].request.headers.accept",
3333
"value": "application/xxx"
3434
},
3535
"specDetails": {

src/__tests__/fixtures/response-content-negotiation/oas.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,17 @@ paths:
1717
content:
1818
application/aaa: {}
1919
application/bbb: {}
20+
/with-encoding:
21+
get:
22+
responses:
23+
"200":
24+
description: OK
25+
content:
26+
"application/aaa; utf-8":
27+
schema:
28+
type: object
29+
required:
30+
- name
31+
properties:
32+
name:
33+
type: number

src/__tests__/fixtures/response-content-negotiation/pact.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"interactions": [
55
{
66
"description": "should pass on successful response content negotiation",
7-
"providerState": "",
87
"request": {
98
"method": "GET",
109
"path": "/with-content",
@@ -19,9 +18,27 @@
1918
}
2019
}
2120
},
21+
{
22+
"description": "should pass on successful response content negotiation, with encoding",
23+
"request": {
24+
"method": "GET",
25+
"path": "/with-encoding",
26+
"headers": {
27+
"accept": "application/aaa"
28+
}
29+
},
30+
"response": {
31+
"status": 200,
32+
"headers": {
33+
"Content-Type": "application/aaa"
34+
},
35+
"body": {
36+
"name": "name"
37+
}
38+
}
39+
},
2240
{
2341
"description": "should error on unknown request accept",
24-
"providerState": "",
2542
"request": {
2643
"method": "GET",
2744
"path": "/no-content",
@@ -35,7 +52,6 @@
3552
},
3653
{
3754
"description": "should error on incompatible request accept",
38-
"providerState": "",
3955
"request": {
4056
"method": "GET",
4157
"path": "/with-content",

src/__tests__/fixtures/response-content-negotiation/results.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"mockDetails": {
66
"interactionDescription": "should error on unknown request accept",
77
"interactionState": "[none]",
8-
"location": "[root].interactions[1].request.headers.accept",
8+
"location": "[root].interactions[2].request.headers.accept",
99
"value": "application/xxx"
1010
},
1111
"specDetails": {
@@ -29,7 +29,7 @@
2929
"mockDetails": {
3030
"interactionDescription": "should error on incompatible request accept",
3131
"interactionState": "[none]",
32-
"location": "[root].interactions[2].request.headers.accept",
32+
"location": "[root].interactions[3].request.headers.accept",
3333
"value": "application/xxx"
3434
},
3535
"specDetails": {

src/compare/requestBody.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { minimumSchema, transformRequestSchema } from "../transform/index";
1717
import { isValidRequest } from "../utils/interaction";
1818
import { dereferenceOas, splitPath } from "../utils/schema";
1919
import { getValidateFunction } from "../utils/validation";
20-
import { findMatchingType } from "./utils/content";
20+
import { findMatchingType, getByContentType } from "./utils/content";
2121

2222
const parseBody = (body: unknown, contentType: string) => {
2323
if (
@@ -56,14 +56,17 @@ export function* compareReqBody(
5656
Object.keys(
5757
dereferenceOas(operation.requestBody || {}, oas)?.content || {},
5858
);
59-
const contentType =
60-
findMatchingType(
61-
requestHeaders.get("content-type") || DEFAULT_CONTENT_TYPE,
62-
availableRequestContentTypes,
63-
) || DEFAULT_CONTENT_TYPE;
59+
60+
const requestContentType = requestHeaders.get("content-type") || "";
61+
const contentType = requestContentType
62+
? findMatchingType(requestContentType, availableRequestContentTypes) ||
63+
requestContentType
64+
: DEFAULT_CONTENT_TYPE;
6465
const schema: SchemaObject | undefined =
65-
dereferenceOas(operation.requestBody || {}, oas)?.content?.[contentType]
66-
?.schema ||
66+
getByContentType(
67+
dereferenceOas(operation.requestBody || {}, oas)?.content,
68+
contentType,
69+
)?.schema ||
6770
dereferenceOas(
6871
(operation.parameters || []).find(
6972
(p: OpenAPIV2.ParameterObject) => p.in === "body",
@@ -72,7 +75,7 @@ export function* compareReqBody(
7275
)?.schema;
7376

7477
if (schema && canValidate(contentType) && isValidRequest(interaction)) {
75-
const value = parseBody(body, contentType);
78+
const value = parseBody(body, requestContentType);
7679
const schemaId = `[root].paths.${path}.${method}.requestBody.content.${contentType}`;
7780
const validate = getValidateFunction(ajv, schemaId, () =>
7881
transformRequestSchema(minimumSchema(schema, oas)),

src/compare/requestHeader.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,7 @@ export function* compareReqHeader(
109109
const requestContentType: string =
110110
requestHeaders.get("content-type")?.split(";")[0] || "";
111111

112-
if (
113-
requestContentType &&
114-
!availableRequestContentType.length &&
115-
isValidRequest(interaction)
116-
) {
112+
if (requestContentType && !availableRequestContentType.length) {
117113
yield {
118114
code: "request.content-type.unknown",
119115
message:

src/compare/responseBody.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { minimumSchema, transformResponseSchema } from "../transform/index";
1616
import { dereferenceOas, splitPath } from "../utils/schema";
1717
import { getValidateFunction } from "../utils/validation";
18-
import { findMatchingType } from "./utils/content";
18+
import { findMatchingType, getByContentType } from "./utils/content";
1919

2020
const canValidate = (contentType = ""): boolean => {
2121
return !!findMatchingType(contentType, ["application/json"]);
@@ -74,7 +74,8 @@ export function* compareResBody(
7474
const dereferencedResponse = dereferenceOas(response, oas);
7575
const schema: SchemaObject | undefined =
7676
(dereferencedResponse as OpenAPIV2.ResponseObject)?.schema ||
77-
dereferencedResponse.content?.[contentType]?.schema;
77+
getByContentType(dereferencedResponse.content || {}, contentType)?.schema;
78+
7879
const value = body;
7980

8081
if (!statusResponse) {

src/compare/utils/content.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { SchemaObject } from "ajv";
2+
13
const PARAMETER_SEPARATOR = ";";
24
const WILDCARD = "*";
35
const TYPE_SUBTYPE_SEPARATOR = "/";
@@ -91,6 +93,17 @@ export function findMatchingType(
9193
return undefined;
9294
}
9395

96+
export const getByContentType = (
97+
object = {},
98+
contentType: string,
99+
): { schema: SchemaObject } => {
100+
const key = findMatchingType(
101+
contentType,
102+
Object.keys(object),
103+
) as keyof object;
104+
return object[key];
105+
};
106+
94107
export const standardHttpRequestHeaders = [
95108
"accept",
96109
"accept-charset",

0 commit comments

Comments
 (0)