From 46e7d7ed6d3916cacc66ff696f4463dde6d2ff0c Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Wed, 3 Aug 2022 10:28:07 +0200 Subject: [PATCH 1/6] Add basic request body checking We now check the request body to see if content was set. For now only works with application/json --- scenarios/simple/checkRequestBody/http.json | 23 ++++++ .../simple/checkRequestBody/openapi.yaml | 23 ++++++ .../simple/requestBodyNotDefined/http.json | 23 ++++++ .../simple/requestBodyNotDefined/openapi.yaml | 17 +++++ src/api/api.test.ts | 18 ++--- src/api/index.ts | 29 +++++-- src/api/model.ts | 54 ++++++++++++- src/rule/index.test.ts | 75 ++++++++++++++++++- src/rule/index.ts | 53 ++++++++++++- src/rule/model.ts | 9 +++ 10 files changed, 299 insertions(+), 25 deletions(-) create mode 100644 scenarios/simple/checkRequestBody/http.json create mode 100644 scenarios/simple/checkRequestBody/openapi.yaml create mode 100644 scenarios/simple/requestBodyNotDefined/http.json create mode 100644 scenarios/simple/requestBodyNotDefined/openapi.yaml diff --git a/scenarios/simple/checkRequestBody/http.json b/scenarios/simple/checkRequestBody/http.json new file mode 100644 index 0000000..20b62ec --- /dev/null +++ b/scenarios/simple/checkRequestBody/http.json @@ -0,0 +1,23 @@ +{ + "log": { + "entries": [ + { + "request": { + "method": "GET", + "url": "http://localhost/ping", + "postData": { + "mimeType": "application/json", + "text": "{\"hello\":\"world\"}" + } + }, + "response": { + "status": 200, + "content": { + "mimeType": "text/plain", + "text": "{\"hello\":\"world\"}" + } + } + } + ] + } +} diff --git a/scenarios/simple/checkRequestBody/openapi.yaml b/scenarios/simple/checkRequestBody/openapi.yaml new file mode 100644 index 0000000..c968c88 --- /dev/null +++ b/scenarios/simple/checkRequestBody/openapi.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Minimal example + version: 0.1.0 +servers: + - url: "http://localhost:8000" +paths: + /ping: + get: + requestBody: + description: desc + content: + application/json: + schema: + type: string + responses: + '200': + description: Returns `pong` + content: + text/plain: + schema: + type: string + example: pong diff --git a/scenarios/simple/requestBodyNotDefined/http.json b/scenarios/simple/requestBodyNotDefined/http.json new file mode 100644 index 0000000..20b62ec --- /dev/null +++ b/scenarios/simple/requestBodyNotDefined/http.json @@ -0,0 +1,23 @@ +{ + "log": { + "entries": [ + { + "request": { + "method": "GET", + "url": "http://localhost/ping", + "postData": { + "mimeType": "application/json", + "text": "{\"hello\":\"world\"}" + } + }, + "response": { + "status": 200, + "content": { + "mimeType": "text/plain", + "text": "{\"hello\":\"world\"}" + } + } + } + ] + } +} diff --git a/scenarios/simple/requestBodyNotDefined/openapi.yaml b/scenarios/simple/requestBodyNotDefined/openapi.yaml new file mode 100644 index 0000000..0e00b98 --- /dev/null +++ b/scenarios/simple/requestBodyNotDefined/openapi.yaml @@ -0,0 +1,17 @@ +openapi: 3.0.0 +info: + title: Minimal example + version: 0.1.0 +servers: + - url: "http://localhost:8000" +paths: + /ping: + get: + responses: + '200': + description: Returns `pong` + content: + text/plain: + schema: + type: string + example: pong diff --git a/src/api/api.test.ts b/src/api/api.test.ts index ddb2228..2d838c0 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -23,19 +23,11 @@ const expected: Root = { x_x_x_x_results: { hits: 0, }, - "application/json": { - schema: expect.objectContaining({ - type: "array", - items: expect.objectContaining({ - properties: expect.objectContaining({ - name: { - type: "string", - example: "doggie", - }, - }), - }), - }), - }, + "application/json": expect.objectContaining({ + x_x_x_x_results: { + hits: 0, + }, + }), }, }, "400": { diff --git a/src/api/index.ts b/src/api/index.ts index 2bf3326..1af1323 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,14 @@ const readApi = async (path: string): Promise => { return api; }; +const getMimeType = (content: a.Content): a.MimeType | undefined => { + if (content["application/json"]) { + return a.newMimeType({ + items: Object.keys(content["application/json"].schema).length, + }); + } +}; + const getContent = (response: a.ApiResponse): a.Content | undefined => { // We only cover application/json for now if (!response.content) { @@ -15,10 +23,7 @@ const getContent = (response: a.ApiResponse): a.Content | undefined => { x_x_x_x_results: a.newReportItem(), }; - if (response.content["application/json"]) { - contentToBeReturned["application/json"] = - response.content["application/json"]; - } + contentToBeReturned["application/json"] = getMimeType(response.content); return contentToBeReturned; }; @@ -53,6 +58,20 @@ const getParameters = (parameters?: a.Parameter[]) => { }); }; +const getRequestBody = ( + requestBody?: a.RequestBody +): a.RequestBody | undefined => { + if (!requestBody) { + return; + } + + return { + content: getContent(requestBody), + required: requestBody.required, + x_x_x_x_results: a.newReportItem(), + }; +}; + const getOperation = ( name: a.HTTPMETHOD, operation?: a.Operation @@ -68,7 +87,7 @@ const getOperation = ( }; operationToBeAdded.parameters = getParameters(operation.parameters); - operationToBeAdded.requestBody; // TODO get request body + operationToBeAdded.requestBody = getRequestBody(operation.requestBody); operationToBeAdded.responses = getResponses(operation.responses); return operationToBeAdded; diff --git a/src/api/model.ts b/src/api/model.ts index 6c420e1..da149e9 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -12,7 +12,10 @@ export type HTTPMETHOD = export type ApiError = | "METHOD NOT FOUND" | "PATH NOT FOUND" - | "STATUS NOT FOUND"; + | "STATUS NOT FOUND" + | "REQUEST BODY NOT FOUND" + | "CONTENT WAS NOT FOUND" + | "BAD MIMETYPE"; export type ReportResult = { hits: number; @@ -122,15 +125,41 @@ export type Parameter = { x_x_x_x_results: ReportResult; }; +export const newRequestBodyWithError = (error: ApiError): RequestBody => { + return { + required: false, + x_x_x_x_results: newReportItemWithError(error), + }; +}; + +export const newRequestBody = (): RequestBody => { + return { + required: false, + x_x_x_x_results: newReportItem(), + }; +}; + export type RequestBody = { required: boolean; - content: Content; + content?: Content; x_x_x_x_results: ReportResult; }; +export const newContentBody = (): Content => { + return { + x_x_x_x_results: newReportItem(), + }; +}; + +export const newContentBodyWithErrors = (error: ApiError): Content => { + return { + x_x_x_x_results: newReportItemWithError(error), + }; +}; + export type Content = { - "application/json"?: any; + "application/json"?: MimeType; x_x_x_x_results: ReportResult; }; @@ -153,3 +182,22 @@ export type ApiResponse = { x_x_x_x_results: ReportResult; }; + +export const newMimeTypeWithError = (error: ApiError): MimeType => { + return { + schema: null, + x_x_x_x_results: newReportItemWithError(error), + }; +}; + +export const newMimeType = (schema: any): MimeType => { + return { + schema: schema, + x_x_x_x_results: newReportItem(), + }; +}; + +export type MimeType = { + schema: any; + x_x_x_x_results: ReportResult; +}; diff --git a/src/rule/index.test.ts b/src/rule/index.test.ts index 377287f..3ad28b1 100644 --- a/src/rule/index.test.ts +++ b/src/rule/index.test.ts @@ -170,13 +170,84 @@ describe("Test Simple Scenarios", () => { }, }, ], + [ + "requestBodyNotDefined", + { + success: false, + apiSubtree: { + "/ping": { + x_name: "/ping", + x_x_x_x_results: { + hits: 0, + }, + get: { + responses: {}, + x_x_x_x_name: "get", + x_x_x_x_results: { + hits: 0, + }, + requestBody: { + required: false, + x_x_x_x_results: { + hits: 0, + error: "REQUEST BODY NOT FOUND", + }, + }, + }, + }, + }, + }, + ], + [ + "checkRequestBody", + { + success: true, + apiSubtree: { + "/ping": { + x_name: "/ping", + x_x_x_x_results: { + hits: 0, + }, + get: { + responses: { + "200": { + x_x_x_x_results: { + hits: 0, + }, + }, + }, + x_x_x_x_name: "get", + x_x_x_x_results: { + hits: 0, + }, + requestBody: { + required: false, + x_x_x_x_results: { + hits: 0, + }, + content: { + x_x_x_x_results: { + hits: 0, + }, + "application/json": { + x_x_x_x_results: { + hits: 0, + }, + }, + }, + }, + }, + }, + }, + }, + ], ]; const scenarios = scenarioNames.map((s) => { return [`./scenarios/simple/${s[0]}`, s[1]]; }); - test.each(scenarios)("Rule Match %#", async (scenario, expected) => { + test.each(scenarios)("Rule Match %s", async (scenario, expected) => { const httpPath = path.join(scenario as string, "http.json"); const apiPath = path.join(scenario as string, "openapi.yaml"); @@ -220,7 +291,7 @@ describe("Test HAR Scenarios", () => { return [`./scenarios/har/${s[0]}`, s[1]]; }); - test.each(scenarios)("Rule Match %#", async (scenario, expected) => { + test.each(scenarios)("Rule Match %s", async (scenario, expected) => { const httpPath = path.join(scenario as string, "http.har"); const apiPath = path.join(scenario as string, "openapi.yaml"); diff --git a/src/rule/index.ts b/src/rule/index.ts index b46f5df..4ff286c 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -120,6 +120,50 @@ export const removeServer = ( return null; }; +const matchRequestBody = ( + operationNode: a.Operation, + request: har.request, + resultOperationNode: a.Operation, + result: Result.Result +) => { + if (!operationNode.requestBody) { + result.success = false; + resultOperationNode.requestBody = a.newRequestBodyWithError( + "REQUEST BODY NOT FOUND" + ); + return; + } + incrementHit(operationNode.requestBody); + resultOperationNode.requestBody = a.newRequestBody(); + + if (!operationNode.requestBody.content) { + result.success = false; + resultOperationNode.requestBody.content = a.newContentBodyWithErrors( + "CONTENT WAS NOT FOUND" + ); + return; + } + incrementHit(operationNode.requestBody.content); + resultOperationNode.requestBody.content = a.newContentBody(); + + const mimeType = request.postData?.mimeType as string; + if ( + mimeType === "application/json" && + !operationNode.requestBody.content[mimeType] + ) { + result.success = false; + resultOperationNode.requestBody.content["application/json"] = + a.newMimeTypeWithError("BAD MIMETYPE"); + return; + } + + if (operationNode.requestBody.content["application/json"]) + incrementHit(operationNode.requestBody.content["application/json"]); + + resultOperationNode.requestBody.content["application/json"] = + a.newMimeType(undefined); // TODO add the schema once we are ready! +}; + const matchParameters = ( operationNode: a.Operation, request: har.request @@ -184,7 +228,8 @@ const matchResponseApi = ( const verifyRequest = ( operationNode: a.Operation, request: har.request, - resultOperationNode: a.Operation + resultOperationNode: a.Operation, + result: Result.Result ): void => { const parameters = matchParameters(operationNode, request); @@ -195,6 +240,10 @@ const verifyRequest = ( }); } + if (request.postData) { + matchRequestBody(operationNode, request, resultOperationNode, result); + } + return; }; @@ -265,7 +314,7 @@ export const match = ( resultPathNode[request.method] = resultOperationNode; // Verify - verifyRequest(operationNode, input.request, resultOperationNode); + verifyRequest(operationNode, input.request, resultOperationNode, result); if (!result.success) { return result; } diff --git a/src/rule/model.ts b/src/rule/model.ts index 20c3ef0..08528a1 100644 --- a/src/rule/model.ts +++ b/src/rule/model.ts @@ -63,5 +63,14 @@ export const flattenToLine = (result: Result): string => { } } + if (operation?.requestBody) { + const requestBody = operation.requestBody; + if (requestBody.x_x_x_x_results.error) { + str += ", Error: ("; + str += requestBody.x_x_x_x_results.error; + str += ")"; + } + } + return str; }; From e1a961e37c610d23532b87be3f2932fa8257060c Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Thu, 4 Aug 2022 17:05:32 +0200 Subject: [PATCH 2/6] change a get to post in scenario to make more http compliant --- scenarios/simple/checkRequestBody/http.json | 2 +- scenarios/simple/checkRequestBody/openapi.yaml | 2 +- src/rule/index.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scenarios/simple/checkRequestBody/http.json b/scenarios/simple/checkRequestBody/http.json index 20b62ec..9e248ae 100644 --- a/scenarios/simple/checkRequestBody/http.json +++ b/scenarios/simple/checkRequestBody/http.json @@ -3,7 +3,7 @@ "entries": [ { "request": { - "method": "GET", + "method": "POST", "url": "http://localhost/ping", "postData": { "mimeType": "application/json", diff --git a/scenarios/simple/checkRequestBody/openapi.yaml b/scenarios/simple/checkRequestBody/openapi.yaml index c968c88..f271d52 100644 --- a/scenarios/simple/checkRequestBody/openapi.yaml +++ b/scenarios/simple/checkRequestBody/openapi.yaml @@ -6,7 +6,7 @@ servers: - url: "http://localhost:8000" paths: /ping: - get: + post: requestBody: description: desc content: diff --git a/src/rule/index.test.ts b/src/rule/index.test.ts index 3ad28b1..8f6e384 100644 --- a/src/rule/index.test.ts +++ b/src/rule/index.test.ts @@ -208,7 +208,7 @@ describe("Test Simple Scenarios", () => { x_x_x_x_results: { hits: 0, }, - get: { + post: { responses: { "200": { x_x_x_x_results: { @@ -216,7 +216,7 @@ describe("Test Simple Scenarios", () => { }, }, }, - x_x_x_x_name: "get", + x_x_x_x_name: "post", x_x_x_x_results: { hits: 0, }, From e72c61916c91d68faa3caa4b1f67653f71f502d8 Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Thu, 4 Aug 2022 17:09:39 +0200 Subject: [PATCH 3/6] rename getPathNode to getPath in api parsing A debate could be had if theses names makes sense, but at least now they are consistent inside of this file. --- src/api/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 1af1323..fd89223 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -93,7 +93,7 @@ const getOperation = ( return operationToBeAdded; }; -const getPathNode = (pathName: string, path: a.Path): a.Path => { +const getPath = (pathName: string, path: a.Path): a.Path => { const pathToBeAdded: a.Path = { x_name: pathName, x_x_x_x_results: a.newReportItem(), @@ -113,7 +113,7 @@ const parseApi = (api: a.Root) => { // Add paths for (const [pathName, path] of Object.entries(api.paths)) { - const p = getPathNode(pathName, path); + const p = getPath(pathName, path); root.paths[pathName] = p; } From 2554cfc9f2d05b5cb49f1b22f3a635255b135b61 Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Thu, 4 Aug 2022 17:11:51 +0200 Subject: [PATCH 4/6] rename variable to make it more clear what it does --- src/rule/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rule/index.ts b/src/rule/index.ts index 4ff286c..82dc96c 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -146,10 +146,10 @@ const matchRequestBody = ( incrementHit(operationNode.requestBody.content); resultOperationNode.requestBody.content = a.newContentBody(); - const mimeType = request.postData?.mimeType as string; + const requestMimeType = request.postData?.mimeType as string; if ( - mimeType === "application/json" && - !operationNode.requestBody.content[mimeType] + requestMimeType === "application/json" && + !operationNode.requestBody.content[requestMimeType] ) { result.success = false; resultOperationNode.requestBody.content["application/json"] = From 536c0ed1f2d5bd90d91e6ea0e0ef953921781509 Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Thu, 4 Aug 2022 17:39:45 +0200 Subject: [PATCH 5/6] Report error if request body is required but missing --- .../http.json | 19 ++++++++ .../openapi.yaml | 24 +++++++++++ src/api/model.ts | 3 +- src/rule/index.test.ts | 43 ++++++++++++++++++- src/rule/index.ts | 11 +++-- 5 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 scenarios/simple/specRequiredRequestBodyButNoneInRequest/http.json create mode 100644 scenarios/simple/specRequiredRequestBodyButNoneInRequest/openapi.yaml diff --git a/scenarios/simple/specRequiredRequestBodyButNoneInRequest/http.json b/scenarios/simple/specRequiredRequestBodyButNoneInRequest/http.json new file mode 100644 index 0000000..f853abe --- /dev/null +++ b/scenarios/simple/specRequiredRequestBodyButNoneInRequest/http.json @@ -0,0 +1,19 @@ +{ + "log": { + "entries": [ + { + "request": { + "method": "POST", + "url": "http://localhost/ping" + }, + "response": { + "status": 200, + "content": { + "mimeType": "text/plain", + "text": "{\"hello\":\"world\"}" + } + } + } + ] + } +} diff --git a/scenarios/simple/specRequiredRequestBodyButNoneInRequest/openapi.yaml b/scenarios/simple/specRequiredRequestBodyButNoneInRequest/openapi.yaml new file mode 100644 index 0000000..2c9cbb8 --- /dev/null +++ b/scenarios/simple/specRequiredRequestBodyButNoneInRequest/openapi.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: Minimal example + version: 0.1.0 +servers: + - url: "http://localhost:8000" +paths: + /ping: + post: + requestBody: + required: true + description: desc + content: + application/json: + schema: + type: string + responses: + '200': + description: Returns `pong` + content: + text/plain: + schema: + type: string + example: pong diff --git a/src/api/model.ts b/src/api/model.ts index da149e9..9b85392 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -15,7 +15,8 @@ export type ApiError = | "STATUS NOT FOUND" | "REQUEST BODY NOT FOUND" | "CONTENT WAS NOT FOUND" - | "BAD MIMETYPE"; + | "BAD MIMETYPE" + | "REQUEST BODY WAS REQUIRED BUT NOTHING IN REQUEST"; export type ReportResult = { hits: number; diff --git a/src/rule/index.test.ts b/src/rule/index.test.ts index 8f6e384..1becdc3 100644 --- a/src/rule/index.test.ts +++ b/src/rule/index.test.ts @@ -181,7 +181,13 @@ describe("Test Simple Scenarios", () => { hits: 0, }, get: { - responses: {}, + responses: { + "200": { + x_x_x_x_results: { + hits: 0, + }, + }, + }, x_x_x_x_name: "get", x_x_x_x_results: { hits: 0, @@ -241,6 +247,40 @@ describe("Test Simple Scenarios", () => { }, }, ], + [ + "specRequiredRequestBodyButNoneInRequest", + { + success: false, + apiSubtree: { + "/ping": { + x_name: "/ping", + x_x_x_x_results: { + hits: 0, + }, + post: { + responses: { + "200": { + x_x_x_x_results: { + hits: 0, + }, + }, + }, + x_x_x_x_name: "post", + x_x_x_x_results: { + hits: 0, + }, + requestBody: { + required: false, + x_x_x_x_results: { + hits: 0, + error: "REQUEST BODY WAS REQUIRED BUT NOTHING IN REQUEST", + }, + }, + }, + }, + }, + }, + ], ]; const scenarios = scenarioNames.map((s) => { @@ -285,6 +325,7 @@ describe("Test HAR Scenarios", () => { apiSubtree: expect.objectContaining({ "/pet": expect.anything() }), }, ], + , ]; const scenarios = scenarioNames.map((s) => { diff --git a/src/rule/index.ts b/src/rule/index.ts index 82dc96c..50947b4 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -240,6 +240,14 @@ const verifyRequest = ( }); } + if (operationNode.requestBody?.required && !request.postData) { + result.success = false; + resultOperationNode.requestBody = a.newRequestBodyWithError( + "REQUEST BODY WAS REQUIRED BUT NOTHING IN REQUEST" + ); + return; + } + if (request.postData) { matchRequestBody(operationNode, request, resultOperationNode, result); } @@ -315,9 +323,6 @@ export const match = ( // Verify verifyRequest(operationNode, input.request, resultOperationNode, result); - if (!result.success) { - return result; - } verifyResponse(operationNode, input.response, result, pathNode.x_name); From f4fff90e8d058e32262067d1e4b9070cc2d7b3ec Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Thu, 4 Aug 2022 18:08:14 +0200 Subject: [PATCH 6/6] allow text/json for requestBody as well --- src/api/model.ts | 3 +++ src/httpParsing/parseHar.ts | 16 ++++++++-------- src/rule/index.ts | 13 +++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/api/model.ts b/src/api/model.ts index 9b85392..568f276 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -159,8 +159,11 @@ export const newContentBodyWithErrors = (error: ApiError): Content => { }; }; +export type Mime = "application/json" | "text/json"; + export type Content = { "application/json"?: MimeType; + "text/json"?: MimeType; x_x_x_x_results: ReportResult; }; diff --git a/src/httpParsing/parseHar.ts b/src/httpParsing/parseHar.ts index dc6e8b8..d3ec37d 100644 --- a/src/httpParsing/parseHar.ts +++ b/src/httpParsing/parseHar.ts @@ -18,15 +18,15 @@ const parseContent = (input: { if (!input) { return undefined; } - if (input.mimeType !== "application/json") { - console.warn("Only application/json is supported!"); - return undefined; + if (input.mimeType === "application/json" || input.mimeType === "text/json") { + return { + mimeType: input.mimeType, + text: input.text, + parsed: null, + }; } - return { - mimeType: input.mimeType, - text: input.text, - parsed: null, - }; + console.warn("Only application/json and text/json is supported!"); + return undefined; }; const parseOneHar = (entry: Record): har.t => { diff --git a/src/rule/index.ts b/src/rule/index.ts index 50947b4..831458e 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -146,19 +146,16 @@ const matchRequestBody = ( incrementHit(operationNode.requestBody.content); resultOperationNode.requestBody.content = a.newContentBody(); - const requestMimeType = request.postData?.mimeType as string; - if ( - requestMimeType === "application/json" && - !operationNode.requestBody.content[requestMimeType] - ) { + const requestMimeType = request.postData?.mimeType as a.Mime; + const mimeType = operationNode.requestBody.content[requestMimeType]; + if (!mimeType) { result.success = false; - resultOperationNode.requestBody.content["application/json"] = + resultOperationNode.requestBody.content[requestMimeType] = a.newMimeTypeWithError("BAD MIMETYPE"); return; } - if (operationNode.requestBody.content["application/json"]) - incrementHit(operationNode.requestBody.content["application/json"]); + incrementHit(mimeType); resultOperationNode.requestBody.content["application/json"] = a.newMimeType(undefined); // TODO add the schema once we are ready!