use super::*; #[test] fn magnet_with_tracker_tiers_and_dht_hint_preserves_bt_status_shape() { let magnet = "magnet:?xt=urn:btih:00112233445566778899aabbccddeeff00112233&dn=bt-dht-tiered.iso&tr=http%3A%2F%2Ftracker-a.example.org%2Fannounce&tr=udp%3A%2F%2Ftracker-b.example.org%3A6969&x.pe=198.51.100.9%3A51413"; let mut dispatcher = InProcessRpcDispatcher::new(); let add = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2AddUri.as_str().to_owned(), params: vec![RpcValue::String(magnet.to_owned())], meta: Default::default(), }); let gid = match add.result { Some(RpcValue::String(gid)) => gid, other => panic!("unexpected addUri magnet result: {other:?}"), }; let status = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2TellStatus.as_str().to_owned(), params: vec![RpcValue::String(gid.clone())], meta: Default::default(), }); match status.result { Some(RpcValue::Object(payload)) => { assert_eq!(payload.get("isBt"), Some(&RpcValue::Bool(true))); assert!(matches!( payload.get("announceList"), Some(RpcValue::Array(tiers)) if !tiers.is_empty() )); assert!(matches!( payload.get("magnetUri"), Some(RpcValue::String(uri)) if uri.starts_with("magnet:?xt=urn:btih:") )); assert!( payload.contains_key("numSeeders"), "compat bt status should expose numSeeders key" ); } other => panic!("unexpected tellStatus payload for tiered magnet: {other:?}"), } let peers = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2GetPeers.as_str().to_owned(), params: vec![RpcValue::String(gid)], meta: Default::default(), }); assert!( matches!(peers.result, Some(RpcValue::Array(_))), "getPeers should remain type-stable for dht/tiered magnet surface" ); } #[test] fn tracker_announce_reingest_replaces_peer_view_with_latest_snapshot() { fn bencode_int(value: i64) -> Vec { format!("i{value}e").into_bytes() } fn bencode_bytes(value: &[u8]) -> Vec { let mut out = format!("{}:", value.len()).into_bytes(); out.extend_from_slice(value); out } fn bencode_list(values: Vec>) -> Vec { let mut out = vec![b'l']; for value in values { out.extend_from_slice(&value); } out.push(b'e'); out } fn bencode_dict(entries: Vec<(&str, Vec)>) -> Vec { let mut out = vec![b'd']; for (key, value) in entries { out.extend_from_slice(format!("{}:{key}", key.len()).as_bytes()); out.extend_from_slice(&value); } out.push(b'e'); out } let magnet = "magnet:?xt=urn:btih:8899aabbccddeeff00112233445566778899aabb&dn=bt-reannounce.iso&tr=http%3A%2F%2Ftracker.example.org%2Fannounce"; let mut dispatcher = InProcessRpcDispatcher::new(); let add = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2AddUri.as_str().to_owned(), params: vec![RpcValue::String(magnet.to_owned())], meta: Default::default(), }); let gid = match add.result { Some(RpcValue::String(gid)) => gid, other => panic!("unexpected addUri result: {other:?}"), }; let announce_a = TrackerResponseModel::from_announce_bytes(&bencode_dict(vec![ ("interval", bencode_int(1200)), ( "peers", bencode_list(vec![bencode_dict(vec![ ("ip", bencode_bytes(b"198.51.100.41")), ("port", bencode_int(6001)), ])]), ), ])) .expect("announce-a should parse"); dispatcher .apply_tracker_announce_result(&gid, &announce_a) .expect("announce-a should ingest"); let announce_b = TrackerResponseModel::from_announce_bytes(&bencode_dict(vec![ ("interval", bencode_int(900)), ( "peers", bencode_list(vec![ bencode_dict(vec![ ("ip", bencode_bytes(b"198.51.100.42")), ("port", bencode_int(6002)), ]), bencode_dict(vec![ ("ip", bencode_bytes(b"198.51.100.43")), ("port", bencode_int(6003)), ]), ]), ), ])) .expect("announce-b should parse"); dispatcher .apply_tracker_announce_result(&gid, &announce_b) .expect("announce-b should ingest"); let peers = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2GetPeers.as_str().to_owned(), params: vec![RpcValue::String(gid)], meta: Default::default(), }); match peers.result { Some(RpcValue::Array(items)) => { assert_eq!( items.len(), 2, "second announce snapshot should be reflected" ); let ports = items .into_iter() .filter_map(|item| match item { RpcValue::Object(peer) => peer.get("port").cloned(), _ => None, }) .collect::>(); assert!(ports.contains(&RpcValue::String("6002".to_owned()))); assert!(ports.contains(&RpcValue::String("6003".to_owned()))); } other => panic!("unexpected getPeers payload after reannounce: {other:?}"), } } #[test] fn bt_mixed_rpc_surface_smoke_preserves_peer_and_server_type_contracts() { let mut dispatcher = InProcessRpcDispatcher::new(); let torrent_payload = "ZDg6YW5ub3VuY2UzNTpodHRwOi8vdHJhY2tlci5leGFtcGxlLm9yZy9hbm5vdW5jZTQ6aW5mb2Q0Om5hbWUxMDp1YnVudHUuaXNvMTI6cGllY2UgbGVuZ3RoaTE2Mzg0ZTY6bGVuZ3RoaTMyNzY4ZTY6cGllY2VzNDA6YWFhYWFhYWFhYWFhYWFhYWFhYWFiYmJiYmJiYmJiYmJiYmJiYmJiYmVl"; let add_torrent = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2AddTorrent.as_str().to_owned(), params: vec![RpcValue::String(torrent_payload.to_owned())], meta: Default::default(), }); let torrent_gid = match add_torrent.result { Some(RpcValue::String(gid)) => gid, other => panic!("unexpected addTorrent result: {other:?}"), }; let mut magnet_gids = Vec::new(); for i in 0..20 { let magnet = format!( "magnet:?xt=urn:btih:{:040x}&dn=bt-mixed-{i}&tr=http%3A%2F%2Ftracker.example.org%2Fannounce", i + 4000 ); let add = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2AddUri.as_str().to_owned(), params: vec![RpcValue::String(magnet)], meta: Default::default(), }); match add.result { Some(RpcValue::String(gid)) => magnet_gids.push(gid), other => panic!("unexpected addUri result in mixed smoke: {other:?}"), } } let mut bt_status_objects = 0usize; for gid in magnet_gids.iter().chain(std::iter::once(&torrent_gid)) { let status = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2TellStatus.as_str().to_owned(), params: vec![RpcValue::String(gid.clone())], meta: Default::default(), }); match status.result { Some(RpcValue::Object(payload)) => { assert_eq!(payload.get("isBt"), Some(&RpcValue::Bool(true))); assert!(payload.contains_key("status")); assert!(payload.contains_key("completedLength")); bt_status_objects += 1; } other => panic!("unexpected tellStatus payload in mixed smoke: {other:?}"), } let servers = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2GetServers.as_str().to_owned(), params: vec![RpcValue::String(gid.clone())], meta: Default::default(), }); let error = servers .error .expect("getServers should reject non-active BT downloads"); assert!( error .message .contains(&format!("No active download for GID#{gid}")) ); } assert_eq!(bt_status_objects, magnet_gids.len() + 1); }