Files
aria2-rust-pro/crates/aria2-rust-pro-rpc/src/dispatcher/tests.rs
T

679 lines
20 KiB
Rust

use std::{
collections::BTreeMap,
fs,
sync::Mutex,
time::{SystemTime, UNIX_EPOCH},
};
use aria2_rust_pro_core::{
BtFileInfo, BtPeerInfo, BtPieceAvailabilityUpdate, BtRuntimeState, DownloadId, PieceId,
PieceState, RuntimeConfig,
};
use aria2_rust_pro_protocol::{
DhtMessageModel, DhtNodeModel, DhtTransport, TrackerRequestModel, TrackerResponseModel,
TrackerScrapeModel, TrackerTransport,
torrent::{
DhtGetPeersQueryModel, DhtMessageBody, DhtQueryModel, PeerWireBitfieldModel,
PeerWireExtensionHandshakeModel, PeerWireHandshakeModel, PeerWireMessageKind,
PeerWireMetadataMessageModel, PeerWireMetadataMessageType, PeerWirePieceBlockModel,
TorrentMessageModel, parse_torrent_metadata,
},
transport::{
PeerWireTransportConnector, PeerWireTransportRequest, PeerWireTransportResponse,
TransportEndpoint, TransportError, TransportErrorKind, TransportScheme,
},
};
use aria2_rust_pro_storage::load_session_file;
use base64::Engine;
use super::{
BtRuntimeCoordinatorAction, BtRuntimeCoordinatorStepStatus, InProcessRpcDispatcher,
bt_metadata_piece_span, decode_hex_string_exact,
};
use crate::{
jsonrpc::{JsonRpcRequest, jsonrpc_request_from_json, jsonrpc_response_to_json},
methods::RpcMethod,
model::{RpcError, RpcMeta, RpcValue},
xmlrpc::{XmlRpcMember, XmlRpcMethodCall, XmlRpcParam, XmlRpcValue},
};
#[doc(hidden)]
fn temp_session_path(name: &str) -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be monotonic enough for test naming")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"aria2-rust-pro-rpc-test-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&root).expect("temp dir should be creatable");
root.join(name)
}
#[doc(hidden)]
fn request(method: RpcMethod, params: Vec<RpcValue>) -> JsonRpcRequest {
JsonRpcRequest {
jsonrpc: Some("2.0".to_owned()),
id: None,
method: method.as_str().to_owned(),
params,
meta: RpcMeta::default(),
}
}
#[doc(hidden)]
fn request_with_method_name(method: &str, params: Vec<RpcValue>) -> JsonRpcRequest {
JsonRpcRequest {
jsonrpc: Some("2.0".to_owned()),
id: None,
method: method.to_owned(),
params,
meta: RpcMeta::default(),
}
}
#[doc(hidden)]
fn add_uri(dispatcher: &mut InProcessRpcDispatcher, uri: &str) -> String {
let response = dispatcher.dispatch_json(request(
RpcMethod::Aria2AddUri,
vec![RpcValue::String(uri.to_owned())],
));
match response.result {
Some(RpcValue::String(gid)) => gid,
other => panic!("unexpected addUri result: {other:?}"),
}
}
#[doc(hidden)]
#[test]
fn add_uri_direct_registers_uri_and_options_without_jsonrpc_roundtrip() {
let mut dispatcher = InProcessRpcDispatcher::new();
let gid = dispatcher
.add_uri_direct(
vec![
"https://example.org/direct-a.iso".to_owned(),
"https://example.org/direct-b.iso".to_owned(),
],
vec![("split".to_owned(), RpcValue::String("8".to_owned()))],
)
.expect("direct addUri should register");
let group = dispatcher
.engine
.handle_mut(download_id(&gid))
.expect("direct addUri group should exist");
assert_eq!(group.uri(), "https://example.org/direct-a.iso");
assert_eq!(
group.uris(),
&[
"https://example.org/direct-a.iso".to_owned(),
"https://example.org/direct-b.iso".to_owned(),
]
);
assert_eq!(group.option_limit("split"), Some(8));
}
#[doc(hidden)]
fn download_id(gid: &str) -> DownloadId {
DownloadId::parse_hex(gid).expect("gid should parse into DownloadId")
}
#[doc(hidden)]
fn compact_peer(ip: [u8; 4], port: u16) -> Vec<u8> {
let mut bytes = Vec::with_capacity(6);
bytes.extend_from_slice(&ip);
bytes.extend_from_slice(&port.to_be_bytes());
bytes
}
#[doc(hidden)]
fn compact_node(node_id_byte: u8, ip: [u8; 4], port: u16) -> Vec<u8> {
let mut bytes = vec![node_id_byte; 20];
bytes.extend_from_slice(&ip);
bytes.extend_from_slice(&port.to_be_bytes());
bytes
}
#[doc(hidden)]
#[derive(Debug)]
struct FakeDhtTransport {
#[doc(hidden)]
response: DhtMessageModel,
#[doc(hidden)]
seen: Mutex<Vec<(DhtNodeModel, DhtMessageModel)>>,
}
impl FakeDhtTransport {
#[doc(hidden)]
fn new(response: DhtMessageModel) -> Self {
Self {
response,
seen: Mutex::new(Vec::new()),
}
}
#[doc(hidden)]
fn seen(&self) -> Vec<(DhtNodeModel, DhtMessageModel)> {
self.seen
.lock()
.expect("seen requests mutex should not be poisoned")
.clone()
}
}
impl DhtTransport for FakeDhtTransport {
#[doc(hidden)]
fn send_message(
&self,
node: &DhtNodeModel,
message: &DhtMessageModel,
) -> Result<DhtMessageModel, TransportError> {
self.seen
.lock()
.expect("seen requests mutex should not be poisoned")
.push((node.clone(), message.clone()));
Ok(self.response.clone())
}
}
#[doc(hidden)]
#[derive(Debug)]
struct FakeTrackerTransport {
#[doc(hidden)]
announce_response: TrackerResponseModel,
#[doc(hidden)]
scrape_response: Option<TrackerScrapeModel>,
#[doc(hidden)]
seen_announces: Mutex<Vec<TrackerRequestModel>>,
#[doc(hidden)]
seen_scrapes: Mutex<Vec<String>>,
}
impl FakeTrackerTransport {
#[doc(hidden)]
fn new(
announce_response: TrackerResponseModel,
scrape_response: Option<TrackerScrapeModel>,
) -> Self {
Self {
announce_response,
scrape_response,
seen_announces: Mutex::new(Vec::new()),
seen_scrapes: Mutex::new(Vec::new()),
}
}
#[doc(hidden)]
fn seen_announces(&self) -> Vec<TrackerRequestModel> {
self.seen_announces
.lock()
.expect("tracker announce mutex should not be poisoned")
.clone()
}
#[doc(hidden)]
fn seen_scrapes(&self) -> Vec<String> {
self.seen_scrapes
.lock()
.expect("tracker scrape mutex should not be poisoned")
.clone()
}
}
impl TrackerTransport for FakeTrackerTransport {
#[doc(hidden)]
fn announce(
&self,
request: &TrackerRequestModel,
) -> Result<TrackerResponseModel, TransportError> {
self.seen_announces
.lock()
.expect("tracker announce mutex should not be poisoned")
.push(request.clone());
Ok(self.announce_response.clone())
}
#[doc(hidden)]
fn scrape(&self, url: &str) -> Result<TrackerScrapeModel, TransportError> {
self.seen_scrapes
.lock()
.expect("tracker scrape mutex should not be poisoned")
.push(url.to_owned());
self.scrape_response.clone().ok_or_else(|| TransportError {
kind: TransportErrorKind::ProtocolViolation,
message: "scrape unavailable".to_owned(),
source: None,
context: None,
})
}
}
#[doc(hidden)]
#[derive(Debug)]
struct FakePeerWireConnector {
#[doc(hidden)]
response_payload: Vec<u8>,
#[doc(hidden)]
seen: Mutex<Vec<PeerWireTransportRequest>>,
}
impl FakePeerWireConnector {
#[doc(hidden)]
fn new(response_payload: Vec<u8>) -> Self {
Self {
response_payload,
seen: Mutex::new(Vec::new()),
}
}
#[doc(hidden)]
fn seen(&self) -> Vec<PeerWireTransportRequest> {
self.seen
.lock()
.expect("peer-wire seen requests mutex should not be poisoned")
.clone()
}
}
#[doc(hidden)]
#[derive(Debug)]
struct SequencedPeerWireConnector {
#[doc(hidden)]
response_payloads: Mutex<Vec<Vec<u8>>>,
#[doc(hidden)]
seen: Mutex<Vec<PeerWireTransportRequest>>,
}
impl SequencedPeerWireConnector {
#[doc(hidden)]
fn new(response_payloads: Vec<Vec<u8>>) -> Self {
Self {
response_payloads: Mutex::new(response_payloads),
seen: Mutex::new(Vec::new()),
}
}
#[doc(hidden)]
fn seen(&self) -> Vec<PeerWireTransportRequest> {
self.seen
.lock()
.expect("sequenced peer-wire seen requests mutex should not be poisoned")
.clone()
}
}
impl PeerWireTransportConnector for SequencedPeerWireConnector {
#[doc(hidden)]
fn connect_peer_wire(
&self,
request: &PeerWireTransportRequest,
) -> Result<PeerWireTransportResponse, TransportError> {
self.seen
.lock()
.expect("sequenced peer-wire seen requests mutex should not be poisoned")
.push(request.clone());
let payload = self
.response_payloads
.lock()
.expect("sequenced peer-wire payload mutex should not be poisoned")
.remove(0);
Ok(PeerWireTransportResponse {
endpoint: TransportEndpoint {
scheme: TransportScheme::BitTorrent,
address: request.endpoint.address.clone(),
},
payload,
})
}
}
impl PeerWireTransportConnector for FakePeerWireConnector {
#[doc(hidden)]
fn connect_peer_wire(
&self,
request: &PeerWireTransportRequest,
) -> Result<PeerWireTransportResponse, TransportError> {
self.seen
.lock()
.expect("peer-wire seen requests mutex should not be poisoned")
.push(request.clone());
Ok(PeerWireTransportResponse {
endpoint: TransportEndpoint {
scheme: TransportScheme::BitTorrent,
address: request.endpoint.address.clone(),
},
payload: self.response_payload.clone(),
})
}
}
#[doc(hidden)]
#[derive(Debug)]
struct RoutedDhtTransport {
#[doc(hidden)]
get_peers_response: DhtMessageModel,
#[doc(hidden)]
announce_peer_response: DhtMessageModel,
#[doc(hidden)]
find_node_response: Option<DhtMessageModel>,
#[doc(hidden)]
ping_response: Option<DhtMessageModel>,
#[doc(hidden)]
seen: Mutex<Vec<(DhtNodeModel, DhtMessageModel)>>,
}
impl RoutedDhtTransport {
#[doc(hidden)]
fn new(get_peers_response: DhtMessageModel, announce_peer_response: DhtMessageModel) -> Self {
Self {
get_peers_response,
announce_peer_response,
find_node_response: None,
ping_response: None,
seen: Mutex::new(Vec::new()),
}
}
#[doc(hidden)]
fn seen(&self) -> Vec<(DhtNodeModel, DhtMessageModel)> {
self.seen
.lock()
.expect("routed dht seen mutex should not be poisoned")
.clone()
}
}
impl DhtTransport for RoutedDhtTransport {
#[doc(hidden)]
fn send_message(
&self,
node: &DhtNodeModel,
message: &DhtMessageModel,
) -> Result<DhtMessageModel, TransportError> {
self.seen
.lock()
.expect("routed dht seen mutex should not be poisoned")
.push((node.clone(), message.clone()));
match &message.body {
DhtMessageBody::Query(DhtQueryModel::GetPeers(_)) => {
Ok(self.get_peers_response.clone())
}
DhtMessageBody::Query(DhtQueryModel::AnnouncePeer(_)) => {
Ok(self.announce_peer_response.clone())
}
DhtMessageBody::Query(DhtQueryModel::FindNode(_)) => self
.find_node_response
.clone()
.ok_or_else(|| TransportError {
kind: TransportErrorKind::ProtocolViolation,
message: "find_node unavailable".to_owned(),
source: None,
context: None,
}),
DhtMessageBody::Query(DhtQueryModel::Ping(_)) => {
self.ping_response.clone().ok_or_else(|| TransportError {
kind: TransportErrorKind::ProtocolViolation,
message: "ping unavailable".to_owned(),
source: None,
context: None,
})
}
_ => Err(TransportError {
kind: TransportErrorKind::ProtocolViolation,
message: "unexpected dht method for routed transport".to_owned(),
source: None,
context: None,
}),
}
}
}
#[doc(hidden)]
fn peer_wire_handshake_and_frames(
info_hash: [u8; 20],
peer_id: [u8; 20],
frames: &[PeerWireMessageKind],
) -> Vec<u8> {
let mut bytes = PeerWireHandshakeModel::new(info_hash, peer_id).serialize();
for frame in frames {
bytes.extend_from_slice(
&TorrentMessageModel::from_peer_wire_kind(frame.clone())
.serialize_peer_wire_frame()
.expect("peer-wire frame should serialize"),
);
}
bytes
}
#[doc(hidden)]
fn peer_from_ip(ip: &str, port: u16) -> BtPeerInfo {
BtPeerInfo {
peer_id: None,
ip: ip.to_owned(),
port,
client_name: None,
interested: false,
choked: true,
download_speed: 0,
upload_speed: 0,
seeder: false,
}
}
#[doc(hidden)]
fn single_file_torrent_bytes(name: &str, comment_len: usize) -> Vec<u8> {
let comment = "x".repeat(comment_len);
format!(
"d8:announce35:http://tracker.example.org/announce7:comment{}:{}4:infod6:lengthi2048e4:name{}:{}12:piece lengthi1024e6:pieces20:aaaaaaaaaaaaaaaaaaaaee",
comment.len(),
comment,
name.len(),
name
)
.into_bytes()
}
#[doc(hidden)]
#[test]
fn bt_runtime_coordinator_snapshot_surfaces_partial_magnet_runtime_and_recommended_actions() {
let mut dispatcher = InProcessRpcDispatcher::new();
let gid = add_uri(
&mut dispatcher,
"magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678&tr=http%3A%2F%2Ftracker.example.org%2Fannounce",
);
let download_id = download_id(&gid);
{
let group = dispatcher
.engine
.handle_mut(download_id)
.expect("download group should exist");
group.set_piece_length(1_024);
group.set_total_length(4_096);
group.set_piece_state(PieceId(0), PieceState::Pending);
group.set_piece_state(PieceId(1), PieceState::Missing);
group.set_dht_token(Some(b"cached-token".to_vec()));
let bt = group.bt_mut().expect("bt runtime state should exist");
bt.dht_nodes = vec!["bad-node".to_owned(), "127.0.0.9:6881".to_owned()];
bt.peers = vec![peer_from_ip("127.0.0.7", 51413)];
}
let snapshot = dispatcher
.bt_runtime_coordinator_snapshot(&gid)
.expect("snapshot should inspect bt runtime");
assert!(snapshot.metadata_only);
assert!(snapshot.metadata_exchange_pending);
assert_eq!(snapshot.tracker_count, 1);
assert_eq!(snapshot.dht_node_count, 2);
assert_eq!(snapshot.addressable_dht_node_count, 1);
assert_eq!(snapshot.peer_count, 1);
assert_eq!(snapshot.connectable_peer_count, 1);
assert_eq!(snapshot.requestable_piece_count, 2);
assert_eq!(
snapshot.recommended_actions,
vec![
BtRuntimeCoordinatorAction::TrackerAnnounce,
BtRuntimeCoordinatorAction::DhtGetPeers,
BtRuntimeCoordinatorAction::DhtAnnouncePeer,
BtRuntimeCoordinatorAction::PeerWireExchange,
]
);
}
#[doc(hidden)]
#[test]
fn drive_bt_runtime_once_executes_newly_unlocked_bt_steps_within_one_iteration() {
let mut dispatcher = InProcessRpcDispatcher::new();
let gid = add_uri(
&mut dispatcher,
"magnet:?xt=urn:btih:fedcba9876543210fedcba9876543210fedcba98&tr=http%3A%2F%2Ftracker.example.org%2Fannounce",
);
let download_id = download_id(&gid);
{
let group = dispatcher
.engine
.handle_mut(download_id)
.expect("download group should exist");
group.set_piece_length(1_024);
group.set_total_length(2_048);
group.set_piece_state(PieceId(0), PieceState::Pending);
group.set_piece_state(PieceId(1), PieceState::Missing);
let bt = group.bt_mut().expect("bt runtime state should exist");
bt.dht_nodes = vec!["127.0.0.11:6881".to_owned()];
}
let tracker = FakeTrackerTransport::new(
TrackerResponseModel {
peers: aria2_rust_pro_protocol::TrackerPeerListModel {
interval_sec: 1_800,
peers: vec![aria2_rust_pro_protocol::torrent::TorrentPeerModel {
ip: "127.0.0.21".to_owned(),
port: 51_413,
peer_id: Some(*b"12345678901234567890"),
client_name: Some("tracker-peer".to_owned()),
interested: false,
choked: false,
}],
min_interval_sec: None,
tracker_id: Some("tracker-id".to_owned()),
},
scrape: None,
},
Some(TrackerScrapeModel {
complete: Some(5),
downloaded: Some(8),
incomplete: Some(3),
files: Vec::new(),
}),
);
let dht = RoutedDhtTransport::new(
DhtMessageModel::get_peers_response(
b"gp".to_vec(),
vec![0x99; 20],
Some(b"announce-token".to_vec()),
Some(compact_node(0x77, [127, 0, 0, 31], 6882)),
Vec::new(),
),
DhtMessageModel::ping_response(b"ap".to_vec(), vec![0x55; 20]),
);
let connector = FakePeerWireConnector::new(peer_wire_handshake_and_frames(
[
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98,
],
*b"-PC0001-LOOP-PEER-01",
&[PeerWireMessageKind::Unchoke],
));
let report = dispatcher
.drive_bt_runtime_once(
&gid,
Some(&tracker),
Some(&dht),
Some(&connector),
Some(1_050),
)
.expect("coordinator loop should run");
assert!(report.initial_snapshot.metadata_exchange_pending);
assert!(report.final_snapshot.metadata_exchange_pending);
assert_eq!(
report
.steps
.iter()
.map(|step| (step.action, step.status))
.collect::<Vec<_>>(),
vec![
(
BtRuntimeCoordinatorAction::AdvanceClock,
BtRuntimeCoordinatorStepStatus::Executed,
),
(
BtRuntimeCoordinatorAction::TrackerAnnounce,
BtRuntimeCoordinatorStepStatus::Executed,
),
(
BtRuntimeCoordinatorAction::DhtGetPeers,
BtRuntimeCoordinatorStepStatus::Executed,
),
(
BtRuntimeCoordinatorAction::DhtAnnouncePeer,
BtRuntimeCoordinatorStepStatus::Executed,
),
(
BtRuntimeCoordinatorAction::PeerWireExchange,
BtRuntimeCoordinatorStepStatus::Executed,
),
]
);
assert_eq!(tracker.seen_announces().len(), 1);
assert_eq!(
tracker.seen_scrapes(),
vec!["http://tracker.example.org/announce".to_owned()]
);
assert_eq!(
dht.seen().len(),
2,
"get_peers plus announce_peer should run"
);
assert_eq!(
connector.seen().len(),
1,
"peer-wire should run after peers arrive"
);
let final_snapshot = dispatcher
.bt_runtime_coordinator_snapshot(&gid)
.expect("final snapshot should remain readable");
assert!(final_snapshot.has_dht_token);
assert!(final_snapshot.connectable_peer_count >= 1);
assert!(final_snapshot.addressable_dht_node_count >= 2);
}
#[doc(hidden)]
fn xml_request(method_name: &str) -> XmlRpcMethodCall {
XmlRpcMethodCall {
method_name: method_name.to_owned(),
params: Vec::new(),
meta: RpcMeta::default(),
}
}
#[doc(hidden)]
fn xml_request_with_params(method_name: &str, params: Vec<XmlRpcValue>) -> XmlRpcMethodCall {
XmlRpcMethodCall {
method_name: method_name.to_owned(),
params: params
.into_iter()
.map(|value| XmlRpcParam { value })
.collect(),
meta: RpcMeta::default(),
}
}
mod bt_and_extension;
mod protocol_surface;
mod queue_and_options;