UNPKG

nestjs-request-deduplication

Version:

[![npm version](https://badge.fury.io/js/nestjs-request-deduplication.svg)](https://www.npmjs.com/package/nestjs-request-deduplication) [![CI](https://github.com/daniyel/nestjs-request-deduplication/actions/workflows/pr-checks.yml/badge.svg)](https://gith

218 lines (217 loc) 10.9 kB
"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), }; }