From 1ba7f9fbda67fe74403b5157a108d845c9916162 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 20 Jan 2023 19:30:51 +0000 Subject: [PATCH] test(http): [#159] add tests for public http tracker --- src/http/mod.rs | 4 ++ src/http/response.rs | 7 +- tests/api/client.rs | 16 ++++- tests/api/mod.rs | 1 - tests/{api => common}/fixtures.rs | 0 tests/common/http.rs | 16 ----- tests/common/mod.rs | 1 + tests/http/asserts.rs | 8 +++ tests/http/client.rs | 29 ++++----- tests/http/mod.rs | 2 + tests/http/requests.rs | 104 ++++++++++++++++++++++++++++++ tests/http/responses.rs | 18 ++++++ tests/http/server.rs | 6 ++ tests/http_tracker.rs | 102 +++++++++++++++++++++++++++-- tests/tracker_api.rs | 4 +- 15 files changed, 272 insertions(+), 46 deletions(-) rename tests/{api => common}/fixtures.rs (100%) create mode 100644 tests/http/requests.rs create mode 100644 tests/http/responses.rs diff --git a/src/http/mod.rs b/src/http/mod.rs index 701dba407..0b5a02a0e 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,3 +1,7 @@ +//! Tracker HTTP/HTTPS Protocol: +//! +//! +//! pub mod error; pub mod filters; pub mod handlers; diff --git a/src/http/response.rs b/src/http/response.rs index 962e72fac..1e9f7fa09 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -2,19 +2,18 @@ use std::collections::HashMap; use std::io::Write; use std::net::IpAddr; -use serde; -use serde::Serialize; +use serde::{self, Deserialize, Serialize}; use crate::protocol::info_hash::InfoHash; -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Peer { pub peer_id: String, pub ip: IpAddr, pub port: u16, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Announce { pub interval: u32, #[serde(rename = "min interval")] diff --git a/tests/api/client.rs b/tests/api/client.rs index 4dea732be..f99805570 100644 --- a/tests/api/client.rs +++ b/tests/api/client.rs @@ -1,7 +1,7 @@ use reqwest::Response; use super::connection_info::ConnectionInfo; -use crate::common::http::{get, Query, QueryParam, ReqwestQuery}; +use crate::common::http::{Query, QueryParam, ReqwestQuery}; /// API Client pub struct Client { @@ -100,3 +100,17 @@ impl Client { format!("http://{}{}{path}", &self.connection_info.bind_address, &self.base_path) } } + +async fn get(path: &str, query: Option) -> Response { + match query { + Some(params) => reqwest::Client::builder() + .build() + .unwrap() + .get(path) + .query(&ReqwestQuery::from(params)) + .send() + .await + .unwrap(), + None => reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap(), + } +} diff --git a/tests/api/mod.rs b/tests/api/mod.rs index bc4187375..8dd6f4c53 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -5,7 +5,6 @@ use torrust_tracker::tracker::Tracker; pub mod asserts; pub mod client; pub mod connection_info; -pub mod fixtures; pub mod server; /// It forces a database error by dropping all tables. diff --git a/tests/api/fixtures.rs b/tests/common/fixtures.rs similarity index 100% rename from tests/api/fixtures.rs rename to tests/common/fixtures.rs diff --git a/tests/common/http.rs b/tests/common/http.rs index 1c2e95671..902752674 100644 --- a/tests/common/http.rs +++ b/tests/common/http.rs @@ -1,22 +1,6 @@ -use reqwest::Response; - pub type ReqwestQuery = Vec; pub type ReqwestQueryParam = (String, String); -pub async fn get(path: &str, query: Option) -> Response { - match query { - Some(params) => reqwest::Client::builder() - .build() - .unwrap() - .get(path) - .query(&ReqwestQuery::from(params)) - .send() - .await - .unwrap(), - None => reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap(), - } -} - #[derive(Clone, Debug)] pub struct ConnectionInfo { pub bind_address: String, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3883215fc..810620359 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1 +1,2 @@ +pub mod fixtures; pub mod http; diff --git a/tests/http/asserts.rs b/tests/http/asserts.rs index b82c681a0..9a1f353c6 100644 --- a/tests/http/asserts.rs +++ b/tests/http/asserts.rs @@ -1,7 +1,15 @@ use reqwest::Response; +use super::responses::Announce; + pub async fn assert_internal_server_error(response: Response) { assert_eq!(response.status(), 200); /* cspell:disable-next-line */ assert_eq!(response.text().await.unwrap(), "d14:failure reason21:internal server errore"); } + +pub async fn assert_announce_response(response: Response, expected_announce_response: &Announce) { + assert_eq!(response.status(), 200); + let announce_response: Announce = serde_bencode::from_str(&response.text().await.unwrap()).unwrap(); + assert_eq!(announce_response, *expected_announce_response); +} diff --git a/tests/http/client.rs b/tests/http/client.rs index 8bf691474..ae51bc02e 100644 --- a/tests/http/client.rs +++ b/tests/http/client.rs @@ -1,35 +1,34 @@ use reqwest::Response; use super::connection_info::ConnectionInfo; -use crate::common::http::{get, Query}; +use super::requests::AnnounceQuery; /// HTTP Tracker Client pub struct Client { connection_info: ConnectionInfo, - base_path: String, } impl Client { pub fn new(connection_info: ConnectionInfo) -> Self { - Self { - connection_info, - base_path: "/".to_string(), - } + Self { connection_info } } - pub async fn announce(&self, params: Query) -> Response { - self.get("announce", params).await + pub async fn announce(&self, query: &AnnounceQuery) -> Response { + let path_with_query = format!("announce?{query}"); + self.get(&path_with_query).await } - pub async fn scrape(&self, params: Query) -> Response { - self.get("scrape", params).await - } - - async fn get(&self, path: &str, params: Query) -> Response { - get(&self.base_url(path), Some(params)).await + pub async fn get(&self, path: &str) -> Response { + reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path)) + .send() + .await + .unwrap() } fn base_url(&self, path: &str) -> String { - format!("http://{}{}{path}", &self.connection_info.bind_address, &self.base_path) + format!("http://{}/{path}", &self.connection_info.bind_address) } } diff --git a/tests/http/mod.rs b/tests/http/mod.rs index 9e79fcd27..2ab8b2c1c 100644 --- a/tests/http/mod.rs +++ b/tests/http/mod.rs @@ -1,4 +1,6 @@ pub mod asserts; pub mod client; pub mod connection_info; +pub mod requests; +pub mod responses; pub mod server; diff --git a/tests/http/requests.rs b/tests/http/requests.rs new file mode 100644 index 000000000..170dc52a9 --- /dev/null +++ b/tests/http/requests.rs @@ -0,0 +1,104 @@ +use std::fmt; +use std::net::IpAddr; + +use percent_encoding::NON_ALPHANUMERIC; +use serde_repr::Serialize_repr; + +pub struct AnnounceQuery { + pub info_hash: ByteArray20, + pub peer_addr: IpAddr, + pub downloaded: BaseTenASCII, + pub uploaded: BaseTenASCII, + pub peer_id: ByteArray20, + pub port: PortNumber, + pub left: BaseTenASCII, + pub event: Option, + pub compact: Option, +} + +impl fmt::Display for AnnounceQuery { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.build()) + } +} + +/// HTTP Tracker Announce Request: +/// +/// +/// +/// Some parameters are not implemented yet. +impl AnnounceQuery { + /// It builds the URL query component for the announce request. + /// + /// This custom URL query params encoding is needed because `reqwest` does not allow + /// bytes arrays in query parameters. More info on this issue: + /// + /// + pub fn build(&self) -> String { + let mut params = vec![ + ( + "info_hash", + percent_encoding::percent_encode(&self.info_hash, NON_ALPHANUMERIC).to_string(), + ), + ("peer_addr", self.peer_addr.to_string()), + ("downloaded", self.downloaded.to_string()), + ("uploaded", self.uploaded.to_string()), + ( + "peer_id", + percent_encoding::percent_encode(&self.peer_id, NON_ALPHANUMERIC).to_string(), + ), + ("port", self.port.to_string()), + ("left", self.left.to_string()), + ]; + + if let Some(event) = &self.event { + params.push(("event", event.to_string())); + } + + if let Some(compact) = &self.compact { + params.push(("compact", compact.to_string())); + } + + params + .iter() + .map(|param| format!("{}={}", param.0, param.1)) + .collect::>() + .join("&") + } +} + +pub type BaseTenASCII = u64; +pub type ByteArray20 = [u8; 20]; +pub type PortNumber = u16; + +pub enum Event { + //tarted, + //Stopped, + Completed, +} + +impl fmt::Display for Event { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + //Event::Started => write!(f, "started"), + //Event::Stopped => write!(f, "stopped"), + Event::Completed => write!(f, "completed"), + } + } +} + +#[derive(Serialize_repr, PartialEq, Debug)] +#[repr(u8)] +pub enum Compact { + //Accepted = 1, + NotAccepted = 0, +} + +impl fmt::Display for Compact { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + //Compact::Accepted => write!(f, "1"), + Compact::NotAccepted => write!(f, "0"), + } + } +} diff --git a/tests/http/responses.rs b/tests/http/responses.rs new file mode 100644 index 000000000..e82197b03 --- /dev/null +++ b/tests/http/responses.rs @@ -0,0 +1,18 @@ +use serde::{self, Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Announce { + pub complete: u32, + pub incomplete: u32, + pub interval: u32, + #[serde(rename = "min interval")] + pub min_interval: u32, + pub peers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct DictionaryPeer { + pub ip: String, + pub peer_id: String, + pub port: u16, +} diff --git a/tests/http/server.rs b/tests/http/server.rs index ff2b40987..130c68b46 100644 --- a/tests/http/server.rs +++ b/tests/http/server.rs @@ -3,6 +3,8 @@ use std::sync::Arc; use torrust_tracker::config::{ephemeral_configuration, Configuration}; use torrust_tracker::jobs::http_tracker; +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker::tracker::peer::Peer; use torrust_tracker::tracker::statistics::Keeper; use torrust_tracker::{ephemeral_instance_keys, logging, static_time, tracker}; @@ -61,4 +63,8 @@ impl Server { pub fn get_connection_info(&self) -> ConnectionInfo { self.connection_info.clone() } + + pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) { + self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + } } diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs index a1e429bb8..79afec46b 100644 --- a/tests/http_tracker.rs +++ b/tests/http_tracker.rs @@ -7,7 +7,6 @@ mod http; mod http_tracker_server { mod receiving_an_announce_request { - use crate::common::http::Query; use crate::http::asserts::assert_internal_server_error; use crate::http::client::Client; use crate::http::server::start_default_http_tracker; @@ -16,16 +15,13 @@ mod http_tracker_server { async fn should_fail_when_the_request_is_empty() { let http_tracker_server = start_default_http_tracker().await; - let response = Client::new(http_tracker_server.get_connection_info()) - .announce(Query::default()) - .await; + let response = Client::new(http_tracker_server.get_connection_info()).get("announce").await; assert_internal_server_error(response).await; } } mod receiving_an_scrape_request { - use crate::common::http::Query; use crate::http::asserts::assert_internal_server_error; use crate::http::client::Client; use crate::http::server::start_default_http_tracker; @@ -34,11 +30,103 @@ mod http_tracker_server { async fn should_fail_when_the_request_is_empty() { let http_tracker_server = start_default_http_tracker().await; + let response = Client::new(http_tracker_server.get_connection_info()).get("scrape").await; + + assert_internal_server_error(response).await; + } + } +} + +mod public_http_tracker_server { + + mod receiving_an_announce_request { + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + + use crate::common::fixtures::sample_peer; + use crate::http::asserts::assert_announce_response; + use crate::http::client::Client; + use crate::http::requests::{AnnounceQuery, Compact, Event}; + use crate::http::responses::{Announce, DictionaryPeer}; + use crate::http::server::start_default_http_tracker; + + fn sample_announce_query(info_hash: &InfoHash) -> AnnounceQuery { + AnnounceQuery { + info_hash: info_hash.0, + peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)), + downloaded: 0, + uploaded: 0, + peer_id: peer::Id(*b"-qB00000000000000001").0, + port: 17548, + left: 0, + event: Some(Event::Completed), + compact: Some(Compact::NotAccepted), + } + } + + #[tokio::test] + async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { + let http_tracker_server = start_default_http_tracker().await; + let response = Client::new(http_tracker_server.get_connection_info()) - .scrape(Query::default()) + .announce(&sample_announce_query( + &InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(), + )) .await; - assert_internal_server_error(response).await; + assert_announce_response( + response, + &Announce { + complete: 1, // the peer for this test + incomplete: 0, + interval: http_tracker_server.tracker.config.announce_interval, + min_interval: http_tracker_server.tracker.config.min_announce_interval, + peers: vec![], + }, + ) + .await; + } + + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers() { + let http_tracker_server = start_default_http_tracker().await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let peer = sample_peer(); + + // Add a peer + http_tracker_server.add_torrent(&info_hash, &peer).await; + + let announce_query = sample_announce_query(&info_hash); + + assert_ne!( + announce_query.peer_id, peer.peer_id.0, + "new peer id must be different from previously announce peer otherwise the peer is removed from the peers list" + ); + + // Announce the new peer. This new peer is non included the response peers list + let response = Client::new(http_tracker_server.get_connection_info()) + .announce(&announce_query) + .await; + + assert_announce_response( + response, + &Announce { + complete: 2, + incomplete: 0, + interval: 120, + min_interval: 120, + peers: vec![DictionaryPeer { + ip: peer.peer_addr.ip().to_string(), + peer_id: String::new(), + port: peer.peer_addr.port(), + }], + }, + ) + .await; } } } diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs index 456b37f7b..47fda3af9 100644 --- a/tests/tracker_api.rs +++ b/tests/tracker_api.rs @@ -113,8 +113,8 @@ mod tracker_apis { use crate::api::asserts::{assert_stats, assert_token_not_valid, assert_unauthorized}; use crate::api::client::Client; use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::fixtures::sample_peer; use crate::api::server::start_default_api; + use crate::common::fixtures::sample_peer; #[tokio::test] async fn should_allow_getting_tracker_statistics() { @@ -185,8 +185,8 @@ mod tracker_apis { }; use crate::api::client::Client; use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::fixtures::sample_peer; use crate::api::server::start_default_api; + use crate::common::fixtures::sample_peer; use crate::common::http::{Query, QueryParam}; #[tokio::test]