diff --git a/app/javascript/src/ActiveDocs/ThreeScaleApiDocs.ts b/app/javascript/src/ActiveDocs/ThreeScaleApiDocs.ts index 9e0606eb02..e1acc3f987 100644 --- a/app/javascript/src/ActiveDocs/ThreeScaleApiDocs.ts +++ b/app/javascript/src/ActiveDocs/ThreeScaleApiDocs.ts @@ -3,9 +3,10 @@ import SwaggerUI from 'swagger-ui' import { execute } from 'swagger-client/es/execute' import { fetchData } from 'utilities/fetchData' +import { safeFromJsonString } from 'utilities/json-utils' import { autocompleteRequestInterceptor } from 'ActiveDocs/OAS3Autocomplete' -import type { ApiDocsServices, BackendApiReportBody, BackendApiTransaction, ExecuteData } from 'Types/SwaggerTypes' +import type { ApiDocsServices, BackendApiReportBody, BackendApiTransaction, BodyValue, BodyValueObject, FormData, ExecuteData } from 'Types/SwaggerTypes' import type { SwaggerUIPlugin } from 'swagger-ui' const getApiSpecUrl = (baseUrl: string, specPath: string): string => { @@ -21,14 +22,9 @@ const appendSwaggerDiv = (container: HTMLElement, id: string): void => { } /** - * when using Record notation, the following error is thrown: - * 'TS2456: Type alias 'BodyValue' circularly references itself.' - */ -// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -type BodyValue = boolean | number | string | { [key: string]: BodyValue } - -/** - * Transforms an object into form data representation, also URL-encoding the values, + * Transforms an object into form data representation. Does not URL-encode, because it will be done by + * swagger-client itself + * Returns an empty object if the argument is not an object * Example: * { * a_string: 'hello', @@ -42,15 +38,19 @@ type BodyValue = boolean | number | string | { [key: string]: BodyValue } * a_string: 'hello', * 'an_array[0][first]': '1', * 'an_array[1][second]': '1', - * 'an_array[1][extra_param]': 'with%20whitespace' + * 'an_array[1][extra_param]': 'with whitespace' * } * @param object */ -export const objectToFormData = (object: BodyValue): Record => { - const buildFormData = (formData: Record, data: BodyValue, parentKey?: string) => { +export const objectToFormData = (object: BodyValue): FormData => { + if (typeof object !== 'object' || Array.isArray(object)) { + return {} + } + const buildFormData = (formData: FormData, data: BodyValue, parentKey?: string) => { if (data && typeof data === 'object') { - Object.keys(data).forEach((key: string) => { - buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key) + const dataObject = data as BodyValueObject + Object.keys(dataObject).forEach((key: string) => { + buildFormData(formData, dataObject[key], parentKey ? `${parentKey}[${key}]` : key) }) } else { if (parentKey) { @@ -58,7 +58,7 @@ export const objectToFormData = (object: BodyValue): Record => { +export const transformReportRequestBody = (body: BackendApiReportBody): FormData => { if (Array.isArray(body.transactions)) { - body.transactions = body.transactions.map(transaction => { - switch (typeof transaction) { - case 'object': - return transaction - case 'string': - try { - return JSON.parse(transaction) as BackendApiTransaction - } catch (error: unknown) { - return null - } - default: - return null + body.transactions = body.transactions.reduce((acc: BackendApiTransaction[], transaction) => { + let value = undefined + if (typeof transaction === 'object') { + value = transaction + } else { + value = safeFromJsonString(transaction) + } + if (value) { + acc.push(value) } - }).filter(element => element != null) as BackendApiTransaction[] + return acc + }, []) } return objectToFormData(body as BodyValue) } diff --git a/app/javascript/src/Types/SwaggerTypes.ts b/app/javascript/src/Types/SwaggerTypes.ts index 5fe7134796..8763bf210d 100644 --- a/app/javascript/src/Types/SwaggerTypes.ts +++ b/app/javascript/src/Types/SwaggerTypes.ts @@ -19,16 +19,6 @@ export interface ApiDocsServices { apis: ApiDocsService[]; } -export interface RequestData extends Request { - url: string; - method: SupportedHTTPMethods; - body: string; - credentials: string; - headers: Record; - requestInterceptor?: ((request: Request) => Promise | Request) | undefined; - responseInterceptor?: ((response: Response) => Promise | Response) | undefined; -} - export interface ExecuteData { contextUrl: string; fetch: (arg: unknown) => unknown; @@ -37,7 +27,7 @@ export interface ExecuteData { operationId: string; parameters: unknown; pathName: string; - requestBody: unknown; + requestBody?: unknown; requestContentType: string; requestInterceptor?: ((request: Request) => Promise | Request) | undefined; responseContentType: string; @@ -66,3 +56,12 @@ export interface BackendApiReportBody { service_id?: string; transactions?: (BackendApiTransaction | string)[]; } + +/** + * when using Record notation, the following error is thrown: + * 'TS2456: Type alias 'BodyValue' circularly references itself.' + */ +export type BodyValue = BodyValue[] | boolean | number | string | { [key: string]: BodyValue } | null | undefined +export type BodyValueObject = Record + +export type FormData = Record diff --git a/spec/javascripts/ActiveDocs/ThreeScaleApiDocs.spec.ts b/spec/javascripts/ActiveDocs/ThreeScaleApiDocs.spec.ts index d2e4db045f..6f638fb412 100644 --- a/spec/javascripts/ActiveDocs/ThreeScaleApiDocs.spec.ts +++ b/spec/javascripts/ActiveDocs/ThreeScaleApiDocs.spec.ts @@ -1,40 +1,88 @@ -import { transformReportRequestBody } from 'ActiveDocs/ThreeScaleApiDocs' +import { objectToFormData, transformReportRequestBody } from 'ActiveDocs/ThreeScaleApiDocs' -import type { BackendApiReportBody } from 'Types/SwaggerTypes' +import type { BackendApiReportBody, BackendApiTransaction, BodyValue } from 'Types/SwaggerTypes' +const transaction1: BackendApiTransaction = { + app_id: 'app-id1', + timestamp: '2023-03-29 00:00:00 -01:00', + usage: { + 'hit1-1': 11, + 'hit1-2': 12 + }, + log: { + request: 'request1', + response: 'response1', + code: '200' + } +} +const transaction2: BackendApiTransaction = { + app_id: 'app-id2', + timestamp: '2023-03-29 00:00:00 -02:00', + usage: { + 'hit2-1': 21, + 'hit2-2': 22 + }, + log: { + request: 'request2', + response: 'response2', + code: '200' + } +} const body: BackendApiReportBody = { service_token: 'token', service_id: '123', transactions: [ - { - app_id: 'app-id1', - timestamp: '2023-03-29 00:00:00 -01:00', - usage: { - 'hit1-1': 11, - 'hit1-2': 12 - }, - log: { - request: 'request1', - response: 'response1', - code: '200' - } - }, - { - app_id: 'app-id2', - timestamp: '2023-03-29 00:00:00 -02:00', - usage: { - 'hit2-1': 21, - 'hit2-2': 22 - }, - log: { - request: 'request2', - response: 'response2', - code: '200' - } - } + transaction1, + transaction2 ] } +describe('objectToFormData', () => { + it('transforms the object to form data', () => { + const object: BodyValue = { + number: 1, + string: 'string with whitespace', + object: { + numField: 2, + strField: 'str', + boolField: true + }, + array: [ + { + foo: 'bar' + }, + { + foo: 'baz' + } + ], + nullField: null, + undefinedField: undefined, + emptyField: '' + } + + expect(objectToFormData(object)).toEqual({ + number: 1, + string: 'string with whitespace', + 'object[numField]': 2, + 'object[strField]': 'str', + 'object[boolField]': true, + 'array[0][foo]': 'bar', + 'array[1][foo]': 'baz', + nullField: '', + undefinedField: '', + emptyField: '' + }) + }) + + it('returns an empty object if argument is not a valid object', () => { + expect(objectToFormData('hello')).toEqual({}) + expect(objectToFormData(true)).toEqual({}) + expect(objectToFormData(123)).toEqual({}) + expect(objectToFormData(['q', 'w', 'r'])).toEqual({}) + }) + +}) + describe('transformReportRequestBody', () => { it('transforms the transactions array when transaction is an object', () => { const result = transformReportRequestBody(body) @@ -58,4 +106,57 @@ describe('transformReportRequestBody', () => { 'transactions[1][log][code]': '200' }) }) + + it('transforms the transactions array when a transaction is a serialized JSON', () => { + const bodyWithSerializedTransaction = { + ...body, + transactions: [ + transaction1, + '{\n "app_id": "app-id2",\n "timestamp": "2023-03-29 00:00:00 -02:00",\n "usage": {\n "hit2-1": 21,\n "hit2-2": 22\n },\n "log": {\n "request": "request2",\n "response": "response2",\n "code": "200"\n }\n}'] + } + const result = transformReportRequestBody(bodyWithSerializedTransaction) + + expect(result).toEqual({ + service_token: 'token', + service_id: '123', + 'transactions[0][app_id]': 'app-id1', + 'transactions[0][timestamp]': '2023-03-29 00:00:00 -01:00', + 'transactions[0][usage][hit1-1]': 11, + 'transactions[0][usage][hit1-2]': 12, + 'transactions[0][log][request]': 'request1', + 'transactions[0][log][response]': 'response1', + 'transactions[0][log][code]': '200', + 'transactions[1][app_id]': 'app-id2', + 'transactions[1][timestamp]': '2023-03-29 00:00:00 -02:00', + 'transactions[1][usage][hit2-1]': 21, + 'transactions[1][usage][hit2-2]': 22, + 'transactions[1][log][request]': 'request2', + 'transactions[1][log][response]': 'response2', + 'transactions[1][log][code]': '200' + }) + }) + + it('skips the transactions with invalid format', () => { + const bodyWithInvalidTransactions = { + ...body, + transactions: [transaction1, 'invalid', 'anotherOne'] + } + const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation() + + const result = transformReportRequestBody(bodyWithInvalidTransactions) + expect(result).toEqual({ + service_token: 'token', + service_id: '123', + 'transactions[0][app_id]': 'app-id1', + 'transactions[0][timestamp]': '2023-03-29 00:00:00 -01:00', + 'transactions[0][usage][hit1-1]': 11, + 'transactions[0][usage][hit1-2]': 12, + 'transactions[0][log][request]': 'request1', + 'transactions[0][log][response]': 'response1', + 'transactions[0][log][code]': '200' + }) + + expect(consoleErrorMock).toHaveBeenCalledTimes(2) + consoleErrorMock.mockRestore() + }) }) diff --git a/spec/javascripts/__mocks__/swagger-client/es/execute/index.js b/spec/javascripts/__mocks__/swagger-client/es/execute/index.js index 8cfd7ace8f..9846f0040e 100644 --- a/spec/javascripts/__mocks__/swagger-client/es/execute/index.js +++ b/spec/javascripts/__mocks__/swagger-client/es/execute/index.js @@ -1,3 +1,3 @@ module.exports = { - execute: jest.fn() + execute: jest.fn() }