diff --git a/packages/oas/src/converter/parts/postdata/BodyConverter.ts b/packages/oas/src/converter/parts/postdata/BodyConverter.ts index 42277121..15cd8064 100644 --- a/packages/oas/src/converter/parts/postdata/BodyConverter.ts +++ b/packages/oas/src/converter/parts/postdata/BodyConverter.ts @@ -15,13 +15,16 @@ export abstract class BodyConverter implements SubConverter { private readonly xmlSerializer = new XmlSerializer(); - private readonly JPG_IMAGE = '/9j/7g=='; // 0xff, 0xd8, 0xff, 0xee - private readonly PNG_IMAGE = 'iVBORw0KGgo='; // 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0A, 0x1a, 0x0a - private readonly ICO_IMAGE = 'AAABAA=='; // 0x00, 0x00, 0x01, 0x00 - private readonly GIF_IMAGE = 'R0lGODdh'; // 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 + private readonly JPG_IMAGE = '\xff\xd8\xff\xe0'; + private readonly PNG_IMAGE = '\x89\x50\x4e\x47\x0d\x0A\x1a\x0a'; + private readonly ICO_IMAGE = '\x00\x00\x01\x00'; + private readonly GIF_IMAGE = '\x47\x49\x46\x38\x37\x61'; private readonly BOUNDARY = '956888039105887155673143'; - private readonly BASE64_PATTERN = - /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/; + private readonly BASE64_FORMATS: readonly string[] = ['byte', 'base64']; + private readonly BINARY_FORMATS: readonly string[] = [ + 'binary', + ...this.BASE64_FORMATS + ]; protected constructor( protected readonly spec: T, @@ -69,69 +72,100 @@ export abstract class BodyConverter return this.encodeXml(value, schema); case 'multipart/form-data': case 'multipart/mixed': - return this.encodeMultipartFormData(value, fields); + return this.encodeMultipartFormData(value, fields, schema); case 'image/x-icon': case 'image/ico': case 'image/vnd.microsoft.icon': - return this.ICO_IMAGE; + return this.encodeBinary(this.ICO_IMAGE, schema); case 'image/jpg': case 'image/jpeg': - return this.JPG_IMAGE; + return this.encodeBinary(this.JPG_IMAGE, schema); case 'image/gif': - return this.GIF_IMAGE; + return this.encodeBinary(this.GIF_IMAGE, schema); case 'image/png': case 'image/*': - return this.PNG_IMAGE; + return this.encodeBinary(this.PNG_IMAGE, schema); default: return this.encodeOther(value); } } + private encodeBinary( + value: unknown, + schema?: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject + ): string { + const encoded = this.encodeOther(value); + + return this.BASE64_FORMATS.includes(schema?.format) + ? btoa(encoded) + : encoded; + } + // TODO: move the logic that receives the content type from the encoding object // to the {@link Oas3RequestBodyConverter} class. private encodeMultipartFormData( value: unknown, - fields?: Record + fields?: Record, + schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject ): string { const EOL = '\r\n'; return Object.entries(value || {}) .map(([key, val]: [string, unknown]) => { + const propertySchema = this.getPropertySchema(key, schema); const contentType = - fields?.[key]?.contentType ?? this.inferMultipartContentType(val); - const filenameRequired = this.filenameRequired(contentType); - const content = this.encodeOther(val); + fields?.[key]?.contentType ?? + this.inferContentType(val, propertySchema); const headers = [ `Content-Disposition: form-data; name="${key}"${ - filenameRequired ? `; filename="${key}"` : '' + this.filenameRequired(contentType) ? `; filename="${key}"` : '' }`, ...(contentType !== 'text/plain' ? [`Content-Type: ${contentType}`] : []), - ...(this.BASE64_PATTERN.test(content) && - contentType === 'application/octet-stream' - ? [`Content-Transfer-Encoding: base64`] + ...(this.BASE64_FORMATS.includes(propertySchema?.format) + ? ['Content-Transfer-Encoding: base64'] : []) ]; - const body = `${headers.join(EOL)}${EOL}${EOL}${content}`; + const body = this.encodeOther(val); - return `--${this.BOUNDARY}${EOL}${body}`; + return `--${this.BOUNDARY}${EOL}${headers.join( + EOL + )}${EOL}${EOL}${body}`; }) .join(EOL) .concat(`${EOL}--${this.BOUNDARY}--`); } + private getPropertySchema( + key: string, + schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject + ): OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject | undefined { + if (schema?.type === 'object') { + return schema.properties?.[key]; + } + + if (schema?.type === 'array') { + return schema.items; + } + + return undefined; + } + private filenameRequired(contentType: string): boolean { return 'application/octet-stream' === contentType; } - private inferMultipartContentType(value: unknown): string { + private inferContentType( + value: unknown, + schema?: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject + ): string { switch (typeof value) { case 'object': return 'application/json'; case 'string': - return this.BASE64_PATTERN.test(value) + return this.BINARY_FORMATS.includes(schema?.format) ? 'application/octet-stream' : 'text/plain'; case 'number': diff --git a/packages/oas/src/utils/index.ts b/packages/oas/src/utils/index.ts index 3a75f721..6a271575 100644 --- a/packages/oas/src/utils/index.ts +++ b/packages/oas/src/utils/index.ts @@ -2,8 +2,8 @@ import { OpenAPI, OpenAPIV2, OpenAPIV3 } from '@har-sdk/core'; export * from './Flattener'; export * from './isObject'; -export * from './params'; export * from './operation'; +export * from './params'; export const isOASV2 = (doc: OpenAPI.Document): doc is OpenAPIV2.Document => 'swagger' in doc; diff --git a/packages/oas/tests/DefaultConverter.spec.ts b/packages/oas/tests/DefaultConverter.spec.ts index bed573b2..57e05da5 100644 --- a/packages/oas/tests/DefaultConverter.spec.ts +++ b/packages/oas/tests/DefaultConverter.spec.ts @@ -142,6 +142,16 @@ describe('DefaultConverter', () => { input: 'xml-models.swagger.yaml', expected: 'xml-models.swagger.result.json', message: 'should properly serialize models to XML (swagger)' + }, + { + input: 'binary-body.swagger.yaml', + expected: 'binary-body.swagger.result.json', + message: 'should properly serialize binary types (swagger)' + }, + { + input: 'binary-body.oas.yaml', + expected: 'binary-body.oas.result.json', + message: 'should properly serialize binary types (oas)' } ].forEach(({ input: inputFile, expected: expectedFile, message }) => { it(message, async () => { diff --git a/packages/oas/tests/fixtures/binary-body.oas.result.json b/packages/oas/tests/fixtures/binary-body.oas.result.json new file mode 100644 index 00000000..9df3f0f5 --- /dev/null +++ b/packages/oas/tests/fixtures/binary-body.oas.result.json @@ -0,0 +1,78 @@ +[ + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/jpeg" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/jpeg", + "text": "\u00ff\u00d8\u00ff\u00e0" + }, + "queryString": [], + "url": "https://petstore.swagger.io/binary" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/png" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/png", + "text": "iVBORw0KGgo=" + }, + "queryString": [], + "url": "https://petstore.swagger.io/byte" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/ico" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/ico", + "text": "AAABAA==" + }, + "queryString": [], + "url": "https://petstore.swagger.io/base64" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "multipart/form-data" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "multipart/form-data; boundary=956888039105887155673143", + "text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"base64\"\r\nContent-Type: image/vnd.microsoft.icon\r\nContent-Transfer-Encoding: base64\r\n\r\nAAABAA==\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"binary\"\r\nContent-Type: image/gif\r\n\r\nGIF87a\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"byte\"\r\nContent-Type: image/*\r\nContent-Transfer-Encoding: base64\r\n\r\niVBORw0KGgo=\r\n--956888039105887155673143--" + }, + "queryString": [], + "url": "https://petstore.swagger.io/multipart" + } +] diff --git a/packages/oas/tests/fixtures/binary-body.oas.yaml b/packages/oas/tests/fixtures/binary-body.oas.yaml new file mode 100644 index 00000000..2a18adc9 --- /dev/null +++ b/packages/oas/tests/fixtures/binary-body.oas.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.0 +info: + title: Binary Data API + version: 1.0.0 +servers: + - url: https://petstore.swagger.io +paths: + /binary: + post: + summary: Upload plain binary data + requestBody: + content: + image/jpeg: + schema: + type: string + format: binary + required: true + responses: + '200': + description: OK + /byte: + post: + summary: Upload byte data + requestBody: + content: + image/png: + schema: + type: string + format: byte + required: true + responses: + '200': + description: OK + /base64: + post: + summary: Upload base64 data + requestBody: + content: + image/ico: + schema: + type: string + format: base64 + required: true + responses: + '200': + description: OK + /multipart: + post: + summary: Upload multipart/form-data with binary data + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + base64: + type: string + format: base64 + binary: + type: string + format: binary + byte: + type: string + format: byte + required: + - base64 + - binary + - byte + encoding: + base64: + contentType: image/vnd.microsoft.icon + binary: + contentType: image/gif + byte: + contentType: image/* + required: true + responses: + '200': + description: OK diff --git a/packages/oas/tests/fixtures/binary-body.swagger.result.json b/packages/oas/tests/fixtures/binary-body.swagger.result.json new file mode 100644 index 00000000..11e0d17e --- /dev/null +++ b/packages/oas/tests/fixtures/binary-body.swagger.result.json @@ -0,0 +1,78 @@ +[ + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/jpeg" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/jpeg", + "text": "\u00ff\u00d8\u00ff\u00e0" + }, + "queryString": [], + "url": "https://petstore.swagger.io/binary" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/png" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/png", + "text": "iVBORw0KGgo=" + }, + "queryString": [], + "url": "https://petstore.swagger.io/byte" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "image/ico" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "image/ico", + "text": "AAABAA==" + }, + "queryString": [], + "url": "https://petstore.swagger.io/base64" + }, + { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "multipart/form-data" + } + ], + "headersSize": 0, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "multipart/form-data; boundary=956888039105887155673143", + "text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"base64\"\r\n\r\nlorem\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"binary\"\r\n\r\nlorem\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"byte\"\r\n\r\nlorem\r\n--956888039105887155673143--" + }, + "queryString": [], + "url": "https://petstore.swagger.io/multipart" + } +] diff --git a/packages/oas/tests/fixtures/binary-body.swagger.yaml b/packages/oas/tests/fixtures/binary-body.swagger.yaml new file mode 100644 index 00000000..47051649 --- /dev/null +++ b/packages/oas/tests/fixtures/binary-body.swagger.yaml @@ -0,0 +1,77 @@ +swagger: '2.0' +info: + version: 1.0.0 + title: Binary Data API +host: 'petstore.swagger.io' +schemes: + - https +paths: + /binary: + post: + summary: Upload plain binary data + consumes: + - image/jpeg + parameters: + - in: body + name: file + required: true + schema: + type: string + format: binary + responses: + '200': + description: OK + /byte: + post: + summary: Upload byte data + consumes: + - image/png + parameters: + - in: body + name: file + required: true + schema: + type: string + format: byte + responses: + '200': + description: OK + /base64: + post: + summary: Upload base64 data + consumes: + - image/ico + parameters: + - in: body + name: file + required: true + schema: + type: string + format: base64 + responses: + '200': + description: OK + /multipart: + post: + summary: Upload multipart/form-data with binary data + consumes: + - multipart/form-data + parameters: + - in: formData + name: base64 + type: file + required: true + x-format: base64 + - in: formData + name: binary + type: file + required: true + x-format: binary + - in: formData + name: byte + type: file + required: true + x-format: byte + responses: + '200': + description: OK diff --git a/packages/oas/tests/fixtures/params-encoding.oas.result.json b/packages/oas/tests/fixtures/params-encoding.oas.result.json index 86a24581..a4fa1e9e 100644 --- a/packages/oas/tests/fixtures/params-encoding.oas.result.json +++ b/packages/oas/tests/fixtures/params-encoding.oas.result.json @@ -10,7 +10,7 @@ ], "postData": { "mimeType": "multipart/form-data; boundary=956888039105887155673143", - "text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\nfbdf5a53-161e-4460-98ad-0e39408d8689\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"address\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"historyMetadata\"\r\nContent-Type: application/xml; charset=utf-8\r\n\r\n\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"profileImage\"\r\nContent-Type: image/png, image/jpeg\r\n\r\niVBORw0KGgo=\r\n--956888039105887155673143--" + "text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\nfbdf5a53-161e-4460-98ad-0e39408d8689\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"address\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"historyMetadata\"\r\nContent-Type: application/xml; charset=utf-8\r\n\r\n\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"profileImage\"\r\nContent-Type: image/png, image/jpeg\r\n\r\n\u0089\u0050\u004e\u0047\u000d\u000A\u001a\u000a\r\n--956888039105887155673143--" }, "headersSize": 0, "httpVersion": "HTTP/1.1",