@whook/whook
Version:
Build strong and efficient REST web services.
336 lines (300 loc) • 10.4 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test, beforeEach, jest, expect } from '@jest/globals';
import streamtest from 'streamtest';
import { YError } from 'yerror';
import initHTTPTransaction from './httpTransaction.js';
import { type WhookAPMService } from './apm.js';
import { type WhookObfuscatorService } from './obfuscator.js';
import { ServerResponse, type IncomingMessage } from 'node:http';
import {
type LogService,
type TimeService,
type DelayService,
} from 'common-services';
import { type WhookResponse } from '../types/http.js';
function streamifyBody(response: WhookResponse): WhookResponse {
return response
? Object.assign({}, response, {
body: streamtest.fromChunks([
Buffer.from(JSON.stringify(response.body)),
]),
})
: { status: 418 };
}
describe('initHTTPTransaction', () => {
const log = jest.fn<LogService>();
const apm = jest.fn<WhookAPMService>();
const time = jest.fn<TimeService>();
const uniqueId = jest.fn<() => string>();
const delay = {
create: jest.fn<DelayService['create']>(),
clear: jest.fn<DelayService['clear']>(),
};
const obfuscator = {
obfuscate: jest.fn<WhookObfuscatorService['obfuscate']>(),
obfuscateSensibleProps:
jest.fn<WhookObfuscatorService['obfuscateSensibleProps']>(),
obfuscateSensibleHeaders:
jest.fn<WhookObfuscatorService['obfuscateSensibleHeaders']>(),
};
beforeEach(() => {
log.mockReset();
apm.mockReset();
time.mockReset();
uniqueId.mockReset();
delay.create.mockReset();
delay.clear.mockReset();
obfuscator.obfuscate.mockReset();
obfuscator.obfuscateSensibleProps.mockReset();
obfuscator.obfuscateSensibleHeaders.mockReset();
});
test('should work', async () => {
const httpTransaction = await initHTTPTransaction({
obfuscator,
log,
apm,
time,
delay,
uniqueId,
});
expect(httpTransaction).toBeTruthy();
expect({
logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
apmCalls: apm.mock.calls,
timeCalls: time.mock.calls,
uniqueIdCalls: uniqueId.mock.calls,
delayCreateCalls: delay.create.mock.calls,
delayClearCalls: delay.clear.mock.calls,
obfuscateCalls: obfuscator.obfuscate.mock.calls,
obfuscateSensiblePropsCalls: obfuscator.obfuscateSensibleProps.mock.calls,
obfuscateSensibleHeadersCalls:
obfuscator.obfuscateSensibleHeaders.mock.calls,
}).toMatchSnapshot();
});
describe('httpTransaction', () => {
const buildResponse = jest.fn<any>();
beforeEach(() => {
buildResponse.mockReset();
});
test('should work', async () => {
time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
uniqueId.mockReturnValue('[id]');
delay.create.mockReturnValueOnce(new Promise(() => undefined));
delay.clear.mockResolvedValue('cancel');
const httpTransaction = await initHTTPTransaction({
obfuscator,
log,
apm,
time,
delay,
uniqueId,
});
const req = {
connection: { encrypted: true },
ts: 1000000,
ip: '127.0.0.1',
method: 'GET',
url: '/v1/users/1?extended=true',
headers: {
'x-forwarded-for': '127.0.0.1',
},
socket: {
bytesRead: 16,
bytesWritten: 64,
},
};
const [res, resBodyPromise] = streamtest.toText() as [
ServerResponse<IncomingMessage>,
Promise<string>,
];
const writeHead = jest.fn<ServerResponse<IncomingMessage>['writeHead']>();
res.writeHead = writeHead as ServerResponse<IncomingMessage>['writeHead'];
buildResponse.mockResolvedValueOnce({
body: streamtest.fromChunks([
Buffer.from(
JSON.stringify({
id: 1,
name: 'John Doe',
}),
),
]),
headers: {
'Content-Type': 'application/json',
},
status: 200,
});
const { request, transaction } = await httpTransaction(
req as unknown as IncomingMessage,
res,
);
const response = await transaction.start(buildResponse);
await transaction.end(response, 'theOperationId');
expect(request.url).toEqual('/v1/users/1?extended=true');
expect(request.method).toEqual('get');
expect(request.headers).toEqual({
'x-forwarded-for': '127.0.0.1',
});
expect(request.body).toEqual(req);
expect(writeHead.mock.calls).toEqual([
[
200,
'OK',
{
'Content-Type': 'application/json',
'Transaction-Id': '[id]',
},
],
]);
const text = await resBodyPromise;
expect(text).toEqual('{"id":1,"name":"John Doe"}');
expect({
request,
buildResponseCalls: buildResponse.mock.calls,
logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
apmCalls: apm.mock.calls,
timeCalls: time.mock.calls,
uniqueIdCalls: uniqueId.mock.calls,
delayCreateCalls: delay.create.mock.calls,
delayClearCalls: delay.clear.mock.calls,
obfuscateCalls: obfuscator.obfuscate.mock.calls,
obfuscateSensiblePropsCalls:
obfuscator.obfuscateSensibleProps.mock.calls,
obfuscateSensibleHeadersCalls:
obfuscator.obfuscateSensibleHeaders.mock.calls,
}).toMatchSnapshot();
});
test('should fail on timeout', async () => {
time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
uniqueId.mockReturnValue('[id]');
delay.create.mockResolvedValueOnce('timeout');
delay.clear.mockResolvedValueOnce('timeout');
let request;
try {
const httpTransaction = await initHTTPTransaction({
obfuscator,
log,
apm,
time,
delay,
uniqueId,
});
const req = {
connection: { encrypted: true },
ts: 1000000,
ip: '127.0.0.1',
method: 'GET',
url: '/v1/users/1?extended=true',
headers: {
'x-forwarded-for': '127.0.0.1',
},
socket: {
bytesRead: 16,
bytesWritten: 64,
},
};
const [res, resBodyPromise] = streamtest.toText() as [
ServerResponse<IncomingMessage>,
Promise<string>,
];
const writeHead =
jest.fn<ServerResponse<IncomingMessage>['writeHead']>();
res.writeHead =
writeHead as ServerResponse<IncomingMessage>['writeHead'];
buildResponse.mockReturnValueOnce(new Promise(() => undefined));
const { request: _request, transaction } = await httpTransaction(
req as unknown as IncomingMessage,
res,
);
request = _request;
await transaction.start(buildResponse);
await transaction.end({ status: 200 });
await resBodyPromise;
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
expect((err as any).code).toEqual('E_TRANSACTION_TIMEOUT');
expect((err as any).httpCode).toEqual(504);
expect({
request,
logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
apmCalls: apm.mock.calls,
timeCalls: time.mock.calls,
uniqueIdCalls: uniqueId.mock.calls,
delayCreateCalls: delay.create.mock.calls,
delayClearCalls: delay.clear.mock.calls,
obfuscateCalls: obfuscator.obfuscate.mock.calls,
obfuscateSensiblePropsCalls:
obfuscator.obfuscateSensibleProps.mock.calls,
obfuscateSensibleHeadersCalls:
obfuscator.obfuscateSensibleHeaders.mock.calls,
}).toMatchSnapshot();
}
});
test('should fail with non-unique transaction id', async () => {
time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
uniqueId.mockReturnValue('[id]');
delay.create.mockReturnValueOnce(new Promise(() => 'cancel'));
delay.clear.mockResolvedValue('cancel');
try {
const httpTransaction = await initHTTPTransaction({
obfuscator,
log,
apm,
time,
delay,
uniqueId,
TRANSACTIONS: { lol: {} },
});
const req = {
connection: { encrypted: true },
ts: 1000000,
ip: '127.0.0.1',
method: 'GET',
url: '/v1/users/1?extended=true',
headers: {
'x-forwarded-for': '127.0.0.1',
'transaction-id': 'lol',
},
socket: {
bytesRead: 16,
bytesWritten: 64,
},
};
const [res, resBodyPromise] = streamtest.toText() as [
ServerResponse<IncomingMessage>,
Promise<string>,
];
const writeHead =
jest.fn<ServerResponse<IncomingMessage>['writeHead']>();
res.writeHead =
writeHead as ServerResponse<IncomingMessage>['writeHead'];
buildResponse.mockReturnValueOnce(Promise.resolve());
const { transaction } = await httpTransaction(
req as unknown as IncomingMessage,
res,
);
await transaction
.start(buildResponse)
.catch(transaction.catch)
.then(streamifyBody)
.then(transaction.end);
await resBodyPromise;
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
expect((err as any).code).toEqual('E_TRANSACTION_ID_NOT_UNIQUE');
expect((err as any).httpCode).toEqual(400);
expect({
logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
timeCalls: time.mock.calls,
uniqueIdCalls: uniqueId.mock.calls,
delayCreateCalls: delay.create.mock.calls,
delayClearCalls: delay.clear.mock.calls,
obfuscateCalls: obfuscator.obfuscate.mock.calls,
obfuscateSensiblePropsCalls:
obfuscator.obfuscateSensibleProps.mock.calls,
obfuscateSensibleHeadersCalls:
obfuscator.obfuscateSensibleHeaders.mock.calls,
}).toMatchSnapshot();
}
});
});
});