Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/api/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ export default [
},
},
},
{
files: ['**/*.spec.ts', '**/*.test.ts'],
rules: {
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
},
},
];
2 changes: 1 addition & 1 deletion apps/api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
collectCoverageFrom: ['**/*.(t|j)s'],
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': ['<rootDir>/$2'],
'^@/(.*)$': ['<rootDir>/$1'],
},
transform: {
'^.+\\.(t|j)s$': ['@swc-node/jest'],
Expand Down
300 changes: 300 additions & 0 deletions apps/api/src/common/filters/http-exception.filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { ArgumentsHost } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import type { FastifyReply, FastifyRequest } from 'fastify';

import { HttpExceptionFilter } from './http-exception.filter';

describe('HttpExceptionFilter', () => {
let filter: HttpExceptionFilter;
let mockRequest: FastifyRequest;
let mockResponse: FastifyReply;
let mockArgumentsHost: ArgumentsHost;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [HttpExceptionFilter],
}).compile();

filter = module.get<HttpExceptionFilter>(HttpExceptionFilter);

// Mock FastifyRequest
mockRequest = {
url: '/test-endpoint',
method: 'GET',
headers: {},
query: {},
params: {},
body: {},
} as FastifyRequest;

// Mock FastifyReply
mockResponse = {
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
} as unknown as FastifyReply;

// Mock ArgumentsHost
mockArgumentsHost = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue(mockRequest),
getResponse: jest.fn().mockReturnValue(mockResponse),
}),
} as unknown as ArgumentsHost;
});

afterEach(() => {
jest.clearAllMocks();
});

describe('catch', () => {
it('should handle string exception response', () => {
const exception = new HttpException(
'Test error message',
HttpStatus.BAD_REQUEST,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error message',
path: '/test-endpoint',
});
Comment on lines +74 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore expected statusCode in string-response assertions

HttpExceptionFilter always appends the derived statusCode alongside response and path. The underlying filter (see Line 57 of http-exception.filter.ts) pushes statusCode onto the payload for primitive responses, so the test will fail without asserting it. Please update the expectation accordingly.

       expect(mockResponse.send).toHaveBeenCalledWith({
         response: 'Test error message',
-        path: '/test-endpoint',
+        statusCode: HttpStatus.BAD_REQUEST,
+        path: '/test-endpoint',
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error message',
path: '/test-endpoint',
});
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error message',
statusCode: HttpStatus.BAD_REQUEST,
path: '/test-endpoint',
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 74 to
78, the test assertion for a primitive (string) response omits the derived
statusCode that HttpExceptionFilter adds to the payload; update the expected
object passed to mockResponse.send to include statusCode: HttpStatus.BAD_REQUEST
(so the expectation should assert response, path, and statusCode fields) and
ensure the value uses the HttpStatus enum constant.

});

it('should handle object exception response', () => {
const exceptionResponse = {
message: 'Validation failed',
error: 'Bad Request',
statusCode: HttpStatus.BAD_REQUEST,
};
const exception = new HttpException(
exceptionResponse,
HttpStatus.BAD_REQUEST,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
message: 'Validation failed',
error: 'Bad Request',
statusCode: HttpStatus.BAD_REQUEST,
path: '/test-endpoint',
});
});

it('should handle different HTTP status codes', () => {
const statusCodes = [
HttpStatus.UNAUTHORIZED,
HttpStatus.FORBIDDEN,
HttpStatus.NOT_FOUND,
HttpStatus.INTERNAL_SERVER_ERROR,
];

statusCodes.forEach((statusCode) => {
const exception = new HttpException('Test error', statusCode);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(statusCode);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
path: '/test-endpoint',
});
});
Comment on lines +111 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Account for statusCode field in looped assertions

Each call to filter.catch adds the current statusCode to the response payload. The looped assertion only checks for response and path, which misses the injected field and causes a mismatch with the filter’s actual behavior.

         expect(mockResponse.send).toHaveBeenCalledWith({
           response: 'Test error',
+          statusCode,
           path: '/test-endpoint',
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
statusCodes.forEach((statusCode) => {
const exception = new HttpException('Test error', statusCode);
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(statusCode);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
path: '/test-endpoint',
});
});
statusCodes.forEach((statusCode) => {
const exception = new HttpException('Test error', statusCode);
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(statusCode);
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
statusCode,
path: '/test-endpoint',
});
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 111 to
121, the test loop currently asserts only the response and path but the filter
also injects the statusCode into the payload; update the assertion to include
the current statusCode (use the statusCode variable from the loop) when
verifying mockResponse.send was called, or build the expected object dynamically
per iteration to include response, path and statusCode; ensure the assertion
uses the same statusCode value for each iteration and reset/clear mocks between
iterations if necessary.

});

it('should handle complex object exception response', () => {
const exceptionResponse = {
message: ['Email is required', 'Password is too short'],
error: 'Validation Error',
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
details: {
field: 'email',
value: '',
},
};
const exception = new HttpException(
exceptionResponse,
HttpStatus.UNPROCESSABLE_ENTITY,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(
HttpStatus.UNPROCESSABLE_ENTITY,
);
expect(mockResponse.send).toHaveBeenCalledWith({
message: ['Email is required', 'Password is too short'],
error: 'Validation Error',
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
path: '/test-endpoint',
details: {
field: 'email',
value: '',
},
});
});

it('should handle empty string exception response', () => {
const exception = new HttpException('', HttpStatus.NO_CONTENT);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
response: '',
path: '/test-endpoint',
});
Comment on lines +161 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include statusCode for empty string payloads

Even when the payload is an empty string, the filter still adds the numeric statusCode before sending the response. The current expectation omits it, so the assertion does not reflect reality.

       expect(mockResponse.send).toHaveBeenCalledWith({
         response: '',
+        statusCode: HttpStatus.NO_CONTENT,
         path: '/test-endpoint',
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
response: '',
path: '/test-endpoint',
});
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
response: '',
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 161 to
165, the test expects the response body for an empty string payload but omits
the numeric statusCode that the filter adds; update the assertion to include
statusCode (use HttpStatus.NO_CONTENT or its numeric value) alongside response:
'' and path: '/test-endpoint' so the mockResponse.send expectation matches the
actual payload sent by the filter.

});

it('should handle null exception response', () => {
const exception = new HttpException(null as any, HttpStatus.NO_CONTENT);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
Comment on lines +171 to +177
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing response field for null payload

When the exception payload is null, the filter normalizes it under the response property (set to null) while also adding statusCode and path. Bringing the test in line with that behavior will prevent false negatives.

       expect(mockResponse.send).toHaveBeenCalledWith({
-        statusCode: HttpStatus.NO_CONTENT,
+        response: null,
+        statusCode: HttpStatus.NO_CONTENT,
         path: '/test-endpoint',
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
response: null,
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 171 to
177, the test expects the filter to call response.send with an object missing
the normalized response field for a null payload; update the expected call to
include response: null along with statusCode: HttpStatus.NO_CONTENT and path:
'/test-endpoint' so the assertion matches the filter's normalization behavior.

});

it('should handle undefined exception response', () => {
const exception = new HttpException(
undefined as any,
HttpStatus.NO_CONTENT,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
Comment on lines +188 to +192
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align undefined-payload assertion with filter output

The filter treats undefined like other primitives, wrapping it under response in the dispatched body. The expectation should assert that response is undefined rather than omitting it altogether.

       expect(mockResponse.send).toHaveBeenCalledWith({
-        statusCode: HttpStatus.NO_CONTENT,
+        response: undefined,
+        statusCode: HttpStatus.NO_CONTENT,
         path: '/test-endpoint',
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT);
expect(mockResponse.send).toHaveBeenCalledWith({
response: undefined,
statusCode: HttpStatus.NO_CONTENT,
path: '/test-endpoint',
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 188 to
192, the test expects the response body to omit the response field for an
undefined payload but the filter wraps undefined under the "response" key;
update the assertion to expect an object containing statusCode:
HttpStatus.NO_CONTENT, path: '/test-endpoint', and response: undefined so the
test aligns with the filter's actual output.

});

it('should handle different request URLs', () => {
const urls = [
'/api/users',
'/api/projects/123',
'/api/auth/login',
'/api/feedback?page=1&limit=10',
];

urls.forEach((url) => {
Object.assign(mockRequest, { url });
const exception = new HttpException(
'Test error',
HttpStatus.BAD_REQUEST,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
path: url,
});
Comment on lines +212 to +215
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reassert statusCode for varied-URL scenarios

Even when the request URL changes, the filter keeps attaching the HTTP status code to the payload. The shared expectation inside this loop should include the statusCode field to mirror the real response structure.

         expect(mockResponse.send).toHaveBeenCalledWith({
           response: 'Test error',
+          statusCode: HttpStatus.BAD_REQUEST,
           path: url,
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
path: url,
});
expect(mockResponse.send).toHaveBeenCalledWith({
response: 'Test error',
statusCode: HttpStatus.BAD_REQUEST,
path: url,
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 212 to
215, the test's shared expectation for varied-URL scenarios omits the statusCode
field from the expected payload; update the expectation inside the loop to
include the statusCode property (use the mock response's status or the expected
status variable, e.g., statusCode: mockResponse.statusCode or status) so the
asserted object matches the real response shape { response: 'Test error', path:
url, statusCode: <expected status> }.

});
});

it('should handle nested object exception response', () => {
const exceptionResponse = {
message: 'Complex error',
error: 'Internal Server Error',
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
nested: {
level1: {
level2: {
value: 'deep nested value',
},
},
},
};
const exception = new HttpException(
exceptionResponse,
HttpStatus.INTERNAL_SERVER_ERROR,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(
HttpStatus.INTERNAL_SERVER_ERROR,
);
expect(mockResponse.send).toHaveBeenCalledWith({
message: 'Complex error',
error: 'Internal Server Error',
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
path: '/test-endpoint',
nested: {
level1: {
level2: {
value: 'deep nested value',
},
},
},
});
});

it('should handle array exception response', () => {
const exceptionResponse = ['Error 1', 'Error 2', 'Error 3'];
const exception = new HttpException(
exceptionResponse,
HttpStatus.BAD_REQUEST,
);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
0: 'Error 1',
1: 'Error 2',
2: 'Error 3',
statusCode: HttpStatus.BAD_REQUEST,
path: '/test-endpoint',
});
});
Comment on lines +257 to +274
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix expectation for array responses

HttpExceptionFilter spreads the array payload and still appends both statusCode and path (see Lines 49–65 of http-exception.filter.ts). The current assertion expects only the spread array plus statusCode, which drops the response property that the filter actually adds. Update the expectation to include the response field so the test reflects real behavior.

Apply this diff:

-      expect(mockResponse.send).toHaveBeenCalledWith({
-        0: 'Error 1',
-        1: 'Error 2',
-        2: 'Error 3',
-        statusCode: HttpStatus.BAD_REQUEST,
-        path: '/test-endpoint',
-      });
+      expect(mockResponse.send).toHaveBeenCalledWith({
+        response: {
+          0: 'Error 1',
+          1: 'Error 2',
+          2: 'Error 3',
+        },
+        statusCode: HttpStatus.BAD_REQUEST,
+        path: '/test-endpoint',
+      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle array exception response', () => {
const exceptionResponse = ['Error 1', 'Error 2', 'Error 3'];
const exception = new HttpException(
exceptionResponse,
HttpStatus.BAD_REQUEST,
);
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
0: 'Error 1',
1: 'Error 2',
2: 'Error 3',
statusCode: HttpStatus.BAD_REQUEST,
path: '/test-endpoint',
});
});
it('should handle array exception response', () => {
const exceptionResponse = ['Error 1', 'Error 2', 'Error 3'];
const exception = new HttpException(
exceptionResponse,
HttpStatus.BAD_REQUEST,
);
filter.catch(exception, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.send).toHaveBeenCalledWith({
response: {
0: 'Error 1',
1: 'Error 2',
2: 'Error 3',
},
statusCode: HttpStatus.BAD_REQUEST,
path: '/test-endpoint',
});
});
🤖 Prompt for AI Agents
In apps/api/src/common/filters/http-exception.filter.spec.ts around lines 259 to
276, the test expectation for array exception responses is missing the added
response field; update the expected object passed to mockResponse.send to
include the response property (containing the original array elements keyed
numerically or as the array) alongside statusCode and path so it matches the
filter’s behavior that spreads the array and appends both statusCode and path.


it('should handle boolean exception response', () => {
const exception = new HttpException(true as any, HttpStatus.OK);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.OK,
path: '/test-endpoint',
});
});

it('should handle number exception response', () => {
const exception = new HttpException(42 as any, HttpStatus.OK);

filter.catch(exception, mockArgumentsHost);

expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK);
expect(mockResponse.send).toHaveBeenCalledWith({
statusCode: HttpStatus.OK,
path: '/test-endpoint',
});
});
});
});
Loading