Skip to content

Commit

Permalink
feat: harMimeTypesParseJson option to save bodies as JSON objects in …
Browse files Browse the repository at this point in the history
…har file (#487)
  • Loading branch information
KayleighYum committed Dec 12, 2023
1 parent e7f900b commit 863b243
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 14 deletions.
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
52 changes: 40 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,47 @@ 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 => {
if (
(mimeType && parseMimeTypesAsJson.includes(mimeType)) ||
(!mimeType && parseMimeTypesAsJson.includes(''))
) {
try {
return {
mimeType,
json: JSON.parse(body.toString('utf-8')),
};
} catch (error) {}
}
return {
mimeType: mimeType ?? '',
text: body.toString('binary'),
};
};

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

0 comments on commit 863b243

Please sign in to comment.