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

216 lines (215 loc) 10.4 kB
import { __awaiter } from "tslib"; import { Test } from '@nestjs/testing'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { RequestDeduplicationInterceptor } from './request-deduplication.interceptor'; import { RequestDeduplicationService } from '../services'; import { REQUEST_DEDUPLICATION_MODULE_OPTIONS } from '../constants'; import { of, firstValueFrom } from 'rxjs'; describe('RequestDeduplicationInterceptor', () => { let interceptor; let requestDeduplicationService; let reflector; let module; beforeEach(() => __awaiter(void 0, void 0, void 0, function* () { const mockRequestDeduplicationService = { processRequest: jest.fn(), }; const mockReflector = { get: jest.fn(), }; module = yield Test.createTestingModule({ providers: [ RequestDeduplicationInterceptor, { provide: RequestDeduplicationService, useValue: mockRequestDeduplicationService, }, { provide: REQUEST_DEDUPLICATION_MODULE_OPTIONS, useValue: { ttl: 1000 }, }, { provide: Reflector, useValue: mockReflector, }, ], }).compile(); interceptor = module.get(RequestDeduplicationInterceptor); requestDeduplicationService = module.get(RequestDeduplicationService); reflector = module.get(Reflector); })); afterEach(() => __awaiter(void 0, void 0, void 0, function* () { yield (module === null || module === void 0 ? void 0 : module.close()); })); const mockCallHandler = { handle: () => of('success'), }; it('should skip deduplication when Skip decorator is present', () => __awaiter(void 0, void 0, void 0, function* () { reflector.get.mockReturnValue(true); const context = createMockExecutionContext({}); const callHandler = createMockCallHandler({ result: 'success' }); const result = yield firstValueFrom(interceptor.intercept(context, callHandler)); expect(result).toBe('success'); expect(requestDeduplicationService.processRequest).not.toHaveBeenCalled(); })); it('should skip deduplication when skipRequestDeduplication flag is true', () => __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 firstValueFrom(interceptor.intercept(context, callHandler)); expect(result).toBe('success'); expect(requestDeduplicationService.processRequest).not.toHaveBeenCalled(); })); it('should throw HttpException for duplicate requests', () => __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 firstValueFrom(interceptor.intercept(context, callHandler)); fail('Expected HttpException to be thrown'); } catch (error) { expect(error).toBeInstanceOf(HttpException); if (error instanceof HttpException) { expect(error.getStatus()).toBe(HttpStatus.CONFLICT); expect(error.getResponse()).toEqual({ status: HttpStatus.CONFLICT, error: 'Duplicate request', }); } } })); it('should process non-duplicate requests', () => __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 firstValueFrom(interceptor.intercept(context, callHandler)); expect(result).toBe('success'); expect(requestDeduplicationService.processRequest).toHaveBeenCalled(); })); it('should allow first request to proceed', () => __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 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', () => __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) => __awaiter(void 0, void 0, void 0, function* () { const context = createMockExecutionContext(reqData); const result$ = interceptor.intercept(context, createMockCallHandler({ result: 'success' })); yield 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', () => __awaiter(void 0, void 0, void 0, function* () { const ctx = createMockExecutionContext({ method: 'POST', originalUrl: '/test', headers: { 'custom-header': 'test' }, }); yield 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', () => __awaiter(void 0, void 0, void 0, function* () { const ctx = createMockExecutionContext({ method: 'POST', originalUrl: '/test', params: { id: '123' }, }); yield 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', () => __awaiter(void 0, void 0, void 0, function* () { const ctx = createMockExecutionContext({ method: 'POST', originalUrl: '/test', body: { data: 'test-data' }, }); yield firstValueFrom(interceptor.intercept(ctx, mockCallHandler)); expect(requestDeduplicationService.processRequest).toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.any(Number)); })); it('should handle undefined request properties', () => __awaiter(void 0, void 0, void 0, function* () { const ctx = createMockExecutionContext({ method: 'POST', originalUrl: '/test', headers: undefined, params: undefined, body: undefined, }); yield firstValueFrom(interceptor.intercept(ctx, mockCallHandler)); expect(requestDeduplicationService.processRequest).toHaveBeenCalled(); })); it('should generate different hashes for different requests', () => __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 firstValueFrom(interceptor.intercept(ctx1, mockCallHandler)); yield firstValueFrom(interceptor.intercept(ctx2, mockCallHandler)); const [[firstCall], [secondCall]] = requestDeduplicationService.processRequest.mock.calls; expect(firstCall).not.toEqual(secondCall); })); }); afterAll(() => __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: () => of(response.result), }; }