use super::*; #[test] fn watch_url_preserves_stable_identity_and_validates_mutable_fields() { let identity = Identity::from_key("miruro:OERe_a0YS4h-DlsBDOeXwX7HOO6316tw:1").unwrap(); assert_eq!(identity.key(), "miruro:OERe_a0YS4h-DlsBDOeXwX7HOO6316tw:1"); assert_eq!( identity.watch_url().unwrap(), "https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/episode?ep=1" ); assert_eq!(Identity::from_key(&identity.key()).unwrap(), identity); assert_ne!( Identity::new(identity.media_id.clone(), 2).unwrap().key(), identity.key() ); for key in [ "miruro:catalog:01", "miruro:catalog:0", "miruro:catalog:2147483648", "miruro:catalog/:1", "miruro:catalog:+1", "miruro::1", ] { assert!(Identity::from_key(key).is_err()); } for invalid in [ Identity { provider: "youtube".into(), ..identity.clone() }, Identity { media_id: "catalog/".into(), ..identity.clone() }, Identity { media_id: "a".repeat(513), ..identity.clone() }, Identity { episode: 0, ..identity.clone() }, Identity { episode: i32::MAX as u32 + 1, ..identity.clone() }, ] { assert!(invalid.watch_url().is_err()); } } #[test] fn episodes_resolve_by_promise_id_with_independent_tables_or_fail_explicitly() { let initial = r#"{"type":"data","nodes":[null,{"type":"data","data":[{"detail":1,"episodes":4},{"id":2,"episode_count":3},"series",12,["Promise",5],9]}]}"#; let unrelated = r#"{"type":"chunk","id":4,"data":["unrelated"]}"#; let episode_chunk = r#"{"type":"chunk","id":9,"data":[[1],{"episode_number":2,"absolute_number":3,"title":4,"canon_type":5},1,49,"Locusts",null]}"#; let detail = decode_detail(&format!("{initial}\n{unrelated}\n{episode_chunk}\n")).unwrap(); assert_eq!(detail.media["episode_count"], 12); assert_eq!(detail.episodes[0]["episode_number"], 1); assert_eq!(detail.episodes[0]["absolute_number"], 49); assert_eq!(detail.episodes[0]["title"], "Locusts"); assert!(detail.episodes[0]["canon_type"].is_null()); for chunk in [ "", // The promised episodes never arrive. r#"{"type":"chunk","id":9,"error":["failed"]}"#, r#"{"type":"chunk","id":9,"data":[[999]]}"#, r#"{"type":"chunk","id":9,"data":[[0]]}"#, // Reference cycle. r#"{"type":"chunk","id":9,"data":[["Date","2026-10-12"]]}"#, ] { assert_eq!( decode_detail(&format!("{initial}\n{chunk}")) .err() .unwrap() .kind(), io::ErrorKind::InvalidData ); } assert_eq!( decode_detail(r#"{"type":"redirect","location":"/"}"#) .err() .unwrap() .kind(), io::ErrorKind::NotFound ); } #[test] fn mirror_failover_remembers_success_and_stops_on_rate_limits_and_bad_requests() { use std::{ io::{BufRead, BufReader, Write}, net::TcpListener, thread, }; let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let base = format!("http://{}", listener.local_addr().unwrap()); let server = thread::spawn(move || { for (path, status, mime, body) in [ ("/one/catalog?q=anime", 403, "text/html", "blocked"), ("/two/catalog?q=anime", 200, "text/html", "protection page"), ( "/three/catalog?q=anime", 200, "application/json", r#"{"data":[{"id":"series"}]}"#, ), ("/three/detail", 503, "text/html", "unavailable"), ( "/one/detail", 200, "text/sveltekit-data", "episode metadata", ), ("/one/limited", 429, "text/html", "rate limited"), ("/one/bad-query", 400, "application/json", "{}"), ("/one/exhausted", 403, "text/html", "blocked"), ("/two/exhausted", 404, "text/html", "gone"), ("/three/exhausted", 502, "text/html", "unavailable"), ("/one/recovered", 200, "application/json", r#"{"data":[]}"#), ] { let (mut stream, _) = listener.accept().unwrap(); stream .set_read_timeout(Some(Duration::from_secs(5))) .unwrap(); let mut reader = BufReader::new(&stream); let mut line = String::new(); reader.read_line(&mut line).unwrap(); assert_eq!(line.trim_end(), format!("GET {path} HTTP/1.1")); loop { line.clear(); reader.read_line(&mut line).unwrap(); if line == "\r\n" { break; } } write!( stream, "HTTP/1.1 {status} Response\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ).unwrap(); } }); let bases = [ format!("{base}/one"), format!("{base}/two"), format!("{base}/three"), ]; let mirrors: Vec<_> = bases.iter().map(String::as_str).collect(); let client = Client::default(); let body = client .get_from( &mirrors, "/catalog", &[("q", "anime")], &["application/json"], ) .unwrap(); assert_eq!( serde_json::from_str::(&body).unwrap()["data"][0]["id"], "series" ); assert_eq!( client .get_from(&mirrors, "/detail", &[], &["text/sveltekit-data"]) .unwrap(), "episode metadata" ); for (path, status) in [("/limited", 429), ("/bad-query", 400)] { let error = client .get_from(&mirrors, path, &[], &["application/json"]) .unwrap_err(); assert!(error.to_string().contains(&format!("HTTP {status}"))); } let error = client .get_from(&mirrors, "/exhausted", &[], &["application/json"]) .unwrap_err(); assert!(error.to_string().contains("failed on 3/3 mirrors")); assert!(error.to_string().contains("HTTP 502")); let body = client .get_from(&mirrors, "/recovered", &[], &["application/json"]) .unwrap(); assert_eq!( serde_json::from_str::(&body).unwrap()["data"], serde_json::json!([]) ); server.join().unwrap(); }