1- use std:: { net :: SocketAddr , time:: Duration } ;
1+ use std:: time:: Duration ;
22
33use http_body_util:: { BodyExt , Full } ;
44use hyper:: {
@@ -7,58 +7,69 @@ use hyper::{
77 service:: service_fn,
88} ;
99use hyper_util:: { client:: legacy:: Client , rt:: TokioIo } ;
10+ use serial_test:: serial;
1011use tokio:: net:: TcpListener ;
1112use turborepo_microfrontends:: Config ;
1213use turborepo_microfrontends_proxy:: { ProxyServer , Router } ;
1314
14- const WEBSOCKET_CLOSE_DELAY : Duration = Duration :: from_millis ( 100 ) ;
15-
1615#[ tokio:: test]
16+ #[ serial]
1717async fn test_port_availability_check_ipv4 ( ) {
18- let config_json = r#"{
19- "options": {
20- "localProxyPort": 9999
21- },
22- "applications": {
23- "web": {
24- "development": {
25- "local": { "port": 3000 }
26- }
27- }
28- }
29- }"# ;
18+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
19+ let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
3020
31- let config = Config :: from_str ( config_json, "test.json" ) . unwrap ( ) ;
32- let server = ProxyServer :: new ( config. clone ( ) ) . unwrap ( ) ;
21+ let config_json = format ! (
22+ r#"{{
23+ "options": {{
24+ "localProxyPort": {port}
25+ }},
26+ "applications": {{
27+ "web": {{
28+ "development": {{
29+ "local": {{ "port": 3000 }}
30+ }}
31+ }}
32+ }}
33+ }}"#
34+ ) ;
3335
34- let _listener = TcpListener :: bind ( "127.0.0.1:9999" ) . await . unwrap ( ) ;
36+ let config = Config :: from_str ( & config_json, "test.json" ) . unwrap ( ) ;
37+ let server = ProxyServer :: new ( config. clone ( ) ) . unwrap ( ) ;
3538
3639 let result = server. check_port_available ( ) . await ;
3740 assert ! ( !result, "Port should not be available when already bound" ) ;
41+
42+ drop ( listener) ;
3843}
3944
4045#[ tokio:: test]
46+ #[ serial]
4147async fn test_port_availability_check_ipv6 ( ) {
42- let config_json = r#"{
43- "options": {
44- "localProxyPort": 9997
45- },
46- "applications": {
47- "web": {
48- "development": {
49- "local": { "port": 3000 }
50- }
51- }
52- }
53- }"# ;
48+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
49+ let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
5450
55- let config = Config :: from_str ( config_json, "test.json" ) . unwrap ( ) ;
56- let server = ProxyServer :: new ( config) . unwrap ( ) ;
51+ let config_json = format ! (
52+ r#"{{
53+ "options": {{
54+ "localProxyPort": {port}
55+ }},
56+ "applications": {{
57+ "web": {{
58+ "development": {{
59+ "local": {{ "port": 3000 }}
60+ }}
61+ }}
62+ }}
63+ }}"#
64+ ) ;
5765
58- let _listener = TcpListener :: bind ( "127.0.0.1:9997" ) . await . unwrap ( ) ;
66+ let config = Config :: from_str ( & config_json, "test.json" ) . unwrap ( ) ;
67+ let server = ProxyServer :: new ( config) . unwrap ( ) ;
5968
6069 let result = server. check_port_available ( ) . await ;
6170 assert ! ( !result, "Port should not be available when already bound" ) ;
71+
72+ drop ( listener) ;
6273}
6374
6475#[ tokio:: test]
@@ -142,20 +153,26 @@ async fn test_multiple_child_apps() {
142153
143154#[ tokio:: test]
144155async fn test_proxy_server_creation ( ) {
145- let config_json = r#"{
146- "options": {
147- "localProxyPort": 4000
148- },
149- "applications": {
150- "web": {
151- "development": {
152- "local": { "port": 3000 }
153- }
154- }
155- }
156- }"# ;
156+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
157+ let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
158+ drop ( listener) ;
157159
158- let config = Config :: from_str ( config_json, "test.json" ) . unwrap ( ) ;
160+ let config_json = format ! (
161+ r#"{{
162+ "options": {{
163+ "localProxyPort": {port}
164+ }},
165+ "applications": {{
166+ "web": {{
167+ "development": {{
168+ "local": {{ "port": 3000 }}
169+ }}
170+ }}
171+ }}
172+ }}"#
173+ ) ;
174+
175+ let config = Config :: from_str ( & config_json, "test.json" ) . unwrap ( ) ;
159176 let server = ProxyServer :: new ( config) ;
160177
161178 assert ! ( server. is_ok( ) ) ;
@@ -197,33 +214,29 @@ async fn test_pattern_matching_edge_cases() {
197214 ) ;
198215}
199216
200- async fn find_available_port_range ( count : usize ) -> Result < Vec < u16 > , Box < dyn std:: error:: Error > > {
201- // Try to find consecutive available ports within the allowed range (3000-9999)
217+ async fn find_available_port_range (
218+ count : usize ,
219+ ) -> Result < ( Vec < u16 > , Vec < TcpListener > ) , Box < dyn std:: error:: Error > > {
202220 let mut available_ports = Vec :: new ( ) ;
221+ let mut listeners = Vec :: new ( ) ;
203222
204223 for port in 3000 ..=9999 {
205- // Skip commonly blocked ports
206224 if [ 3306 , 5432 , 6379 ] . contains ( & port) {
207225 continue ;
208226 }
209- if TcpListener :: bind ( format ! ( "127.0.0.1:{port}" ) ) . await . is_ok ( ) {
227+ if let Ok ( listener ) = TcpListener :: bind ( format ! ( "127.0.0.1:{port}" ) ) . await {
210228 available_ports. push ( port) ;
229+ listeners. push ( listener) ;
211230 if available_ports. len ( ) == count {
212- return Ok ( available_ports) ;
231+ return Ok ( ( available_ports, listeners ) ) ;
213232 }
214233 }
215234 }
216235 Err ( "Not enough available ports in allowed range" . into ( ) )
217236}
218237
219- async fn mock_server (
220- port : u16 ,
221- response_text : & ' static str ,
222- ) -> Result < tokio:: task:: JoinHandle < ( ) > , Box < dyn std:: error:: Error > > {
223- let addr = SocketAddr :: from ( ( [ 127 , 0 , 0 , 1 ] , port) ) ;
224- let listener = TcpListener :: bind ( addr) . await ?;
225-
226- let handle = tokio:: spawn ( async move {
238+ fn mock_server ( listener : TcpListener , response_text : & ' static str ) -> tokio:: task:: JoinHandle < ( ) > {
239+ tokio:: spawn ( async move {
227240 loop {
228241 let ( stream, _) = listener. accept ( ) . await . unwrap ( ) ;
229242 let io = TokioIo :: new ( stream) ;
@@ -241,21 +254,27 @@ async fn mock_server(
241254 . serve_connection ( io, service)
242255 . await ;
243256 }
244- } ) ;
245-
246- tokio:: time:: sleep ( WEBSOCKET_CLOSE_DELAY ) . await ;
247- Ok ( handle)
257+ } )
248258}
249259
250- #[ tokio:: test]
260+ #[ tokio:: test( flavor = "multi_thread" ) ]
261+ #[ serial]
251262async fn test_end_to_end_proxy ( ) {
252- let ports = find_available_port_range ( 3 ) . await . unwrap ( ) ;
263+ let ( ports, mut listeners ) = find_available_port_range ( 3 ) . await . unwrap ( ) ;
253264 let web_port = ports[ 0 ] ;
254265 let docs_port = ports[ 1 ] ;
255266 let proxy_port = ports[ 2 ] ;
256267
257- let web_handle = mock_server ( web_port, "web app" ) . await . unwrap ( ) ;
258- let docs_handle = mock_server ( docs_port, "docs app" ) . await . unwrap ( ) ;
268+ let web_listener = listeners. remove ( 0 ) ;
269+ let docs_listener = listeners. remove ( 0 ) ;
270+ let proxy_listener = listeners. remove ( 0 ) ;
271+
272+ drop ( proxy_listener) ;
273+
274+ let web_handle = mock_server ( web_listener, "web app" ) ;
275+ let docs_handle = mock_server ( docs_listener, "docs app" ) ;
276+
277+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
259278
260279 let config_json = format ! (
261280 r#"{{
@@ -288,30 +307,36 @@ async fn test_end_to_end_proxy() {
288307 let shutdown_handle = server. shutdown_handle ( ) ;
289308
290309 tokio:: spawn ( async move {
291- server. run ( ) . await . unwrap ( ) ;
310+ let _ = server. run ( ) . await ;
292311 } ) ;
293312
294- tokio:: time:: sleep ( Duration :: from_millis ( 200 ) ) . await ;
313+ tokio:: time:: sleep ( Duration :: from_millis ( 300 ) ) . await ;
295314
296315 let connector = hyper_util:: client:: legacy:: connect:: HttpConnector :: new ( ) ;
297316 let client: Client < _ , Full < Bytes > > =
298317 Client :: builder ( hyper_util:: rt:: TokioExecutor :: new ( ) ) . build ( connector) ;
299318
300- let web_response = client
301- . get ( format ! ( "http://127.0.0.1:{proxy_port}/" ) . parse ( ) . unwrap ( ) )
302- . await
303- . unwrap ( ) ;
319+ let web_response = tokio:: time:: timeout (
320+ Duration :: from_secs ( 5 ) ,
321+ client. get ( format ! ( "http://127.0.0.1:{proxy_port}/" ) . parse ( ) . unwrap ( ) ) ,
322+ )
323+ . await
324+ . expect ( "Request timed out" )
325+ . expect ( "Request failed" ) ;
304326 let web_body = web_response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
305327 assert_eq ! ( web_body, "web app" ) ;
306328
307- let docs_response = client
308- . get (
329+ let docs_response = tokio:: time:: timeout (
330+ Duration :: from_secs ( 5 ) ,
331+ client. get (
309332 format ! ( "http://127.0.0.1:{proxy_port}/docs" )
310333 . parse ( )
311334 . unwrap ( ) ,
312- )
313- . await
314- . unwrap ( ) ;
335+ ) ,
336+ )
337+ . await
338+ . expect ( "Request timed out" )
339+ . expect ( "Request failed" ) ;
315340 let docs_body = docs_response
316341 . into_body ( )
317342 . collect ( )
@@ -320,14 +345,17 @@ async fn test_end_to_end_proxy() {
320345 . to_bytes ( ) ;
321346 assert_eq ! ( docs_body, "docs app" ) ;
322347
323- let docs_subpath_response = client
324- . get (
348+ let docs_subpath_response = tokio:: time:: timeout (
349+ Duration :: from_secs ( 5 ) ,
350+ client. get (
325351 format ! ( "http://127.0.0.1:{proxy_port}/docs/api/reference" )
326352 . parse ( )
327353 . unwrap ( ) ,
328- )
329- . await
330- . unwrap ( ) ;
354+ ) ,
355+ )
356+ . await
357+ . expect ( "Request timed out" )
358+ . expect ( "Request failed" ) ;
331359 let docs_subpath_body = docs_subpath_response
332360 . into_body ( )
333361 . collect ( )
@@ -337,10 +365,12 @@ async fn test_end_to_end_proxy() {
337365 assert_eq ! ( docs_subpath_body, "docs app" ) ;
338366
339367 let _ = shutdown_handle. send ( ( ) ) ;
340- let _ = tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , shutdown_complete_rx) . await ;
368+ let _ = tokio:: time:: timeout ( Duration :: from_secs ( 3 ) , shutdown_complete_rx) . await ;
341369
342370 web_handle. abort ( ) ;
343371 docs_handle. abort ( ) ;
372+
373+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
344374}
345375
346376#[ tokio:: test]
0 commit comments