@@ -41,7 +41,16 @@ describe('LoggingMiddleware', () => {
4141 listeners [ event ] = listeners [ event ] ?? [ ] ;
4242 listeners [ event ] . push ( cb ) ;
4343 } ) as unknown as Response [ 'on' ] ,
44- } as Partial < Response > ;
44+ getEventListeners : ( ) => listeners ,
45+ json : jest . fn ( function ( data : any ) {
46+ this . json = jest . fn ( function ( d ) { return this ; } ) ;
47+ return this ;
48+ } ) ,
49+ send : jest . fn ( function ( data : any ) {
50+ this . send = jest . fn ( function ( d ) { return this ; } ) ;
51+ return this ;
52+ } ) ,
53+ } as any ;
4554 }
4655
4756 it ( 'should be defined' , ( ) => {
@@ -114,4 +123,94 @@ describe('LoggingMiddleware', () => {
114123
115124 expect ( next ) . toHaveBeenCalled ( ) ;
116125 } ) ;
126+
127+ it ( 'logs response body with redaction on finish' , ( ) => {
128+ const errorSpy = jest . spyOn ( Logger . prototype , 'error' ) . mockImplementation ( ( ) => { } ) ;
129+ const req = makeReq ( ) ;
130+ const res = makeRes ( ) as any ;
131+ const next : NextFunction = jest . fn ( ) ;
132+
133+ middleware . use ( req as Request , res as Response , next ) ;
134+
135+ // Capture and use the json method
136+ res . json ( { token : 'secret_token' , userId : 'user123' } ) ;
137+
138+ // Trigger finish event
139+ const listeners = res . getEventListeners ( ) ;
140+ if ( listeners [ 'finish' ] ) {
141+ listeners [ 'finish' ] . forEach ( ( cb : ( ) => void ) => cb ( ) ) ;
142+ }
143+
144+ // Response should be logged but with redacted token
145+ const loggedMessages = [ ...logSpy . mock . calls , ...errorSpy . mock . calls ] ;
146+ const finishLogCall = loggedMessages . find ( ( call : any [ ] ) => call [ 0 ] ?. includes ( 'Outgoing Response' ) ) ;
147+ expect ( finishLogCall ) . toBeDefined ( ) ;
148+ expect ( finishLogCall ?. [ 0 ] ) . toContain ( REDACTED ) ;
149+ expect ( finishLogCall ?. [ 0 ] ) . toContain ( 'user123' ) ;
150+ } ) ;
151+
152+ it ( 'redacts sensitive fields in response body' , ( ) => {
153+ const req = makeReq ( ) ;
154+ const res = makeRes ( ) as any ;
155+ const next : NextFunction = jest . fn ( ) ;
156+
157+ middleware . use ( req as Request , res as Response , next ) ;
158+
159+ // Send a response with sensitive data
160+ res . json ( { email : 'user@example.com' , name : 'Alice' , password : 'secret' } ) ;
161+
162+ // Trigger finish event
163+ const listeners = res . getEventListeners ( ) ;
164+ if ( listeners [ 'finish' ] ) {
165+ listeners [ 'finish' ] . forEach ( ( cb : ( ) => void ) => cb ( ) ) ;
166+ }
167+
168+ const loggedMessages = [ ...logSpy . mock . calls ] ;
169+ const finishLogCall = loggedMessages . find ( ( call : any [ ] ) => call [ 0 ] ?. includes ( 'Outgoing Response' ) ) ;
170+ expect ( finishLogCall ?. [ 0 ] ) . not . toContain ( 'user@example.com' ) ;
171+ expect ( finishLogCall ?. [ 0 ] ) . not . toContain ( 'secret' ) ;
172+ expect ( finishLogCall ?. [ 0 ] ) . toContain ( REDACTED ) ;
173+ expect ( finishLogCall ?. [ 0 ] ) . toContain ( 'Alice' ) ;
174+ } ) ;
175+
176+ it ( 'logs (no body) when response has no body' , ( ) => {
177+ const req = makeReq ( { method : 'GET' } ) ;
178+ const res = makeRes ( ) as any ;
179+ const next : NextFunction = jest . fn ( ) ;
180+
181+ middleware . use ( req as Request , res as Response , next ) ;
182+
183+ // Don't call json or send, so responseBody stays undefined
184+ // Trigger finish event
185+ const listeners = res . getEventListeners ( ) ;
186+ if ( listeners [ 'finish' ] ) {
187+ listeners [ 'finish' ] . forEach ( ( cb : ( ) => void ) => cb ( ) ) ;
188+ }
189+
190+ const loggedMessages = [ ...logSpy . mock . calls ] ;
191+ const finishLogCall = loggedMessages . find ( ( call : any [ ] ) => call [ 0 ] ?. includes ( 'Outgoing Response' ) ) ;
192+ expect ( finishLogCall ?. [ 0 ] ) . toContain ( '(no body)' ) ;
193+ } ) ;
194+
195+ it ( 'handles stale context gracefully when finish event fires' , ( ) => {
196+ const req = makeReq ( ) ;
197+ const res = makeRes ( ) as any ;
198+ const next : NextFunction = jest . fn ( ) ;
199+
200+ middleware . use ( req as Request , res as Response , next ) ;
201+
202+ // Manually clear context to simulate stale state
203+ const requestContextService = new RequestContextService ( ) ;
204+ // requestContextService.clearContext(); // This is a no-op, so context remains
205+
206+ res . json ( { userId : 'user123' } ) ;
207+
208+ // Trigger finish event - should not throw
209+ const listeners = res . getEventListeners ( ) ;
210+ expect ( ( ) => {
211+ if ( listeners [ 'finish' ] ) {
212+ listeners [ 'finish' ] . forEach ( ( cb : ( ) => void ) => cb ( ) ) ;
213+ }
214+ } ) . not . toThrow ( ) ;
215+ } ) ;
117216} ) ;
0 commit comments