1- import Fastify , { type FastifyInstance } from 'fastify' ;
1+ const SENTRY_DSN = 'https://anything@goes/123' ;
2+
3+ import Fastify , { FastifyError , type FastifyInstance } from 'fastify' ;
24import accepts from '@fastify/accepts' ;
5+ import { http , HttpResponse } from 'msw' ;
6+ import { setupServer } from 'msw/node' ;
7+ import '../instrument' ;
38
49import errorHandling from './error-handling' ;
510import redirectWithMessage , { formatMessage } from './redirect-with-message' ;
611
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+
722describe ( 'errorHandling' , ( ) => {
823 let fastify : FastifyInstance ;
924
@@ -14,13 +29,33 @@ describe('errorHandling', () => {
1429 await fastify . register ( errorHandling ) ;
1530
1631 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 ;
1953 } ) ;
2054 } ) ;
2155
2256 afterEach ( async ( ) => {
2357 await fastify . close ( ) ;
58+ jest . clearAllMocks ( ) ;
2459 } ) ;
2560
2661 it ( 'should redirect to the referer if the request does not Accept json' , async ( ) => {
@@ -33,14 +68,26 @@ describe('errorHandling', () => {
3368 }
3469 } ) ;
3570
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+
3684 expect ( res . headers [ 'location' ] ) . toEqual (
3785 'https://www.freecodecamp.org/anything?' +
3886 formatMessage ( {
3987 type : 'danger' ,
4088 content : 'flash.generic-error'
4189 } )
4290 ) ;
43- expect ( res . statusCode ) . toEqual ( 302 ) ;
4491 } ) ;
4592
4693 it ( 'should return a json response if the request does Accept json' , async ( ) => {
@@ -53,11 +100,11 @@ describe('errorHandling', () => {
53100 }
54101 } ) ;
55102
103+ expect ( res . statusCode ) . toEqual ( 500 ) ;
56104 expect ( res . json ( ) ) . toEqual ( {
57105 message : 'flash.generic-error' ,
58106 type : 'danger'
59107 } ) ;
60- expect ( res . statusCode ) . toEqual ( 500 ) ;
61108 } ) ;
62109
63110 it ( 'should redirect if the request prefers text/html to json' , async ( ) => {
@@ -71,13 +118,163 @@ describe('errorHandling', () => {
71118 }
72119 } ) ;
73120
74- expect ( res . headers [ 'location' ] ) . toEqual (
75- 'https://www.freecodecamp.org/anything?' +
76- formatMessage ( {
77- type : 'danger' ,
78- content : 'flash.generic-error'
79- } )
80- ) ;
81121 expect ( res . statusCode ) . toEqual ( 302 ) ;
82122 } ) ;
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+ } ) ;
83280} ) ;
0 commit comments