From 5c32f8494e3c040f593c41d4d1f4be4c99afb5cc Mon Sep 17 00:00:00 2001 From: Christoffer Olsson Date: Wed, 3 Aug 2022 10:28:07 +0200 Subject: [PATCH] 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 1836493..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; };