#![expect( missing_docs, reason = "integration test scenarios are documented by case names rather than item-level rustdoc" )] use std::collections::BTreeMap; use aria2_rust_pro_cli as _; use aria2_rust_pro_compat as _; use aria2_rust_pro_core as _; use aria2_rust_pro_protocol::{ DhtMessageModel, DhtNodeModel, torrent::{PeerWireExtensionHandshakeModel, PeerWireMessageKind, PeerWireMetadataMessageModel}, }; use aria2_rust_pro_rpc::{InProcessRpcDispatcher, RpcMethod, RpcValue}; use aria2_rust_pro_storage as _; use aria2_rust_pro_tests as _; use criterion as _; #[path = "../test_support/support.rs"] mod support; fn tell_status(dispatcher: &mut InProcessRpcDispatcher, gid: &str) -> BTreeMap { support::rpc_result_object(dispatcher.dispatch_json(support::rpc_request( RpcMethod::Aria2TellStatus, vec![RpcValue::String(gid.to_owned())], ))) } fn get_files(dispatcher: &mut InProcessRpcDispatcher, gid: &str) -> Vec { support::rpc_result_array(dispatcher.dispatch_json(support::rpc_request( RpcMethod::Aria2GetFiles, vec![RpcValue::String(gid.to_owned())], ))) } fn first_file<'a>(files: &'a [RpcValue], context: &str) -> &'a BTreeMap { match files.first() { Some(RpcValue::Object(file)) => file, other => panic!("unexpected first file payload for {context}: {other:?}"), } } fn seed_peer_and_promote_metadata( dispatcher: &mut InProcessRpcDispatcher, gid: &str, info_hash_hex: &str, ) { dispatcher .apply_dht_get_peers_result( gid, &DhtNodeModel { node_id: String::new(), address: "203.0.113.77".to_owned(), port: 51413, }, &DhtMessageModel::get_peers_response( b"gp".to_vec(), vec![0x55; 20], Some(b"dht-token".to_vec()), None, vec![support::compact_peer([198, 51, 100, 42], 51415)], ), ) .expect("dht get_peers should seed a connectable peer"); let connector = support::FakePeerWireConnector::new(support::peer_wire_handshake_and_frames( support::decode_hex_20(info_hash_hex), *b"-AZ2060-META-PROMO01", &[ PeerWireMessageKind::Unchoke, PeerWireMessageKind::Extension( PeerWireExtensionHandshakeModel { extensions: BTreeMap::from([("ut_metadata".to_owned(), 3_u8)]), client_name: Some("libtorrent/2.0.11".to_owned()), metadata_size: Some( u32::try_from(support::BT_TORRENT_FIXTURE_BYTES.len()) .expect("fixture metadata length should fit u32"), ), request_queue: Some(64), } .to_peer_wire_message(), ), PeerWireMessageKind::Extension( PeerWireMetadataMessageModel::data( 0, u32::try_from(support::BT_TORRENT_FIXTURE_BYTES.len()) .expect("fixture metadata length should fit u32"), support::BT_TORRENT_FIXTURE_BYTES.to_vec(), ) .to_peer_wire_message(3), ), ], )); dispatcher .execute_peer_wire_exchange(gid, &connector) .expect("peer-wire metadata exchange should promote the magnet session"); } #[test] fn magnet_piece_observation_clears_metadata_only_and_pending_snapshot_flags() { let mut dispatcher = InProcessRpcDispatcher::new(); let reference_gid = support::add_torrent(&mut dispatcher); let reference_status = tell_status(&mut dispatcher, &reference_gid); let magnet_uri = support::rpc_string_field(&reference_status, "magnetUri"); let info_hash = support::rpc_string_field(&reference_status, "infoHash"); let gid = support::add_magnet(&mut dispatcher, &magnet_uri); let before = tell_status(&mut dispatcher, &gid); assert!(support::rpc_bool_field(&before, "metadataOnly")); assert_eq!(support::rpc_string_field(&before, "magnetUri"), magnet_uri); let before_snapshot = dispatcher .bt_runtime_coordinator_snapshot(&gid) .expect("snapshot should inspect metadata-only magnet state"); assert!(before_snapshot.metadata_only); assert!(before_snapshot.metadata_exchange_pending); seed_peer_and_promote_metadata(&mut dispatcher, &gid, &info_hash); let after = tell_status(&mut dispatcher, &gid); assert!(!support::rpc_bool_field(&after, "metadataOnly")); assert_eq!(support::rpc_string_field(&after, "magnetUri"), magnet_uri); let after_snapshot = dispatcher .bt_runtime_coordinator_snapshot(&gid) .expect("snapshot should inspect promoted magnet state"); assert!(!after_snapshot.metadata_only); assert!(!after_snapshot.metadata_exchange_pending); } #[test] fn promoted_magnet_session_should_match_reference_torrent_file_surface() { let mut dispatcher = InProcessRpcDispatcher::new(); let reference_gid = support::add_torrent(&mut dispatcher); let reference_status = tell_status(&mut dispatcher, &reference_gid); let reference_files = get_files(&mut dispatcher, &reference_gid); let reference_file = first_file(&reference_files, "reference torrent"); let magnet_uri = support::rpc_string_field(&reference_status, "magnetUri"); let info_hash = support::rpc_string_field(&reference_status, "infoHash"); let gid = support::add_magnet(&mut dispatcher, &magnet_uri); seed_peer_and_promote_metadata(&mut dispatcher, &gid, &info_hash); let promoted_status = tell_status(&mut dispatcher, &gid); assert!( !support::rpc_bool_field(&promoted_status, "metadataOnly"), "metadata promotion should leave metadata-only mode before comparing the public torrent surface" ); assert_eq!( support::rpc_u64_field(&promoted_status, "totalLength"), support::rpc_u64_field(&reference_status, "totalLength"), "promoted magnet sessions should hydrate the same totalLength visible on the equivalent .torrent bootstrap" ); let promoted_files = get_files(&mut dispatcher, &gid); let promoted_file = first_file(&promoted_files, "promoted magnet"); assert_eq!( promoted_file.get("path"), reference_file.get("path"), "promoted magnet sessions should expose the real torrent file path instead of a synthetic placeholder row" ); assert_eq!( promoted_file.get("length"), reference_file.get("length"), "promoted magnet sessions should expose the same per-file length as the equivalent .torrent bootstrap" ); }