188 lines
6.5 KiB
Rust
188 lines
6.5 KiB
Rust
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::<Value>(&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::<Value>(&body).unwrap()["data"],
|
|
serde_json::json!([])
|
|
);
|
|
server.join().unwrap();
|
|
}
|