Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide option to save bodies as JSON objects #487

Merged
1 change: 1 addition & 0 deletions packages/app/configuration/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ describe('configuration', () => {
expect(configuration.onExit.value()).toBeUndefined();
expect(configuration.hook.value(undefined as any)).toBeUndefined();
expect(configuration.console.value).toBe(console);
expect(configuration.harMimeTypesParseJson.value).toEqual([]);
});
});
});
6 changes: 6 additions & 0 deletions packages/app/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ export async function getConfiguration({
apiValue: apiConfiguration.mocksHarKeyManager,
defaultValue: defaultHarKeyManager,
}),
harMimeTypesParseJson: buildProperty<Array<string>>({
cliValue: null,
fileValue: fileConfiguration.harMimeTypesParseJson,
apiValue: apiConfiguration.harMimeTypesParseJson,
defaultValue: [],
}),
mode: buildProperty<Mode>({
cliValue: cliConfiguration.mode,
fileValue: fileConfiguration.mode,
Expand Down
9 changes: 9 additions & 0 deletions packages/app/configuration/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,15 @@ export interface ConfigurationSpec extends CLIConfigurationSpec {
* Useful to capture the logs of the application.
*/
readonly console?: ConsoleSpec;

/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har',
* specifies a list of mime types that will attempt to parse the request/response body as JSON.
* If the list includes an empty string: '' and there is no mimeType set in the request, it will attempt to parse the body as JSON.
* This will only be applicable to request bodies if {@link IMock.saveInputRequestBody|saveInputRequestBody} is set to true
* Default value will be [] and will only be overridden by {@link IMock.setHarMimeTypesParseJson|setHarMimeTypesParseJson}
*/
readonly harMimeTypesParseJson?: string[];
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
17 changes: 17 additions & 0 deletions packages/app/mocking/impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,22 @@ describe('mocking', () => {
expect(response.status).toEqual(status);
});
});

describe('harMimeTypesParseJson', () => {
it('should be able to be overridden', () => {
const mock = new Mock({
options: {
root: 'root',
userConfiguration: {},
},
request: {
url: { pathname: '/url/path' },
method: 'post',
},
} as any);
mock.setHarMimeTypesParseJson(['application/json']);
expect(mock.harMimeTypesParseJson).toEqual(['application/json']);
});
});
});
});
22 changes: 20 additions & 2 deletions packages/app/mocking/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export class Mock implements IMock {
private _mocksHarKeyManager = new UserProperty<HarKeyManager>({
getDefaultInput: () => this.options.userConfiguration.mocksHarKeyManager.value,
});

private _harMimeTypesParseJson = new UserProperty<Array<string>>({
getDefaultInput: () => this.options.userConfiguration.harMimeTypesParseJson.value,
});

private _mockHarKey = new UserProperty<NonSanitizedArray<string>, string | undefined>({
transform: ({ inputOrigin, input }) =>
inputOrigin === 'none' ? this.defaultMockHarKey : joinPath(input),
Expand Down Expand Up @@ -335,6 +340,13 @@ export class Mock implements IMock {
this._setUserProperty(this._skipLog, value);
}

public get harMimeTypesParseJson(): string[] {
return this._harMimeTypesParseJson.output;
}
public setHarMimeTypesParseJson(value: string[]): void {
this._setUserProperty(this._harMimeTypesParseJson, value);
}

//////////////////////////////////////////////////////////////////////////////
// Path management
//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -563,7 +575,11 @@ export class Mock implements IMock {

@CachedProperty()
private get _harFmtPostData(): HarFormatPostData | undefined {
return toHarPostData(this.request.body, this.request.headers['content-type']);
return toHarPostData(
this.request.body,
this.request.headers['content-type'],
this.harMimeTypesParseJson,
);
}

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -653,6 +669,7 @@ export class Mock implements IMock {
message: CONF.messages.writingHarFile,
data: this._harFmtFile.path,
});
const harMimeTypesParseJson = this.harMimeTypesParseJson;
const entry: HarFormatEntry = {
_kassetteChecksumContent:
this.saveChecksumContent && this.checksumContent ? this.checksumContent : undefined,
Expand All @@ -671,7 +688,7 @@ export class Mock implements IMock {
cookies: [], // cookies parsing is not implemented
headersSize: -1,
bodySize: body?.length ?? 0,
content: toHarContent(body, data.headers?.['content-type']),
content: toHarContent(body, data.headers?.['content-type'], harMimeTypesParseJson),
},
};
if (this.saveInputRequestData) {
Expand Down Expand Up @@ -703,6 +720,7 @@ export class Mock implements IMock {
entry._kassetteForwardedRequest.postData = toHarPostData(
payload.requestOptions.body,
payload.requestOptions.headers['content-type'],
this.harMimeTypesParseJson,
);
}
}
Expand Down
15 changes: 15 additions & 0 deletions packages/app/mocking/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,21 @@ export interface IMock {
*/
setMocksHarKeyManager(value: HarKeyManager | null): void;

/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har',
* specifies a list of mime types that will attempt to parse the request/response body as JSON.
* This will only be applicable to request bodies if {@link IMock.saveInputRequestBody|saveInputRequestBody} is set to true
* Default value will be [] and will only be overridden by {@link IMock.setHarMimeTypesParseJson|setHarMimeTypesParseJson}
*/
readonly harMimeTypesParseJson: string[];

/**
* Sets the {@link IMock.harMimeTypesParseJson|harMimeTypesParseJson} value.
*
* @param value - The mime types that should attempt to parse the body as json
*/
setHarMimeTypesParseJson(value: string[]): void;

/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'folder', specifies the local path of the mock, relative to {@link IMock.mocksFolder|mocksFolder}.
* It is either the one set by the user through {@link IMock.setLocalPath|setLocalPath} or {@link IMock.defaultLocalPath|defaultLocalPath}.
Expand Down
2 changes: 2 additions & 0 deletions packages/app/server/configuration/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe('server configuration', () => {
saveInputRequestBody: { value: true, origin: 'default' },
saveForwardedRequestData: { value: true, origin: 'default' },
saveForwardedRequestBody: { value: true, origin: 'default' },
harMimeTypesParseJson: { value: [], origin: 'default' },
},
});

Expand Down Expand Up @@ -183,6 +184,7 @@ Root folder used for relative paths resolution: ${highlighted('C:/dummy/root/fol
saveInputRequestBody: { value: true, origin: 'default' },
saveForwardedRequestData: { value: true, origin: 'default' },
saveForwardedRequestBody: { value: true, origin: 'default' },
harMimeTypesParseJson: { value: [], origin: 'default' },
},
});

Expand Down
10 changes: 10 additions & 0 deletions packages/lib/har/harTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ export interface HarFormatPostData {
* Any comment as a string. This is not used by kassette.
*/
comment?: string;

/**
* Response body saved as an object.
*/
json?: any;
}

/**
Expand Down Expand Up @@ -364,6 +369,11 @@ export interface HarFormatContent {
* Any comment as a string. This is not used by kassette.
*/
comment?: string;

/**
* Response body saved as an object.
*/
json?: any;
}

/**
Expand Down
89 changes: 89 additions & 0 deletions packages/lib/har/harUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { stringifyPretty } from '../json';
import {
fromHarContent,
fromHarHeaders,
Expand All @@ -8,6 +9,7 @@ import {
toHarHttpVersion,
toHarPostData,
toHarQueryString,
checkMimeTypeListAndParseBody,
} from './harUtils';

describe('harUtils', () => {
Expand Down Expand Up @@ -152,6 +154,36 @@ describe('harUtils', () => {
});
expect(buffer.equals(outputBuffer)).toBeTruthy();
});

it('should parse json data', () => {
const content = '{"test": "hello"}';
const buffer = Buffer.from(content, 'utf8');
expect(toHarContent(buffer, 'application/json', ['application/json'])).toEqual({
mimeType: 'application/json',
size: 17,
json: { test: 'hello' },
});
});

it('should not parse json data when mimeType is not application/json', () => {
const content = '{"test": "hello"}';
const buffer = Buffer.from(content, 'utf8');
expect(toHarContent(buffer, 'text/plain', ['application/json'])).toEqual({
mimeType: 'text/plain',
size: 17,
text: content,
});
});

it('should not parse json data when parseMimeTypesAsJson is empty', () => {
const content = '{"test": "hello"}';
const buffer = Buffer.from(content, 'utf8');
expect(toHarContent(buffer, 'text/plain')).toEqual({
mimeType: 'text/plain',
size: 17,
text: content,
});
});
});

describe('postData', () => {
Expand All @@ -178,6 +210,63 @@ describe('harUtils', () => {
text: content,
});
});

it('should parse json data', () => {
const content = '{"test": "hello"}';
expect(
toHarPostData(Buffer.from(content, 'utf8'), 'application/json', ['application/json']),
).toEqual({
mimeType: 'application/json',
json: { test: 'hello' },
});
});

it('should not parse json data when mimeType is not application/json', () => {
const content = '{"test": "hello"}';
expect(
toHarPostData(Buffer.from(content, 'utf8'), 'text/plain', ['application/json']),
).toEqual({
mimeType: 'text/plain',
text: content,
});
});
});

describe('fromHarContent', () => {
it('should return content if json is set', () => {
const content = { test: 'hello' };
const buffer = Buffer.from(stringifyPretty(content), 'utf8');
const returned = fromHarContent({
mimeType: 'application/json',
size: 17,
json: content,
});
expect(buffer.equals(returned)).toBeTruthy();
});
});

describe('checkMimeTypeListAndParseBody', () => {
it('should return text if cant parse JSON', () => {
const content = 'Hello!';
const returned = checkMimeTypeListAndParseBody(
['application/json'],
content,
'application/json',
);
expect(returned).toEqual({
mimeType: 'application/json',
text: content,
});
});

it('should parse json if no mimeType is passed and mimeTypeList contains empty string', () => {
const content = '{"test": "hello"}';
const returned = checkMimeTypeListAndParseBody([''], content);
expect(returned).toEqual({
mimeType: undefined,
json: { test: 'hello' },
});
});
});

describe('version', () => {
Expand Down
55 changes: 43 additions & 12 deletions packages/lib/har/harUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { appendHeader, headersContainer } from '../headers';
import { extension } from 'mime-types';
import { isBinary } from 'istextorbinary';
import { IncomingHttpHeaders } from 'http';
import { stringifyPretty } from '../json';

export const emptyHar = (): HarFormat => ({
log: {
Expand Down Expand Up @@ -64,16 +65,18 @@ export const toHarContentBase64 = (body: Buffer, mimeType?: string): HarFormatCo
encoding: 'base64',
});

export const toHarContent = (body: string | Buffer | null, mimeType?: string): HarFormatContent => {
export const toHarContent = (
body: string | Buffer | null,
mimeType?: string,
parseMimeTypesAsJson: string[] = [],
): HarFormatContent => {
if (Buffer.isBuffer(body)) {
if (isBinary(mimeType ? `file.${extension(mimeType)}` : null, body)) {
return toHarContentBase64(body, mimeType);
}

return {
mimeType: mimeType ?? '',
...checkMimeTypeListAndParseBody(parseMimeTypesAsJson, body, mimeType),
size: body?.length ?? 0,
text: body.toString('binary'),
};
}
return {
Expand All @@ -84,22 +87,50 @@ export const toHarContent = (body: string | Buffer | null, mimeType?: string): H
};

export const fromHarContent = (content?: HarFormatContent) => {
if (content?.text) {
if (content?.text !== undefined) {
return Buffer.from(content.text, content.encoding === 'base64' ? 'base64' : 'binary');
}
if (content?.json !== undefined) {
return Buffer.from(stringifyPretty(content.json), 'utf8');
}
return Buffer.alloc(0);
};

export const checkMimeTypeListAndParseBody = (
parseMimeTypesAsJson: string[],
body: string | Buffer,
mimeType?: string,
): HarFormatPostData => {
const defaultTextReturn = {
mimeType: mimeType ?? '',
text: body.toString('binary'),
divdavem marked this conversation as resolved.
Show resolved Hide resolved
};
if (
(mimeType && parseMimeTypesAsJson.includes(mimeType)) ||
(!mimeType && parseMimeTypesAsJson.includes(''))
) {
try {
return {
mimeType,
json: JSON.parse(body.toString('utf-8')),
};
} catch (error) {
return defaultTextReturn;
}
}
return defaultTextReturn;
};

export const toHarPostData = (
body?: string | Buffer,
mimeType?: string,
): HarFormatPostData | undefined =>
body && body.length > 0
? {
mimeType: mimeType,
text: body.toString('binary'),
}
: undefined;
parseMimeTypesAsJson: string[] = [],
): HarFormatPostData | undefined => {
if (body && body.length > 0) {
return checkMimeTypeListAndParseBody(parseMimeTypesAsJson, body, mimeType);
}
return undefined;
};

export const toHarQueryString = (searchParams: URLSearchParams): HarFormatNameValuePair[] => {
const res: HarFormatNameValuePair[] = [];
Expand Down