diff --git a/Cargo.lock b/Cargo.lock index 78f2ae6b5ccf0..9798fa90afda9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4958,6 +4958,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.21" @@ -5003,6 +5012,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "security-framework" version = "2.8.2" @@ -5207,6 +5222,31 @@ dependencies = [ "serial-core", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7035,6 +7075,7 @@ dependencies = [ "hyper 1.4.1", "hyper-rustls", "hyper-util", + "serial_test", "thiserror 1.0.63", "tokio", "tokio-tungstenite 0.21.0", diff --git a/crates/turborepo-microfrontends-proxy/Cargo.toml b/crates/turborepo-microfrontends-proxy/Cargo.toml index b318cbbd1004e..a3557d817543b 100644 --- a/crates/turborepo-microfrontends-proxy/Cargo.toml +++ b/crates/turborepo-microfrontends-proxy/Cargo.toml @@ -24,4 +24,5 @@ turborepo-microfrontends = { path = "../turborepo-microfrontends" } url = "2.2.2" [dev-dependencies] +serial_test = "3.0" tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/turborepo-microfrontends-proxy/tests/integration_test.rs b/crates/turborepo-microfrontends-proxy/tests/integration_test.rs index 73afc8e7af630..b746d8c508f09 100644 --- a/crates/turborepo-microfrontends-proxy/tests/integration_test.rs +++ b/crates/turborepo-microfrontends-proxy/tests/integration_test.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, time::Duration}; +use std::time::Duration; use http_body_util::{BodyExt, Full}; use hyper::{ @@ -7,58 +7,69 @@ use hyper::{ service::service_fn, }; use hyper_util::{client::legacy::Client, rt::TokioIo}; +use serial_test::serial; use tokio::net::TcpListener; use turborepo_microfrontends::Config; use turborepo_microfrontends_proxy::{ProxyServer, Router}; -const WEBSOCKET_CLOSE_DELAY: Duration = Duration::from_millis(100); - #[tokio::test] +#[serial] async fn test_port_availability_check_ipv4() { - let config_json = r#"{ - "options": { - "localProxyPort": 9999 - }, - "applications": { - "web": { - "development": { - "local": { "port": 3000 } - } - } - } - }"#; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); - let config = Config::from_str(config_json, "test.json").unwrap(); - let server = ProxyServer::new(config.clone()).unwrap(); + let config_json = format!( + r#"{{ + "options": {{ + "localProxyPort": {port} + }}, + "applications": {{ + "web": {{ + "development": {{ + "local": {{ "port": 3000 }} + }} + }} + }} + }}"# + ); - let _listener = TcpListener::bind("127.0.0.1:9999").await.unwrap(); + let config = Config::from_str(&config_json, "test.json").unwrap(); + let server = ProxyServer::new(config.clone()).unwrap(); let result = server.check_port_available().await; assert!(!result, "Port should not be available when already bound"); + + drop(listener); } #[tokio::test] +#[serial] async fn test_port_availability_check_ipv6() { - let config_json = r#"{ - "options": { - "localProxyPort": 9997 - }, - "applications": { - "web": { - "development": { - "local": { "port": 3000 } - } - } - } - }"#; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); - let config = Config::from_str(config_json, "test.json").unwrap(); - let server = ProxyServer::new(config).unwrap(); + let config_json = format!( + r#"{{ + "options": {{ + "localProxyPort": {port} + }}, + "applications": {{ + "web": {{ + "development": {{ + "local": {{ "port": 3000 }} + }} + }} + }} + }}"# + ); - let _listener = TcpListener::bind("127.0.0.1:9997").await.unwrap(); + let config = Config::from_str(&config_json, "test.json").unwrap(); + let server = ProxyServer::new(config).unwrap(); let result = server.check_port_available().await; assert!(!result, "Port should not be available when already bound"); + + drop(listener); } #[tokio::test] @@ -142,20 +153,26 @@ async fn test_multiple_child_apps() { #[tokio::test] async fn test_proxy_server_creation() { - let config_json = r#"{ - "options": { - "localProxyPort": 4000 - }, - "applications": { - "web": { - "development": { - "local": { "port": 3000 } - } - } - } - }"#; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + drop(listener); - let config = Config::from_str(config_json, "test.json").unwrap(); + let config_json = format!( + r#"{{ + "options": {{ + "localProxyPort": {port} + }}, + "applications": {{ + "web": {{ + "development": {{ + "local": {{ "port": 3000 }} + }} + }} + }} + }}"# + ); + + let config = Config::from_str(&config_json, "test.json").unwrap(); let server = ProxyServer::new(config); assert!(server.is_ok()); @@ -197,33 +214,29 @@ async fn test_pattern_matching_edge_cases() { ); } -async fn find_available_port_range(count: usize) -> Result, Box> { - // Try to find consecutive available ports within the allowed range (3000-9999) +async fn find_available_port_range( + count: usize, +) -> Result<(Vec, Vec), Box> { let mut available_ports = Vec::new(); + let mut listeners = Vec::new(); for port in 3000..=9999 { - // Skip commonly blocked ports if [3306, 5432, 6379].contains(&port) { continue; } - if TcpListener::bind(format!("127.0.0.1:{port}")).await.is_ok() { + if let Ok(listener) = TcpListener::bind(format!("127.0.0.1:{port}")).await { available_ports.push(port); + listeners.push(listener); if available_ports.len() == count { - return Ok(available_ports); + return Ok((available_ports, listeners)); } } } Err("Not enough available ports in allowed range".into()) } -async fn mock_server( - port: u16, - response_text: &'static str, -) -> Result, Box> { - let addr = SocketAddr::from(([127, 0, 0, 1], port)); - let listener = TcpListener::bind(addr).await?; - - let handle = tokio::spawn(async move { +fn mock_server(listener: TcpListener, response_text: &'static str) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { loop { let (stream, _) = listener.accept().await.unwrap(); let io = TokioIo::new(stream); @@ -241,21 +254,27 @@ async fn mock_server( .serve_connection(io, service) .await; } - }); - - tokio::time::sleep(WEBSOCKET_CLOSE_DELAY).await; - Ok(handle) + }) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] +#[serial] async fn test_end_to_end_proxy() { - let ports = find_available_port_range(3).await.unwrap(); + let (ports, mut listeners) = find_available_port_range(3).await.unwrap(); let web_port = ports[0]; let docs_port = ports[1]; let proxy_port = ports[2]; - let web_handle = mock_server(web_port, "web app").await.unwrap(); - let docs_handle = mock_server(docs_port, "docs app").await.unwrap(); + let web_listener = listeners.remove(0); + let docs_listener = listeners.remove(0); + let proxy_listener = listeners.remove(0); + + drop(proxy_listener); + + let web_handle = mock_server(web_listener, "web app"); + let docs_handle = mock_server(docs_listener, "docs app"); + + tokio::time::sleep(Duration::from_millis(100)).await; let config_json = format!( r#"{{ @@ -288,30 +307,36 @@ async fn test_end_to_end_proxy() { let shutdown_handle = server.shutdown_handle(); tokio::spawn(async move { - server.run().await.unwrap(); + let _ = server.run().await; }); - tokio::time::sleep(Duration::from_millis(200)).await; + tokio::time::sleep(Duration::from_millis(300)).await; let connector = hyper_util::client::legacy::connect::HttpConnector::new(); let client: Client<_, Full> = Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector); - let web_response = client - .get(format!("http://127.0.0.1:{proxy_port}/").parse().unwrap()) - .await - .unwrap(); + let web_response = tokio::time::timeout( + Duration::from_secs(5), + client.get(format!("http://127.0.0.1:{proxy_port}/").parse().unwrap()), + ) + .await + .expect("Request timed out") + .expect("Request failed"); let web_body = web_response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(web_body, "web app"); - let docs_response = client - .get( + let docs_response = tokio::time::timeout( + Duration::from_secs(5), + client.get( format!("http://127.0.0.1:{proxy_port}/docs") .parse() .unwrap(), - ) - .await - .unwrap(); + ), + ) + .await + .expect("Request timed out") + .expect("Request failed"); let docs_body = docs_response .into_body() .collect() @@ -320,14 +345,17 @@ async fn test_end_to_end_proxy() { .to_bytes(); assert_eq!(docs_body, "docs app"); - let docs_subpath_response = client - .get( + let docs_subpath_response = tokio::time::timeout( + Duration::from_secs(5), + client.get( format!("http://127.0.0.1:{proxy_port}/docs/api/reference") .parse() .unwrap(), - ) - .await - .unwrap(); + ), + ) + .await + .expect("Request timed out") + .expect("Request failed"); let docs_subpath_body = docs_subpath_response .into_body() .collect() @@ -337,10 +365,12 @@ async fn test_end_to_end_proxy() { assert_eq!(docs_subpath_body, "docs app"); let _ = shutdown_handle.send(()); - let _ = tokio::time::timeout(Duration::from_secs(2), shutdown_complete_rx).await; + let _ = tokio::time::timeout(Duration::from_secs(3), shutdown_complete_rx).await; web_handle.abort(); docs_handle.abort(); + + tokio::time::sleep(Duration::from_millis(100)).await; } #[tokio::test]