diff --git a/src/apis/context/stats/routes.rs b/src/apis/context/stats/routes.rs deleted file mode 100644 index 8791ed25a..000000000 --- a/src/apis/context/stats/routes.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::sync::Arc; - -use axum::routing::get; -use axum::Router; - -use super::handlers::get_stats_handler; -use crate::tracker::Tracker; - -pub fn add(router: Router, tracker: Arc) -> Router { - router.route("/api/stats", get(get_stats_handler).with_state(tracker)) -} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index fd7fdb6e5..1bc257916 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,8 +1,6 @@ -pub mod context; -pub mod middlewares; -pub mod responses; pub mod routes; pub mod server; +pub mod v1; use serde::Deserialize; diff --git a/src/apis/routes.rs b/src/apis/routes.rs index c567e50da..2545d6b88 100644 --- a/src/apis/routes.rs +++ b/src/apis/routes.rs @@ -2,18 +2,17 @@ use std::sync::Arc; use axum::{middleware, Router}; -use super::context::{auth_key, stats, torrent, whitelist}; -use super::middlewares::auth::auth; +use super::v1; +use super::v1::middlewares::auth::auth; use crate::tracker::Tracker; #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc) -> Router { let router = Router::new(); - let router = auth_key::routes::add(router, tracker.clone()); - let router = stats::routes::add(router, tracker.clone()); - let router = whitelist::routes::add(router, tracker.clone()); - let router = torrent::routes::add(router, tracker.clone()); + let prefix = "/api"; + + let router = v1::routes::add(prefix, router, tracker.clone()); router.layer(middleware::from_fn_with_state(tracker.config.clone(), auth)) } diff --git a/src/apis/context/auth_key/handlers.rs b/src/apis/v1/context/auth_key/handlers.rs similarity index 91% rename from src/apis/context/auth_key/handlers.rs rename to src/apis/v1/context/auth_key/handlers.rs index af78b3f4c..d21f08299 100644 --- a/src/apis/context/auth_key/handlers.rs +++ b/src/apis/v1/context/auth_key/handlers.rs @@ -9,8 +9,8 @@ use serde::Deserialize; use super::responses::{ auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response, }; -use crate::apis::context::auth_key::resources::AuthKey; -use crate::apis::responses::{invalid_auth_key_param_response, ok_response}; +use crate::apis::v1::context::auth_key::resources::AuthKey; +use crate::apis::v1::responses::{invalid_auth_key_param_response, ok_response}; use crate::tracker::auth::Key; use crate::tracker::Tracker; diff --git a/src/apis/context/auth_key/mod.rs b/src/apis/v1/context/auth_key/mod.rs similarity index 100% rename from src/apis/context/auth_key/mod.rs rename to src/apis/v1/context/auth_key/mod.rs diff --git a/src/apis/context/auth_key/resources.rs b/src/apis/v1/context/auth_key/resources.rs similarity index 100% rename from src/apis/context/auth_key/resources.rs rename to src/apis/v1/context/auth_key/resources.rs diff --git a/src/apis/context/auth_key/responses.rs b/src/apis/v1/context/auth_key/responses.rs similarity index 88% rename from src/apis/context/auth_key/responses.rs rename to src/apis/v1/context/auth_key/responses.rs index 8c1bf58dc..9b8fcebe2 100644 --- a/src/apis/context/auth_key/responses.rs +++ b/src/apis/v1/context/auth_key/responses.rs @@ -3,8 +3,8 @@ use std::error::Error; use axum::http::{header, StatusCode}; use axum::response::{IntoResponse, Response}; -use crate::apis::context::auth_key::resources::AuthKey; -use crate::apis::responses::unhandled_rejection_response; +use crate::apis::v1::context::auth_key::resources::AuthKey; +use crate::apis::v1::responses::unhandled_rejection_response; /// # Panics /// diff --git a/src/apis/context/auth_key/routes.rs b/src/apis/v1/context/auth_key/routes.rs similarity index 70% rename from src/apis/context/auth_key/routes.rs rename to src/apis/v1/context/auth_key/routes.rs index 2a4f5b9dd..9b155c2a5 100644 --- a/src/apis/context/auth_key/routes.rs +++ b/src/apis/v1/context/auth_key/routes.rs @@ -6,20 +6,20 @@ use axum::Router; use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; use crate::tracker::Tracker; -pub fn add(router: Router, tracker: Arc) -> Router { +pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Keys router .route( // code-review: Axum does not allow two routes with the same path but different path variable name. // In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths: - // POST /api/key - // DELETE /api/key/:key - "/api/key/:seconds_valid_or_key", + // POST /key + // DELETE /key/:key + &format!("{prefix}/key/:seconds_valid_or_key"), post(generate_auth_key_handler) .with_state(tracker.clone()) .delete(delete_auth_key_handler) .with_state(tracker.clone()), ) // Keys command - .route("/api/keys/reload", get(reload_keys_handler).with_state(tracker)) + .route(&format!("{prefix}/keys/reload"), get(reload_keys_handler).with_state(tracker)) } diff --git a/src/apis/context/mod.rs b/src/apis/v1/context/mod.rs similarity index 100% rename from src/apis/context/mod.rs rename to src/apis/v1/context/mod.rs diff --git a/src/apis/context/stats/handlers.rs b/src/apis/v1/context/stats/handlers.rs similarity index 100% rename from src/apis/context/stats/handlers.rs rename to src/apis/v1/context/stats/handlers.rs diff --git a/src/apis/context/stats/mod.rs b/src/apis/v1/context/stats/mod.rs similarity index 100% rename from src/apis/context/stats/mod.rs rename to src/apis/v1/context/stats/mod.rs diff --git a/src/apis/context/stats/resources.rs b/src/apis/v1/context/stats/resources.rs similarity index 100% rename from src/apis/context/stats/resources.rs rename to src/apis/v1/context/stats/resources.rs diff --git a/src/apis/context/stats/responses.rs b/src/apis/v1/context/stats/responses.rs similarity index 100% rename from src/apis/context/stats/responses.rs rename to src/apis/v1/context/stats/responses.rs diff --git a/src/apis/v1/context/stats/routes.rs b/src/apis/v1/context/stats/routes.rs new file mode 100644 index 000000000..07f88aa70 --- /dev/null +++ b/src/apis/v1/context/stats/routes.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use axum::routing::get; +use axum::Router; + +use super::handlers::get_stats_handler; +use crate::tracker::Tracker; + +pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { + router.route(&format!("{prefix}/stats"), get(get_stats_handler).with_state(tracker)) +} diff --git a/src/apis/context/torrent/handlers.rs b/src/apis/v1/context/torrent/handlers.rs similarity index 96% rename from src/apis/context/torrent/handlers.rs rename to src/apis/v1/context/torrent/handlers.rs index 1a8280e75..fc816cdbf 100644 --- a/src/apis/context/torrent/handlers.rs +++ b/src/apis/v1/context/torrent/handlers.rs @@ -8,7 +8,7 @@ use serde::{de, Deserialize, Deserializer}; use super::resources::torrent::ListItem; use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response}; -use crate::apis::responses::invalid_info_hash_param_response; +use crate::apis::v1::responses::invalid_info_hash_param_response; use crate::apis::InfoHashParam; use crate::protocol::info_hash::InfoHash; use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination}; diff --git a/src/apis/context/torrent/mod.rs b/src/apis/v1/context/torrent/mod.rs similarity index 100% rename from src/apis/context/torrent/mod.rs rename to src/apis/v1/context/torrent/mod.rs diff --git a/src/apis/context/torrent/resources/mod.rs b/src/apis/v1/context/torrent/resources/mod.rs similarity index 100% rename from src/apis/context/torrent/resources/mod.rs rename to src/apis/v1/context/torrent/resources/mod.rs diff --git a/src/apis/context/torrent/resources/peer.rs b/src/apis/v1/context/torrent/resources/peer.rs similarity index 100% rename from src/apis/context/torrent/resources/peer.rs rename to src/apis/v1/context/torrent/resources/peer.rs diff --git a/src/apis/context/torrent/resources/torrent.rs b/src/apis/v1/context/torrent/resources/torrent.rs similarity index 96% rename from src/apis/context/torrent/resources/torrent.rs rename to src/apis/v1/context/torrent/resources/torrent.rs index 1099dc923..48f4c58f0 100644 --- a/src/apis/context/torrent/resources/torrent.rs +++ b/src/apis/v1/context/torrent/resources/torrent.rs @@ -75,8 +75,8 @@ mod tests { use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; use super::Torrent; - use crate::apis::context::torrent::resources::peer::Peer; - use crate::apis::context::torrent::resources::torrent::ListItem; + use crate::apis::v1::context::torrent::resources::peer::Peer; + use crate::apis::v1::context::torrent::resources::torrent::ListItem; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; use crate::tracker::peer; diff --git a/src/apis/context/torrent/responses.rs b/src/apis/v1/context/torrent/responses.rs similarity index 100% rename from src/apis/context/torrent/responses.rs rename to src/apis/v1/context/torrent/responses.rs diff --git a/src/apis/context/torrent/routes.rs b/src/apis/v1/context/torrent/routes.rs similarity index 55% rename from src/apis/context/torrent/routes.rs rename to src/apis/v1/context/torrent/routes.rs index 234f17223..00faa9665 100644 --- a/src/apis/context/torrent/routes.rs +++ b/src/apis/v1/context/torrent/routes.rs @@ -6,12 +6,12 @@ use axum::Router; use super::handlers::{get_torrent_handler, get_torrents_handler}; use crate::tracker::Tracker; -pub fn add(router: Router, tracker: Arc) -> Router { +pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Torrents router .route( - "/api/torrent/:info_hash", + &format!("{prefix}/torrent/:info_hash"), get(get_torrent_handler).with_state(tracker.clone()), ) - .route("/api/torrents", get(get_torrents_handler).with_state(tracker)) + .route(&format!("{prefix}/torrents"), get(get_torrents_handler).with_state(tracker)) } diff --git a/src/apis/context/whitelist/handlers.rs b/src/apis/v1/context/whitelist/handlers.rs similarity index 95% rename from src/apis/context/whitelist/handlers.rs rename to src/apis/v1/context/whitelist/handlers.rs index c1e90a509..325f20e26 100644 --- a/src/apis/context/whitelist/handlers.rs +++ b/src/apis/v1/context/whitelist/handlers.rs @@ -7,7 +7,7 @@ use axum::response::Response; use super::responses::{ failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response, }; -use crate::apis::responses::{invalid_info_hash_param_response, ok_response}; +use crate::apis::v1::responses::{invalid_info_hash_param_response, ok_response}; use crate::apis::InfoHashParam; use crate::protocol::info_hash::InfoHash; use crate::tracker::Tracker; diff --git a/src/apis/context/whitelist/mod.rs b/src/apis/v1/context/whitelist/mod.rs similarity index 100% rename from src/apis/context/whitelist/mod.rs rename to src/apis/v1/context/whitelist/mod.rs diff --git a/src/apis/context/whitelist/responses.rs b/src/apis/v1/context/whitelist/responses.rs similarity index 90% rename from src/apis/context/whitelist/responses.rs rename to src/apis/v1/context/whitelist/responses.rs index dd2727898..197d4c90b 100644 --- a/src/apis/context/whitelist/responses.rs +++ b/src/apis/v1/context/whitelist/responses.rs @@ -2,7 +2,7 @@ use std::error::Error; use axum::response::Response; -use crate::apis::responses::unhandled_rejection_response; +use crate::apis::v1::responses::unhandled_rejection_response; #[must_use] pub fn failed_to_remove_torrent_from_whitelist_response(e: E) -> Response { diff --git a/src/apis/context/whitelist/routes.rs b/src/apis/v1/context/whitelist/routes.rs similarity index 62% rename from src/apis/context/whitelist/routes.rs rename to src/apis/v1/context/whitelist/routes.rs index 1349f8bc1..06011b462 100644 --- a/src/apis/context/whitelist/routes.rs +++ b/src/apis/v1/context/whitelist/routes.rs @@ -6,17 +6,19 @@ use axum::Router; use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; use crate::tracker::Tracker; -pub fn add(router: Router, tracker: Arc) -> Router { +pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { + let prefix = format!("{prefix}/whitelist"); + router // Whitelisted torrents .route( - "/api/whitelist/:info_hash", + &format!("{prefix}/:info_hash"), post(add_torrent_to_whitelist_handler).with_state(tracker.clone()), ) .route( - "/api/whitelist/:info_hash", + &format!("{prefix}/:info_hash"), delete(remove_torrent_from_whitelist_handler).with_state(tracker.clone()), ) // Whitelist commands - .route("/api/whitelist/reload", get(reload_whitelist_handler).with_state(tracker)) + .route(&format!("{prefix}/reload"), get(reload_whitelist_handler).with_state(tracker)) } diff --git a/src/apis/middlewares/auth.rs b/src/apis/v1/middlewares/auth.rs similarity index 96% rename from src/apis/middlewares/auth.rs rename to src/apis/v1/middlewares/auth.rs index f2745d42e..e729072b6 100644 --- a/src/apis/middlewares/auth.rs +++ b/src/apis/v1/middlewares/auth.rs @@ -7,7 +7,7 @@ use axum::response::{IntoResponse, Response}; use serde::Deserialize; use torrust_tracker_configuration::{Configuration, HttpApi}; -use crate::apis::responses::unhandled_rejection_response; +use crate::apis::v1::responses::unhandled_rejection_response; #[derive(Deserialize, Debug)] pub struct QueryParams { diff --git a/src/apis/middlewares/mod.rs b/src/apis/v1/middlewares/mod.rs similarity index 100% rename from src/apis/middlewares/mod.rs rename to src/apis/v1/middlewares/mod.rs diff --git a/src/apis/v1/mod.rs b/src/apis/v1/mod.rs new file mode 100644 index 000000000..e87984b8e --- /dev/null +++ b/src/apis/v1/mod.rs @@ -0,0 +1,4 @@ +pub mod context; +pub mod middlewares; +pub mod responses; +pub mod routes; diff --git a/src/apis/responses.rs b/src/apis/v1/responses.rs similarity index 100% rename from src/apis/responses.rs rename to src/apis/v1/responses.rs diff --git a/src/apis/v1/routes.rs b/src/apis/v1/routes.rs new file mode 100644 index 000000000..d45319c4b --- /dev/null +++ b/src/apis/v1/routes.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use axum::Router; + +use super::context::{auth_key, stats, torrent, whitelist}; +use crate::tracker::Tracker; + +pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { + // Without `v1` prefix. + // We keep the old API endpoints without `v1` prefix for backward compatibility. + // todo: remove when the torrust index backend is using the `v1` prefix. + let router = auth_key::routes::add(prefix, router, tracker.clone()); + let router = stats::routes::add(prefix, router, tracker.clone()); + let router = whitelist::routes::add(prefix, router, tracker.clone()); + let router = torrent::routes::add(prefix, router, tracker.clone()); + + // With `v1` prefix + let v1_prefix = format!("{prefix}/v1"); + let router = auth_key::routes::add(&v1_prefix, router, tracker.clone()); + let router = stats::routes::add(&v1_prefix, router, tracker.clone()); + let router = whitelist::routes::add(&v1_prefix, router, tracker.clone()); + torrent::routes::add(&v1_prefix, router, tracker) +} diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs deleted file mode 100644 index 730da93d5..000000000 --- a/tests/http_tracker.rs +++ /dev/null @@ -1,1443 +0,0 @@ -/// Integration tests for HTTP tracker server -/// -/// ```text -/// cargo test `http_tracker_server` -- --nocapture -/// ``` -mod common; -mod http; - -pub type V1 = torrust_tracker::http::v1::launcher::Launcher; - -mod http_tracker { - - mod v1 { - - use torrust_tracker_test_helpers::configuration; - - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn test_environment_should_be_started_and_stopped() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - test_env.stop().await; - } - - mod for_all_config_modes { - - mod and_running_on_reverse_proxy { - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() { - // If the tracker is running behind a reverse proxy, the peer IP is the - // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client. - - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let params = QueryBuilder::default().query().params(); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let params = QueryBuilder::default().query().params(); - - let response = Client::new(*test_env.bind_address()) - .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") - .await; - - assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_announce_request { - - // Announce request documentation: - // - // BEP 03. The BitTorrent Protocol Specification - // https://www.bittorrent.org/beps/bep_0003.html - // - // BEP 23. Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Announce - - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::str::FromStr; - - use local_ip_address::local_ip; - use reqwest::Response; - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{ - assert_announce_response, assert_bad_announce_request_error_response, - assert_cannot_parse_query_param_error_response, assert_cannot_parse_query_params_error_response, - assert_compact_announce_response, assert_empty_announce_response, assert_is_announce_response, - assert_missing_query_params_for_announce_request_error_response, - }; - use crate::http::client::Client; - use crate::http::requests::announce::{Compact, QueryBuilder}; - use crate::http::responses; - use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_respond_if_only_the_mandatory_fields_are_provided() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - params.remove_optional_params(); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_url_query_component_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let response = Client::new(*test_env.bind_address()).get("announce").await; - - assert_missing_query_params_for_announce_request_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_url_query_parameters_are_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let invalid_query_param = "a=b=c"; - - let response = Client::new(*test_env.bind_address()) - .get(&format!("announce?{invalid_query_param}")) - .await; - - assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_a_mandatory_field_is_missing() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - // Without `info_hash` param - - let mut params = QueryBuilder::default().query().params(); - - params.info_hash = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "missing param info_hash").await; - - // Without `peer_id` param - - let mut params = QueryBuilder::default().query().params(); - - params.peer_id = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "missing param peer_id").await; - - // Without `port` param - - let mut params = QueryBuilder::default().query().params(); - - params.port = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "missing param port").await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - for invalid_value in &invalid_info_hashes() { - params.set("info_hash", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_cannot_parse_query_params_error_response(response, "").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_fail_when_the_peer_address_param_is_invalid() { - // AnnounceQuery does not even contain the `peer_addr` - // The peer IP is obtained in two ways: - // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP. - // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request HTTP header. - - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_downloaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("downloaded", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_uploaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("uploaded", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_peer_id_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "-qB0000000000000000", // 19 bytes - "-qB000000000000000000", // 21 bytes - ]; - - for invalid_value in invalid_values { - params.set("peer_id", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_port_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("port", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_left_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("left", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_event_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "Started", // It should be lowercase to be valid: `started` - "Stopped", // It should be lowercase to be valid: `stopped` - "Completed", // It should be lowercase to be valid: `completed` - ]; - - for invalid_value in invalid_values { - params.set("event", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_compact_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("compact", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_bad_announce_request_error_response(response, "invalid param value").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) - .query(), - ) - .await; - - assert_announce_response( - response, - &Announce { - complete: 1, // the peer for this test - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_list_of_previously_announced_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2. This new peer is non included on the response peer list - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .query(), - ) - .await; - - // It should only contain the previously announced peer - assert_announce_response( - response, - &Announce { - complete: 2, - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![DictionaryPeer::from(previously_announced_peer)], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Announce a peer using IPV4 - let peer_using_ipv4 = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) - .build(); - test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await; - - // Announce a peer using IPV6 - let peer_using_ipv6 = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_peer_addr(&SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), - 8080, - )) - .build(); - test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; - - // Announce the new Peer. - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000003")) - .query(), - ) - .await; - - // The newly announced peer is not included on the response peer list, - // but all the previously announced peers should be included regardless the IP version they are using. - assert_announce_response( - response, - &Announce { - complete: 3, - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let peer = PeerBuilder::default().build(); - - // Add a peer - test_env.add_torrent_peer(&info_hash, &peer).await; - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer.peer_id) - .query(); - - assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); - - let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; - - assert_empty_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_compact_response() { - // Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2 accepting compact responses - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_compact(Compact::Accepted) - .query(), - ) - .await; - - let expected_response = responses::announce::Compact { - complete: 2, - incomplete: 0, - interval: 120, - min_interval: 120, - peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), - }; - - assert_compact_announce_response(response, &expected_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_return_the_compact_response_by_default() { - // code-review: the HTTP tracker does not return the compact response by default if the "compact" - // param is not provided in the announce URL. The BEP 23 suggest to do so. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2 without passing the "compact" param - // By default it should respond with the compact peer list - // https://www.bittorrent.org/beps/bep_0023.html - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .without_compact() - .query(), - ) - .await; - - assert!(!is_a_compact_announce_response(response).await); - - test_env.stop().await; - } - - async fn is_a_compact_announce_response(response: Response) -> bool { - let bytes = response.bytes().await.unwrap(); - let compact_announce = serde_bencode::from_bytes::(&bytes); - compact_announce.is_ok() - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_connections_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_connections_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_connections_handled, 0); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_announces_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_announces_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() - { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_announces_handled, 0); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let client_ip = local_ip().unwrap(); - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), client_ip); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. - - client <-> tracker <-> Internet - 127.0.0.1 external_ip = "2.137.87.41" - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2.137.87.41").unwrap(), - )) - .await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. - - client <-> tracker <-> Internet - ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), - )) - .await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header( - ) { - /* - client <-> http proxy <-> tracker <-> Internet - ip: header: config: peer addr: - 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let client = Client::new(*test_env.bind_address()); - - let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); - - client - .announce_with_header( - &announce_query, - "X-Forwarded-For", - "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", - ) - .await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap()); - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - - // Scrape documentation: - // - // BEP 48. Tracker Protocol Extension: Scrape - // https://www.bittorrent.org/beps/bep_0048.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Scrape - - use std::net::IpAddr; - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{ - assert_cannot_parse_query_params_error_response, - assert_missing_query_params_for_scrape_request_error_response, assert_scrape_response, - }; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::requests::scrape::QueryBuilder; - use crate::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - //#[tokio::test] - #[allow(dead_code)] - async fn should_fail_when_the_request_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let response = Client::new(*test_env.bind_address()).get("scrape").await; - - assert_missing_query_params_for_scrape_request_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let mut params = QueryBuilder::default().query().params(); - - for invalid_value in &invalid_info_hashes() { - params.set_one_info_hash_param(invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_cannot_parse_query_params_error_response(response, "").await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() - { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_no_bytes_pending_to_download() - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 1, - downloaded: 0, - incomplete: 0, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_accept_multiple_infohashes() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .add_info_hash(&info_hash1) - .add_info_hash(&info_hash2) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file(info_hash1.bytes(), File::zeroed()) - .add_file(info_hash2.bytes(), File::zeroed()) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_scrapes_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_scrapes_handled, 1); - - drop(stats); - - test_env.stop().await; - } - } - } - - mod configured_as_whitelisted { - - mod and_receiving_an_announce_request { - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_torrent_not_in_whitelist_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_allow_announcing_a_whitelisted_torrent() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::assert_scrape_response; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - } - } - - mod configured_as_private { - - mod and_receiving_an_announce_request { - use std::str::FromStr; - use std::time::Duration; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_respond_to_authenticated_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .announce(&QueryBuilder::default().query()) - .await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_authentication_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_if_the_key_query_param_cannot_be_parsed() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let invalid_key = "INVALID_KEY"; - - let response = Client::new(*test_env.bind_address()) - .get(&format!( - "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0" - )) - .await; - - assert_authentication_error_response(response).await; - } - - #[tokio::test] - async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - // The tracker does not have this key - let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), unregistered_key) - .announce(&QueryBuilder::default().query()) - .await; - - assert_authentication_error_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - - use std::str::FromStr; - use std::time::Duration; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::{assert_authentication_error_response, assert_scrape_response}; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::V1; - - #[tokio::test] - async fn should_fail_if_the_key_query_param_cannot_be_parsed() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let invalid_key = "INVALID_KEY"; - - let response = Client::new(*test_env.bind_address()) - .get(&format!( - "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0" - )) - .await; - - assert_authentication_error_response(response).await; - } - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_real_file_stats_when_the_client_is_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() { - // There is not authentication error - // code-review: should this really be this way? - - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), false_key) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - } - } - - mod configured_as_private_and_whitelisted { - - mod and_receiving_an_announce_request {} - - mod receiving_an_scrape_request {} - } - } -} diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 000000000..5d66d9074 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,7 @@ +//! Integration tests. +//! +//! ```text +//! cargo test --test integration +//! ``` +mod common; +mod servers; diff --git a/tests/api/connection_info.rs b/tests/servers/api/connection_info.rs similarity index 100% rename from tests/api/connection_info.rs rename to tests/servers/api/connection_info.rs diff --git a/tests/api/mod.rs b/tests/servers/api/mod.rs similarity index 88% rename from tests/api/mod.rs rename to tests/servers/api/mod.rs index f59210b22..7022da9b4 100644 --- a/tests/api/mod.rs +++ b/tests/servers/api/mod.rs @@ -2,11 +2,9 @@ use std::sync::Arc; use torrust_tracker::tracker::Tracker; -pub mod asserts; -pub mod client; pub mod connection_info; pub mod test_environment; -pub mod tests; +pub mod v1; /// It forces a database error by dropping all tables. /// That makes any query fail. diff --git a/tests/api/test_environment.rs b/tests/servers/api/test_environment.rs similarity index 100% rename from tests/api/test_environment.rs rename to tests/servers/api/test_environment.rs diff --git a/tests/api/asserts.rs b/tests/servers/api/v1/asserts.rs similarity index 95% rename from tests/api/asserts.rs rename to tests/servers/api/v1/asserts.rs index c7567e6fe..d37bcdbb4 100644 --- a/tests/api/asserts.rs +++ b/tests/servers/api/v1/asserts.rs @@ -1,9 +1,9 @@ // code-review: should we use macros to return the exact line where the assert fails? use reqwest::Response; -use torrust_tracker::apis::context::auth_key::resources::AuthKey; -use torrust_tracker::apis::context::stats::resources::Stats; -use torrust_tracker::apis::context::torrent::resources::torrent::{ListItem, Torrent}; +use torrust_tracker::apis::v1::context::auth_key::resources::AuthKey; +use torrust_tracker::apis::v1::context::stats::resources::Stats; +use torrust_tracker::apis::v1::context::torrent::resources::torrent::{ListItem, Torrent}; // Resource responses diff --git a/tests/api/client.rs b/tests/servers/api/v1/client.rs similarity index 96% rename from tests/api/client.rs rename to tests/servers/api/v1/client.rs index f99805570..2b6db2e77 100644 --- a/tests/api/client.rs +++ b/tests/servers/api/v1/client.rs @@ -1,7 +1,7 @@ use reqwest::Response; -use super::connection_info::ConnectionInfo; use crate::common::http::{Query, QueryParam, ReqwestQuery}; +use crate::servers::api::connection_info::ConnectionInfo; /// API Client pub struct Client { @@ -13,7 +13,7 @@ impl Client { pub fn new(connection_info: ConnectionInfo) -> Self { Self { connection_info, - base_path: "/api/".to_string(), + base_path: "/api/v1/".to_string(), } } diff --git a/tests/api/tests/authentication.rs b/tests/servers/api/v1/contract/authentication.rs similarity index 92% rename from tests/api/tests/authentication.rs rename to tests/servers/api/v1/contract/authentication.rs index 5183c8909..fb8de1810 100644 --- a/tests/api/tests/authentication.rs +++ b/tests/servers/api/v1/contract/authentication.rs @@ -1,9 +1,9 @@ use torrust_tracker_test_helpers::configuration; -use crate::api::asserts::{assert_token_not_valid, assert_unauthorized}; -use crate::api::client::Client; -use crate::api::test_environment::running_test_environment; use crate::common::http::{Query, QueryParam}; +use crate::servers::api::test_environment::running_test_environment; +use crate::servers::api::v1::asserts::{assert_token_not_valid, assert_unauthorized}; +use crate::servers::api::v1::client::Client; #[tokio::test] async fn should_authenticate_requests_by_using_a_token_query_param() { diff --git a/tests/api/tests/configuration.rs b/tests/servers/api/v1/contract/configuration.rs similarity index 86% rename from tests/api/tests/configuration.rs rename to tests/servers/api/v1/contract/configuration.rs index f81201191..e4b608607 100644 --- a/tests/api/tests/configuration.rs +++ b/tests/servers/api/v1/contract/configuration.rs @@ -1,6 +1,6 @@ use torrust_tracker_test_helpers::configuration; -use crate::api::test_environment::stopped_test_environment; +use crate::servers::api::test_environment::stopped_test_environment; #[tokio::test] #[should_panic] diff --git a/tests/api/tests/context/auth_key.rs b/tests/servers/api/v1/contract/context/auth_key.rs similarity index 96% rename from tests/api/tests/context/auth_key.rs rename to tests/servers/api/v1/contract/context/auth_key.rs index ee7121615..a99272e84 100644 --- a/tests/api/tests/context/auth_key.rs +++ b/tests/servers/api/v1/contract/context/auth_key.rs @@ -3,14 +3,14 @@ use std::time::Duration; use torrust_tracker::tracker::auth::Key; use torrust_tracker_test_helpers::configuration; -use crate::api::asserts::{ +use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::servers::api::force_database_error; +use crate::servers::api::test_environment::running_test_environment; +use crate::servers::api::v1::asserts::{ assert_auth_key_utf8, assert_failed_to_delete_key, assert_failed_to_generate_key, assert_failed_to_reload_keys, assert_invalid_auth_key_param, assert_invalid_key_duration_param, assert_ok, 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::force_database_error; -use crate::api::test_environment::running_test_environment; +use crate::servers::api::v1::client::Client; #[tokio::test] async fn should_allow_generating_a_new_auth_key() { diff --git a/tests/api/tests/context/mod.rs b/tests/servers/api/v1/contract/context/mod.rs similarity index 100% rename from tests/api/tests/context/mod.rs rename to tests/servers/api/v1/contract/context/mod.rs diff --git a/tests/api/tests/context/stats.rs b/tests/servers/api/v1/contract/context/stats.rs similarity index 83% rename from tests/api/tests/context/stats.rs rename to tests/servers/api/v1/contract/context/stats.rs index 99ae405b7..3929a4270 100644 --- a/tests/api/tests/context/stats.rs +++ b/tests/servers/api/v1/contract/context/stats.rs @@ -1,14 +1,14 @@ use std::str::FromStr; -use torrust_tracker::apis::context::stats::resources::Stats; +use torrust_tracker::apis::v1::context::stats::resources::Stats; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker_test_helpers::configuration; -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::test_environment::running_test_environment; use crate::common::fixtures::PeerBuilder; +use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::servers::api::test_environment::running_test_environment; +use crate::servers::api::v1::asserts::{assert_stats, assert_token_not_valid, assert_unauthorized}; +use crate::servers::api::v1::client::Client; #[tokio::test] async fn should_allow_getting_tracker_statistics() { diff --git a/tests/api/tests/context/torrent.rs b/tests/servers/api/v1/contract/context/torrent.rs similarity index 93% rename from tests/api/tests/context/torrent.rs rename to tests/servers/api/v1/contract/context/torrent.rs index 998c2afaf..702a8bcd4 100644 --- a/tests/api/tests/context/torrent.rs +++ b/tests/servers/api/v1/contract/context/torrent.rs @@ -1,20 +1,22 @@ use std::str::FromStr; -use torrust_tracker::apis::context::torrent::resources::peer::Peer; -use torrust_tracker::apis::context::torrent::resources::torrent::{self, Torrent}; +use torrust_tracker::apis::v1::context::torrent::resources::peer::Peer; +use torrust_tracker::apis::v1::context::torrent::resources::torrent::{self, Torrent}; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker_test_helpers::configuration; -use crate::api::asserts::{ +use crate::common::fixtures::PeerBuilder; +use crate::common::http::{Query, QueryParam}; +use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::servers::api::test_environment::running_test_environment; +use crate::servers::api::v1::asserts::{ assert_bad_request, assert_invalid_infohash_param, assert_not_found, assert_token_not_valid, assert_torrent_info, assert_torrent_list, assert_torrent_not_known, assert_unauthorized, }; -use crate::api::client::Client; -use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; -use crate::api::test_environment::running_test_environment; -use crate::api::tests::fixtures::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; -use crate::common::fixtures::PeerBuilder; -use crate::common::http::{Query, QueryParam}; +use crate::servers::api::v1::client::Client; +use crate::servers::api::v1::contract::fixtures::{ + invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found, +}; #[tokio::test] async fn should_allow_getting_torrents() { diff --git a/tests/api/tests/context/whitelist.rs b/tests/servers/api/v1/contract/context/whitelist.rs similarity index 95% rename from tests/api/tests/context/whitelist.rs rename to tests/servers/api/v1/contract/context/whitelist.rs index 29ea573c0..67992642f 100644 --- a/tests/api/tests/context/whitelist.rs +++ b/tests/servers/api/v1/contract/context/whitelist.rs @@ -3,15 +3,17 @@ use std::str::FromStr; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker_test_helpers::configuration; -use crate::api::asserts::{ +use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::servers::api::force_database_error; +use crate::servers::api::test_environment::running_test_environment; +use crate::servers::api::v1::asserts::{ assert_failed_to_reload_whitelist, assert_failed_to_remove_torrent_from_whitelist, assert_failed_to_whitelist_torrent, assert_invalid_infohash_param, assert_not_found, assert_ok, 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::force_database_error; -use crate::api::test_environment::running_test_environment; -use crate::api::tests::fixtures::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; +use crate::servers::api::v1::client::Client; +use crate::servers::api::v1::contract::fixtures::{ + invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found, +}; #[tokio::test] async fn should_allow_whitelisting_a_torrent() { diff --git a/tests/api/tests/fixtures.rs b/tests/servers/api/v1/contract/fixtures.rs similarity index 100% rename from tests/api/tests/fixtures.rs rename to tests/servers/api/v1/contract/fixtures.rs diff --git a/tests/api/tests/mod.rs b/tests/servers/api/v1/contract/mod.rs similarity index 100% rename from tests/api/tests/mod.rs rename to tests/servers/api/v1/contract/mod.rs diff --git a/tests/servers/api/v1/mod.rs b/tests/servers/api/v1/mod.rs new file mode 100644 index 000000000..37298b377 --- /dev/null +++ b/tests/servers/api/v1/mod.rs @@ -0,0 +1,3 @@ +pub mod asserts; +pub mod client; +pub mod contract; diff --git a/tests/http/asserts.rs b/tests/servers/http/asserts.rs similarity index 99% rename from tests/http/asserts.rs rename to tests/servers/http/asserts.rs index 932b48be4..3a2e67bf0 100644 --- a/tests/http/asserts.rs +++ b/tests/servers/http/asserts.rs @@ -4,7 +4,7 @@ use reqwest::Response; use super::responses::announce::{Announce, Compact, DeserializedCompact}; use super::responses::scrape; -use crate::http::responses::error::Error; +use crate::servers::http::responses::error::Error; pub fn assert_bencoded_error(response_text: &String, expected_failure_reason: &str, location: &'static Location<'static>) { let error_failure_reason = serde_bencode::from_str::(response_text) diff --git a/tests/http/client.rs b/tests/servers/http/client.rs similarity index 100% rename from tests/http/client.rs rename to tests/servers/http/client.rs diff --git a/tests/http/connection_info.rs b/tests/servers/http/connection_info.rs similarity index 100% rename from tests/http/connection_info.rs rename to tests/servers/http/connection_info.rs diff --git a/tests/http/mod.rs b/tests/servers/http/mod.rs similarity index 98% rename from tests/http/mod.rs rename to tests/servers/http/mod.rs index b0d896c99..cb2885df0 100644 --- a/tests/http/mod.rs +++ b/tests/servers/http/mod.rs @@ -3,6 +3,7 @@ pub mod client; pub mod requests; pub mod responses; pub mod test_environment; +pub mod v1; use percent_encoding::NON_ALPHANUMERIC; diff --git a/tests/http/requests/announce.rs b/tests/servers/http/requests/announce.rs similarity index 99% rename from tests/http/requests/announce.rs rename to tests/servers/http/requests/announce.rs index 87aa3425f..414c118ef 100644 --- a/tests/http/requests/announce.rs +++ b/tests/servers/http/requests/announce.rs @@ -6,7 +6,7 @@ use serde_repr::Serialize_repr; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker::tracker::peer::Id; -use crate::http::{percent_encode_byte_array, ByteArray20}; +use crate::servers::http::{percent_encode_byte_array, ByteArray20}; pub struct Query { pub info_hash: ByteArray20, diff --git a/tests/http/requests/mod.rs b/tests/servers/http/requests/mod.rs similarity index 100% rename from tests/http/requests/mod.rs rename to tests/servers/http/requests/mod.rs diff --git a/tests/http/requests/scrape.rs b/tests/servers/http/requests/scrape.rs similarity index 97% rename from tests/http/requests/scrape.rs rename to tests/servers/http/requests/scrape.rs index 979dad540..d7f7cd581 100644 --- a/tests/http/requests/scrape.rs +++ b/tests/servers/http/requests/scrape.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use torrust_tracker::protocol::info_hash::InfoHash; -use crate::http::{percent_encode_byte_array, ByteArray20}; +use crate::servers::http::{percent_encode_byte_array, ByteArray20}; pub struct Query { pub info_hash: Vec, diff --git a/tests/http/responses/announce.rs b/tests/servers/http/responses/announce.rs similarity index 100% rename from tests/http/responses/announce.rs rename to tests/servers/http/responses/announce.rs diff --git a/tests/http/responses/error.rs b/tests/servers/http/responses/error.rs similarity index 100% rename from tests/http/responses/error.rs rename to tests/servers/http/responses/error.rs diff --git a/tests/http/responses/mod.rs b/tests/servers/http/responses/mod.rs similarity index 100% rename from tests/http/responses/mod.rs rename to tests/servers/http/responses/mod.rs diff --git a/tests/http/responses/scrape.rs b/tests/servers/http/responses/scrape.rs similarity index 99% rename from tests/http/responses/scrape.rs rename to tests/servers/http/responses/scrape.rs index 1aea517cf..221ff0a38 100644 --- a/tests/http/responses/scrape.rs +++ b/tests/servers/http/responses/scrape.rs @@ -4,7 +4,7 @@ use std::str; use serde::{self, Deserialize, Serialize}; use serde_bencode::value::Value; -use crate::http::{ByteArray20, InfoHash}; +use crate::servers::http::{ByteArray20, InfoHash}; #[derive(Debug, PartialEq, Default)] pub struct Response { diff --git a/tests/http/test_environment.rs b/tests/servers/http/test_environment.rs similarity index 100% rename from tests/http/test_environment.rs rename to tests/servers/http/test_environment.rs diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs new file mode 100644 index 000000000..eda42f1ee --- /dev/null +++ b/tests/servers/http/v1/contract.rs @@ -0,0 +1,1425 @@ +use torrust_tracker_test_helpers::configuration; + +use crate::servers::http::test_environment::running_test_environment; + +pub type V1 = torrust_tracker::http::v1::launcher::Launcher; + +#[tokio::test] +async fn test_environment_should_be_started_and_stopped() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + test_env.stop().await; +} + +mod for_all_config_modes { + + mod and_running_on_reverse_proxy { + use torrust_tracker_test_helpers::configuration; + + use crate::servers::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; + use crate::servers::http::client::Client; + use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() { + // If the tracker is running behind a reverse proxy, the peer IP is the + // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client. + + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + + let params = QueryBuilder::default().query().params(); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + + let params = QueryBuilder::default().query().params(); + + let response = Client::new(*test_env.bind_address()) + .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") + .await; + + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; + + test_env.stop().await; + } + } + + mod receiving_an_announce_request { + + // Announce request documentation: + // + // BEP 03. The BitTorrent Protocol Specification + // https://www.bittorrent.org/beps/bep_0003.html + // + // BEP 23. Tracker Returns Compact Peer Lists + // https://www.bittorrent.org/beps/bep_0023.html + // + // Vuze (bittorrent client) docs: + // https://wiki.vuze.com/w/Announce + + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::str::FromStr; + + use local_ip_address::local_ip; + use reqwest::Response; + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; + + use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; + use crate::servers::http::asserts::{ + assert_announce_response, assert_bad_announce_request_error_response, assert_cannot_parse_query_param_error_response, + assert_cannot_parse_query_params_error_response, assert_compact_announce_response, assert_empty_announce_response, + assert_is_announce_response, assert_missing_query_params_for_announce_request_error_response, + }; + use crate::servers::http::client::Client; + use crate::servers::http::requests::announce::{Compact, QueryBuilder}; + use crate::servers::http::responses; + use crate::servers::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_respond_if_only_the_mandatory_fields_are_provided() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + params.remove_optional_params(); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_is_announce_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_url_query_component_is_empty() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let response = Client::new(*test_env.bind_address()).get("announce").await; + + assert_missing_query_params_for_announce_request_error_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_url_query_parameters_are_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let invalid_query_param = "a=b=c"; + + let response = Client::new(*test_env.bind_address()) + .get(&format!("announce?{invalid_query_param}")) + .await; + + assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_a_mandatory_field_is_missing() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + // Without `info_hash` param + + let mut params = QueryBuilder::default().query().params(); + + params.info_hash = None; + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "missing param info_hash").await; + + // Without `peer_id` param + + let mut params = QueryBuilder::default().query().params(); + + params.peer_id = None; + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "missing param peer_id").await; + + // Without `port` param + + let mut params = QueryBuilder::default().query().params(); + + params.port = None; + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "missing param port").await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_info_hash_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + for invalid_value in &invalid_info_hashes() { + params.set("info_hash", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_cannot_parse_query_params_error_response(response, "").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_not_fail_when_the_peer_address_param_is_invalid() { + // AnnounceQuery does not even contain the `peer_addr` + // The peer IP is obtained in two ways: + // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP. + // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request HTTP header. + + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_is_announce_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_downloaded_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("downloaded", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_uploaded_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("uploaded", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_peer_id_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = [ + "0", + "-1", + "1.1", + "a", + "-qB0000000000000000", // 19 bytes + "-qB000000000000000000", // 21 bytes + ]; + + for invalid_value in invalid_values { + params.set("peer_id", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_port_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("port", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_left_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("left", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_event_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = [ + "0", + "-1", + "1.1", + "a", + "Started", // It should be lowercase to be valid: `started` + "Stopped", // It should be lowercase to be valid: `stopped` + "Completed", // It should be lowercase to be valid: `completed` + ]; + + for invalid_value in invalid_values { + params.set("event", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_compact_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("compact", invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) + .query(), + ) + .await; + + assert_announce_response( + response, + &Announce { + complete: 1, // the peer for this test + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![], + }, + ) + .await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + + // Announce the new Peer 2. This new peer is non included on the response peer list + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .query(), + ) + .await; + + // It should only contain the previously announced peer + assert_announce_response( + response, + &Announce { + complete: 2, + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![DictionaryPeer::from(previously_announced_peer)], + }, + ) + .await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Announce a peer using IPV4 + let peer_using_ipv4 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) + .build(); + test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await; + + // Announce a peer using IPV6 + let peer_using_ipv6 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + 8080, + )) + .build(); + test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; + + // Announce the new Peer. + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000003")) + .query(), + ) + .await; + + // The newly announced peer is not included on the response peer list, + // but all the previously announced peers should be included regardless the IP version they are using. + assert_announce_response( + response, + &Announce { + complete: 3, + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], + }, + ) + .await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let peer = PeerBuilder::default().build(); + + // Add a peer + test_env.add_torrent_peer(&info_hash, &peer).await; + + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer.peer_id) + .query(); + + assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); + + let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; + + assert_empty_announce_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_compact_response() { + // Tracker Returns Compact Peer Lists + // https://www.bittorrent.org/beps/bep_0023.html + + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + + // Announce the new Peer 2 accepting compact responses + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_compact(Compact::Accepted) + .query(), + ) + .await; + + let expected_response = responses::announce::Compact { + complete: 2, + incomplete: 0, + interval: 120, + min_interval: 120, + peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), + }; + + assert_compact_announce_response(response, &expected_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_not_return_the_compact_response_by_default() { + // code-review: the HTTP tracker does not return the compact response by default if the "compact" + // param is not provided in the announce URL. The BEP 23 suggest to do so. + + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + + // Announce the new Peer 2 without passing the "compact" param + // By default it should respond with the compact peer list + // https://www.bittorrent.org/beps/bep_0023.html + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .without_compact() + .query(), + ) + .await; + + assert!(!is_a_compact_announce_response(response).await); + + test_env.stop().await; + } + + async fn is_a_compact_announce_response(response: Response) -> bool { + let bytes = response.bytes().await.unwrap(); + let compact_announce = serde_bencode::from_bytes::(&bytes); + compact_announce.is_ok() + } + + #[tokio::test] + async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().query()) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp4_connections_handled, 1); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .announce(&QueryBuilder::default().query()) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp6_connections_handled, 1); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() { + // The tracker ignores the peer address in the request param. It uses the client remote ip address. + + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .query(), + ) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp6_connections_handled, 0); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().query()) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp4_announces_handled, 1); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .announce(&QueryBuilder::default().query()) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp6_announces_handled, 1); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() { + // The tracker ignores the peer address in the request param. It uses the client remote ip address. + + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .query(), + ) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp6_announces_handled, 0); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let client_ip = local_ip().unwrap(); + + let client = Client::bind(*test_env.bind_address(), client_ip); + + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); + + client.announce(&announce_query).await; + + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; + + assert_eq!(peer_addr.ip(), client_ip); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + + test_env.stop().await; + } + + #[tokio::test] + async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( + ) { + /* We assume that both the client and tracker share the same public IP. + + client <-> tracker <-> Internet + 127.0.0.1 external_ip = "2.137.87.41" + */ + + let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( + IpAddr::from_str("2.137.87.41").unwrap(), + )) + .await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); + let client_ip = loopback_ip; + + let client = Client::bind(*test_env.bind_address(), client_ip); + + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); + + client.announce(&announce_query).await; + + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; + + assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + + test_env.stop().await; + } + + #[tokio::test] + async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( + ) { + /* We assume that both the client and tracker share the same public IP. + + client <-> tracker <-> Internet + ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" + */ + + let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( + IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), + )) + .await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); + let client_ip = loopback_ip; + + let client = Client::bind(*test_env.bind_address(), client_ip); + + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); + + client.announce(&announce_query).await; + + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; + + assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + + test_env.stop().await; + } + + #[tokio::test] + async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header( + ) { + /* + client <-> http proxy <-> tracker <-> Internet + ip: header: config: peer addr: + 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 + */ + + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + let client = Client::new(*test_env.bind_address()); + + let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); + + client + .announce_with_header( + &announce_query, + "X-Forwarded-For", + "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", + ) + .await; + + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; + + assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap()); + + test_env.stop().await; + } + } + + mod receiving_an_scrape_request { + + // Scrape documentation: + // + // BEP 48. Tracker Protocol Extension: Scrape + // https://www.bittorrent.org/beps/bep_0048.html + // + // Vuze (bittorrent client) docs: + // https://wiki.vuze.com/w/Scrape + + use std::net::IpAddr; + use std::str::FromStr; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; + + use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; + use crate::servers::http::asserts::{ + assert_cannot_parse_query_params_error_response, assert_missing_query_params_for_scrape_request_error_response, + assert_scrape_response, + }; + use crate::servers::http::client::Client; + use crate::servers::http::requests; + use crate::servers::http::requests::scrape::QueryBuilder; + use crate::servers::http::responses::scrape::{self, File, ResponseBuilder}; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + //#[tokio::test] + #[allow(dead_code)] + async fn should_fail_when_the_request_is_empty() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let response = Client::new(*test_env.bind_address()).get("scrape").await; + + assert_missing_query_params_for_scrape_request_error_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_info_hash_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let mut params = QueryBuilder::default().query().params(); + + for invalid_value in &invalid_info_hashes() { + params.set_one_info_hash_param(invalid_value); + + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + + assert_cannot_parse_query_params_error_response(response, "").await; + } + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_no_bytes_pending_to_download() + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 1, + downloaded: 0, + incomplete: 0, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_accept_multiple_infohashes() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .add_info_hash(&info_hash1) + .add_info_hash(&info_hash2) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file(info_hash1.bytes(), File::zeroed()) + .add_file(info_hash2.bytes(), File::zeroed()) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp4_scrapes_handled, 1); + + drop(stats); + + test_env.stop().await; + } + + #[tokio::test] + async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let stats = test_env.tracker.get_stats().await; + + assert_eq!(stats.tcp6_scrapes_handled, 1); + + drop(stats); + + test_env.stop().await; + } + } +} + +mod configured_as_whitelisted { + + mod and_receiving_an_announce_request { + use std::str::FromStr; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; + use crate::servers::http::client::Client; + use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; + + assert_torrent_not_in_whitelist_error_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_allow_announcing_a_whitelisted_torrent() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .tracker + .add_torrent_to_whitelist(&info_hash) + .await + .expect("should add the torrent to the whitelist"); + + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; + + assert_is_announce_response(response).await; + + test_env.stop().await; + } + } + + mod receiving_an_scrape_request { + use std::str::FromStr; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; + + use crate::common::fixtures::PeerBuilder; + use crate::servers::http::asserts::assert_scrape_response; + use crate::servers::http::client::Client; + use crate::servers::http::requests; + use crate::servers::http::responses::scrape::{File, ResponseBuilder}; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + test_env + .tracker + .add_torrent_to_whitelist(&info_hash) + .await + .expect("should add the torrent to the whitelist"); + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + } +} + +mod configured_as_private { + + mod and_receiving_an_announce_request { + use std::str::FromStr; + use std::time::Duration; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::auth::Key; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; + use crate::servers::http::client::Client; + use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_respond_to_authenticated_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + + let response = Client::authenticated(*test_env.bind_address(), key.id()) + .announce(&QueryBuilder::default().query()) + .await; + + assert_is_announce_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; + + assert_authentication_error_response(response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_fail_if_the_key_query_param_cannot_be_parsed() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let invalid_key = "INVALID_KEY"; + + let response = Client::new(*test_env.bind_address()) + .get(&format!( + "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0" + )) + .await; + + assert_authentication_error_response(response).await; + } + + #[tokio::test] + async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + // The tracker does not have this key + let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); + + let response = Client::authenticated(*test_env.bind_address(), unregistered_key) + .announce(&QueryBuilder::default().query()) + .await; + + assert_authentication_error_response(response).await; + + test_env.stop().await; + } + } + + mod receiving_an_scrape_request { + + use std::str::FromStr; + use std::time::Duration; + + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::auth::Key; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; + + use crate::common::fixtures::PeerBuilder; + use crate::servers::http::asserts::{assert_authentication_error_response, assert_scrape_response}; + use crate::servers::http::client::Client; + use crate::servers::http::requests; + use crate::servers::http::responses::scrape::{File, ResponseBuilder}; + use crate::servers::http::test_environment::running_test_environment; + use crate::servers::http::v1::contract::V1; + + #[tokio::test] + async fn should_fail_if_the_key_query_param_cannot_be_parsed() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let invalid_key = "INVALID_KEY"; + + let response = Client::new(*test_env.bind_address()) + .get(&format!( + "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0" + )) + .await; + + assert_authentication_error_response(response).await; + } + + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_real_file_stats_when_the_client_is_authenticated() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + + let response = Client::authenticated(*test_env.bind_address(), key.id()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() { + // There is not authentication error + // code-review: should this really be this way? + + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); + + let response = Client::authenticated(*test_env.bind_address(), false_key) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } + } +} + +mod configured_as_private_and_whitelisted { + + mod and_receiving_an_announce_request {} + + mod receiving_an_scrape_request {} +} diff --git a/tests/servers/http/v1/mod.rs b/tests/servers/http/v1/mod.rs new file mode 100644 index 000000000..2943dbb50 --- /dev/null +++ b/tests/servers/http/v1/mod.rs @@ -0,0 +1 @@ +pub mod contract; diff --git a/tests/servers/mod.rs b/tests/servers/mod.rs new file mode 100644 index 000000000..c19f72020 --- /dev/null +++ b/tests/servers/mod.rs @@ -0,0 +1,5 @@ +extern crate rand; + +mod api; +mod http; +mod udp; diff --git a/tests/udp/asserts.rs b/tests/servers/udp/asserts.rs similarity index 100% rename from tests/udp/asserts.rs rename to tests/servers/udp/asserts.rs diff --git a/tests/udp/client.rs b/tests/servers/udp/client.rs similarity index 98% rename from tests/udp/client.rs rename to tests/servers/udp/client.rs index 0bec03d03..a13845b97 100644 --- a/tests/udp/client.rs +++ b/tests/servers/udp/client.rs @@ -5,7 +5,7 @@ use aquatic_udp_protocol::{Request, Response}; use tokio::net::UdpSocket; use torrust_tracker::udp::MAX_PACKET_SIZE; -use crate::udp::source_address; +use crate::servers::udp::source_address; #[allow(clippy::module_name_repetitions)] pub struct UdpClient { diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs new file mode 100644 index 000000000..311cf5e49 --- /dev/null +++ b/tests/servers/udp/contract.rs @@ -0,0 +1,160 @@ +// UDP tracker documentation: +// +// BEP 15. UDP Tracker Protocol for BitTorrent +// https://www.bittorrent.org/beps/bep_0015.html + +use core::panic; + +use aquatic_udp_protocol::{ConnectRequest, ConnectionId, Response, TransactionId}; +use torrust_tracker::udp::MAX_PACKET_SIZE; +use torrust_tracker_test_helpers::configuration; + +use crate::servers::udp::asserts::is_error_response; +use crate::servers::udp::client::{new_udp_client_connected, UdpTrackerClient}; +use crate::servers::udp::test_environment::running_test_environment; + +fn empty_udp_request() -> [u8; MAX_PACKET_SIZE] { + [0; MAX_PACKET_SIZE] +} + +fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { + [0; MAX_PACKET_SIZE] +} + +async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { + let connect_request = ConnectRequest { transaction_id }; + + client.send(connect_request.into()).await; + + let response = client.receive().await; + + match response { + Response::Connect(connect_response) => connect_response.connection_id, + _ => panic!("error connecting to udp server {:?}", response), + } +} + +#[tokio::test] +async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let client = new_udp_client_connected(&test_env.bind_address().to_string()).await; + + client.send(&empty_udp_request()).await; + + let mut buffer = empty_buffer(); + client.receive(&mut buffer).await; + let response = Response::from_bytes(&buffer, true).unwrap(); + + assert!(is_error_response(&response, "bad request")); +} + +mod receiving_a_connection_request { + use aquatic_udp_protocol::{ConnectRequest, TransactionId}; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::udp::asserts::is_connect_response; + use crate::servers::udp::client::new_udp_tracker_client_connected; + use crate::servers::udp::test_environment::running_test_environment; + + #[tokio::test] + async fn should_return_a_connect_response() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; + + let connect_request = ConnectRequest { + transaction_id: TransactionId(123), + }; + + client.send(connect_request.into()).await; + + let response = client.receive().await; + + assert!(is_connect_response(&response, TransactionId(123))); + } +} + +mod receiving_an_announce_request { + use std::net::Ipv4Addr; + + use aquatic_udp_protocol::{ + AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, + TransactionId, + }; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::udp::asserts::is_ipv4_announce_response; + use crate::servers::udp::client::new_udp_tracker_client_connected; + use crate::servers::udp::contract::send_connection_request; + use crate::servers::udp::test_environment::running_test_environment; + + #[tokio::test] + async fn should_return_an_announce_response() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; + + let connection_id = send_connection_request(TransactionId(123), &client).await; + + // Send announce request + + let announce_request = AnnounceRequest { + connection_id: ConnectionId(connection_id.0), + transaction_id: TransactionId(123i32), + info_hash: InfoHash([0u8; 20]), + peer_id: PeerId([255u8; 20]), + bytes_downloaded: NumberOfBytes(0i64), + bytes_uploaded: NumberOfBytes(0i64), + bytes_left: NumberOfBytes(0i64), + event: AnnounceEvent::Started, + ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), + key: PeerKey(0u32), + peers_wanted: NumberOfPeers(1i32), + port: Port(client.udp_client.socket.local_addr().unwrap().port()), + }; + + client.send(announce_request.into()).await; + + let response = client.receive().await; + + assert!(is_ipv4_announce_response(&response)); + } +} + +mod receiving_an_scrape_request { + use aquatic_udp_protocol::{ConnectionId, InfoHash, ScrapeRequest, TransactionId}; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::udp::asserts::is_scrape_response; + use crate::servers::udp::client::new_udp_tracker_client_connected; + use crate::servers::udp::contract::send_connection_request; + use crate::servers::udp::test_environment::running_test_environment; + + #[tokio::test] + async fn should_return_a_scrape_response() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; + + let connection_id = send_connection_request(TransactionId(123), &client).await; + + // Send scrape request + + // Full scrapes are not allowed you need to pass an array of info hashes otherwise + // it will return "bad request" error with empty vector + let info_hashes = vec![InfoHash([0u8; 20])]; + + let scrape_request = ScrapeRequest { + connection_id: ConnectionId(connection_id.0), + transaction_id: TransactionId(123i32), + info_hashes, + }; + + client.send(scrape_request.into()).await; + + let response = client.receive().await; + + assert!(is_scrape_response(&response)); + } +} diff --git a/tests/udp/mod.rs b/tests/servers/udp/mod.rs similarity index 91% rename from tests/udp/mod.rs rename to tests/servers/udp/mod.rs index f45a4a4f9..d39c37153 100644 --- a/tests/udp/mod.rs +++ b/tests/servers/udp/mod.rs @@ -1,5 +1,6 @@ pub mod asserts; pub mod client; +pub mod contract; pub mod test_environment; /// Generates the source address for the UDP client diff --git a/tests/udp/test_environment.rs b/tests/servers/udp/test_environment.rs similarity index 100% rename from tests/udp/test_environment.rs rename to tests/servers/udp/test_environment.rs diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs deleted file mode 100644 index 3219bc987..000000000 --- a/tests/tracker_api.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// Integration tests for the tracker API -/// -/// ```text -/// cargo test --test tracker_api -/// ``` -mod api; -mod common; diff --git a/tests/udp_tracker.rs b/tests/udp_tracker.rs deleted file mode 100644 index 3fe78c03d..000000000 --- a/tests/udp_tracker.rs +++ /dev/null @@ -1,173 +0,0 @@ -/// Integration tests for UDP tracker server -/// -/// ```text -/// cargo test `udp_tracker_server` -- --nocapture -/// ``` -extern crate rand; - -mod common; -mod udp; - -mod udp_tracker_server { - - // UDP tracker documentation: - // - // BEP 15. UDP Tracker Protocol for BitTorrent - // https://www.bittorrent.org/beps/bep_0015.html - - use core::panic; - - use aquatic_udp_protocol::{ConnectRequest, ConnectionId, Response, TransactionId}; - use torrust_tracker::udp::MAX_PACKET_SIZE; - use torrust_tracker_test_helpers::configuration; - - use crate::udp::asserts::is_error_response; - use crate::udp::client::{new_udp_client_connected, UdpTrackerClient}; - use crate::udp::test_environment::running_test_environment; - - fn empty_udp_request() -> [u8; MAX_PACKET_SIZE] { - [0; MAX_PACKET_SIZE] - } - - fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { - [0; MAX_PACKET_SIZE] - } - - async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { - let connect_request = ConnectRequest { transaction_id }; - - client.send(connect_request.into()).await; - - let response = client.receive().await; - - match response { - Response::Connect(connect_response) => connect_response.connection_id, - _ => panic!("error connecting to udp server {:?}", response), - } - } - - #[tokio::test] - async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let client = new_udp_client_connected(&test_env.bind_address().to_string()).await; - - client.send(&empty_udp_request()).await; - - let mut buffer = empty_buffer(); - client.receive(&mut buffer).await; - let response = Response::from_bytes(&buffer, true).unwrap(); - - assert!(is_error_response(&response, "bad request")); - } - - mod receiving_a_connection_request { - use aquatic_udp_protocol::{ConnectRequest, TransactionId}; - use torrust_tracker_test_helpers::configuration; - - use crate::udp::asserts::is_connect_response; - use crate::udp::client::new_udp_tracker_client_connected; - use crate::udp::test_environment::running_test_environment; - - #[tokio::test] - async fn should_return_a_connect_response() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; - - let connect_request = ConnectRequest { - transaction_id: TransactionId(123), - }; - - client.send(connect_request.into()).await; - - let response = client.receive().await; - - assert!(is_connect_response(&response, TransactionId(123))); - } - } - - mod receiving_an_announce_request { - use std::net::Ipv4Addr; - - use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, - TransactionId, - }; - use torrust_tracker_test_helpers::configuration; - - use crate::udp::asserts::is_ipv4_announce_response; - use crate::udp::client::new_udp_tracker_client_connected; - use crate::udp::test_environment::running_test_environment; - use crate::udp_tracker_server::send_connection_request; - - #[tokio::test] - async fn should_return_an_announce_response() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; - - let connection_id = send_connection_request(TransactionId(123), &client).await; - - // Send announce request - - let announce_request = AnnounceRequest { - connection_id: ConnectionId(connection_id.0), - transaction_id: TransactionId(123i32), - info_hash: InfoHash([0u8; 20]), - peer_id: PeerId([255u8; 20]), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port: Port(client.udp_client.socket.local_addr().unwrap().port()), - }; - - client.send(announce_request.into()).await; - - let response = client.receive().await; - - assert!(is_ipv4_announce_response(&response)); - } - } - - mod receiving_an_scrape_request { - use aquatic_udp_protocol::{ConnectionId, InfoHash, ScrapeRequest, TransactionId}; - use torrust_tracker_test_helpers::configuration; - - use crate::udp::asserts::is_scrape_response; - use crate::udp::client::new_udp_tracker_client_connected; - use crate::udp::test_environment::running_test_environment; - use crate::udp_tracker_server::send_connection_request; - - #[tokio::test] - async fn should_return_a_scrape_response() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; - - let connection_id = send_connection_request(TransactionId(123), &client).await; - - // Send scrape request - - // Full scrapes are not allowed you need to pass an array of info hashes otherwise - // it will return "bad request" error with empty vector - let info_hashes = vec![InfoHash([0u8; 20])]; - - let scrape_request = ScrapeRequest { - connection_id: ConnectionId(connection_id.0), - transaction_id: TransactionId(123i32), - info_hashes, - }; - - client.send(scrape_request.into()).await; - - let response = client.receive().await; - - assert!(is_scrape_response(&response)); - } - } -}