@@ -13,6 +13,11 @@ process.env.JWT_SECRET = "test-jwt-secret-min-32-chars-long!!";
1313jest . unstable_mockModule ( "../db/connection.js" , ( ) => ( {
1414 default : { query : jest . fn ( ) } ,
1515 query : jest . fn ( ) ,
16+ getClient : jest . fn < ( ) => Promise < unknown > > ( ) ,
17+ withTransaction : jest . fn <
18+ ( fn : ( client : unknown ) => Promise < unknown > ) => Promise < unknown >
19+ > ( ( fn ) => fn ( { query : jest . fn ( ) , release : jest . fn ( ) } ) ) ,
20+ pool : { query : jest . fn ( ) } ,
1621 closePool : jest . fn ( ) ,
1722} ) ) ;
1823
@@ -27,17 +32,17 @@ jest.unstable_mockModule("../services/cacheService.js", () => ({
2732
2833jest . unstable_mockModule ( "../services/sorobanService.js" , ( ) => ( {
2934 sorobanService : {
30- ping : jest . fn ( ) . mockResolvedValue ( "ok" ) ,
35+ ping : jest . fn < ( ) => Promise < string > > ( ) . mockResolvedValue ( "ok" ) ,
3136 } ,
3237} ) ) ;
3338
3439const mockNotificationService = {
35- getNotificationsForUser : jest . fn ( ) ,
36- getUnreadCount : jest . fn ( ) ,
37- markRead : jest . fn ( ) ,
38- markAllRead : jest . fn ( ) ,
39- subscribe : jest . fn ( ) ,
40- createNotification : jest . fn ( ) ,
40+ getNotificationsForUser : jest . fn < ( ... args : unknown [ ] ) => Promise < unknown > > ( ) ,
41+ getUnreadCount : jest . fn < ( ... args : unknown [ ] ) => Promise < unknown > > ( ) ,
42+ markRead : jest . fn < ( ... args : unknown [ ] ) => Promise < unknown > > ( ) ,
43+ markAllRead : jest . fn < ( ... args : unknown [ ] ) => Promise < unknown > > ( ) ,
44+ subscribe : jest . fn < ( ... args : unknown [ ] ) => unknown > ( ) ,
45+ createNotification : jest . fn < ( ... args : unknown [ ] ) => Promise < unknown > > ( ) ,
4146} ;
4247
4348jest . unstable_mockModule ( "../services/notificationService.js" , ( ) => ( {
@@ -52,6 +57,41 @@ const bearer = (publicKey: string) => ({
5257 Authorization : `Bearer ${ generateJwtToken ( publicKey ) } ` ,
5358} ) ;
5459
60+ /**
61+ * SSE endpoints keep the connection open indefinitely, so a normal awaited
62+ * supertest request never resolves. This helper opens the stream, resolves once
63+ * the response headers have arrived (by which point the controller has already
64+ * run its synchronous on-connect logic: getNotificationsForUser + subscribe),
65+ * then aborts the underlying request so the test can complete.
66+ */
67+ const sseRequest = (
68+ publicKey ?: string ,
69+ ) : Promise < { status : number ; headers : Record < string , string > } > =>
70+ new Promise ( ( resolve , reject ) => {
71+ const req = request ( app ) . get ( "/api/notifications/stream" ) ;
72+ if ( publicKey ) {
73+ req . set ( bearer ( publicKey ) ) ;
74+ }
75+ req
76+ . buffer ( false )
77+ . parse ( ( res , callback ) => {
78+ const httpRes = res as unknown as {
79+ statusCode : number ;
80+ headers : Record < string , string > ;
81+ } ;
82+ resolve ( { status : httpRes . statusCode , headers : httpRes . headers } ) ;
83+ // Abort the streaming connection; we already have what we need.
84+ ( res as unknown as { destroy : ( ) => void } ) . destroy ( ) ;
85+ callback ( null , { } ) ;
86+ } )
87+ . end ( ( err ) => {
88+ // A forcibly-destroyed socket surfaces as an aborted error; ignore it.
89+ if ( err && ! / a b o r t e d | s o c k e t h a n g u p | E C O N N R E S E T / i. test ( err . message ) ) {
90+ reject ( err ) ;
91+ }
92+ } ) ;
93+ } ) ;
94+
5595beforeEach ( ( ) => {
5696 jest . clearAllMocks ( ) ;
5797} ) ;
@@ -355,9 +395,7 @@ describe("GET /api/notifications/stream", () => {
355395 mockNotificationService . getNotificationsForUser . mockResolvedValueOnce ( [ ] ) ;
356396 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
357397
358- const response = await request ( app )
359- . get ( "/api/notifications/stream" )
360- . set ( bearer ( TEST_USER ) ) ;
398+ const response = await sseRequest ( TEST_USER ) ;
361399
362400 // SSE streams are typically handled with 200 status and event-stream content-type
363401 expect ( response . status ) . toBe ( 200 ) ;
@@ -381,9 +419,7 @@ describe("GET /api/notifications/stream", () => {
381419 ] ) ;
382420 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
383421
384- const response = await request ( app )
385- . get ( "/api/notifications/stream" )
386- . set ( bearer ( TEST_USER ) ) ;
422+ const response = await sseRequest ( TEST_USER ) ;
387423
388424 expect ( response . status ) . toBe ( 200 ) ;
389425 expect (
@@ -396,9 +432,7 @@ describe("GET /api/notifications/stream", () => {
396432 mockNotificationService . getNotificationsForUser . mockResolvedValueOnce ( [ ] ) ;
397433 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
398434
399- const response = await request ( app )
400- . get ( "/api/notifications/stream" )
401- . set ( bearer ( TEST_USER ) ) ;
435+ const response = await sseRequest ( TEST_USER ) ;
402436
403437 expect ( response . status ) . toBe ( 200 ) ;
404438 expect ( response . headers [ "content-type" ] ) . toContain ( "text/event-stream" ) ;
@@ -411,7 +445,7 @@ describe("GET /api/notifications/stream", () => {
411445 mockNotificationService . getNotificationsForUser . mockResolvedValueOnce ( [ ] ) ;
412446 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
413447
414- await request ( app ) . get ( "/api/notifications/stream" ) . set ( bearer ( TEST_USER ) ) ;
448+ await sseRequest ( TEST_USER ) ;
415449
416450 expect ( mockNotificationService . subscribe ) . toHaveBeenCalledWith (
417451 TEST_USER ,
@@ -424,9 +458,7 @@ describe("GET /api/notifications/stream", () => {
424458 mockNotificationService . getNotificationsForUser . mockResolvedValueOnce ( [ ] ) ;
425459 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
426460
427- const response = await request ( app )
428- . get ( "/api/notifications/stream" )
429- . set ( bearer ( TEST_USER ) ) ;
461+ const response = await sseRequest ( TEST_USER ) ;
430462
431463 expect ( response . status ) . toBe ( 200 ) ;
432464 } ) ;
@@ -481,7 +513,7 @@ describe("Notification Controller - Authorization", () => {
481513 mockNotificationService . getNotificationsForUser . mockResolvedValueOnce ( [ ] ) ;
482514 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
483515
484- await request ( app ) . get ( "/api/notifications/stream" ) . set ( bearer ( TEST_USER ) ) ;
516+ await sseRequest ( TEST_USER ) ;
485517
486518 // Should subscribe with TEST_USER
487519 expect ( mockNotificationService . subscribe ) . toHaveBeenCalledWith (
@@ -548,9 +580,7 @@ describe("Notification Controller - Happy Path Scenarios", () => {
548580 ) ;
549581 mockNotificationService . subscribe . mockReturnValueOnce ( mockUnsubscribe ) ;
550582
551- const response = await request ( app )
552- . get ( "/api/notifications/stream" )
553- . set ( bearer ( TEST_USER ) ) ;
583+ const response = await sseRequest ( TEST_USER ) ;
554584
555585 expect ( response . status ) . toBe ( 200 ) ;
556586 expect ( mockNotificationService . getNotificationsForUser ) . toHaveBeenCalled ( ) ;
0 commit comments