From ea92ceb61c1d765e2ba882186da97b433996b971 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 25 Nov 2022 17:58:50 +0000 Subject: [PATCH 1/8] test: [#61] add e2e test to API torrent info endpoint before refactoring --- src/api/resources/mod.rs | 1 + src/api/resources/torrent_resource.rs | 26 ++++++++++ tests/api.rs | 75 ++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/api/resources/torrent_resource.rs diff --git a/src/api/resources/mod.rs b/src/api/resources/mod.rs index 4b4f2214c..a229539dd 100644 --- a/src/api/resources/mod.rs +++ b/src/api/resources/mod.rs @@ -7,3 +7,4 @@ //! - [ ] ... //! - [ ] ... pub mod auth_key_resource; +pub mod torrent_resource; diff --git a/src/api/resources/torrent_resource.rs b/src/api/resources/torrent_resource.rs new file mode 100644 index 000000000..c9f6a1451 --- /dev/null +++ b/src/api/resources/torrent_resource.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, PartialEq)] +pub struct TorrentResource { + pub info_hash: String, + pub completed: u32, + pub leechers: u32, + pub peers: Vec, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct TorrentPeerResource { + pub peer_id: PeerIdResource, + pub peer_addr: String, + pub updated: i64, + pub uploaded: i64, + pub downloaded: i64, + pub left: i64, + pub event: String, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct PeerIdResource { + pub id: String, + pub client: String, +} diff --git a/tests/api.rs b/tests/api.rs index 278f9d4fb..2a0ded24a 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -8,17 +8,22 @@ mod common; mod tracker_api { use core::panic; use std::env; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; + use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; use tokio::task::JoinHandle; use torrust_tracker::api::resources::auth_key_resource::AuthKeyResource; + use torrust_tracker::api::resources::torrent_resource::{PeerIdResource, TorrentPeerResource, TorrentResource}; use torrust_tracker::jobs::tracker_api; + use torrust_tracker::peer::TorrentPeer; + use torrust_tracker::protocol::clock::DurationSinceUnixEpoch; use torrust_tracker::tracker::key::AuthKey; use torrust_tracker::tracker::statistics::StatsTracker; use torrust_tracker::tracker::TorrentTracker; - use torrust_tracker::{ephemeral_instance_keys, logging, static_time, Configuration, InfoHash}; + use torrust_tracker::{ephemeral_instance_keys, logging, static_time, Configuration, InfoHash, PeerId}; use crate::common::ephemeral_random_port; @@ -87,6 +92,74 @@ mod tracker_api { assert_eq!(res.status(), 200); } + fn sample_torrent_peer() -> (TorrentPeer, TorrentPeerResource) { + ( + TorrentPeer { + peer_id: PeerId(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1669397478934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + }, + TorrentPeerResource { + peer_id: PeerIdResource { + id: "2d71423030303030303030303030303030303030".to_string(), + client: "qBittorrent".to_string(), + }, + peer_addr: "126.0.0.1:8080".to_string(), + updated: 1669397478934000i64, + uploaded: 0i64, + downloaded: 0i64, + left: 0i64, + event: "Started".to_string(), + }, + ) + } + + #[tokio::test] + async fn should_allow_getting_a_torrent_info() { + let configuration = tracker_configuration(); + let api_server = new_running_api_server(configuration.clone()).await; + + let bind_address = api_server.bind_address.unwrap().clone(); + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); + + let (peer, peer_resource) = sample_torrent_peer(); + + // Add the torrent to the tracker + api_server + .tracker + .unwrap() + .update_torrent_with_peer_and_get_stats(&info_hash, &peer) + .await; + + let url = format!("http://{}/api/torrent/{}?token={}", &bind_address, &info_hash, &api_token); + + let torrent_resource = reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + assert_eq!( + torrent_resource, + TorrentResource { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + completed: 0, + leechers: 0, + peers: vec![peer_resource] + } + ); + } + fn tracker_configuration() -> Arc { let mut config = Configuration::default(); config.log_level = Some("off".to_owned()); From 801dfe6d8df0292f5e9afe25ebcf265d53c0834d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 25 Nov 2022 19:27:13 +0000 Subject: [PATCH 2/8] refactor: [#61] use TorrentResource in torrent info API endpoint --- src/api/resources/torrent_resource.rs | 29 +++++++++++++++++++-------- src/api/server.rs | 21 ++++++++++++++++--- src/protocol/common.rs | 17 +++++++++------- tests/api.rs | 15 +++++++------- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/api/resources/torrent_resource.rs b/src/api/resources/torrent_resource.rs index c9f6a1451..3c59852e1 100644 --- a/src/api/resources/torrent_resource.rs +++ b/src/api/resources/torrent_resource.rs @@ -1,26 +1,39 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, PartialEq)] +use crate::PeerId; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct TorrentResource { pub info_hash: String, + pub seeders: u32, pub completed: u32, pub leechers: u32, - pub peers: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub peers: Option>, } -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct TorrentPeerResource { pub peer_id: PeerIdResource, pub peer_addr: String, - pub updated: i64, + pub updated: u128, pub uploaded: i64, pub downloaded: i64, pub left: i64, pub event: String, } -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct PeerIdResource { - pub id: String, - pub client: String, + pub id: Option, + pub client: Option, +} + +impl From for PeerIdResource { + fn from(peer_id: PeerId) -> Self { + PeerIdResource { + id: peer_id.get_id(), + client: peer_id.get_client_name().map(|client_name| client_name.to_string()), + } + } } diff --git a/src/api/server.rs b/src/api/server.rs index 9f215710e..06e2af251 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use warp::{filters, reply, serve, Filter}; use super::resources::auth_key_resource::AuthKeyResource; +use super::resources::torrent_resource::{PeerIdResource, TorrentPeerResource, TorrentResource}; use crate::peer::TorrentPeer; use crate::protocol::common::*; use crate::tracker::TorrentTracker; @@ -109,6 +110,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp .iter() .map(|(info_hash, torrent_entry)| { let (seeders, completed, leechers) = torrent_entry.get_stats(); + // todo: use TorrentResource Torrent { info_hash, seeders, @@ -206,12 +208,25 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp let peers = torrent_entry.get_peers(None); - Ok(reply::json(&Torrent { - info_hash: &info_hash, + let peer_resources = peers + .iter() + .map(|peer| TorrentPeerResource { + peer_id: PeerIdResource::from(peer.peer_id.clone()), + peer_addr: peer.peer_addr.to_string(), + updated: peer.updated.as_millis(), + uploaded: peer.uploaded.0, + downloaded: peer.downloaded.0, + left: peer.left.0, + event: format!("{:?}", peer.event), + }) + .collect(); + + Ok(reply::json(&TorrentResource { + info_hash: info_hash.to_string(), seeders, completed, leechers, - peers: Some(peers), + peers: Some(peer_resources), })) }); diff --git a/src/protocol/common.rs b/src/protocol/common.rs index 431521764..da6d95e40 100644 --- a/src/protocol/common.rs +++ b/src/protocol/common.rs @@ -232,6 +232,14 @@ impl std::fmt::Display for PeerId { } impl PeerId { + pub fn get_id(&self) -> Option { + let buff_size = self.0.len() * 2; + let mut tmp: Vec = vec![0; buff_size]; + binascii::bin2hex(&self.0, &mut tmp).unwrap(); + + std::str::from_utf8(&tmp).ok().map(|id| id.to_string()) + } + pub fn get_client_name(&self) -> Option<&'static str> { if self.0[0] == b'M' { return Some("BitTorrent"); @@ -316,19 +324,14 @@ impl Serialize for PeerId { where S: serde::Serializer, { - let buff_size = self.0.len() * 2; - let mut tmp: Vec = vec![0; buff_size]; - binascii::bin2hex(&self.0, &mut tmp).unwrap(); - let id = std::str::from_utf8(&tmp).ok(); - #[derive(Serialize)] struct PeerIdInfo<'a> { - id: Option<&'a str>, + id: Option, client: Option<&'a str>, } let obj = PeerIdInfo { - id, + id: self.get_id(), client: self.get_client_name(), }; obj.serialize(serializer) diff --git a/tests/api.rs b/tests/api.rs index 2a0ded24a..a5606b0a9 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -105,14 +105,14 @@ mod tracker_api { }, TorrentPeerResource { peer_id: PeerIdResource { - id: "2d71423030303030303030303030303030303030".to_string(), - client: "qBittorrent".to_string(), + id: Some("2d71423030303030303030303030303030303030".to_string()), + client: Some("qBittorrent".to_string()), }, peer_addr: "126.0.0.1:8080".to_string(), - updated: 1669397478934000i64, - uploaded: 0i64, - downloaded: 0i64, - left: 0i64, + updated: 1669397478934000, + uploaded: 0, + downloaded: 0, + left: 0, event: "Started".to_string(), }, ) @@ -153,9 +153,10 @@ mod tracker_api { torrent_resource, TorrentResource { info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, completed: 0, leechers: 0, - peers: vec![peer_resource] + peers: Some(vec![peer_resource]) } ); } From 7298701f5d92d854139eedd296606dbc78f5e080 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 13:41:27 +0000 Subject: [PATCH 3/8] refactor: [#61] extract converter from TorrentPeer to TorrentPeerResource --- src/api/resources/torrent_resource.rs | 15 ++++++++++++ src/api/server.rs | 15 ++---------- src/protocol/common.rs | 2 +- src/tracker/peer.rs | 2 +- tests/api.rs | 35 +++++++++------------------ 5 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/api/resources/torrent_resource.rs b/src/api/resources/torrent_resource.rs index 3c59852e1..ecf2a3fda 100644 --- a/src/api/resources/torrent_resource.rs +++ b/src/api/resources/torrent_resource.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use crate::peer::TorrentPeer; use crate::PeerId; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -37,3 +38,17 @@ impl From for PeerIdResource { } } } + +impl From for TorrentPeerResource { + fn from(peer: TorrentPeer) -> Self { + TorrentPeerResource { + peer_id: PeerIdResource::from(peer.peer_id), + peer_addr: peer.peer_addr.to_string(), + updated: peer.updated.as_millis(), + uploaded: peer.uploaded.0, + downloaded: peer.downloaded.0, + left: peer.left.0, + event: format!("{:?}", peer.event), + } + } +} diff --git a/src/api/server.rs b/src/api/server.rs index 06e2af251..85c177b8b 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use warp::{filters, reply, serve, Filter}; use super::resources::auth_key_resource::AuthKeyResource; -use super::resources::torrent_resource::{PeerIdResource, TorrentPeerResource, TorrentResource}; +use super::resources::torrent_resource::{TorrentPeerResource, TorrentResource}; use crate::peer::TorrentPeer; use crate::protocol::common::*; use crate::tracker::TorrentTracker; @@ -208,18 +208,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp let peers = torrent_entry.get_peers(None); - let peer_resources = peers - .iter() - .map(|peer| TorrentPeerResource { - peer_id: PeerIdResource::from(peer.peer_id.clone()), - peer_addr: peer.peer_addr.to_string(), - updated: peer.updated.as_millis(), - uploaded: peer.uploaded.0, - downloaded: peer.downloaded.0, - left: peer.left.0, - event: format!("{:?}", peer.event), - }) - .collect(); + let peer_resources = peers.iter().map(|peer| TorrentPeerResource::from(**peer)).collect(); Ok(reply::json(&TorrentResource { info_hash: info_hash.to_string(), diff --git a/src/protocol/common.rs b/src/protocol/common.rs index da6d95e40..ce1cbf253 100644 --- a/src/protocol/common.rs +++ b/src/protocol/common.rs @@ -217,7 +217,7 @@ impl<'v> serde::de::Visitor<'v> for InfoHashVisitor { } } -#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord)] +#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)] pub struct PeerId(pub [u8; 20]); impl std::fmt::Display for PeerId { diff --git a/src/tracker/peer.rs b/src/tracker/peer.rs index 7a2599f82..42ef6a60b 100644 --- a/src/tracker/peer.rs +++ b/src/tracker/peer.rs @@ -9,7 +9,7 @@ use crate::protocol::clock::{DefaultClock, DurationSinceUnixEpoch, Time}; use crate::protocol::common::{AnnounceEventDef, NumberOfBytesDef, PeerId}; use crate::protocol::utils::ser_unix_time_value; -#[derive(PartialEq, Eq, Debug, Clone, Serialize)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Copy)] pub struct TorrentPeer { pub peer_id: PeerId, pub peer_addr: SocketAddr, diff --git a/tests/api.rs b/tests/api.rs index a5606b0a9..0f6214ffb 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -93,29 +93,18 @@ mod tracker_api { } fn sample_torrent_peer() -> (TorrentPeer, TorrentPeerResource) { - ( - TorrentPeer { - peer_id: PeerId(*b"-qB00000000000000000"), - peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), - updated: DurationSinceUnixEpoch::new(1669397478934, 0), - uploaded: NumberOfBytes(0), - downloaded: NumberOfBytes(0), - left: NumberOfBytes(0), - event: AnnounceEvent::Started, - }, - TorrentPeerResource { - peer_id: PeerIdResource { - id: Some("2d71423030303030303030303030303030303030".to_string()), - client: Some("qBittorrent".to_string()), - }, - peer_addr: "126.0.0.1:8080".to_string(), - updated: 1669397478934000, - uploaded: 0, - downloaded: 0, - left: 0, - event: "Started".to_string(), - }, - ) + let torrent_peer = TorrentPeer { + peer_id: PeerId(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1669397478934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + }; + let torrent_peer_resource = TorrentPeerResource::from(torrent_peer); + + (torrent_peer, torrent_peer_resource) } #[tokio::test] From 284c91be299e814c7df6ac33ec050e69817085e8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 15:45:10 +0000 Subject: [PATCH 4/8] test: [#61] add e2e test for torrent list API endpoint --- src/tracker/torrent.rs | 4 ++-- tests/api.rs | 45 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/tracker/torrent.rs b/src/tracker/torrent.rs index 4e602d359..335554006 100644 --- a/src/tracker/torrent.rs +++ b/src/tracker/torrent.rs @@ -32,7 +32,7 @@ impl TorrentEntry { let _ = self.peers.remove(&peer.peer_id); } AnnounceEvent::Completed => { - let peer_old = self.peers.insert(peer.peer_id.clone(), peer.clone()); + let peer_old = self.peers.insert(peer.peer_id, *peer); // Don't count if peer was not previously known if peer_old.is_some() { self.completed += 1; @@ -40,7 +40,7 @@ impl TorrentEntry { } } _ => { - let _ = self.peers.insert(peer.peer_id.clone(), peer.clone()); + let _ = self.peers.insert(peer.peer_id, *peer); } } diff --git a/tests/api.rs b/tests/api.rs index 0f6214ffb..ce419724a 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -16,7 +16,7 @@ mod tracker_api { use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; use tokio::task::JoinHandle; use torrust_tracker::api::resources::auth_key_resource::AuthKeyResource; - use torrust_tracker::api::resources::torrent_resource::{PeerIdResource, TorrentPeerResource, TorrentResource}; + use torrust_tracker::api::resources::torrent_resource::{TorrentPeerResource, TorrentResource}; use torrust_tracker::jobs::tracker_api; use torrust_tracker::peer::TorrentPeer; use torrust_tracker::protocol::clock::DurationSinceUnixEpoch; @@ -150,6 +150,49 @@ mod tracker_api { ); } + #[tokio::test] + async fn should_allow_getting_torrents() { + let configuration = tracker_configuration(); + let api_server = new_running_api_server(configuration.clone()).await; + + let bind_address = api_server.bind_address.unwrap().clone(); + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); + + let (peer, _peer_resource) = sample_torrent_peer(); + + // Add the torrent to the tracker + api_server + .tracker + .unwrap() + .update_torrent_with_peer_and_get_stats(&info_hash, &peer) + .await; + + let url = format!("http://{}/api/torrents?token={}", &bind_address, &api_token); + + let torrent_resources = reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::>() + .await + .unwrap(); + + assert_eq!( + torrent_resources, + vec![TorrentResource { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: None // Torrent list does not include peer list + }] + ); + } + fn tracker_configuration() -> Arc { let mut config = Configuration::default(); config.log_level = Some("off".to_owned()); From b974ce0eba7614bbe1ce79b03a73ab20143b75f6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 16:05:41 +0000 Subject: [PATCH 5/8] refactor: [#61] use TorrentListItemResource in torrent list API endpoint --- src/api/resources/mod.rs | 4 ++-- src/api/resources/torrent_resource.rs | 10 ++++++++++ src/api/server.rs | 18 +++--------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/api/resources/mod.rs b/src/api/resources/mod.rs index a229539dd..e139207b5 100644 --- a/src/api/resources/mod.rs +++ b/src/api/resources/mod.rs @@ -3,8 +3,8 @@ //! WIP. Not all endpoints have their resource structs. //! //! - [x] AuthKeys -//! - [ ] ... -//! - [ ] ... +//! - [ ] TorrentResource, TorrentListItemResource, TorrentPeerResource, PeerIdResource +//! - [ ] StatsResource //! - [ ] ... pub mod auth_key_resource; pub mod torrent_resource; diff --git a/src/api/resources/torrent_resource.rs b/src/api/resources/torrent_resource.rs index ecf2a3fda..88d0463cb 100644 --- a/src/api/resources/torrent_resource.rs +++ b/src/api/resources/torrent_resource.rs @@ -13,6 +13,16 @@ pub struct TorrentResource { pub peers: Option>, } +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct TorrentListItemResource { + pub info_hash: String, + pub seeders: u32, + pub completed: u32, + pub leechers: u32, + // todo: this is always None. Remove field from endpoint? + pub peers: Option>, +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct TorrentPeerResource { pub peer_id: PeerIdResource, diff --git a/src/api/server.rs b/src/api/server.rs index 85c177b8b..ef514749b 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -8,8 +8,7 @@ use serde::{Deserialize, Serialize}; use warp::{filters, reply, serve, Filter}; use super::resources::auth_key_resource::AuthKeyResource; -use super::resources::torrent_resource::{TorrentPeerResource, TorrentResource}; -use crate::peer::TorrentPeer; +use super::resources::torrent_resource::{TorrentListItemResource, TorrentPeerResource, TorrentResource}; use crate::protocol::common::*; use crate::tracker::TorrentTracker; @@ -19,16 +18,6 @@ struct TorrentInfoQuery { limit: Option, } -#[derive(Serialize)] -struct Torrent<'a> { - info_hash: &'a InfoHash, - seeders: u32, - completed: u32, - leechers: u32, - #[serde(skip_serializing_if = "Option::is_none")] - peers: Option>, -} - #[derive(Serialize)] struct Stats { torrents: u32, @@ -110,9 +99,8 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp .iter() .map(|(info_hash, torrent_entry)| { let (seeders, completed, leechers) = torrent_entry.get_stats(); - // todo: use TorrentResource - Torrent { - info_hash, + TorrentListItemResource { + info_hash: info_hash.to_string(), seeders, completed, leechers, From 7e03714ef49076ac562ca9fc8179dd7495534e82 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 16:29:40 +0000 Subject: [PATCH 6/8] refactor: [#61] use StatsResource in API stats endpoint --- src/api/resources/mod.rs | 1 + src/api/resources/stats_resource.rs | 21 ++++++++++ src/api/server.rs | 23 +---------- tests/api.rs | 59 ++++++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 src/api/resources/stats_resource.rs diff --git a/src/api/resources/mod.rs b/src/api/resources/mod.rs index e139207b5..d214d8a59 100644 --- a/src/api/resources/mod.rs +++ b/src/api/resources/mod.rs @@ -7,4 +7,5 @@ //! - [ ] StatsResource //! - [ ] ... pub mod auth_key_resource; +pub mod stats_resource; pub mod torrent_resource; diff --git a/src/api/resources/stats_resource.rs b/src/api/resources/stats_resource.rs new file mode 100644 index 000000000..7fc9f1376 --- /dev/null +++ b/src/api/resources/stats_resource.rs @@ -0,0 +1,21 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct StatsResource { + pub torrents: u32, + pub seeders: u32, + pub completed: u32, + pub leechers: u32, + pub tcp4_connections_handled: u32, + pub tcp4_announces_handled: u32, + pub tcp4_scrapes_handled: u32, + pub tcp6_connections_handled: u32, + pub tcp6_announces_handled: u32, + pub tcp6_scrapes_handled: u32, + pub udp4_connections_handled: u32, + pub udp4_announces_handled: u32, + pub udp4_scrapes_handled: u32, + pub udp6_connections_handled: u32, + pub udp6_announces_handled: u32, + pub udp6_scrapes_handled: u32, +} diff --git a/src/api/server.rs b/src/api/server.rs index ef514749b..41e6f7074 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use warp::{filters, reply, serve, Filter}; use super::resources::auth_key_resource::AuthKeyResource; +use super::resources::stats_resource::StatsResource; use super::resources::torrent_resource::{TorrentListItemResource, TorrentPeerResource, TorrentResource}; use crate::protocol::common::*; use crate::tracker::TorrentTracker; @@ -18,26 +19,6 @@ struct TorrentInfoQuery { limit: Option, } -#[derive(Serialize)] -struct Stats { - torrents: u32, - seeders: u32, - completed: u32, - leechers: u32, - tcp4_connections_handled: u32, - tcp4_announces_handled: u32, - tcp4_scrapes_handled: u32, - tcp6_connections_handled: u32, - tcp6_announces_handled: u32, - tcp6_scrapes_handled: u32, - udp4_connections_handled: u32, - udp4_announces_handled: u32, - udp4_scrapes_handled: u32, - udp6_connections_handled: u32, - udp6_announces_handled: u32, - udp6_scrapes_handled: u32, -} - #[derive(Serialize, Debug)] #[serde(tag = "status", rename_all = "snake_case")] enum ActionStatus<'a> { @@ -122,7 +103,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp .and(filters::path::end()) .map(move || api_stats.clone()) .and_then(|tracker: Arc| async move { - let mut results = Stats { + let mut results = StatsResource { torrents: 0, seeders: 0, completed: 0, diff --git a/tests/api.rs b/tests/api.rs index ce419724a..37cdd5415 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -16,6 +16,7 @@ mod tracker_api { use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; use tokio::task::JoinHandle; use torrust_tracker::api::resources::auth_key_resource::AuthKeyResource; + use torrust_tracker::api::resources::stats_resource::StatsResource; use torrust_tracker::api::resources::torrent_resource::{TorrentPeerResource, TorrentResource}; use torrust_tracker::jobs::tracker_api; use torrust_tracker::peer::TorrentPeer; @@ -118,7 +119,7 @@ mod tracker_api { let (peer, peer_resource) = sample_torrent_peer(); - // Add the torrent to the tracker + // Add a torrent to the tracker api_server .tracker .unwrap() @@ -161,7 +162,7 @@ mod tracker_api { let (peer, _peer_resource) = sample_torrent_peer(); - // Add the torrent to the tracker + // Add a torrent to the tracker api_server .tracker .unwrap() @@ -193,6 +194,60 @@ mod tracker_api { ); } + #[tokio::test] + async fn should_allow_getting_tracker_statistics() { + let configuration = tracker_configuration(); + let api_server = new_running_api_server(configuration.clone()).await; + + let bind_address = api_server.bind_address.unwrap().clone(); + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); + + let (peer, _peer_resource) = sample_torrent_peer(); + + // Add a torrent to the tracker + api_server + .tracker + .unwrap() + .update_torrent_with_peer_and_get_stats(&info_hash, &peer) + .await; + + let url = format!("http://{}/api/stats?token={}", &bind_address, &api_token); + + let stats = reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + assert_eq!( + stats, + StatsResource { + torrents: 1, + seeders: 1, + completed: 0, + leechers: 0, + tcp4_connections_handled: 0, + tcp4_announces_handled: 0, + tcp4_scrapes_handled: 0, + tcp6_connections_handled: 0, + tcp6_announces_handled: 0, + tcp6_scrapes_handled: 0, + udp4_connections_handled: 0, + udp4_announces_handled: 0, + udp4_scrapes_handled: 0, + udp6_connections_handled: 0, + udp6_announces_handled: 0, + udp6_scrapes_handled: 0, + } + ); + } + fn tracker_configuration() -> Arc { let mut config = Configuration::default(); config.log_level = Some("off".to_owned()); From bc3d246fc4b5d3ed11b0831abd3dffe722a8dad0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 16:46:16 +0000 Subject: [PATCH 7/8] feat(api): in torrent endpoint rename field to Marked as deprecated. It will be a breaking change in version v3.0.0. --- src/api/resources/stats_resource.rs | 2 +- src/api/resources/torrent_resource.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/resources/stats_resource.rs b/src/api/resources/stats_resource.rs index 7fc9f1376..2fbaf42c1 100644 --- a/src/api/resources/stats_resource.rs +++ b/src/api/resources/stats_resource.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct StatsResource { diff --git a/src/api/resources/torrent_resource.rs b/src/api/resources/torrent_resource.rs index 88d0463cb..11e9d7196 100644 --- a/src/api/resources/torrent_resource.rs +++ b/src/api/resources/torrent_resource.rs @@ -27,7 +27,9 @@ pub struct TorrentListItemResource { pub struct TorrentPeerResource { pub peer_id: PeerIdResource, pub peer_addr: String, + #[deprecated(since = "2.0.0", note = "please use `updated_milliseconds_ago` instead")] pub updated: u128, + pub updated_milliseconds_ago: u128, pub uploaded: i64, pub downloaded: i64, pub left: i64, @@ -55,6 +57,7 @@ impl From for TorrentPeerResource { peer_id: PeerIdResource::from(peer.peer_id), peer_addr: peer.peer_addr.to_string(), updated: peer.updated.as_millis(), + updated_milliseconds_ago: peer.updated.as_millis(), uploaded: peer.uploaded.0, downloaded: peer.downloaded.0, left: peer.left.0, From e1b84f6eb75dcd9ba0cc6803a0f7221d9b761ef8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 28 Nov 2022 19:37:09 +0000 Subject: [PATCH 8/8] refactor: [#61] extract struct ApiClient for API testing --- tests/api.rs | 251 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 154 insertions(+), 97 deletions(-) diff --git a/tests/api.rs b/tests/api.rs index 37cdd5415..475da9a24 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -14,10 +14,11 @@ mod tracker_api { use std::sync::Arc; use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + use reqwest::Response; use tokio::task::JoinHandle; use torrust_tracker::api::resources::auth_key_resource::AuthKeyResource; use torrust_tracker::api::resources::stats_resource::StatsResource; - use torrust_tracker::api::resources::torrent_resource::{TorrentPeerResource, TorrentResource}; + use torrust_tracker::api::resources::torrent_resource::{TorrentListItemResource, TorrentPeerResource, TorrentResource}; use torrust_tracker::jobs::tracker_api; use torrust_tracker::peer::TorrentPeer; use torrust_tracker::protocol::clock::DurationSinceUnixEpoch; @@ -30,16 +31,13 @@ mod tracker_api { #[tokio::test] async fn should_allow_generating_a_new_auth_key() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; - let bind_address = api_server.bind_address.unwrap().clone(); let seconds_valid = 60; - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); - let url = format!("http://{}/api/key/{}?token={}", &bind_address, &seconds_valid, &api_token); - - let auth_key: AuthKeyResource = reqwest::Client::new().post(url).send().await.unwrap().json().await.unwrap(); + let auth_key = ApiClient::new(api_server.get_connection_info().unwrap()) + .generate_auth_key(seconds_valid) + .await; // Verify the key with the tracker assert!(api_server @@ -52,16 +50,13 @@ mod tracker_api { #[tokio::test] async fn should_allow_whitelisting_a_torrent() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; - let bind_address = api_server.bind_address.unwrap().clone(); - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let url = format!("http://{}/api/whitelist/{}?token={}", &bind_address, &info_hash, &api_token); - - let res = reqwest::Client::new().post(url.clone()).send().await.unwrap(); + let res = ApiClient::new(api_server.get_connection_info().unwrap()) + .whitelist_a_torrent(&info_hash) + .await; assert_eq!(res.status(), 200); assert!( @@ -75,47 +70,25 @@ mod tracker_api { #[tokio::test] async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; - let bind_address = api_server.bind_address.unwrap().clone(); - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let url = format!("http://{}/api/whitelist/{}?token={}", &bind_address, &info_hash, &api_token); + let api_client = ApiClient::new(api_server.get_connection_info().unwrap()); - // First whitelist request - let res = reqwest::Client::new().post(url.clone()).send().await.unwrap(); + let res = api_client.whitelist_a_torrent(&info_hash).await; assert_eq!(res.status(), 200); - // Second whitelist request - let res = reqwest::Client::new().post(url.clone()).send().await.unwrap(); + let res = api_client.whitelist_a_torrent(&info_hash).await; assert_eq!(res.status(), 200); } - fn sample_torrent_peer() -> (TorrentPeer, TorrentPeerResource) { - let torrent_peer = TorrentPeer { - peer_id: PeerId(*b"-qB00000000000000000"), - peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), - updated: DurationSinceUnixEpoch::new(1669397478934, 0), - uploaded: NumberOfBytes(0), - downloaded: NumberOfBytes(0), - left: NumberOfBytes(0), - event: AnnounceEvent::Started, - }; - let torrent_peer_resource = TorrentPeerResource::from(torrent_peer); - - (torrent_peer, torrent_peer_resource) - } - #[tokio::test] async fn should_allow_getting_a_torrent_info() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; + let api_connection_info = api_server.get_connection_info().unwrap(); - let bind_address = api_server.bind_address.unwrap().clone(); let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); let (peer, peer_resource) = sample_torrent_peer(); @@ -126,18 +99,7 @@ mod tracker_api { .update_torrent_with_peer_and_get_stats(&info_hash, &peer) .await; - let url = format!("http://{}/api/torrent/{}?token={}", &bind_address, &info_hash, &api_token); - - let torrent_resource = reqwest::Client::builder() - .build() - .unwrap() - .get(url) - .send() - .await - .unwrap() - .json::() - .await - .unwrap(); + let torrent_resource = ApiClient::new(api_connection_info).get_torrent(&info_hash.to_string()).await; assert_eq!( torrent_resource, @@ -153,15 +115,14 @@ mod tracker_api { #[tokio::test] async fn should_allow_getting_torrents() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; - let bind_address = api_server.bind_address.unwrap().clone(); let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); let (peer, _peer_resource) = sample_torrent_peer(); + let api_connection_info = api_server.get_connection_info().unwrap(); + // Add a torrent to the tracker api_server .tracker @@ -169,22 +130,11 @@ mod tracker_api { .update_torrent_with_peer_and_get_stats(&info_hash, &peer) .await; - let url = format!("http://{}/api/torrents?token={}", &bind_address, &api_token); - - let torrent_resources = reqwest::Client::builder() - .build() - .unwrap() - .get(url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap(); + let torrent_resources = ApiClient::new(api_connection_info).get_torrents().await; assert_eq!( torrent_resources, - vec![TorrentResource { + vec![TorrentListItemResource { info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), seeders: 1, completed: 0, @@ -196,15 +146,14 @@ mod tracker_api { #[tokio::test] async fn should_allow_getting_tracker_statistics() { - let configuration = tracker_configuration(); - let api_server = new_running_api_server(configuration.clone()).await; + let api_server = ApiServer::new_running_instance().await; - let bind_address = api_server.bind_address.unwrap().clone(); let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); let (peer, _peer_resource) = sample_torrent_peer(); + let api_connection_info = api_server.get_connection_info().unwrap(); + // Add a torrent to the tracker api_server .tracker @@ -212,21 +161,10 @@ mod tracker_api { .update_torrent_with_peer_and_get_stats(&info_hash, &peer) .await; - let url = format!("http://{}/api/stats?token={}", &bind_address, &api_token); - - let stats = reqwest::Client::builder() - .build() - .unwrap() - .get(url) - .send() - .await - .unwrap() - .json::() - .await - .unwrap(); + let stats_resource = ApiClient::new(api_connection_info).get_tracker_statistics().await; assert_eq!( - stats, + stats_resource, StatsResource { torrents: 1, seeders: 1, @@ -248,6 +186,21 @@ mod tracker_api { ); } + fn sample_torrent_peer() -> (TorrentPeer, TorrentPeerResource) { + let torrent_peer = TorrentPeer { + peer_id: PeerId(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1669397478934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + }; + let torrent_peer_resource = TorrentPeerResource::from(torrent_peer); + + (torrent_peer, torrent_peer_resource) + } + fn tracker_configuration() -> Arc { let mut config = Configuration::default(); config.log_level = Some("off".to_owned()); @@ -264,17 +217,26 @@ mod tracker_api { Arc::new(config) } - async fn new_running_api_server(configuration: Arc) -> ApiServer { - let mut api_server = ApiServer::new(); - api_server.start(configuration).await; - api_server + #[derive(Clone)] + struct ApiConnectionInfo { + pub bind_address: String, + pub api_token: String, + } + + impl ApiConnectionInfo { + pub fn new(bind_address: &str, api_token: &str) -> Self { + Self { + bind_address: bind_address.to_string(), + api_token: api_token.to_string(), + } + } } - pub struct ApiServer { + struct ApiServer { pub started: AtomicBool, pub job: Option>, - pub bind_address: Option, pub tracker: Option>, + pub connection_info: Option, } impl ApiServer { @@ -282,14 +244,28 @@ mod tracker_api { Self { started: AtomicBool::new(false), job: None, - bind_address: None, tracker: None, + connection_info: None, } } + pub async fn new_running_instance() -> ApiServer { + let configuration = tracker_configuration(); + ApiServer::new_running_custom_instance(configuration.clone()).await + } + + async fn new_running_custom_instance(configuration: Arc) -> ApiServer { + let mut api_server = ApiServer::new(); + api_server.start(configuration).await; + api_server + } + pub async fn start(&mut self, configuration: Arc) { if !self.started.load(Ordering::Relaxed) { - self.bind_address = Some(configuration.http_api.bind_address.clone()); + self.connection_info = Some(ApiConnectionInfo::new( + &configuration.http_api.bind_address.clone(), + &configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(), + )); // Set the time of Torrust app starting lazy_static::initialize(&static_time::TIME_AT_APP_START); @@ -318,5 +294,86 @@ mod tracker_api { self.started.store(true, Ordering::Relaxed); } } + + pub fn get_connection_info(&self) -> Option { + self.connection_info.clone() + } + } + + struct ApiClient { + connection_info: ApiConnectionInfo, + } + + impl ApiClient { + pub fn new(connection_info: ApiConnectionInfo) -> Self { + Self { connection_info } + } + + pub async fn generate_auth_key(&self, seconds_valid: i32) -> AuthKeyResource { + let url = format!( + "http://{}/api/key/{}?token={}", + &self.connection_info.bind_address, &seconds_valid, &self.connection_info.api_token + ); + reqwest::Client::new().post(url).send().await.unwrap().json().await.unwrap() + } + + pub async fn whitelist_a_torrent(&self, info_hash: &str) -> Response { + let url = format!( + "http://{}/api/whitelist/{}?token={}", + &self.connection_info.bind_address, &info_hash, &self.connection_info.api_token + ); + reqwest::Client::new().post(url.clone()).send().await.unwrap() + } + + pub async fn get_torrent(&self, info_hash: &str) -> TorrentResource { + let url = format!( + "http://{}/api/torrent/{}?token={}", + &self.connection_info.bind_address, &info_hash, &self.connection_info.api_token + ); + reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + } + + pub async fn get_torrents(&self) -> Vec { + let url = format!( + "http://{}/api/torrents?token={}", + &self.connection_info.bind_address, &self.connection_info.api_token + ); + reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::>() + .await + .unwrap() + } + + pub async fn get_tracker_statistics(&self) -> StatsResource { + let url = format!( + "http://{}/api/stats?token={}", + &self.connection_info.bind_address, &self.connection_info.api_token + ); + reqwest::Client::builder() + .build() + .unwrap() + .get(url) + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + } } }