@@ -5,6 +5,7 @@ import * as filesRoute from './routes/files';
55import { partialSpyOn } from 'tests/vitest/utils.helper' ;
66import express from 'express' ;
77import { loggerMock } from 'tests/vitest/mocks.helper' ;
8+ import { Server } from 'http' ;
89
910const buildHydrationRouterMock = partialSpyOn ( contentsRoute , 'buildHydrationRouter' ) ;
1011const buildFilesRouterMock = partialSpyOn ( filesRoute , 'buildFilesRouter' ) ;
@@ -18,46 +19,100 @@ function createMockContainer() {
1819describe ( 'HydrationApi' , ( ) => {
1920 let container : Container ;
2021 let hydrationApi : HydrationApi ;
22+ let connectionHandler : ( ( socket : import ( 'node:net' ) . Socket ) => void ) | null ;
23+
24+ function mockServerListen ( api : HydrationApi ) {
25+ const closeMock = vi . fn ( ( callback ?: ( error ?: Error ) => void ) => {
26+ callback ?.( ) ;
27+ } ) ;
28+
29+ const server = {
30+ on : vi . fn ( ( event : string , handler : ( socket : import ( 'node:net' ) . Socket ) => void ) => {
31+ if ( event === 'connection' ) {
32+ connectionHandler = handler ;
33+ }
34+
35+ return server ;
36+ } ) ,
37+ close : closeMock ,
38+ } as unknown as Server ;
39+
40+ const app = ( api as unknown as { app : express . Express } ) . app ;
41+ vi . spyOn ( app , 'listen' ) . mockImplementation ( ( _port : number , callback ?: ( ) => void ) => {
42+ callback ?.( ) ;
43+ return server ;
44+ } ) ;
45+
46+ return { closeMock } ;
47+ }
2148
2249 beforeEach ( ( ) => {
2350 container = createMockContainer ( ) ;
51+ connectionHandler = null ;
2452
2553 buildHydrationRouterMock . mockReturnValue ( express . Router ( ) ) ;
2654 buildFilesRouterMock . mockReturnValue ( express . Router ( ) ) ;
2755 } ) ;
2856
2957 afterEach ( async ( ) => {
30- await hydrationApi . stop ( ) ;
58+ if ( hydrationApi ) {
59+ await hydrationApi . stop ( ) ;
60+ }
3161 } ) ;
3262
3363 describe ( 'start' , ( ) => {
3464 it ( 'should start the server and log the port' , async ( ) => {
3565 hydrationApi = new HydrationApi ( container ) ;
66+ mockServerListen ( hydrationApi ) ;
3667
3768 await hydrationApi . start ( { debug : false , timeElapsed : false } ) ;
3869
39- expect ( loggerMock . debug ) . toBeCalledWith ( {
70+ expect ( loggerMock . debug ) . toHaveBeenCalledWith ( {
4071 msg : '[HYDRATION API] running on port 4567' ,
4172 } ) ;
4273 } ) ;
4374
4475 it ( 'should build hydration and files routers with the container' , async ( ) => {
4576 hydrationApi = new HydrationApi ( container ) ;
77+ mockServerListen ( hydrationApi ) ;
4678
4779 await hydrationApi . start ( { debug : false , timeElapsed : false } ) ;
4880
49- expect ( buildHydrationRouterMock ) . toBeCalledWith ( container ) ;
50- expect ( buildFilesRouterMock ) . toBeCalledWith ( container ) ;
81+ expect ( buildHydrationRouterMock ) . toHaveBeenCalledWith ( container ) ;
82+ expect ( buildFilesRouterMock ) . toHaveBeenCalledWith ( container ) ;
5183 } ) ;
5284
5385 it ( 'should enable debug logging middleware when debug is true' , async ( ) => {
5486 hydrationApi = new HydrationApi ( container ) ;
87+ mockServerListen ( hydrationApi ) ;
88+
89+ const app = ( hydrationApi as unknown as { app : express . Express } ) . app ;
90+ const appUseMock = vi . spyOn ( app , 'use' ) ;
5591
5692 await hydrationApi . start ( { debug : true , timeElapsed : false } ) ;
5793
58- await fetch ( 'http://localhost:4567/hydration/test' ) ;
59- // The request itself may 404, but the debug middleware should have logged
60- expect ( loggerMock . debug ) . toBeCalledWith (
94+ const middlewareCandidates = appUseMock . mock . calls . flatMap ( ( args ) => args as unknown [ ] ) ;
95+ const debugMiddlewareCandidate = middlewareCandidates . find (
96+ ( arg ) =>
97+ typeof arg === 'function' &&
98+ ( arg as ( req : express . Request , res : express . Response , next : express . NextFunction ) => void ) . length === 3 ,
99+ ) ;
100+ const debugMiddleware = debugMiddlewareCandidate as express . RequestHandler | undefined ;
101+
102+ expect ( debugMiddleware ) . toBeDefined ( ) ;
103+
104+ const next = vi . fn ( ) ;
105+ debugMiddleware ?.(
106+ {
107+ method : 'GET' ,
108+ url : '/hydration/test' ,
109+ } as express . Request ,
110+ { } as express . Response ,
111+ next ,
112+ ) ;
113+
114+ expect ( next ) . toHaveBeenCalledTimes ( 1 ) ;
115+ expect ( loggerMock . debug ) . toHaveBeenCalledWith (
61116 expect . objectContaining ( {
62117 msg : expect . stringContaining ( '[HYDRATION API]' ) ,
63118 } ) ,
@@ -74,34 +129,37 @@ describe('HydrationApi', () => {
74129
75130 it ( 'should stop the server after it was started' , async ( ) => {
76131 hydrationApi = new HydrationApi ( container ) ;
132+ mockServerListen ( hydrationApi ) ;
77133 await hydrationApi . start ( { debug : false , timeElapsed : false } ) ;
78134
79135 await expect ( hydrationApi . stop ( ) ) . resolves . toBeUndefined ( ) ;
80136 } ) ;
81137
82138 it ( 'should destroy open sockets when stopping' , async ( ) => {
83139 hydrationApi = new HydrationApi ( container ) ;
140+ mockServerListen ( hydrationApi ) ;
84141 await hydrationApi . start ( { debug : false , timeElapsed : false } ) ;
85142
86- // Create a connection to generate an open socket
87- const socket = new ( await import ( 'node:net' ) ) . Socket ( ) ;
88- await new Promise < void > ( ( resolve , reject ) => {
89- socket . connect ( 4567 , '127.0.0.1' , ( ) => resolve ( ) ) ;
90- socket . on ( 'error' , reject ) ;
91- } ) ;
143+ const socket = {
144+ destroy : vi . fn ( ) ,
145+ once : vi . fn ( ) ,
146+ } as unknown as import ( 'node:net' ) . Socket ;
92147
93- // Small delay to ensure the server registers the socket
94- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
148+ expect ( connectionHandler ) . toBeDefined ( ) ;
149+ connectionHandler ?. ( socket ) ;
95150
96151 await expect ( hydrationApi . stop ( ) ) . resolves . toBeUndefined ( ) ;
152+ expect ( socket . destroy ) . toHaveBeenCalledTimes ( 1 ) ;
97153 } ) ;
98154
99155 it ( 'should be safe to call stop multiple times' , async ( ) => {
100156 hydrationApi = new HydrationApi ( container ) ;
157+ const { closeMock } = mockServerListen ( hydrationApi ) ;
101158 await hydrationApi . start ( { debug : false , timeElapsed : false } ) ;
102159
103160 await hydrationApi . stop ( ) ;
104161 await expect ( hydrationApi . stop ( ) ) . resolves . toBeUndefined ( ) ;
162+ expect ( closeMock ) . toHaveBeenCalledTimes ( 1 ) ;
105163 } ) ;
106164 } ) ;
107165} ) ;
0 commit comments