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: cliConfiguration.harMimeTypesParseJson,
fileValue: fileConfiguration.harMimeTypesParseJson,
apiValue: apiConfiguration.harMimeTypesParseJson,
defaultValue: [],
}),
mode: buildProperty<Mode>({
cliValue: cliConfiguration.mode,
fileValue: fileConfiguration.mode,
Expand Down
8 changes: 8 additions & 0 deletions packages/app/configuration/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,14 @@ export interface CLIConfigurationSpec {
* Enables http/2.0 protocol in the kassette server.
*/
readonly http2?: boolean;

/**
* 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[];
divdavem marked this conversation as resolved.
Show resolved Hide resolved
}

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

describe('harMimeTypesParseJson', () => {
it('should return default and be able to be overridden', () => {
divdavem marked this conversation as resolved.
Show resolved Hide resolved
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.harMimeTypesParseJson,
this.request.body,
this.request.headers['content-type'],
);
}

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -653,6 +669,7 @@ export class Mock implements IMock {
message: CONF.messages.writingHarFile,
data: this._harFmtFile.path,
});
const saveParsedJsonBody = this.harMimeTypesParseJson;
divdavem marked this conversation as resolved.
Show resolved Hide resolved
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(saveParsedJsonBody, body, data.headers?.['content-type']),
},
};
if (this.saveInputRequestData) {
Expand Down Expand Up @@ -701,6 +718,7 @@ export class Mock implements IMock {
entry._kassetteForwardedRequest = {};
}
entry._kassetteForwardedRequest.postData = toHarPostData(
this.harMimeTypesParseJson,
payload.requestOptions.body,
payload.requestOptions.headers['content-type'],
);
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
88 changes: 75 additions & 13 deletions packages/lib/har/harUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('harUtils', () => {

describe('content', () => {
it('should work with empty content', () => {
expect(toHarContent(null)).toEqual({ mimeType: '', size: 0, text: '' });
expect(toHarContent([], null)).toEqual({ mimeType: '', size: 0, text: '' });
const emptyBuffer = fromHarContent({});
expect(Buffer.isBuffer(emptyBuffer)).toBeTruthy();
expect(emptyBuffer.length).toBe(0);
Expand All @@ -101,13 +101,13 @@ describe('harUtils', () => {
'hex',
).toString('base64');
const buffer = Buffer.from(content, 'base64');
expect(toHarContent(buffer, 'application/octet-stream')).toEqual({
expect(toHarContent([], buffer, 'application/octet-stream')).toEqual({
mimeType: 'application/octet-stream',
size: 25,
encoding: 'base64',
text: content,
});
expect(toHarContent(buffer)).toEqual({
expect(toHarContent([], buffer)).toEqual({
mimeType: '',
size: 25,
encoding: 'base64',
Expand All @@ -118,29 +118,29 @@ describe('harUtils', () => {
size: 25,
encoding: 'base64',
text: content,
});
}) as Buffer;
divdavem marked this conversation as resolved.
Show resolved Hide resolved
expect(buffer.equals(outputBuffer)).toBeTruthy();
});

it('should work with text content', () => {
const content = 'Hello!';
const buffer = Buffer.from(content, 'utf8');
expect(toHarContent(buffer, 'text/plain')).toEqual({
expect(toHarContent([], buffer, 'text/plain')).toEqual({
mimeType: 'text/plain',
size: 6,
text: content,
});
expect(toHarContent(buffer)).toEqual({
expect(toHarContent([], buffer)).toEqual({
mimeType: '',
size: 6,
text: content,
});
expect(toHarContent(content, 'text/plain')).toEqual({
expect(toHarContent([], content, 'text/plain')).toEqual({
mimeType: 'text/plain',
size: 6,
text: content,
});
expect(toHarContent(content)).toEqual({
expect(toHarContent([], content)).toEqual({
mimeType: '',
size: 6,
text: content,
Expand All @@ -149,35 +149,97 @@ describe('harUtils', () => {
mimeType: 'text/plain',
size: 6,
text: content,
});
}) as Buffer;
expect(buffer.equals(outputBuffer)).toBeTruthy();
});

it('should parse json data', () => {
const content = '{"test": "hello"}';
const buffer = Buffer.from(content, 'utf8');
expect(toHarContent(['application/json'], buffer, '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(['application/json'], buffer, 'text/plain')).toEqual({
mimeType: 'text/plain',
size: 17,
text: content,
});
});

it('should not parse json data when saveAsString is true', () => {
divdavem marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
it('should work with no data', () => {
expect(toHarPostData(Buffer.alloc(0))).toBe(undefined);
expect(toHarPostData([], Buffer.alloc(0))).toBe(undefined);
});

it('should work with binary data', () => {
const buffer = Buffer.from('000102030405060708090a0b0c0d0e0f414243444546474849', 'hex');
expect(toHarPostData(buffer, 'application/octet-stream')).toEqual({
expect(toHarPostData([], buffer, 'application/octet-stream')).toEqual({
mimeType: 'application/octet-stream',
text: buffer.toString('binary'),
});
});

it('should work with text data', () => {
const content = 'Hello!';
expect(toHarPostData(Buffer.from(content, 'utf8'), 'text/plain')).toEqual({
expect(toHarPostData([], Buffer.from(content, 'utf8'), 'text/plain')).toEqual({
mimeType: 'text/plain',
text: content,
});
expect(toHarPostData(content, 'text/plain')).toEqual({
expect(toHarPostData([], content, 'text/plain')).toEqual({
mimeType: 'text/plain',
text: content,
});
});

it('should parse json data', () => {
const content = '{"test": "hello"}';
expect(
toHarPostData(['application/json'], Buffer.from(content, 'utf8'), '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(['application/json'], Buffer.from(content, 'utf8'), 'text/plain'),
).toEqual({
mimeType: 'text/plain',
text: content,
});
});
});

describe('fromHarContent', () => {
it('should return content if json is set', () => {
const content = { test: 'hello' };
const returned = fromHarContent({
mimeType: 'application/json',
size: 17,
json: content,
});
expect(returned).toEqual(content);
});
});

describe('version', () => {
Expand Down
Loading