diff --git a/package-lock.json b/package-lock.json index af3da03..dfc1f21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", + "ajv": "^8.11.0", "argparse": "^2.0.1", "chalk": "4.1.2", "js-yaml": "^4.1.0", diff --git a/package.json b/package.json index 9a652de..df99ad3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ], "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", + "ajv": "^8.11.0", "argparse": "^2.0.1", "chalk": "4.1.2", "js-yaml": "^4.1.0", diff --git a/scenarios/har/1/openapi.yaml b/scenarios/har/1/openapi.yaml index 1de1a81..594ebd7 100644 --- a/scenarios/har/1/openapi.yaml +++ b/scenarios/har/1/openapi.yaml @@ -201,7 +201,6 @@ paths: required: true schema: type: integer - format: int64 responses: '200': description: successful operation @@ -234,7 +233,6 @@ paths: required: true schema: type: integer - format: int64 - name: name in: query description: Name of pet that needs to be updated @@ -271,7 +269,6 @@ paths: required: true schema: type: integer - format: int64 responses: '400': description: Invalid pet value @@ -293,7 +290,6 @@ paths: required: true schema: type: integer - format: int64 - name: additionalMetadata in: query description: Additional Metadata @@ -334,7 +330,6 @@ paths: type: object additionalProperties: type: integer - format: int32 security: - api_key: [] /store/order: @@ -382,7 +377,6 @@ paths: required: true schema: type: integer - format: int64 responses: '200': description: successful operation @@ -413,7 +407,6 @@ paths: required: true schema: type: integer - format: int64 responses: '400': description: Invalid ID supplied @@ -503,12 +496,10 @@ paths: description: calls per hour allowed by the user schema: type: integer - format: int32 X-Expires-After: description: date in UTC when token expires schema: type: string - format: date-time content: application/xml: schema: @@ -610,23 +601,18 @@ externalDocs: components: schemas: Order: - x-swagger-router-model: io.swagger.petstore.model.Order properties: id: type: integer - format: int64 example: 10 petId: type: integer - format: int64 example: 198772 quantity: type: integer - format: int32 example: 7 shipDate: type: string - format: date-time status: type: string description: Order Status @@ -637,14 +623,11 @@ components: example: approved complete: type: boolean - xml: - name: order type: object Customer: properties: id: type: integer - format: int64 example: 100000 username: type: string @@ -677,24 +660,18 @@ components: name: address type: object Category: - x-swagger-router-model: io.swagger.petstore.model.Category properties: id: type: integer - format: int64 example: 1 name: type: string example: Dogs - xml: - name: category type: object User: - x-swagger-router-model: io.swagger.petstore.model.User properties: id: type: integer - format: int64 example: 10 username: type: string @@ -716,32 +693,23 @@ components: example: 12345 userStatus: type: integer - format: int32 example: 1 description: User Status - xml: - name: user type: object Tag: - x-swagger-router-model: io.swagger.petstore.model.Tag properties: id: type: integer - format: int64 name: type: string - xml: - name: tag type: object Pet: - x-swagger-router-model: io.swagger.petstore.model.Pet required: - name - photoUrls properties: id: type: integer - format: int64 example: 10 name: type: string @@ -771,14 +739,11 @@ components: - available - pending - sold - xml: - name: pet type: object ApiResponse: properties: code: type: integer - format: int32 type: type: string message: diff --git a/scenarios/har/2/http.har b/scenarios/har/2/http.har index 859cfe0..69acfa5 100644 --- a/scenarios/har/2/http.har +++ b/scenarios/har/2/http.har @@ -4231,7 +4231,7 @@ "bodySize": 139, "postData": { "mimeType": "application/json", - "text": "{\n \"id\": 10,\n \"petId\": 198772,\n \"quantity\": 7,\n \"shipDate\": \"2022-07-19T08:35:06.175Z\",\n \"status\": \"approved\",\n \"complete\": truasde\n}" + "text": "{\n \"id\": 10,\n \"petId\": 198772,\n \"quantity\": 7,\n \"shipDate\": \"2022-07-19T08:35:06.175Z\",\n \"status\": \"approved\",\n \"complete\": true\n}" } }, "response": { @@ -9144,4 +9144,4 @@ } ] } -} \ No newline at end of file +} diff --git a/scenarios/har/2/openapi.yaml b/scenarios/har/2/openapi.yaml index 1de1a81..e41eb59 100644 --- a/scenarios/har/2/openapi.yaml +++ b/scenarios/har/2/openapi.yaml @@ -201,7 +201,6 @@ paths: required: true schema: type: integer - format: int64 responses: '200': description: successful operation @@ -234,7 +233,6 @@ paths: required: true schema: type: integer - format: int64 - name: name in: query description: Name of pet that needs to be updated @@ -271,7 +269,6 @@ paths: required: true schema: type: integer - format: int64 responses: '400': description: Invalid pet value @@ -293,7 +290,6 @@ paths: required: true schema: type: integer - format: int64 - name: additionalMetadata in: query description: Additional Metadata @@ -316,7 +312,6 @@ paths: application/octet-stream: schema: type: string - format: binary /store/inventory: get: tags: @@ -334,7 +329,6 @@ paths: type: object additionalProperties: type: integer - format: int32 security: - api_key: [] /store/order: @@ -382,7 +376,6 @@ paths: required: true schema: type: integer - format: int64 responses: '200': description: successful operation @@ -413,7 +406,6 @@ paths: required: true schema: type: integer - format: int64 responses: '400': description: Invalid ID supplied @@ -503,12 +495,10 @@ paths: description: calls per hour allowed by the user schema: type: integer - format: int32 X-Expires-After: description: date in UTC when token expires schema: type: string - format: date-time content: application/xml: schema: @@ -610,23 +600,18 @@ externalDocs: components: schemas: Order: - x-swagger-router-model: io.swagger.petstore.model.Order properties: id: type: integer - format: int64 example: 10 petId: type: integer - format: int64 example: 198772 quantity: type: integer - format: int32 example: 7 shipDate: type: string - format: date-time status: type: string description: Order Status @@ -637,14 +622,11 @@ components: example: approved complete: type: boolean - xml: - name: order type: object Customer: properties: id: type: integer - format: int64 example: 100000 username: type: string @@ -656,8 +638,6 @@ components: xml: wrapped: true name: addresses - xml: - name: customer type: object Address: properties: @@ -673,28 +653,20 @@ components: zip: type: string example: 94301 - xml: - name: address type: object Category: - x-swagger-router-model: io.swagger.petstore.model.Category properties: id: type: integer - format: int64 example: 1 name: type: string example: Dogs - xml: - name: category type: object User: - x-swagger-router-model: io.swagger.petstore.model.User properties: id: type: integer - format: int64 example: 10 username: type: string @@ -716,32 +688,23 @@ components: example: 12345 userStatus: type: integer - format: int32 example: 1 description: User Status - xml: - name: user type: object Tag: - x-swagger-router-model: io.swagger.petstore.model.Tag properties: id: type: integer - format: int64 name: type: string - xml: - name: tag type: object Pet: - x-swagger-router-model: io.swagger.petstore.model.Pet required: - name - photoUrls properties: id: type: integer - format: int64 example: 10 name: type: string @@ -771,20 +734,15 @@ components: - available - pending - sold - xml: - name: pet type: object ApiResponse: properties: code: type: integer - format: int32 type: type: string message: type: string - xml: - name: '##default' type: object requestBodies: Pet: diff --git a/scenarios/simple/checkRequestBodyOk/http.json b/scenarios/simple/checkRequestBodyOk/http.json new file mode 100644 index 0000000..20b62ec --- /dev/null +++ b/scenarios/simple/checkRequestBodyOk/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/checkRequestBodyOk/openapi.yaml b/scenarios/simple/checkRequestBodyOk/openapi.yaml new file mode 100644 index 0000000..9acca0b --- /dev/null +++ b/scenarios/simple/checkRequestBodyOk/openapi.yaml @@ -0,0 +1,26 @@ +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: object + properties: + hello: + type: string + responses: + '200': + description: Returns `pong` + content: + text/plain: + schema: + type: string + example: pong diff --git a/scenarios/simple/checkRequestBodyWrongInputType/http.json b/scenarios/simple/checkRequestBodyWrongInputType/http.json new file mode 100644 index 0000000..20b62ec --- /dev/null +++ b/scenarios/simple/checkRequestBodyWrongInputType/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/checkRequestBodyWrongInputType/openapi.yaml b/scenarios/simple/checkRequestBodyWrongInputType/openapi.yaml new file mode 100644 index 0000000..c968c88 --- /dev/null +++ b/scenarios/simple/checkRequestBodyWrongInputType/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..681f9e1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,12 @@ const readApi = async (path: string): Promise => { return api; }; +const getMimeType = (content: a.Content): a.MimeType | undefined => { + if (content["application/json"] && content["application/json"].schema) { + return a.newMimeType(content["application/json"].schema); + } +}; + const getContent = (response: a.ApiResponse): a.Content | undefined => { // We only cover application/json for now if (!response.content) { @@ -15,16 +21,13 @@ 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; }; const getResponses = ( - responses: Record + responses: Record, ): Record => { const responsesToBeAdded: Record = {}; @@ -53,9 +56,23 @@ 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 + operation?: a.Operation, ): a.Operation | undefined => { if (!operation) { return; @@ -68,7 +85,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..16b597f 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,3 +1,9 @@ +import Ajv, { AnySchema, ValidateFunction } from "ajv"; +const ajv = new Ajv(); // options can be passed, e.g. {allErrors: true} + +ajv.addKeyword("example"); +ajv.addKeyword("xml"); + export type HTTPMETHOD = | "connect" | "delete" @@ -12,7 +18,12 @@ 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" + | "BAD SCHEMA" + | "JSON NOT ACCORDING TO SCHEMA"; export type ReportResult = { hits: number; @@ -90,7 +101,7 @@ export const newOperationNode = (name: HTTPMETHOD): Operation => { export const newOperationNodeWithError = ( name: HTTPMETHOD, - error: ApiError + error: ApiError, ): Operation => { return { responses: {}, @@ -102,7 +113,7 @@ export const newOperationNodeWithError = ( export const newParamaterNode = ( name: string, in_: string, - required: boolean + required: boolean, ): Parameter => { return { name, @@ -122,15 +133,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 +190,29 @@ export type ApiResponse = { x_x_x_x_results: ReportResult; }; + +export const newMimeTypeWithError = (error: ApiError): MimeType => { + return { + x_x_x_x_results: newReportItemWithError(error), + }; +}; + +export const newMimeType = (schema?: AnySchema): MimeType => { + if (schema) { + const validate = ajv.compile(schema); + return { + schema: schema, + validate: validate, + x_x_x_x_results: newReportItem(), + }; + } + return { + x_x_x_x_results: newReportItem(), + }; +}; + +export type MimeType = { + schema?: AnySchema; + validate?: ValidateFunction; + x_x_x_x_results: ReportResult; +}; diff --git a/src/httpParsing/model.ts b/src/httpParsing/model.ts index 363576e..82ed9c2 100644 --- a/src/httpParsing/model.ts +++ b/src/httpParsing/model.ts @@ -1,4 +1,16 @@ -// An empty object that contains all expected fields for a http request +export type JSONValue = string | number | boolean | JSONObject | JSONArray; + +export type JSONObject = { + [x: string]: JSONValue; +}; + +export type JSONArray = Array; + +export type postData = { + mimeType: string; + parsed?: JSONObject | JSONArray; + text: string; +}; export type response = { status: number | "default"; @@ -20,11 +32,7 @@ export type request = { value: string; } ]; - postData?: { - mimeType: string; - parsed?: Record; - text: string; - }; + postData?: postData; }; export type t = { diff --git a/src/httpParsing/parseHar.test.ts b/src/httpParsing/parseHar.test.ts index f0c6ada..424dc47 100644 --- a/src/httpParsing/parseHar.test.ts +++ b/src/httpParsing/parseHar.test.ts @@ -1,4 +1,4 @@ -import { getParsedHar } from "./parseHar"; +import { getParsedHar, parseHar } from "./parseHar"; import * as har from "./model"; import path from "path"; @@ -21,7 +21,7 @@ const expected: har.t = { }, }; -test("Parse HAR", async () => { +test("Parse HAR from disk", async () => { const path = __dirname + "/src/httpParsing/petstore3.swagger.io2.har"; const parsedHar = await getParsedHar(path); @@ -30,3 +30,67 @@ test("Parse HAR", async () => { expect(firstHar).toEqual(expected); }); + +describe("Parse inlined HAR", () => { + const a = { + log: { + entries: [ + { + request: { + method: "PUT", + url: "https://editor.swagger.io/api/pet", + headers: [], + queryString: [], + cookies: [], + postData: { + mimeType: "application/json", + text: '{"hello":"world"}', + }, + }, + response: { + status: 405, + statusText: "", + headers: [], + cookies: [], + content: {}, + }, + }, + { + request: { + method: "PUT", + url: "https://editor.swagger.io/api/pet", + httpVersion: "http/2.0", + headers: [], + queryString: [], + cookies: [], + postData: { + mimeType: "application/json", + text: '[{"this": "valid"}]', + }, + }, + response: { + status: 405, + statusText: "", + httpVersion: "http/2.0", + headers: [], + cookies: [], + content: {}, + }, + }, + ], + }, + }; + + let parsedHar: har.t[] = []; + + beforeAll(() => { + parsedHar = parseHar(a); + }); + + it("Sets postData on items", () => { + expect(parsedHar[0].request.postData).toBeDefined(); + expect(parsedHar[1].request.postData).toBeDefined(); + expect(parsedHar[0].request.postData?.parsed).toEqual({ hello: "world" }); + expect(parsedHar[1].request.postData?.parsed).toEqual([{ this: "valid" }]); + }); +}); diff --git a/src/httpParsing/parseHar.ts b/src/httpParsing/parseHar.ts index c8032de..59d5852 100644 --- a/src/httpParsing/parseHar.ts +++ b/src/httpParsing/parseHar.ts @@ -11,7 +11,27 @@ const getEntries = (harFile: Record) => { return entries; }; +const parseContent = (input: { + mimeType: string; + text: string; +}): har.postData | undefined => { + if (!input) { + return undefined; + } + if (input.mimeType !== "application/json") { + console.warn("Only application/json is supported!"); + return undefined; + } + return { + mimeType: input.mimeType, + text: input.text, + parsed: JSON.parse(input.text), + }; +}; + const parseOneHar = (entry: Record): har.t => { + const request = entry.request; + const requestContent = parseContent(request.postData); const response: har.response = { status: entry.response.status, statusText: entry.response.status, @@ -21,7 +41,6 @@ const parseOneHar = (entry: Record): har.t => { text: entry.response.content.text, }, }; - const request = entry.request; return { response: { status: response.status, @@ -31,15 +50,18 @@ const parseOneHar = (entry: Record): har.t => { method: request.method.toLowerCase(), url: new URL(request.url), path: new URL(request.url).pathname, - postData: request.postData, + postData: requestContent, }, }; }; -export const getParsedHar = async (path: string): Promise => { - const harFile = await readFileasJson(path); +export const parseHar = (harFile: Record): har.t[] => { const entries = getEntries(harFile); + return entries.map(parseOneHar); +}; - const parsedEntries = entries.map(parseOneHar); +export const getParsedHar = async (filePath: string): Promise => { + const harFile = await readFileasJson(filePath); + const parsedEntries = parseHar(harFile); return parsedEntries; }; diff --git a/src/rule/index.test.ts b/src/rule/index.test.ts index 377287f..3f18bfa 100644 --- a/src/rule/index.test.ts +++ b/src/rule/index.test.ts @@ -170,13 +170,134 @@ describe("Test Simple Scenarios", () => { }, }, ], + [ + "requestBodyNotDefined", + { + success: false, + 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, + error: "REQUEST BODY NOT FOUND", + }, + }, + }, + }, + }, + }, + ], + [ + "checkRequestBodyOk", + { + 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, + }, + }, + }, + }, + }, + }, + }, + }, + ], + [ + "checkRequestBodyWrongInputType", + { + success: false, + 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: { + error: "JSON NOT ACCORDING TO SCHEMA", + 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 +341,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..e531586 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -4,7 +4,7 @@ import * as Result from "./model"; export const findPathNodeFromPath = ( root: a.Root, - path: string + path: string, ): a.Path | null => { // It's important that an HTTP request with e.g /user/asd can match /user/{user_id} const toBeRetuend = root.paths[path]; @@ -74,7 +74,7 @@ const isAbsoluteUrlSloppy = (urlString: string) => { export const removeServer = ( api: a.Root, url: URL, - url_server_prefix: string + url_server_prefix: string, ): string | null => { // For now we can only have one server const server = api.servers[0].url; @@ -120,9 +120,73 @@ 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(); + + const contentNode = operationNode.requestBody.content; + if (!contentNode) { + result.success = false; + resultOperationNode.requestBody.content = a.newContentBodyWithErrors( + "CONTENT WAS NOT FOUND", + ); + return; + } + incrementHit(contentNode); + resultOperationNode.requestBody.content = a.newContentBody(); + + const mimeType = request.postData?.mimeType as string; + if (mimeType === "application/json" && !contentNode[mimeType]) { + result.success = false; + resultOperationNode.requestBody.content["application/json"] = + a.newMimeTypeWithError("BAD MIMETYPE"); + return; + } + + const mimeNode = contentNode["application/json"]; + if (!mimeNode) { + result.success = false; + resultOperationNode.requestBody.content["application/json"] = + a.newMimeTypeWithError("BAD MIMETYPE"); + return; + } + + const validateFunction = mimeNode.validate; + const schema = mimeNode.schema; + if (!validateFunction || !schema) { + result.success = false; + resultOperationNode.requestBody.content["application/json"] = + a.newMimeTypeWithError("BAD SCHEMA"); + return; + } + + const validated = validateFunction(request.postData?.parsed); + if (!validated) { + result.success = false; + resultOperationNode.requestBody.content["application/json"] = + a.newMimeTypeWithError("JSON NOT ACCORDING TO SCHEMA"); + return; + } + + resultOperationNode.requestBody.content["application/json"] = a.newMimeType(); + incrementHit(mimeNode); +}; + const matchParameters = ( operationNode: a.Operation, - request: har.request + request: har.request, ): a.Parameter[] | null => { if (!operationNode.parameters) { return null; @@ -141,7 +205,7 @@ const matchParameters = ( const matchPath = ( api: a.Root, request: har.request, - url_server_prefix: string + url_server_prefix: string, ): a.Path | null => { const pathWithNoServer = removeServer(api, request.url, url_server_prefix); if (!pathWithNoServer) { @@ -168,7 +232,7 @@ const matchOperation = (pathNode: a.Path, request: har.request) => { const matchResponseApi = ( operationNode: a.Operation, - response: har.response + response: har.response, ): a.ApiResponse | null => { const statusToMatch = response.status; @@ -184,7 +248,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 +260,10 @@ const verifyRequest = ( }); } + if (request.postData) { + matchRequestBody(operationNode, request, resultOperationNode, result); + } + return; }; @@ -202,7 +271,7 @@ const verifyResponse = ( operation: a.Operation, response: har.response, result: Result.Result, - pathName: string + pathName: string, ): void => { if (!operation) { console.log("WARNING NO OPERATION WAS PASSED TO MATCH RESPONSE"); @@ -227,7 +296,7 @@ const verifyResponse = ( export const match = ( api: a.Root, input: har.t, - url_server_prefix: string + url_server_prefix: string, ): Result.Result => { // This object mutates const result: Result.Result = { @@ -243,7 +312,7 @@ export const match = ( result.success = false; result.apiSubtree[request.path] = a.newPathNodeWithError( request.path, - "PATH NOT FOUND" + "PATH NOT FOUND", ); return result; } @@ -257,7 +326,7 @@ export const match = ( const methodToMatch = request.method; resultPathNode[methodToMatch] = a.newOperationNodeWithError( methodToMatch, - "METHOD NOT FOUND" + "METHOD NOT FOUND", ); return result; } @@ -265,10 +334,7 @@ export const match = ( resultPathNode[request.method] = resultOperationNode; // Verify - verifyRequest(operationNode, input.request, resultOperationNode); - if (!result.success) { - return result; - } + verifyRequest(operationNode, input.request, resultOperationNode, result); verifyResponse(operationNode, input.response, result, pathNode.x_name); diff --git a/src/rule/model.ts b/src/rule/model.ts index 20c3ef0..1690317 100644 --- a/src/rule/model.ts +++ b/src/rule/model.ts @@ -63,5 +63,31 @@ 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 += ")"; + } + + const content = requestBody.content; + if (content) { + if (content.x_x_x_x_results.error) { + str += ", Error: ("; + str += content.x_x_x_x_results.error; + str += ")"; + } + + const mimeType = content["application/json"]; + if (mimeType && mimeType.x_x_x_x_results.error) { + str += ", Error: ("; + str += mimeType.x_x_x_x_results.error; + str += ")"; + } + } + requestBody.content?.["application/json"]; + } + return str; };