1
- import Fastify , { type FastifyInstance } from 'fastify' ;
1
+ const SENTRY_DSN = 'https://anything@goes/123' ;
2
+
3
+ import Fastify , { FastifyError , type FastifyInstance } from 'fastify' ;
2
4
import accepts from '@fastify/accepts' ;
5
+ import { http , HttpResponse } from 'msw' ;
6
+ import { setupServer } from 'msw/node' ;
7
+ import '../instrument' ;
3
8
4
9
import errorHandling from './error-handling' ;
5
10
import redirectWithMessage , { formatMessage } from './redirect-with-message' ;
6
11
12
+ jest . mock ( '../utils/env' , ( ) => {
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
14
+ return {
15
+ ...jest . requireActual ( '../utils/env' ) ,
16
+ SENTRY_DSN
17
+ } ;
18
+ } ) ;
19
+
20
+ const delay = ( ms : number ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
21
+
7
22
describe ( 'errorHandling' , ( ) => {
8
23
let fastify : FastifyInstance ;
9
24
@@ -14,13 +29,33 @@ describe('errorHandling', () => {
14
29
await fastify . register ( errorHandling ) ;
15
30
16
31
fastify . get ( '/test' , async ( _req , _reply ) => {
17
- throw new Error ( 'a very bad thing happened' ) ;
18
- return { ok : true } ;
32
+ const error = Error ( 'a very bad thing happened' ) as FastifyError ;
33
+ error . statusCode = 500 ;
34
+ throw error ;
35
+ } ) ;
36
+ fastify . get ( '/test-bad-request' , async ( _req , _reply ) => {
37
+ const error = Error ( 'a very bad thing happened' ) as FastifyError ;
38
+ error . statusCode = 400 ;
39
+ throw error ;
40
+ } ) ;
41
+ fastify . get ( '/test-csrf-token' , async ( _req , _reply ) => {
42
+ const error = Error ( ) as FastifyError ;
43
+ error . code = 'FST_CSRF_INVALID_TOKEN' ;
44
+ error . statusCode = 403 ;
45
+ throw error ;
46
+ } ) ;
47
+
48
+ fastify . get ( '/test-csrf-secret' , async ( _req , _reply ) => {
49
+ const error = Error ( ) as FastifyError ;
50
+ error . code = 'FST_CSRF_MISSING_SECRET' ;
51
+ error . statusCode = 403 ;
52
+ throw error ;
19
53
} ) ;
20
54
} ) ;
21
55
22
56
afterEach ( async ( ) => {
23
57
await fastify . close ( ) ;
58
+ jest . clearAllMocks ( ) ;
24
59
} ) ;
25
60
26
61
it ( 'should redirect to the referer if the request does not Accept json' , async ( ) => {
@@ -33,14 +68,26 @@ describe('errorHandling', () => {
33
68
}
34
69
} ) ;
35
70
71
+ expect ( res . statusCode ) . toEqual ( 302 ) ;
72
+ } ) ;
73
+
74
+ it ( 'should add a generic flash message if it is a server error (i.e. 500+)' , async ( ) => {
75
+ const res = await fastify . inject ( {
76
+ method : 'GET' ,
77
+ url : '/test' ,
78
+ headers : {
79
+ referer : 'https://www.freecodecamp.org/anything' ,
80
+ accept : 'text/plain'
81
+ }
82
+ } ) ;
83
+
36
84
expect ( res . headers [ 'location' ] ) . toEqual (
37
85
'https://www.freecodecamp.org/anything?' +
38
86
formatMessage ( {
39
87
type : 'danger' ,
40
88
content : 'flash.generic-error'
41
89
} )
42
90
) ;
43
- expect ( res . statusCode ) . toEqual ( 302 ) ;
44
91
} ) ;
45
92
46
93
it ( 'should return a json response if the request does Accept json' , async ( ) => {
@@ -53,11 +100,11 @@ describe('errorHandling', () => {
53
100
}
54
101
} ) ;
55
102
103
+ expect ( res . statusCode ) . toEqual ( 500 ) ;
56
104
expect ( res . json ( ) ) . toEqual ( {
57
105
message : 'flash.generic-error' ,
58
106
type : 'danger'
59
107
} ) ;
60
- expect ( res . statusCode ) . toEqual ( 500 ) ;
61
108
} ) ;
62
109
63
110
it ( 'should redirect if the request prefers text/html to json' , async ( ) => {
@@ -71,13 +118,163 @@ describe('errorHandling', () => {
71
118
}
72
119
} ) ;
73
120
74
- expect ( res . headers [ 'location' ] ) . toEqual (
75
- 'https://www.freecodecamp.org/anything?' +
76
- formatMessage ( {
77
- type : 'danger' ,
78
- content : 'flash.generic-error'
79
- } )
80
- ) ;
81
121
expect ( res . statusCode ) . toEqual ( 302 ) ;
82
122
} ) ;
123
+
124
+ it ( 'should respect the error status code' , async ( ) => {
125
+ const res = await fastify . inject ( {
126
+ method : 'GET' ,
127
+ url : '/test-bad-request'
128
+ } ) ;
129
+
130
+ expect ( res . statusCode ) . toEqual ( 400 ) ;
131
+ } ) ;
132
+
133
+ it ( 'should return the error message if the status is not 500 ' , async ( ) => {
134
+ const res = await fastify . inject ( {
135
+ method : 'GET' ,
136
+ url : '/test-bad-request'
137
+ } ) ;
138
+
139
+ expect ( res . json ( ) ) . toEqual ( {
140
+ message : 'a very bad thing happened' ,
141
+ type : 'danger'
142
+ } ) ;
143
+ } ) ;
144
+
145
+ it ( 'should convert CSRF errors to a generic error message' , async ( ) => {
146
+ const resToken = await fastify . inject ( {
147
+ method : 'GET' ,
148
+ url : '/test-csrf-token'
149
+ } ) ;
150
+ const resSecret = await fastify . inject ( {
151
+ method : 'GET' ,
152
+ url : '/test-csrf-secret'
153
+ } ) ;
154
+
155
+ expect ( resToken . json ( ) ) . toEqual ( {
156
+ message : 'flash.generic-error' ,
157
+ type : 'danger'
158
+ } ) ;
159
+ expect ( resSecret . json ( ) ) . toEqual ( {
160
+ message : 'flash.generic-error' ,
161
+ type : 'danger'
162
+ } ) ;
163
+ } ) ;
164
+
165
+ it ( 'should call fastify.log.error when an unhandled error occurs' , async ( ) => {
166
+ const logSpy = jest . spyOn ( fastify . log , 'error' ) ;
167
+
168
+ await fastify . inject ( {
169
+ method : 'GET' ,
170
+ url : '/test'
171
+ } ) ;
172
+
173
+ expect ( logSpy ) . toHaveBeenCalledWith ( Error ( 'a very bad thing happened' ) ) ;
174
+ } ) ;
175
+
176
+ it ( 'should call fastify.log.warn when a bad request error occurs' , async ( ) => {
177
+ const logSpy = jest . spyOn ( fastify . log , 'warn' ) ;
178
+
179
+ await fastify . inject ( {
180
+ method : 'GET' ,
181
+ url : '/test-bad-request'
182
+ } ) ;
183
+
184
+ expect ( logSpy ) . toHaveBeenCalledWith ( Error ( 'a very bad thing happened' ) ) ;
185
+ } ) ;
186
+
187
+ it ( 'should NOT log when a CSRF error is thrown' , async ( ) => {
188
+ const errorLogSpy = jest . spyOn ( fastify . log , 'error' ) ;
189
+ const warnLogSpy = jest . spyOn ( fastify . log , 'warn' ) ;
190
+
191
+ await fastify . inject ( {
192
+ method : 'GET' ,
193
+ url : '/test-csrf-token'
194
+ } ) ;
195
+
196
+ expect ( errorLogSpy ) . not . toHaveBeenCalled ( ) ;
197
+ expect ( warnLogSpy ) . not . toHaveBeenCalled ( ) ;
198
+
199
+ await fastify . inject ( {
200
+ method : 'GET' ,
201
+ url : '/test-csrf-secret'
202
+ } ) ;
203
+
204
+ expect ( errorLogSpy ) . not . toHaveBeenCalled ( ) ;
205
+ expect ( warnLogSpy ) . not . toHaveBeenCalled ( ) ;
206
+ } ) ;
207
+
208
+ describe ( 'Sentry integration' , ( ) => {
209
+ let mockServer : ReturnType < typeof setupServer > ;
210
+
211
+ beforeAll ( ( ) => {
212
+ // The assumption is that Sentry is the only library making requests. Also, we
213
+ // only want to know if a request was made, not what it was.
214
+ const sentryHandler = http . post ( '*' , ( ) =>
215
+ HttpResponse . json ( { success : true } )
216
+ ) ;
217
+ mockServer = setupServer ( sentryHandler ) ;
218
+ mockServer . listen ( ) ;
219
+ } ) ;
220
+
221
+ afterEach ( ( ) => {
222
+ mockServer . resetHandlers ( ) ;
223
+ } ) ;
224
+
225
+ afterAll ( ( ) => {
226
+ mockServer . close ( ) ;
227
+ } ) ;
228
+
229
+ const createRequestListener = ( ) =>
230
+ new Promise ( resolve => {
231
+ mockServer . events . on ( 'request:start' , ( ) => {
232
+ resolve ( true ) ;
233
+ } ) ;
234
+ } ) ;
235
+
236
+ it ( 'should capture the error with Sentry' , async ( ) => {
237
+ const receivedRequest = createRequestListener ( ) ;
238
+
239
+ await fastify . inject ( {
240
+ method : 'GET' ,
241
+ url : '/test'
242
+ } ) ;
243
+
244
+ expect ( await Promise . race ( [ receivedRequest , delay ( 1000 ) ] ) ) . toBe ( true ) ;
245
+ } ) ;
246
+
247
+ it ( 'should NOT capture CSRF token errors with Sentry' , async ( ) => {
248
+ const receivedRequest = createRequestListener ( ) ;
249
+
250
+ await fastify . inject ( {
251
+ method : 'GET' ,
252
+ url : '/test-csrf-token'
253
+ } ) ;
254
+
255
+ expect ( await Promise . race ( [ receivedRequest , delay ( 200 ) ] ) ) . toBeUndefined ( ) ;
256
+ } ) ;
257
+
258
+ it ( 'should NOT capture CSRF secret errors with Sentry' , async ( ) => {
259
+ const receivedRequest = createRequestListener ( ) ;
260
+
261
+ await fastify . inject ( {
262
+ method : 'GET' ,
263
+ url : '/test-csrf-secret'
264
+ } ) ;
265
+
266
+ expect ( await Promise . race ( [ receivedRequest , delay ( 200 ) ] ) ) . toBeUndefined ( ) ;
267
+ } ) ;
268
+
269
+ it ( 'should NOT capture bad requests with Sentry' , async ( ) => {
270
+ const receivedRequest = createRequestListener ( ) ;
271
+
272
+ await fastify . inject ( {
273
+ method : 'GET' ,
274
+ url : '/test-bad-request'
275
+ } ) ;
276
+
277
+ expect ( await Promise . race ( [ receivedRequest , delay ( 200 ) ] ) ) . toBeUndefined ( ) ;
278
+ } ) ;
279
+ } ) ;
83
280
} ) ;
0 commit comments