error-reporting.test.ts189 lines · main
1import * as Sentry from '@sentry/nextjs'
2import { beforeEach, describe, expect, it, vi } from 'vitest'
3
4import { captureCriticalError } from './error-reporting'
5import { ResponseError } from '@/types'
6
7vi.mock('@sentry/nextjs', () => ({
8 captureException: vi.fn(),
9 withScope: vi.fn((cb: (scope: any) => void) => {
10 const scope = { setTag: vi.fn() }
11 cb(scope)
12 }),
13}))
14
15describe('error-reporting', () => {
16 beforeEach(() => {
17 vi.clearAllMocks()
18 })
19
20 describe('captureCriticalError', () => {
21 it('should not capture error if message is empty', () => {
22 const error = { message: '' }
23 captureCriticalError(error, 'test context')
24
25 expect(Sentry.captureException).not.toHaveBeenCalled()
26 })
27
28 it('should capture regular Error objects', () => {
29 const error = new Error('Something went wrong')
30 captureCriticalError(error, 'test action')
31
32 expect(Sentry.captureException).toHaveBeenCalledWith(
33 expect.objectContaining({ message: 'Something went wrong', name: 'CriticalError' })
34 )
35 })
36
37 it('should not capture whitelisted error messages', () => {
38 const error = new Error('email must be an email')
39 captureCriticalError(error, 'validation')
40
41 expect(Sentry.captureException).not.toHaveBeenCalled()
42 })
43
44 it('should not capture errors with partial whitelisted message', () => {
45 const error = new Error('User error: A user with this email already exists in the system')
46 captureCriticalError(error, 'sign up')
47
48 expect(Sentry.captureException).not.toHaveBeenCalled()
49 })
50
51 it('should capture errors that are not whitelisted', () => {
52 const error = new Error('Database connection failed')
53 captureCriticalError(error, 'database')
54
55 expect(Sentry.captureException).toHaveBeenCalledWith(
56 expect.objectContaining({ message: 'Database connection failed', name: 'CriticalError' })
57 )
58 })
59
60 it('should capture 5XX ResponseError', () => {
61 const error = new ResponseError(
62 'Internal server error',
63 500,
64 undefined,
65 undefined,
66 '/api/test'
67 )
68 captureCriticalError(error, 'api call')
69
70 expect(Sentry.captureException).toHaveBeenCalledWith(
71 expect.objectContaining({
72 message: 'requestPathname /api/test w/ message: Internal server error',
73 name: 'CriticalError',
74 })
75 )
76 })
77
78 it('should not capture 4XX ResponseError', () => {
79 const error = new ResponseError('Not found', 404, undefined, undefined, '/api/test')
80 captureCriticalError(error, 'api call')
81
82 expect(Sentry.captureException).not.toHaveBeenCalled()
83 })
84
85 it('should capture ResponseError with 5XX status code', () => {
86 const error = new ResponseError('Gateway timeout', 504, undefined, undefined, '/api/gateway')
87 captureCriticalError(error, 'gateway request')
88
89 expect(Sentry.captureException).toHaveBeenCalledWith(
90 expect.objectContaining({
91 message: 'requestPathname /api/gateway w/ message: Gateway timeout',
92 name: 'CriticalError',
93 })
94 )
95 })
96
97 it('should capture ResponseError without code or requestPathname', () => {
98 const error = new ResponseError('Unknown error')
99 captureCriticalError(error, 'unknown')
100
101 expect(Sentry.captureException).toHaveBeenCalledWith(
102 expect.objectContaining({
103 message: 'Response Error (no code or requestPathname) w/ message: Unknown error',
104 name: 'CriticalError',
105 })
106 )
107 })
108
109 it('should capture unknown error objects with message property', () => {
110 const error = { message: 'Custom error object' }
111 captureCriticalError(error, 'custom')
112
113 expect(Sentry.captureException).toHaveBeenCalledWith(
114 expect.objectContaining({ message: 'Custom error object', name: 'CriticalError' })
115 )
116 })
117
118 it('should not capture unknown error without message', () => {
119 const error = { foo: 'bar' }
120 captureCriticalError(error as any, 'no message')
121
122 expect(Sentry.captureException).not.toHaveBeenCalled()
123 })
124
125 it('should not capture whitelisted password validation error', () => {
126 const error = new Error(
127 'Password is known to be weak and easy to guess, please choose a different one'
128 )
129 captureCriticalError(error, 'password update')
130
131 expect(Sentry.captureException).not.toHaveBeenCalled()
132 })
133
134 it('should not capture whitelisted TOTP error', () => {
135 const error = new Error('Invalid TOTP code entered')
136 captureCriticalError(error, 'mfa verification')
137
138 expect(Sentry.captureException).not.toHaveBeenCalled()
139 })
140
141 it('should not capture whitelisted project name error', () => {
142 const error = new Error('name should not contain a . string')
143 captureCriticalError(error, 'create project')
144
145 expect(Sentry.captureException).not.toHaveBeenCalled()
146 })
147
148 it('should capture non-whitelisted errors even if similar to whitelisted ones', () => {
149 const error = new Error('email format is invalid')
150 captureCriticalError(error, 'validation')
151
152 expect(Sentry.captureException).toHaveBeenCalledWith(
153 expect.objectContaining({ message: 'email format is invalid', name: 'CriticalError' })
154 )
155 })
156
157 it('should handle Error objects with empty message', () => {
158 const error = new Error('')
159 captureCriticalError(error, 'empty error')
160
161 expect(Sentry.captureException).not.toHaveBeenCalled()
162 })
163
164 it('should handle ResponseError at boundary of 4XX/5XX (499)', () => {
165 const error = new ResponseError(
166 'Client closed request',
167 499,
168 undefined,
169 undefined,
170 '/api/test'
171 )
172 captureCriticalError(error, 'request')
173
174 expect(Sentry.captureException).not.toHaveBeenCalled()
175 })
176
177 it('should handle ResponseError at boundary of 4XX/5XX (500)', () => {
178 const error = new ResponseError('Internal error', 500, undefined, undefined, '/api/test')
179 captureCriticalError(error, 'request')
180
181 expect(Sentry.captureException).toHaveBeenCalledWith(
182 expect.objectContaining({
183 message: 'requestPathname /api/test w/ message: Internal error',
184 name: 'CriticalError',
185 })
186 )
187 })
188 })
189})