mod scope_tests { use super::*; use http_body_util::BodyExt; async fn fill(cache: &Arc, ticket: Ticket) { let settings = Settings { enabled: true, ..Settings::default() }; let mut response = Response::new(Body::from("scope-payload")); response.headers_mut().insert( "cache-control", HeaderValue::from_static("public, max-age=30"), ); let plan = plan( StatusCode::OK, response.headers(), &settings, Duration::ZERO, ) .unwrap(); cache .wrap(response, ticket, plan, &settings) .into_body() .collect() .await .unwrap(); } #[tokio::test] async fn selective_purge_preserves_other_hosts_and_cancels_old_fills() { let cache = Cache::new(1); let ticket = |scope| { cache .ticket_scoped( 1, scope, "http", "same.test", "root", "/", &HeaderMap::new(), ) .unwrap() }; let a = ticket("a"); let b = ticket("b"); fill(&cache, a.clone()).await; fill(&cache, b.clone()).await; assert_eq!(cache.scope_view("a").entries, 1); assert_eq!(cache.purge_scope("a"), 1); assert!(cache.get(&a).is_none()); assert!(cache.get(&b).is_some()); fill(&cache, a).await; assert_eq!(cache.scope_view("a").entries, 0); fill(&cache, ticket("a")).await; assert_eq!(cache.scope_view("a").entries, 1); } #[test] fn stale_snapshot_cannot_issue_cache_ticket_after_config_commit() { let cache = Cache::new(1); cache.set_control_revision(7); let ticket = |revision| cache.ticket_scoped_for_config( revision, 1, "vhost-a", "http", "same.test", "main", "/resource", &HeaderMap::new() ); assert!(ticket(7).is_some(), "the current snapshot can cache"); cache.set_control_revision(8); assert_eq!(cache.revision(), 1); assert!(ticket(7).is_none(), "late request from the previous snapshot must bypass"); assert!(ticket(8).is_some(), "the new snapshot can reuse the same cache identity"); } #[test] fn per_vhost_settings_inherit_and_cannot_expand_shared_capacity() { let global = Settings::default(); let effective = Overrides { enabled: Some(true), max_ttl_seconds: Some(30), expose_status: Some(true), ..Overrides::default() } .resolve(&global) .unwrap(); assert!(effective.enabled && effective.expose_status); let mut globally_disabled = global.clone(); globally_disabled.enabled = false; assert!(!Overrides { enabled: Some(true), ..Overrides::default() } .resolve(&globally_disabled) .unwrap() .enabled); assert!(!Overrides { enabled: Some(false), ..Overrides::default() } .resolve(&global) .unwrap() .enabled); assert!(!Overrides::default() .resolve(&globally_disabled) .unwrap() .enabled); assert_eq!(effective.max_bytes, global.max_bytes); assert_eq!(effective.max_ttl_seconds, 30); assert!(Overrides { max_object_bytes: Some(global.max_object_bytes + 1), ..Overrides::default() } .resolve(&global) .is_err()); assert!(Overrides { max_ttl_seconds: Some(1), default_ttl_seconds: Some(2), ..Overrides::default() } .resolve(&global) .is_err()); } } #[test] fn unsafe_force_cache_is_per_vhost_and_matches_path_or_target_prefix() { let force = UnsafeForce { enabled: true, targets: vec!["/convert?url=".into(), "/assets".into()], }; force.validate().unwrap(); assert!(force.matches( "/convert", "/convert?url=https%3A%2F%2Fexample.test%2Flist.txt" )); assert!(force.matches("/assets/app.js", "/assets/app.js?v=1")); assert!(!force.matches("/api", "/api?url=x")); assert!(UnsafeForce { enabled: true, targets: vec!["https://example.test/".into()], } .validate() .is_err()); } #[test] fn forced_request_policy_keeps_protocol_safety_but_ignores_cookie_and_client_no_store() { let mut headers = HeaderMap::new(); headers.insert("cookie", HeaderValue::from_static("session=abc")); headers.insert("authorization", HeaderValue::from_static("Bearer token")); headers.insert("cache-control", HeaderValue::from_static("no-store")); assert_eq!( forced_request_policy(&Method::GET, &headers, true), RequestPolicy::Use ); assert_eq!( forced_request_policy(&Method::POST, &headers, true), RequestPolicy::Bypass("request_method") ); }