nestjs-request-deduplication
Version:
[](https://www.npmjs.com/package/nestjs-request-deduplication) [](https://gith
216 lines (215 loc) • 10.4 kB
JavaScript
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),
};
}