|
| 1 | +import { |
| 2 | + BadRequestException, |
| 3 | + HttpException, |
| 4 | + HttpStatus, |
| 5 | + NotFoundException, |
| 6 | +} from '@nestjs/common'; |
| 7 | +import { |
| 8 | + SubscriptionsExceptionFilter, |
| 9 | + SubscriptionErrorResponse, |
| 10 | +} from './subscriptions-exception.filter'; |
| 11 | + |
| 12 | +describe('SubscriptionsExceptionFilter', () => { |
| 13 | + let filter: SubscriptionsExceptionFilter; |
| 14 | + let mockJson: jest.Mock; |
| 15 | + let mockStatus: jest.Mock; |
| 16 | + let mockHost: any; |
| 17 | + |
| 18 | + beforeEach(() => { |
| 19 | + filter = new SubscriptionsExceptionFilter(); |
| 20 | + mockJson = jest.fn(); |
| 21 | + mockStatus = jest.fn().mockReturnValue({ json: mockJson }); |
| 22 | + mockHost = { |
| 23 | + switchToHttp: () => ({ |
| 24 | + getResponse: () => ({ status: mockStatus }), |
| 25 | + getRequest: () => ({ url: '/v1/subscriptions/checkout', method: 'POST' }), |
| 26 | + }), |
| 27 | + }; |
| 28 | + }); |
| 29 | + |
| 30 | + function getResponseBody(): SubscriptionErrorResponse { |
| 31 | + return mockJson.mock.calls[0][0]; |
| 32 | + } |
| 33 | + |
| 34 | + it('returns consistent shape for BadRequestException', () => { |
| 35 | + filter.catch(new BadRequestException('Invalid plan'), mockHost); |
| 36 | + |
| 37 | + const body = getResponseBody(); |
| 38 | + expect(mockStatus).toHaveBeenCalledWith(400); |
| 39 | + expect(body).toEqual({ |
| 40 | + statusCode: 400, |
| 41 | + error: 'BAD_REQUEST', |
| 42 | + message: 'Invalid plan', |
| 43 | + timestamp: expect.any(String), |
| 44 | + path: '/v1/subscriptions/checkout', |
| 45 | + }); |
| 46 | + }); |
| 47 | + |
| 48 | + it('returns consistent shape for NotFoundException', () => { |
| 49 | + filter.catch(new NotFoundException('Checkout not found'), mockHost); |
| 50 | + |
| 51 | + const body = getResponseBody(); |
| 52 | + expect(mockStatus).toHaveBeenCalledWith(404); |
| 53 | + expect(body.statusCode).toBe(404); |
| 54 | + expect(body.error).toBe('NOT_FOUND'); |
| 55 | + expect(body.message).toBe('Checkout not found'); |
| 56 | + expect(body.path).toBe('/v1/subscriptions/checkout'); |
| 57 | + }); |
| 58 | + |
| 59 | + it('returns consistent shape for HttpException with object response', () => { |
| 60 | + filter.catch( |
| 61 | + new HttpException( |
| 62 | + { error: 'NETWORK_MISMATCH', message: 'Wallet network mismatch' }, |
| 63 | + HttpStatus.BAD_REQUEST, |
| 64 | + ), |
| 65 | + mockHost, |
| 66 | + ); |
| 67 | + |
| 68 | + const body = getResponseBody(); |
| 69 | + expect(body.statusCode).toBe(400); |
| 70 | + expect(body.error).toBe('NETWORK_MISMATCH'); |
| 71 | + expect(body.message).toBe('Wallet network mismatch'); |
| 72 | + }); |
| 73 | + |
| 74 | + it('returns consistent shape for HttpException with string response', () => { |
| 75 | + filter.catch( |
| 76 | + new HttpException('Something went wrong', HttpStatus.FORBIDDEN), |
| 77 | + mockHost, |
| 78 | + ); |
| 79 | + |
| 80 | + const body = getResponseBody(); |
| 81 | + expect(body.statusCode).toBe(403); |
| 82 | + expect(body.error).toBe('FORBIDDEN'); |
| 83 | + expect(body.message).toBe('Something went wrong'); |
| 84 | + }); |
| 85 | + |
| 86 | + it('returns 500 for unknown errors', () => { |
| 87 | + filter.catch(new Error('unexpected'), mockHost); |
| 88 | + |
| 89 | + const body = getResponseBody(); |
| 90 | + expect(mockStatus).toHaveBeenCalledWith(500); |
| 91 | + expect(body.statusCode).toBe(500); |
| 92 | + expect(body.error).toBe('INTERNAL_ERROR'); |
| 93 | + expect(body.message).toBe('unexpected'); |
| 94 | + }); |
| 95 | + |
| 96 | + it('returns 500 for non-Error thrown values', () => { |
| 97 | + filter.catch('string error', mockHost); |
| 98 | + |
| 99 | + const body = getResponseBody(); |
| 100 | + expect(body.statusCode).toBe(500); |
| 101 | + expect(body.error).toBe('INTERNAL_ERROR'); |
| 102 | + expect(body.message).toBe('Internal server error'); |
| 103 | + }); |
| 104 | + |
| 105 | + it('includes timestamp in ISO format', () => { |
| 106 | + filter.catch(new BadRequestException('test'), mockHost); |
| 107 | + |
| 108 | + const body = getResponseBody(); |
| 109 | + expect(() => new Date(body.timestamp)).not.toThrow(); |
| 110 | + expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp); |
| 111 | + }); |
| 112 | + |
| 113 | + it('includes request path', () => { |
| 114 | + filter.catch(new BadRequestException('test'), mockHost); |
| 115 | + |
| 116 | + const body = getResponseBody(); |
| 117 | + expect(body.path).toBe('/v1/subscriptions/checkout'); |
| 118 | + }); |
| 119 | + |
| 120 | + it('handles 429 Too Many Requests', () => { |
| 121 | + filter.catch( |
| 122 | + new HttpException('Too many requests', HttpStatus.TOO_MANY_REQUESTS), |
| 123 | + mockHost, |
| 124 | + ); |
| 125 | + |
| 126 | + const body = getResponseBody(); |
| 127 | + expect(body.statusCode).toBe(429); |
| 128 | + expect(body.error).toBe('TOO_MANY_REQUESTS'); |
| 129 | + }); |
| 130 | + |
| 131 | + it('joins array messages into single string', () => { |
| 132 | + filter.catch( |
| 133 | + new HttpException( |
| 134 | + { message: ['field1 is required', 'field2 must be a number'], error: 'Bad Request' }, |
| 135 | + HttpStatus.BAD_REQUEST, |
| 136 | + ), |
| 137 | + mockHost, |
| 138 | + ); |
| 139 | + |
| 140 | + const body = getResponseBody(); |
| 141 | + expect(body.message).toBe('field1 is required; field2 must be a number'); |
| 142 | + }); |
| 143 | + |
| 144 | + it('all error responses have exactly the same keys', () => { |
| 145 | + const exceptions = [ |
| 146 | + new BadRequestException('bad'), |
| 147 | + new NotFoundException('missing'), |
| 148 | + new HttpException('forbidden', 403), |
| 149 | + new Error('crash'), |
| 150 | + ]; |
| 151 | + |
| 152 | + const expectedKeys = ['statusCode', 'error', 'message', 'timestamp', 'path']; |
| 153 | + |
| 154 | + for (const exception of exceptions) { |
| 155 | + mockJson.mockClear(); |
| 156 | + mockStatus.mockClear().mockReturnValue({ json: mockJson }); |
| 157 | + filter.catch(exception, mockHost); |
| 158 | + const body = getResponseBody(); |
| 159 | + expect(Object.keys(body).sort()).toEqual(expectedKeys.sort()); |
| 160 | + } |
| 161 | + }); |
| 162 | +}); |
0 commit comments