//! Cross-language pin: Rust `format_invalidate_header` must be byte-identical //! to `cores/mizan-python/.../invalidation.py::format_invalidate_header`. //! //! The `X-Mizan-Invalidate` header is co-equal with the JSON body channel in //! the spec; Edge parses it to purge. The Python reference is the oracle. use mizan_core::{format_invalidate_header, InvalidationTarget}; use serde_json::json; use std::path::PathBuf; use std::process::Command; fn afi_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../tests/afi") .canonicalize() .expect("tests/afi exists") } fn py_header(json_list: &str) -> String { let code = format!( "import json; from mizan_core.invalidation import format_invalidate_header; \ print(format_invalidate_header(json.loads(r'''{json_list}''')))", ); let out = Command::new("uv") .args(["run", "python", "-c", &code]) .current_dir(afi_dir()) .output() .expect("invoke uv run python"); assert!( out.status.success(), "python reference failed: {}", String::from_utf8_lossy(&out.stderr) ); // Trim the trailing newline only — the header value itself may be empty. let s = String::from_utf8(out.stdout).unwrap(); s.strip_suffix('\n').unwrap_or(&s).to_string() } fn scoped(ctx: &str, params: &[(&str, serde_json::Value)]) -> InvalidationTarget { InvalidationTarget::ScopedContext { context: ctx.to_string(), params: params.iter().map(|(k, v)| (k.to_string(), v.clone())).collect(), } } #[test] fn matches_python_reference() { let cases: Vec<(Vec, &str)> = vec![ (vec![InvalidationTarget::Context("user".into())], r#"["user"]"#), ( vec![ InvalidationTarget::Context("user".into()), InvalidationTarget::Context("notifications".into()), ], r#"["user", "notifications"]"#, ), ( vec![scoped("user", &[("user_id", json!(5))])], r#"[{"context": "user", "params": {"user_id": 5}}]"#, ), ( vec![scoped("search", &[("q", json!("hello world"))])], r#"[{"context": "search", "params": {"q": "hello world"}}]"#, ), ( // Multiple params → sorted by key, semicolon-joined. vec![scoped("u", &[("b", json!("2")), ("a", json!("1"))])], r#"[{"context": "u", "params": {"b": "2", "a": "1"}}]"#, ), ( // Special chars that must percent-encode: &, =, /, space, unicode. vec![scoped("c", &[("k", json!("a&b=c/d e—ñ"))])], r#"[{"context": "c", "params": {"k": "a&b=c/d e—ñ"}}]"#, ), ( // Mixed bare + scoped. vec![ scoped("user", &[("user_id", json!(5))]), InvalidationTarget::Context("notifications".into()), ], r#"[{"context": "user", "params": {"user_id": 5}}, "notifications"]"#, ), ]; for (targets, json_list) in cases { let rust = format_invalidate_header(&targets); let expected = py_header(json_list); assert_eq!(rust, expected, "header mismatch for {json_list}"); } }