nestjs-request-deduplication
Version:
[](https://www.npmjs.com/package/nestjs-request-deduplication) [](https://gith
218 lines (217 loc) • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const testing_1 = require("@nestjs/testing");
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const request_deduplication_interceptor_1 = require("./request-deduplication.interceptor");
const services_1 = require("../services");
const constants_1 = require("../constants");
const rxjs_1 = require("rxjs");
describe('RequestDeduplicationInterceptor', () => {
let interceptor;
let requestDeduplicationService;
let reflector;
let module;
beforeEach(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const mockRequestDeduplicationService = {
processRequest: jest.fn(),
};
const mockReflector = {
get: jest.fn(),
};
module = yield testing_1.Test.createTestingModule({
providers: [
request_deduplication_interceptor_1.RequestDeduplicationInterceptor,
{
provide: services_1.RequestDeduplicationService,
useValue: mockRequestDeduplicationService,
},
{
provide: constants_1.REQUEST_DEDUPLICATION_MODULE_OPTIONS,
useValue: { ttl: 1000 },
},
{
provide: core_1.Reflector,
useValue: mockReflector,
},
],
}).compile();
interceptor = module.get(request_deduplication_interceptor_1.RequestDeduplicationInterceptor);
requestDeduplicationService = module.get(services_1.RequestDeduplicationService);
reflector = module.get(core_1.Reflector);
}));
afterEach(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
yield (module === null || module === void 0 ? void 0 : module.close());
}));
const mockCallHandler = {
handle: () => (0, rxjs_1.of)('success'),
};
it('should skip deduplication when Skip decorator is present', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
reflector.get.mockReturnValue(true);
const context = createMockExecutionContext({});
const callHandler = createMockCallHandler({ result: 'success' });
const result = yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(context, callHandler));
expect(result).toBe('success');
expect(requestDeduplicationService.processRequest).not.toHaveBeenCalled();
}));
it('should skip deduplication when skipRequestDeduplication flag is true', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
reflector.get.mockReturnValue(false);
const context = createMockExecutionContext({
skipRequestDeduplication: true,
});
const callHandler = createMockCallHandler({ result: 'success' });
const result = yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(context, callHandler));
expect(result).toBe('success');
expect(requestDeduplicationService.processRequest).not.toHaveBeenCalled();
}));
it('should throw HttpException for duplicate requests', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
reflector.get.mockReturnValue(false);
const context = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
body: { data: 'test' },
});
const callHandler = createMockCallHandler({ result: 'success' });
requestDeduplicationService.processRequest.mockResolvedValue(false);
try {
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(context, callHandler));
fail('Expected HttpException to be thrown');
}
catch (error) {
expect(error).toBeInstanceOf(common_1.HttpException);
if (error instanceof common_1.HttpException) {
expect(error.getStatus()).toBe(common_1.HttpStatus.CONFLICT);
expect(error.getResponse()).toEqual({
status: common_1.HttpStatus.CONFLICT,
error: 'Duplicate request',
});
}
}
}));
it('should process non-duplicate requests', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
reflector.get.mockReturnValue(false);
const context = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
body: { data: 'test' },
});
const callHandler = createMockCallHandler({ result: 'success' });
requestDeduplicationService.processRequest.mockResolvedValue(true);
const result = yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(context, callHandler));
expect(result).toBe('success');
expect(requestDeduplicationService.processRequest).toHaveBeenCalled();
}));
it('should allow first request to proceed', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
reflector.get.mockReturnValue(false);
const context = createMockExecutionContext({
method: 'GET',
originalUrl: '/test',
body: { data: 'test' },
});
const callHandler = createMockCallHandler({ result: 'success' });
requestDeduplicationService.processRequest.mockResolvedValue(true);
const result = yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(context, callHandler));
expect(result).toBe('success');
expect(requestDeduplicationService.processRequest).toHaveBeenCalledWith(expect.any(String), 'request_exists', 1000);
}));
it('should generate different keys for different requests', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const requests = [
{ method: 'GET', originalUrl: '/test1', body: {} },
{ method: 'GET', originalUrl: '/test2', body: {} },
{ method: 'POST', originalUrl: '/test1', body: { data: 'test' } },
];
requestDeduplicationService.processRequest.mockResolvedValue(true);
const processPromises = requests.map((reqData) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const context = createMockExecutionContext(reqData);
const result$ = interceptor.intercept(context, createMockCallHandler({ result: 'success' }));
yield (0, rxjs_1.firstValueFrom)(result$);
}));
yield Promise.all(processPromises);
const keys = new Set(requestDeduplicationService.processRequest.mock.calls.map((call) => call[0]));
expect(keys.size).toBe(requests.length);
expect(requestDeduplicationService.processRequest).toHaveBeenCalledTimes(requests.length);
}));
describe('request hash generation', () => {
beforeEach(() => {
// Set up default mock response for processRequest
requestDeduplicationService.processRequest.mockResolvedValue(true);
});
it('should generate hash including headers', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const ctx = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
headers: { 'custom-header': 'test' },
});
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx, mockCallHandler));
expect(requestDeduplicationService.processRequest).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(Number));
}));
it('should generate hash including query params', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const ctx = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
params: { id: '123' },
});
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx, mockCallHandler));
expect(requestDeduplicationService.processRequest).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(Number));
}));
it('should generate hash including request body', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const ctx = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
body: { data: 'test-data' },
});
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx, mockCallHandler));
expect(requestDeduplicationService.processRequest).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(Number));
}));
it('should handle undefined request properties', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const ctx = createMockExecutionContext({
method: 'POST',
originalUrl: '/test',
headers: undefined,
params: undefined,
body: undefined,
});
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx, mockCallHandler));
expect(requestDeduplicationService.processRequest).toHaveBeenCalled();
}));
it('should generate different hashes for different requests', () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
const ctx1 = createMockExecutionContext({
method: 'POST',
originalUrl: '/test/1',
headers: { header: '1' },
params: { param: '1' },
body: { body: '1' },
});
const ctx2 = createMockExecutionContext({
method: 'POST',
originalUrl: '/test/2',
headers: { header: '2' },
params: { param: '2' },
body: { body: '2' },
});
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx1, mockCallHandler));
yield (0, rxjs_1.firstValueFrom)(interceptor.intercept(ctx2, mockCallHandler));
const [[firstCall], [secondCall]] = requestDeduplicationService.processRequest.mock.calls;
expect(firstCall).not.toEqual(secondCall);
}));
});
afterAll(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
yield (module === null || module === void 0 ? void 0 : module.close());
}));
});
function createMockExecutionContext(request) {
const mockHandler = jest.fn();
return {
switchToHttp: () => ({
getRequest: () => (Object.assign({ method: 'GET', originalUrl: '/test', body: {} }, request)),
}),
getHandler: () => mockHandler,
getClass: () => ({}),
};
}
function createMockCallHandler(response) {
return {
handle: () => (0, rxjs_1.of)(response.result),
};
}