From abee45f25be0d1a122885a97e7865ca12d399b5d Mon Sep 17 00:00:00 2001 From: yie1d Date: Thu, 27 Aug 2026 15:32:36 +0800 Subject: [PATCH] test(runtime): stabilize cross-platform live fixtures --- src/runtime/action.rs | 7 +- src/runtime/artifact.rs | 5 +- src/runtime/evaluate.rs | 7 +- src/runtime/frame.rs | 9 +- src/runtime/locator/resolver.rs | 9 +- src/runtime/snapshot.rs | 9 +- src/runtime/storage.rs | 948 +++++++++++++----------- tests/runtime_context_session_chrome.rs | 9 +- tests/runtime_tls_chrome.rs | 26 +- 9 files changed, 555 insertions(+), 474 deletions(-) diff --git a/src/runtime/action.rs b/src/runtime/action.rs index 1ceb9b4..05c75d5 100644 --- a/src/runtime/action.rs +++ b/src/runtime/action.rs @@ -1633,7 +1633,7 @@ drop.addEventListener('dragover', e=>e.preventDefault()); drop.addEventListener( const root=document.querySelector('#shadow').attachShadow({{mode:'open'}}); root.innerHTML=``; root.querySelector('button').onclick=()=>window.shadowClicked=true; -"# +"# ); let parent_server = tokio::spawn(serve_live_locator_fixture( parent_listener, @@ -1649,14 +1649,13 @@ root.querySelector('button').onclick=()=>window.shadowClicked=true; let runtime = BrowserRuntime::launch( LaunchOptions::default() .headless(true) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), ) .await .unwrap(); let session = runtime.default_session().await.unwrap(); let page = session - .new_page(format!("http://parent.test:{parent_port}/")) + .new_page(format!("http://127.0.0.1:{parent_port}/")) .await .unwrap(); tokio::time::timeout(Duration::from_secs(5), async { diff --git a/src/runtime/artifact.rs b/src/runtime/artifact.rs index 57a65f1..44c3e37 100644 --- a/src/runtime/artifact.rs +++ b/src/runtime/artifact.rs @@ -2793,7 +2793,7 @@ mod tests { let child_body = "
".to_owned(); let nested_body = "
".to_owned(); let parent_body = format!( - r#"
"# + r#"
"# ); let parent_server = tokio::spawn(serve_artifact_fixture( parent_listener, @@ -2809,7 +2809,6 @@ mod tests { LaunchOptions::default() .headless(true) .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1") .arg(explicitly_allowed_ports_arg([ parent_address.port(), child_address.port(), @@ -2823,7 +2822,7 @@ mod tests { let initial_epoch = initial_main.document_epoch(); let initial_generation = page.generation(); let mut events = page.subscribe_events().await.unwrap(); - let target_url = format!("http://parent.test:{parent_port}/"); + let target_url = format!("http://127.0.0.1:{parent_port}/"); let navigation = page.goto(target_url.as_str()).await.unwrap(); assert_eq!(navigation.final_url(), target_url); let committed = tokio::time::timeout(std::time::Duration::from_secs(8), async { diff --git a/src/runtime/evaluate.rs b/src/runtime/evaluate.rs index f8ca943..7242ef5 100644 --- a/src/runtime/evaluate.rs +++ b/src/runtime/evaluate.rs @@ -1295,7 +1295,7 @@ mod tests { let parent_port = parent_listener.local_addr().unwrap().port(); let parent_body = format!( r#" - "# + "# ); let same_body = "".to_owned(); let oopif_body = "".to_owned(); @@ -1313,14 +1313,13 @@ mod tests { let runtime = BrowserRuntime::launch( LaunchOptions::default() .headless(true) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), ) .await .unwrap(); let session = runtime.default_session().await.unwrap(); let page = session - .new_page(format!("http://parent.test:{parent_port}/")) + .new_page(format!("http://127.0.0.1:{parent_port}/")) .await .unwrap(); let main = page.main_frame().await.unwrap(); diff --git a/src/runtime/frame.rs b/src/runtime/frame.rs index 2a3f06a..59847fa 100644 --- a/src/runtime/frame.rs +++ b/src/runtime/frame.rs @@ -6281,7 +6281,7 @@ mod tests { let child_server = tokio::spawn(serve_fixture( child_listener, format!( - r#"child"# + r#"child"# ), )); let grandchild_server = tokio::spawn(serve_fixture( @@ -6291,21 +6291,20 @@ mod tests { let parent_server = tokio::spawn(serve_fixture( parent_listener, format!( - r#""# + r#""# ), )); let runtime = BrowserRuntime::launch( LaunchOptions::default() .headless(true) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), ) .await .expect("launch Chrome"); let session = runtime.default_session().await.expect("default session"); let page = session - .new_page(format!("http://parent.test:{parent_port}/")) + .new_page(format!("http://127.0.0.1:{parent_port}/")) .await .expect("open fixture"); let main = page.main_frame().await.expect("main frame"); diff --git a/src/runtime/locator/resolver.rs b/src/runtime/locator/resolver.rs index ac4812e..5d556fa 100644 --- a/src/runtime/locator/resolver.rs +++ b/src/runtime/locator/resolver.rs @@ -1274,7 +1274,7 @@ pub(crate) mod tests { closedRoot.innerHTML = ``; -"# +"# ); let parent_server = tokio::spawn(serve_live_locator_fixture( parent_listener, @@ -1290,14 +1290,13 @@ pub(crate) mod tests { let runtime = BrowserRuntime::launch( LaunchOptions::default() .headless(true) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), ) .await .expect("launch private Chrome"); let session = runtime.default_session().await.expect("default session"); let page = session - .new_page(format!("http://parent.test:{parent_port}/")) + .new_page(format!("http://127.0.0.1:{parent_port}/")) .await .expect("open locator fixture"); let main = page.main_frame().await.expect("main frame"); @@ -1508,7 +1507,7 @@ pub(crate) mod tests { let stale_document = page.locator("#text-only"); cdpkit::page::methods::Navigate::new(format!( - "http://parent.test:{parent_port}/?replacement" + "http://127.0.0.1:{parent_port}/?replacement" )) .send(page.cdp_session()) .await diff --git a/src/runtime/snapshot.rs b/src/runtime/snapshot.rs index 840bf70..a677bac 100644 --- a/src/runtime/snapshot.rs +++ b/src/runtime/snapshot.rs @@ -1589,7 +1589,7 @@ const shadow = document.querySelector('#shadow').attachShadow({{mode:'open'}}); shadow.innerHTML = ''; document.querySelector('#focused').focus(); -"#, +"#, "X".repeat(8_192) ); let parent_server = tokio::spawn(serve_fixture(parent_listener, parent, child.clone())); @@ -1597,8 +1597,7 @@ document.querySelector('#focused').focus(); let runtime = BrowserRuntime::launch( LaunchOptions::default() .headless(true) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), ) .await .unwrap(); @@ -1606,7 +1605,7 @@ document.querySelector('#focused').focus(); .default_session() .await .unwrap() - .new_page(format!("http://parent.test:{parent_port}/")) + .new_page(format!("http://127.0.0.1:{parent_port}/")) .await .unwrap(); tokio::time::timeout(Duration::from_secs(5), async { @@ -1756,7 +1755,7 @@ document.querySelector('#focused').focus(); ); } cdpkit::page::methods::Navigate::new(format!( - "http://parent.test:{parent_port}/?replacement" + "http://127.0.0.1:{parent_port}/?replacement" )) .send(page.cdp_session()) .await diff --git a/src/runtime/storage.rs b/src/runtime/storage.rs index 742b048..d41f28d 100644 --- a/src/runtime/storage.rs +++ b/src/runtime/storage.rs @@ -1639,73 +1639,30 @@ mod tests { (port, task) } - #[tokio::test] - #[ignore = "requires installed Chrome and loopback sockets"] - async fn live_chrome_storage_routes_contexts_origins_frames_and_page_session_storage() { - let (second_port, second_server) = start_http_fixture(|path| match path { - "/oopif" => "oopif leafoopif storage".into(), - _ => format!( - "second storagesecond storage for {path}" - ), - }) - .await; - let (first_port, first_server) = start_http_fixture(move |path| match path { - "/page-one" => format!( - "page one" - ), - "/same" => "same leafsame storage".into(), - _ => format!( - "first storagefirst storage for {path}" - ), - }) - .await; - let runtime = super::super::BrowserRuntime::launch( - super::super::LaunchOptions::default() - .headless(true) - .arg("--site-per-process"), - ) - .await - .unwrap(); - let default_session = runtime.default_session().await.unwrap(); - let default_url = format!("http://127.0.0.1:{first_port}/default"); - let default_page = default_session.new_page("about:blank").await.unwrap(); - fence_live_document(&default_page, &default_url).await; - default_page - .local_storage() - .set("default-sentinel", "preserve") - .await - .unwrap(); - default_session - .set_cookie( - BrowserCookie::new("default-sentinel", "preserve") - .url(format!("http://127.0.0.1:{first_port}/")), - ) - .await - .unwrap(); - let session = runtime - .isolated_session(super::super::IsolatedSessionOptions::default()) - .await - .unwrap(); - let page_one_url = format!("http://127.0.0.1:{first_port}/page-one"); - let same_frame_url = format!("http://127.0.0.1:{first_port}/same"); - let oopif_frame_url = format!("http://localhost:{second_port}/oopif"); - let page_one = session.new_page("about:blank").await.unwrap(); - let mut page_one_events = page_one.subscribe_events().await.unwrap(); - fence_live_document(&page_one, &page_one_url).await; - let page_two_url = format!("http://127.0.0.1:{second_port}/"); - let page_two = session.new_page("about:blank").await.unwrap(); - fence_live_document(&page_two, &page_two_url).await; - let page_same_origin_url = format!("http://127.0.0.1:{first_port}/other"); - let page_same_origin = session.new_page("about:blank").await.unwrap(); - fence_live_document(&page_same_origin, &page_same_origin_url).await; - let other_session = runtime - .isolated_session(super::super::IsolatedSessionOptions::default()) - .await - .unwrap(); - let other_page_url = format!("http://127.0.0.1:{first_port}/other-context"); - let other_page = other_session.new_page("about:blank").await.unwrap(); - fence_live_document(&other_page, &other_page_url).await; + struct LiveStorageScopes<'a> { + default_session: &'a BrowserSession, + default_page: &'a Page, + session: &'a BrowserSession, + page_one: &'a Page, + page_two: &'a Page, + page_same_origin: &'a Page, + other_session: &'a BrowserSession, + other_page: &'a Page, + first_port: u16, + } + async fn assert_context_and_page_storage(scopes: LiveStorageScopes<'_>) { + let LiveStorageScopes { + default_session, + default_page, + session, + page_one, + page_two, + page_same_origin, + other_session, + other_page, + first_port, + } = scopes; page_one .local_storage() .set("context", "one") @@ -1831,376 +1788,9 @@ mod tests { .as_deref(), Some("other") ); + } - let main = page_one.main_frame().await.unwrap(); - let main_frame_id = main.id().as_str().to_owned(); - let main_session = main.cdp_session().await.unwrap().id().to_owned(); - let route_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5); - let (oopif_frame_id, old_oopif_session, new_oopif_session, oopif_target_id) = loop { - let envelope = tokio::time::timeout_at(route_deadline, page_one_events.next()) - .await - .unwrap_or_else(|_| panic!("deadline waiting for the OOPIF FrameRouteChanged")) - .expect("page-one event stream closed before the OOPIF route changed") - .unwrap_or_else(|error| { - panic!("page-one event stream failed before the OOPIF route changed: {error}") - }); - match envelope.into_event() { - super::super::PageEvent::FrameRouteChanged { - frame_id, - previous_session_id, - session_id, - target_id: Some(target_id), - } if session_id != main_session => { - break ( - frame_id.as_str().to_owned(), - previous_session_id, - session_id, - target_id, - ); - } - _ => {} - } - }; - assert!( - !oopif_target_id.is_empty(), - "OOPIF route event must identify its target" - ); - - let frames = page_one.frames().await.unwrap(); - let active_frames = frames - .into_iter() - .map(|frame| (frame.id().as_str().to_owned(), frame)) - .collect::>(); - assert_eq!( - active_frames.len(), - 3, - "page-one must have exactly its main frame and the two fixture leaf frames" - ); - assert!( - active_frames.contains_key(&main_frame_id), - "active frame snapshot must contain the main frame" - ); - let oopif_frame = active_frames - .get(&oopif_frame_id) - .expect("OOPIF route event must identify an active frame") - .clone(); - let mut same_frames = active_frames - .iter() - .filter(|(frame_id, _)| { - frame_id.as_str() != main_frame_id && frame_id.as_str() != oopif_frame_id - }) - .map(|(_, frame)| frame.clone()); - let same_frame = same_frames - .next() - .expect("active frame snapshot must contain the same-process child"); - assert!( - same_frames.next().is_none(), - "active frame snapshot must contain only one same-process child" - ); - let same_parent = same_frame.parent().await.unwrap().unwrap(); - let oopif_parent = oopif_frame.parent().await.unwrap().unwrap(); - assert_eq!(same_parent.id().as_str(), main_frame_id); - assert_eq!(oopif_parent.id().as_str(), main_frame_id); - assert_eq!(same_parent.id(), oopif_parent.id()); - - let same_session = same_frame.cdp_session().await.unwrap().id().to_owned(); - let oopif_session = oopif_frame.cdp_session().await.unwrap().id().to_owned(); - assert_eq!(same_session, main_session); - assert_eq!(old_oopif_session, main_session); - assert_eq!(oopif_session, new_oopif_session); - assert_ne!(oopif_session, main_session); - - let same_location: Value = same_frame - .evaluate("({url: location.href, origin: location.origin})") - .await - .unwrap(); - let oopif_location: Value = oopif_frame - .evaluate("({url: location.href, origin: location.origin})") - .await - .unwrap(); - assert_eq!( - same_location, - json!({ - "url": same_frame_url, - "origin": format!("http://127.0.0.1:{first_port}"), - }) - ); - assert_eq!( - oopif_location, - json!({ - "url": oopif_frame_url, - "origin": format!("http://localhost:{second_port}"), - }) - ); - - let children = [same_frame, oopif_frame]; - for (index, child) in children.iter().enumerate() { - child - .local_storage() - .set("frame", format!("child-{index}")) - .await - .unwrap(); - assert_eq!( - child.local_storage().get("frame").await.unwrap().as_deref(), - Some(format!("child-{index}").as_str()) - ); - child - .session_storage() - .set("frame-session", format!("child-{index}")) - .await - .unwrap(); - assert_eq!( - child - .session_storage() - .get("frame-session") - .await - .unwrap() - .as_deref(), - Some(format!("child-{index}").as_str()) - ); - } - - let exported = session - .export_auth_state(&[page_two.clone(), page_one.clone()]) - .await - .unwrap(); - assert_eq!(exported.origins().len(), 2); - assert!(exported - .origins() - .windows(2) - .all(|pair| pair[0].origin() < pair[1].origin())); - page_one.local_storage().clear().await.unwrap(); - page_two.local_storage().clear().await.unwrap(); - session - .import_auth_state( - &exported, - AuthStateImport::new(AuthImportMode::Merge) - .page(page_one.clone()) - .page(page_two.clone()), - ) - .await - .unwrap(); - assert_eq!( - page_one - .local_storage() - .get("one") - .await - .unwrap() - .as_deref(), - Some("value-one") - ); - assert_eq!( - page_two - .local_storage() - .get("two") - .await - .unwrap() - .as_deref(), - Some("value-two") - ); - page_one - .local_storage() - .set("extra", "remove-me") - .await - .unwrap(); - session - .set_cookie( - BrowserCookie::new("extra", "remove-me") - .url(format!("http://127.0.0.1:{first_port}/")), - ) - .await - .unwrap(); - session - .import_auth_state( - &exported, - AuthStateImport::new(AuthImportMode::ReplaceAuthScope) - .page(page_one.clone()) - .page(page_two.clone()), - ) - .await - .unwrap(); - assert_eq!(page_one.local_storage().get("extra").await.unwrap(), None); - assert_eq!( - page_one - .local_storage() - .get("one") - .await - .unwrap() - .as_deref(), - Some("value-one") - ); - let replaced_cookies = session.cookies().await.unwrap(); - assert!(!replaced_cookies - .iter() - .any(|cookie| cookie.name() == "extra")); - assert!(replaced_cookies - .iter() - .any(|cookie| cookie.name() == "context-cookie" && cookie.value() == "one")); - - let origin = format!("http://127.0.0.1:{first_port}"); - session - .set_cookies(vec![ - BrowserCookie::new("sid", "delete-me") - .url(format!("{origin}/app")) - .path("/app"), - BrowserCookie::new("sid", "keep-me") - .url(format!("{origin}/other")) - .path("/other"), - ]) - .await - .unwrap(); - let exact = session - .cookies() - .await - .unwrap() - .into_iter() - .find(|cookie| cookie.name() == "sid" && cookie.path_value() == Some("/app")) - .unwrap(); - session - .delete_cookie( - CookieDeletion::new("sid") - .domain(exact.domain_value().unwrap()) - .path("/app"), - ) - .await - .unwrap(); - let remaining = session.cookies().await.unwrap(); - assert!(!remaining - .iter() - .any(|cookie| cookie.name() == "sid" && cookie.path_value() == Some("/app"))); - assert!(remaining.iter().any(|cookie| cookie.name() == "sid" - && cookie.path_value() == Some("/other") - && cookie.value() == "keep-me")); - - session - .set_cookies(vec![ - BrowserCookie::new("host-neighbor", "delete-host") - .url(format!("{origin}/scope")) - .path("/scope"), - BrowserCookie::new("host-neighbor", "keep-localhost") - .url(format!("http://localhost:{second_port}/scope")) - .path("/scope"), - ]) - .await - .unwrap(); - let host_cookie = session - .cookies() - .await - .unwrap() - .into_iter() - .find(|cookie| cookie.name() == "host-neighbor" && cookie.value() == "delete-host") - .unwrap(); - session - .delete_cookie( - CookieDeletion::new("host-neighbor") - .domain(host_cookie.domain_value().unwrap()) - .path("/scope"), - ) - .await - .unwrap(); - let host_remaining = session.cookies().await.unwrap(); - assert!(!host_remaining - .iter() - .any(|cookie| cookie.name() == "host-neighbor" && cookie.value() == "delete-host")); - assert!(host_remaining - .iter() - .any(|cookie| cookie.name() == "host-neighbor" && cookie.value() == "keep-localhost")); - - let closed_page = session - .new_page(format!("{origin}/closed-preflight")) - .await - .unwrap(); - let closed_page_report = closed_page.close().await; - assert!( - closed_page_report.is_complete(), - "closed-page cleanup failures: {:#?}; report: {closed_page_report:#?}", - closed_page_report.failures() - ); - let preflight_state = AuthenticationState::from_parts( - vec![BrowserCookie::new("must-not-apply", "secret").url(format!("{origin}/"))], - vec![OriginStorageState::new(origin.clone(), Vec::new())], - ); - let preflight_error = session - .import_auth_state( - &preflight_state, - AuthStateImport::new(AuthImportMode::Merge).page(closed_page), - ) - .await - .unwrap_err(); - assert_eq!( - preflight_error.action_completed(), - ActionCompletion::NotStarted - ); - assert!(!session - .cookies() - .await - .unwrap() - .iter() - .any(|cookie| cookie.name() == "must-not-apply")); - - let partial_state = AuthenticationState::from_parts( - vec![BrowserCookie::new("partial-cookie", "applied").url(format!("{origin}/"))], - vec![ - OriginStorageState::new( - origin.clone(), - (0..2_000) - .map(|index| StorageEntry::new(format!("batch-{index}"), "value")) - .collect(), - ), - OriginStorageState::new( - format!("http://127.0.0.1:{second_port}"), - vec![StorageEntry::new("must-not-land", "secret")], - ), - ], - ); - let import_session = session.clone(); - let import_page_one = page_one.clone(); - let import_page_two = page_two.clone(); - let import = tokio::spawn(async move { - import_session - .import_auth_state( - &partial_state, - AuthStateImport::new(AuthImportMode::Merge) - .page(import_page_one) - .page(import_page_two), - ) - .await - }); - tokio::time::timeout(std::time::Duration::from_secs(5), async { - loop { - if session - .cookies() - .await - .unwrap() - .iter() - .any(|cookie| cookie.name() == "partial-cookie") - { - break; - } - tokio::task::yield_now().await; - } - }) - .await - .unwrap(); - page_two - .goto(format!("http://127.0.0.1:{second_port}/invalidate-import")) - .await - .unwrap(); - let partial_error = import.await.unwrap().unwrap_err(); - assert_eq!(partial_error.action_completed(), ActionCompletion::Unknown); - assert!(partial_error.to_string().contains("mutation batches")); - assert!(!partial_error.to_string().contains("secret")); - assert!(session - .cookies() - .await - .unwrap() - .iter() - .any(|cookie| cookie.name() == "partial-cookie" && cookie.value() == "applied")); - session - .delete_cookie(CookieDeletion::new("partial-cookie").url(format!("{origin}/"))) - .await - .unwrap(); - + async fn assert_partitioned_cookie_lifecycle(session: &BrowserSession, origin: &str) { let partition = CookiePartitionKey { top_level_site: "http://127.0.0.1".into(), has_cross_site_ancestor: false, @@ -2242,7 +1832,16 @@ mod tests { assert_eq!(error.storage_failure(), Some(&StorageFailure::Unsupported)); } } + } + async fn close_live_storage_fixture( + runtime: super::super::BrowserRuntime, + default_session: BrowserSession, + session: BrowserSession, + other_session: BrowserSession, + first_server: tokio::task::JoinHandle<()>, + second_server: tokio::task::JoinHandle<()>, + ) { let cleanup_session = runtime .isolated_session( super::super::IsolatedSessionOptions::default().close_pages_before_context(true), @@ -2285,4 +1884,483 @@ mod tests { first_server.abort(); second_server.abort(); } + + #[tokio::test] + #[ignore = "requires installed Chrome and loopback sockets"] + async fn live_chrome_storage_routes_contexts_origins_frames_and_page_session_storage() { + Box::pin(live_chrome_storage_scenario()).await; + } + + async fn live_chrome_storage_scenario() { + let (second_port, second_server) = start_http_fixture(|path| match path { + "/oopif" => "oopif leafoopif storage".into(), + _ => format!( + "second storagesecond storage for {path}" + ), + }) + .await; + let (first_port, first_server) = start_http_fixture(move |path| match path { + "/page-one" => format!( + "page one" + ), + "/same" => "same leafsame storage".into(), + _ => format!( + "first storagefirst storage for {path}" + ), + }) + .await; + let runtime = super::super::BrowserRuntime::launch( + super::super::LaunchOptions::default() + .headless(true) + .arg("--site-per-process"), + ) + .await + .unwrap(); + let default_session = runtime.default_session().await.unwrap(); + let default_url = format!("http://127.0.0.1:{first_port}/default"); + let default_page = default_session.new_page("about:blank").await.unwrap(); + fence_live_document(&default_page, &default_url).await; + default_page + .local_storage() + .set("default-sentinel", "preserve") + .await + .unwrap(); + default_session + .set_cookie( + BrowserCookie::new("default-sentinel", "preserve") + .url(format!("http://127.0.0.1:{first_port}/")), + ) + .await + .unwrap(); + let session = runtime + .isolated_session(super::super::IsolatedSessionOptions::default()) + .await + .unwrap(); + let page_one_url = format!("http://127.0.0.1:{first_port}/page-one"); + let same_frame_url = format!("http://127.0.0.1:{first_port}/same"); + let oopif_frame_url = format!("http://localhost:{second_port}/oopif"); + let page_one = session.new_page("about:blank").await.unwrap(); + let mut page_one_events = page_one.subscribe_events().await.unwrap(); + fence_live_document(&page_one, &page_one_url).await; + let page_two_url = format!("http://127.0.0.1:{second_port}/"); + let page_two = session.new_page("about:blank").await.unwrap(); + fence_live_document(&page_two, &page_two_url).await; + let page_same_origin_url = format!("http://127.0.0.1:{first_port}/other"); + let page_same_origin = session.new_page("about:blank").await.unwrap(); + fence_live_document(&page_same_origin, &page_same_origin_url).await; + let other_session = runtime + .isolated_session(super::super::IsolatedSessionOptions::default()) + .await + .unwrap(); + let other_page_url = format!("http://127.0.0.1:{first_port}/other-context"); + let other_page = other_session.new_page("about:blank").await.unwrap(); + fence_live_document(&other_page, &other_page_url).await; + + Box::pin(assert_context_and_page_storage(LiveStorageScopes { + default_session: &default_session, + default_page: &default_page, + session: &session, + page_one: &page_one, + page_two: &page_two, + page_same_origin: &page_same_origin, + other_session: &other_session, + other_page: &other_page, + first_port, + })) + .await; + + Box::pin(async { + let main = page_one.main_frame().await.unwrap(); + let main_frame_id = main.id().as_str().to_owned(); + let main_session = main.cdp_session().await.unwrap().id().to_owned(); + let route_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5); + let (oopif_frame_id, old_oopif_session, new_oopif_session, oopif_target_id) = + loop { + let envelope = tokio::time::timeout_at(route_deadline, page_one_events.next()) + .await + .unwrap_or_else(|_| panic!("deadline waiting for the OOPIF FrameRouteChanged")) + .expect("page-one event stream closed before the OOPIF route changed") + .unwrap_or_else(|error| { + panic!("page-one event stream failed before the OOPIF route changed: {error}") + }); + match envelope.into_event() { + super::super::PageEvent::FrameRouteChanged { + frame_id, + previous_session_id, + session_id, + target_id: Some(target_id), + } if session_id != main_session => { + break ( + frame_id.as_str().to_owned(), + previous_session_id, + session_id, + target_id, + ); + } + _ => {} + } + }; + assert!( + !oopif_target_id.is_empty(), + "OOPIF route event must identify its target" + ); + + let frames = page_one.frames().await.unwrap(); + let active_frames = frames + .into_iter() + .map(|frame| (frame.id().as_str().to_owned(), frame)) + .collect::>(); + assert_eq!( + active_frames.len(), + 3, + "page-one must have exactly its main frame and the two fixture leaf frames" + ); + assert!( + active_frames.contains_key(&main_frame_id), + "active frame snapshot must contain the main frame" + ); + let oopif_frame = active_frames + .get(&oopif_frame_id) + .expect("OOPIF route event must identify an active frame") + .clone(); + let mut same_frames = active_frames + .iter() + .filter(|(frame_id, _)| { + frame_id.as_str() != main_frame_id && frame_id.as_str() != oopif_frame_id + }) + .map(|(_, frame)| frame.clone()); + let same_frame = same_frames + .next() + .expect("active frame snapshot must contain the same-process child"); + assert!( + same_frames.next().is_none(), + "active frame snapshot must contain only one same-process child" + ); + let same_parent = same_frame.parent().await.unwrap().unwrap(); + let oopif_parent = oopif_frame.parent().await.unwrap().unwrap(); + assert_eq!(same_parent.id().as_str(), main_frame_id); + assert_eq!(oopif_parent.id().as_str(), main_frame_id); + assert_eq!(same_parent.id(), oopif_parent.id()); + + let same_session = same_frame.cdp_session().await.unwrap().id().to_owned(); + let oopif_session = oopif_frame.cdp_session().await.unwrap().id().to_owned(); + assert_eq!(same_session, main_session); + assert_eq!(old_oopif_session, main_session); + assert_eq!(oopif_session, new_oopif_session); + assert_ne!(oopif_session, main_session); + + let same_location: Value = same_frame + .evaluate("({url: location.href, origin: location.origin})") + .await + .unwrap(); + let oopif_location: Value = oopif_frame + .evaluate("({url: location.href, origin: location.origin})") + .await + .unwrap(); + assert_eq!( + same_location, + json!({ + "url": same_frame_url, + "origin": format!("http://127.0.0.1:{first_port}"), + }) + ); + assert_eq!( + oopif_location, + json!({ + "url": oopif_frame_url, + "origin": format!("http://localhost:{second_port}"), + }) + ); + + let children = [same_frame, oopif_frame]; + for (index, child) in children.iter().enumerate() { + child + .local_storage() + .set("frame", format!("child-{index}")) + .await + .unwrap(); + assert_eq!( + child.local_storage().get("frame").await.unwrap().as_deref(), + Some(format!("child-{index}").as_str()) + ); + child + .session_storage() + .set("frame-session", format!("child-{index}")) + .await + .unwrap(); + assert_eq!( + child + .session_storage() + .get("frame-session") + .await + .unwrap() + .as_deref(), + Some(format!("child-{index}").as_str()) + ); + } + }) + .await; + + Box::pin(async { + let exported = session + .export_auth_state(&[page_two.clone(), page_one.clone()]) + .await + .unwrap(); + assert_eq!(exported.origins().len(), 2); + assert!(exported + .origins() + .windows(2) + .all(|pair| pair[0].origin() < pair[1].origin())); + page_one.local_storage().clear().await.unwrap(); + page_two.local_storage().clear().await.unwrap(); + session + .import_auth_state( + &exported, + AuthStateImport::new(AuthImportMode::Merge) + .page(page_one.clone()) + .page(page_two.clone()), + ) + .await + .unwrap(); + assert_eq!( + page_one + .local_storage() + .get("one") + .await + .unwrap() + .as_deref(), + Some("value-one") + ); + assert_eq!( + page_two + .local_storage() + .get("two") + .await + .unwrap() + .as_deref(), + Some("value-two") + ); + page_one + .local_storage() + .set("extra", "remove-me") + .await + .unwrap(); + session + .set_cookie( + BrowserCookie::new("extra", "remove-me") + .url(format!("http://127.0.0.1:{first_port}/")), + ) + .await + .unwrap(); + session + .import_auth_state( + &exported, + AuthStateImport::new(AuthImportMode::ReplaceAuthScope) + .page(page_one.clone()) + .page(page_two.clone()), + ) + .await + .unwrap(); + assert_eq!(page_one.local_storage().get("extra").await.unwrap(), None); + assert_eq!( + page_one + .local_storage() + .get("one") + .await + .unwrap() + .as_deref(), + Some("value-one") + ); + let replaced_cookies = session.cookies().await.unwrap(); + assert!(!replaced_cookies + .iter() + .any(|cookie| cookie.name() == "extra")); + assert!(replaced_cookies + .iter() + .any(|cookie| cookie.name() == "context-cookie" && cookie.value() == "one")); + }) + .await; + + let origin = format!("http://127.0.0.1:{first_port}"); + Box::pin(async { + session + .set_cookies(vec![ + BrowserCookie::new("sid", "delete-me") + .url(format!("{origin}/app")) + .path("/app"), + BrowserCookie::new("sid", "keep-me") + .url(format!("{origin}/other")) + .path("/other"), + ]) + .await + .unwrap(); + let exact = session + .cookies() + .await + .unwrap() + .into_iter() + .find(|cookie| cookie.name() == "sid" && cookie.path_value() == Some("/app")) + .unwrap(); + session + .delete_cookie( + CookieDeletion::new("sid") + .domain(exact.domain_value().unwrap()) + .path("/app"), + ) + .await + .unwrap(); + let remaining = session.cookies().await.unwrap(); + assert!(!remaining + .iter() + .any(|cookie| cookie.name() == "sid" && cookie.path_value() == Some("/app"))); + assert!(remaining.iter().any(|cookie| cookie.name() == "sid" + && cookie.path_value() == Some("/other") + && cookie.value() == "keep-me")); + + session + .set_cookies(vec![ + BrowserCookie::new("host-neighbor", "delete-host") + .url(format!("{origin}/scope")) + .path("/scope"), + BrowserCookie::new("host-neighbor", "keep-localhost") + .url(format!("http://localhost:{second_port}/scope")) + .path("/scope"), + ]) + .await + .unwrap(); + let host_cookie = session + .cookies() + .await + .unwrap() + .into_iter() + .find(|cookie| cookie.name() == "host-neighbor" && cookie.value() == "delete-host") + .unwrap(); + session + .delete_cookie( + CookieDeletion::new("host-neighbor") + .domain(host_cookie.domain_value().unwrap()) + .path("/scope"), + ) + .await + .unwrap(); + let host_remaining = session.cookies().await.unwrap(); + assert!(!host_remaining + .iter() + .any(|cookie| cookie.name() == "host-neighbor" && cookie.value() == "delete-host")); + assert!(host_remaining.iter().any( + |cookie| cookie.name() == "host-neighbor" && cookie.value() == "keep-localhost" + )); + }) + .await; + + Box::pin(async { + let closed_page = session + .new_page(format!("{origin}/closed-preflight")) + .await + .unwrap(); + let closed_page_report = closed_page.close().await; + assert!( + closed_page_report.is_complete(), + "closed-page cleanup failures: {:#?}; report: {closed_page_report:#?}", + closed_page_report.failures() + ); + let preflight_state = AuthenticationState::from_parts( + vec![BrowserCookie::new("must-not-apply", "secret").url(format!("{origin}/"))], + vec![OriginStorageState::new(origin.clone(), Vec::new())], + ); + let preflight_error = session + .import_auth_state( + &preflight_state, + AuthStateImport::new(AuthImportMode::Merge).page(closed_page), + ) + .await + .unwrap_err(); + assert_eq!( + preflight_error.action_completed(), + ActionCompletion::NotStarted + ); + assert!(!session + .cookies() + .await + .unwrap() + .iter() + .any(|cookie| cookie.name() == "must-not-apply")); + + let partial_state = AuthenticationState::from_parts( + vec![BrowserCookie::new("partial-cookie", "applied").url(format!("{origin}/"))], + vec![ + OriginStorageState::new( + origin.clone(), + (0..2_000) + .map(|index| StorageEntry::new(format!("batch-{index}"), "value")) + .collect(), + ), + OriginStorageState::new( + format!("http://127.0.0.1:{second_port}"), + vec![StorageEntry::new("must-not-land", "secret")], + ), + ], + ); + let import_session = session.clone(); + let import_page_one = page_one.clone(); + let import_page_two = page_two.clone(); + let import = tokio::spawn(async move { + import_session + .import_auth_state( + &partial_state, + AuthStateImport::new(AuthImportMode::Merge) + .page(import_page_one) + .page(import_page_two), + ) + .await + }); + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if session + .cookies() + .await + .unwrap() + .iter() + .any(|cookie| cookie.name() == "partial-cookie") + { + break; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + page_two + .goto(format!("http://127.0.0.1:{second_port}/invalidate-import")) + .await + .unwrap(); + let partial_error = import.await.unwrap().unwrap_err(); + assert_eq!(partial_error.action_completed(), ActionCompletion::Unknown); + assert!(partial_error.to_string().contains("mutation batches")); + assert!(!partial_error.to_string().contains("secret")); + assert!(session + .cookies() + .await + .unwrap() + .iter() + .any(|cookie| cookie.name() == "partial-cookie" && cookie.value() == "applied")); + session + .delete_cookie(CookieDeletion::new("partial-cookie").url(format!("{origin}/"))) + .await + .unwrap(); + }) + .await; + + Box::pin(assert_partitioned_cookie_lifecycle(&session, &origin)).await; + + Box::pin(close_live_storage_fixture( + runtime, + default_session, + session, + other_session, + first_server, + second_server, + )) + .await; + } } diff --git a/tests/runtime_context_session_chrome.rs b/tests/runtime_context_session_chrome.rs index fe6ff2f..4ab9273 100644 --- a/tests/runtime_context_session_chrome.rs +++ b/tests/runtime_context_session_chrome.rs @@ -428,7 +428,7 @@ async fn chrome_nested_future_oopif_routes_publish_configured_facts_without_view FixtureResponse::html("nested") })) .await; - let nested_url = format!("{}/nested", nested.origin("nested.test")); + let nested_url = format!("{}/nested", nested.origin("nested.localhost")); let child = LoopbackFixture::start(Arc::new(move |_| { FixtureResponse::html(format!( "child" @@ -448,14 +448,13 @@ async fn chrome_nested_future_oopif_routes_publish_configured_facts_without_view LaunchOptions::default() .headless(true) .user_data_dir(profile.path()) - .arg("--site-per-process") - .arg("--host-resolver-rules=MAP *.test 127.0.0.1"), + .arg("--site-per-process"), LaunchOptions::arg, ); let runtime = BrowserRuntime::launch(options) .await .expect("launch private Chrome"); - let parent_origin = parent.origin("parent.test"); + let parent_origin = parent.origin("localhost"); let session = runtime .isolated_session(IsolatedSessionOptions::default().context(configured_route(1000, 740))) .await @@ -467,7 +466,7 @@ async fn chrome_nested_future_oopif_routes_publish_configured_facts_without_view .await .expect("subscribe before creating future OOPIF routes"); - let child_url = format!("{}/child", child.origin("child.test")); + let child_url = format!("{}/child", child.origin("127.0.0.1")); let add_child = format!( "(() => {{ const frame = document.createElement('iframe'); frame.src = {child_url:?}; document.body.append(frame); return true; }})()" ); diff --git a/tests/runtime_tls_chrome.rs b/tests/runtime_tls_chrome.rs index d73676c..a301efd 100644 --- a/tests/runtime_tls_chrome.rs +++ b/tests/runtime_tls_chrome.rs @@ -23,7 +23,7 @@ use tokio_rustls::TlsAcceptor; use tokio_util::sync::CancellationToken; const TEST_DEADLINE: Duration = Duration::from_secs(15); -const TLS_HOST: &str = "tls.test"; +const TLS_HOST: &str = "localhost"; const TLS_MARKER: &str = "browserkit-task12-local-tls"; struct TlsFixture { @@ -36,7 +36,7 @@ impl TlsFixture { async fn start() -> Self { let CertifiedKey { cert, key_pair } = generate_simple_self_signed(vec![TLS_HOST.to_owned()]) - .expect("self-signed tls.test cert"); + .expect("self-signed localhost cert"); let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der())); let config = ServerConfig::builder() .with_no_client_auth() @@ -68,7 +68,18 @@ impl TlsFixture { let Ok(stream) = acceptor.accept(stream).await else { return; }; - serve_marker(stream).await.expect("serve TLS marker response"); + match serve_marker(stream).await { + Ok(()) => {} + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::BrokenPipe + | std::io::ErrorKind::UnexpectedEof + ) => {} + Err(error) => panic!("serve TLS marker response: {error}"), + } }); } } @@ -127,14 +138,13 @@ async fn serve_marker( stream.shutdown().await } -fn chrome_args(port: u16) -> [String; 6] { +fn chrome_args(port: u16) -> [String; 5] { [ "--disable-background-networking".to_owned(), "--disable-component-update".to_owned(), "--disable-default-apps".to_owned(), "--disable-sync".to_owned(), format!("--explicitly-allowed-ports={port}"), - format!("--host-resolver-rules=MAP {TLS_HOST} 127.0.0.1"), ] } @@ -228,9 +238,9 @@ async fn browser_footprint(runtime: &BrowserRuntime) -> (BTreeSet, BTree } #[tokio::test] -#[ignore = "requires installed Chrome; uses a self-signed tls.test fixture, private profile, and loopback only"] +#[ignore = "requires installed Chrome; uses a self-signed localhost fixture, private profile, and loopback only"] async fn chrome_local_tls_launched_default_and_isolated_scopes_do_not_leak() { - tokio::time::timeout(Duration::from_secs(90), launched_tls_scenario()) + tokio::time::timeout(Duration::from_secs(90), Box::pin(launched_tls_scenario())) .await .expect("launched local TLS test deadline"); } @@ -431,7 +441,7 @@ fn chrome_executable() -> PathBuf { #[tokio::test] #[ignore = "requires installed Chrome; attaches only to a dedicated private-profile process and loopback TLS"] async fn chrome_local_tls_attached_default_preflight_and_isolated_scope() { - tokio::time::timeout(Duration::from_secs(60), attached_tls_scenario()) + tokio::time::timeout(Duration::from_secs(60), Box::pin(attached_tls_scenario())) .await .expect("attached local TLS test deadline"); }