diff --git a/Cargo.lock b/Cargo.lock index cfd8aaba8..ce179501f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2972,10 +2972,56 @@ dependencies = [ "thiserror", "tokio", "toml", + "torrust-tracker-configuration", + "torrust-tracker-located-error", + "torrust-tracker-primitives", + "torrust-tracker-test-helpers", "uuid 1.2.1", "warp", ] +[[package]] +name = "torrust-tracker-configuration" +version = "2.3.0" +dependencies = [ + "config", + "log", + "serde", + "serde_with", + "thiserror", + "toml", + "torrust-tracker-located-error", + "torrust-tracker-primitives", + "uuid 1.2.1", +] + +[[package]] +name = "torrust-tracker-located-error" +version = "2.3.0" +dependencies = [ + "log", + "thiserror", +] + +[[package]] +name = "torrust-tracker-primitives" +version = "2.3.0" +dependencies = [ + "derive_more", + "serde", +] + +[[package]] +name = "torrust-tracker-test-helpers" +version = "2.3.0" +dependencies = [ + "lazy_static", + "rand", + "tokio", + "torrust-tracker-configuration", + "torrust-tracker-primitives", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index fa126a152..740a5805e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,19 @@ [package] -edition = "2021" name = "torrust-tracker" -version = "2.3.0" -license = "AGPL-3.0" -authors = ["Mick van Dijke "] description = "A feature rich BitTorrent tracker." -repository = "https://github.com/torrust/torrust-tracker" - -[profile.dev] -debug = 1 -opt-level = 1 -lto = "thin" +license = "AGPL-3.0" +authors.workspace = true +edition.workspace = true +version.workspace = true -[profile.release] -debug = 1 -opt-level = 3 -lto = "fat" +[workspace.package] +authors = ["Nautilus Cyberneering , Mick van Dijke "] +edition = "2021" +repository = "https://github.com/torrust/torrust-tracker" +version = "2.3.0" [dependencies] -tokio = { version = "1", features = [ - "rt-multi-thread", - "net", - "sync", - "macros", - "signal", -] } - +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] } serde = { version = "1.0", features = ["derive"] } serde_bencode = "^0.2.3" serde_json = "1.0" @@ -34,37 +22,32 @@ hex = "0.4.3" percent-encoding = "2" binascii = "0.1" lazy_static = "1.4" - openssl = { version = "0.10", features = ["vendored"] } - warp = { version = "0.3", features = ["tls"] } - config = "0.13" toml = "0.5" - log = { version = "0.4", features = ["release_max_level_info"] } fern = "0.6" chrono = "0.4" - r2d2 = "0.8" r2d2_mysql = "21" r2d2_sqlite = { version = "0.21", features = ["bundled"] } - rand = "0.8" derive_more = "0.99" thiserror = "1.0" futures = "0.3" async-trait = "0.1" - aquatic_udp_protocol = "0.2" uuid = { version = "1", features = ["v4"] } axum = "0.6.1" axum-server = { version = "0.4.4", features = ["tls-rustls"] } axum-client-ip = "0.4.0" bip_bencode = "0.4.4" +torrust-tracker-primitives = { path = "packages/primitives" } +torrust-tracker-configuration = { path = "packages/configuration" } +torrust-tracker-located-error = { path = "packages/located-error" } multimap = "0.8.3" - [dev-dependencies] mockall = "0.11" reqwest = { version = "0.11.13", features = ["json"] } @@ -72,3 +55,22 @@ serde_urlencoded = "0.7.1" serde_repr = "0.1.10" serde_bytes = "0.11.8" local-ip-address = "0.5.1" +torrust-tracker-test-helpers = { path = "packages/test-helpers" } + +[workspace] +members = [ + "packages/configuration", + "packages/primitives", + "packages/test-helpers", + "packages/located-error", +] + +[profile.dev] +debug = 1 +opt-level = 1 +lto = "thin" + +[profile.release] +debug = 1 +opt-level = 3 +lto = "fat" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml new file mode 100644 index 000000000..a6f1740a0 --- /dev/null +++ b/packages/configuration/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "torrust-tracker-configuration" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_with = "2.0" +config = "0.13" +toml = "0.5" +log = { version = "0.4", features = ["release_max_level_info"] } +thiserror = "1.0" +torrust-tracker-primitives = { path = "../primitives" } +torrust-tracker-located-error = { path = "../located-error" } +uuid = { version = "1", features = ["v4"] } diff --git a/src/config.rs b/packages/configuration/src/lib.rs similarity index 83% rename from src/config.rs rename to packages/configuration/src/lib.rs index 7ed0f9fa7..d42c82df9 100644 --- a/src/config.rs +++ b/packages/configuration/src/lib.rs @@ -8,24 +8,20 @@ use std::{env, fs}; use config::{Config, ConfigError, File, FileFormat}; use log::warn; -use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; -use {std, toml}; +use torrust_tracker_located_error::{Located, LocatedError}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; -use crate::databases::driver::Driver; -use crate::located_error::{Located, LocatedError}; -use crate::tracker::mode; - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { pub enabled: bool, pub bind_address: String, } #[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { pub enabled: bool, pub bind_address: String, @@ -62,8 +58,8 @@ impl HttpApi { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { pub log_level: Option, - pub mode: mode::Mode, - pub db_driver: Driver, + pub mode: TrackerMode, + pub db_driver: DatabaseDriver, pub db_path: String, pub announce_interval: u32, pub min_announce_interval: u32, @@ -105,58 +101,12 @@ impl From for Error { } } -/// This configuration is used for testing. It generates random config values so they do not collide -/// if you run more than one tracker at the same time. -/// -/// # Panics -/// -/// Will panic if it can't convert the temp file path to string -#[must_use] -pub fn ephemeral_configuration() -> Configuration { - // todo: disable services that are not needed. - // For example: a test for the UDP tracker should disable the API and HTTP tracker. - - let mut config = Configuration { - log_level: Some("off".to_owned()), // Change to `debug` for tests debugging - ..Default::default() - }; - - // Ephemeral socket address for API - let api_port = random_port(); - config.http_api.enabled = true; - config.http_api.bind_address = format!("127.0.0.1:{}", &api_port); - - // Ephemeral socket address for UDP tracker - let upd_port = random_port(); - config.udp_trackers[0].enabled = true; - config.udp_trackers[0].bind_address = format!("127.0.0.1:{}", &upd_port); - - // Ephemeral socket address for HTTP tracker - let http_port = random_port(); - config.http_trackers[0].enabled = true; - config.http_trackers[0].bind_address = format!("127.0.0.1:{}", &http_port); - - // Ephemeral sqlite database - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("data_{}_{}_{}.db", &api_port, &upd_port, &http_port)); - config.db_path = temp_file.to_str().unwrap().to_owned(); - - config -} - -fn random_port() -> u16 { - // todo: this may produce random test failures because two tests can try to bind the same port. - // We could create a pool of available ports (with read/write lock) - let mut rng = thread_rng(); - rng.gen_range(49152..65535) -} - impl Default for Configuration { fn default() -> Self { let mut configuration = Configuration { log_level: Option::from(String::from("info")), - mode: mode::Mode::Public, - db_driver: Driver::Sqlite3, + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, db_path: String::from("./storage/database/data.db"), announce_interval: 120, min_announce_interval: 120, @@ -266,7 +216,7 @@ impl Configuration { #[cfg(test)] mod tests { - use crate::config::Configuration; + use crate::Configuration; #[cfg(test)] fn default_config_toml() -> String { @@ -325,7 +275,7 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.external_ip, Option::Some(String::from("0.0.0.0"))); + assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); } #[test] diff --git a/packages/located-error/Cargo.toml b/packages/located-error/Cargo.toml new file mode 100644 index 000000000..c4b2ef726 --- /dev/null +++ b/packages/located-error/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "torrust-tracker-located-error" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +log = { version = "0.4", features = ["release_max_level_info"] } +thiserror = "1.0" diff --git a/src/located_error.rs b/packages/located-error/src/lib.rs similarity index 100% rename from src/located_error.rs rename to packages/located-error/src/lib.rs diff --git a/packages/primitives/Cargo.toml b/packages/primitives/Cargo.toml new file mode 100644 index 000000000..9aec28384 --- /dev/null +++ b/packages/primitives/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "torrust-tracker-primitives" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +derive_more = "0.99" diff --git a/src/tracker/mode.rs b/packages/primitives/src/lib.rs similarity index 70% rename from src/tracker/mode.rs rename to packages/primitives/src/lib.rs index a0dba6e67..bcd48145f 100644 --- a/src/tracker/mode.rs +++ b/packages/primitives/src/lib.rs @@ -1,8 +1,14 @@ -use serde; use serde::{Deserialize, Serialize}; +// TODO: Move to the database crate once that gets its own crate. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] +pub enum DatabaseDriver { + Sqlite3, + MySQL, +} + #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] -pub enum Mode { +pub enum TrackerMode { // Will track every new info hash and serve every peer. #[serde(rename = "public")] Public, diff --git a/packages/test-helpers/Cargo.toml b/packages/test-helpers/Cargo.toml new file mode 100644 index 000000000..2f942bac7 --- /dev/null +++ b/packages/test-helpers/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "torrust-tracker-test-helpers" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] } +lazy_static = "1.4" +rand = "0.8.5" +torrust-tracker-configuration = { path = "../configuration"} +torrust-tracker-primitives = { path = "../primitives"} diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs new file mode 100644 index 000000000..0b7a269ff --- /dev/null +++ b/packages/test-helpers/src/configuration.rs @@ -0,0 +1,123 @@ +use std::env; +use std::net::IpAddr; + +use torrust_tracker_configuration::Configuration; +use torrust_tracker_primitives::TrackerMode; + +use crate::random; + +/// This configuration is used for testing. It generates random config values so they do not collide +/// if you run more than one tracker at the same time. +/// +/// # Panics +/// +/// Will panic if it can't convert the temp file path to string +#[must_use] +pub fn ephemeral() -> Configuration { + // todo: disable services that are not needed. + // For example: a test for the UDP tracker should disable the API and HTTP tracker. + + let mut config = Configuration { + log_level: Some("off".to_owned()), // Change to `debug` for tests debugging + ..Default::default() + }; + + // Ephemeral socket address for API + let api_port = 0u16; + config.http_api.enabled = true; + config.http_api.bind_address = format!("127.0.0.1:{}", &api_port); + + // Ephemeral socket address for UDP tracker + let udp_port = 0u16; + config.udp_trackers[0].enabled = true; + config.udp_trackers[0].bind_address = format!("127.0.0.1:{}", &udp_port); + + // Ephemeral socket address for HTTP tracker + let http_port = 0u16; + config.http_trackers[0].enabled = true; + config.http_trackers[0].bind_address = format!("127.0.0.1:{}", &http_port); + + // Ephemeral sqlite database + let temp_directory = env::temp_dir(); + let random_db_id = random::string(16); + let temp_file = temp_directory.join(format!("data_{random_db_id}.db")); + config.db_path = temp_file.to_str().unwrap().to_owned(); + + config +} + +#[must_use] +pub fn ephemeral_with_reverse_proxy() -> Configuration { + let mut cfg = ephemeral(); + + cfg.on_reverse_proxy = true; + + cfg +} + +#[must_use] +pub fn ephemeral_without_reverse_proxy() -> Configuration { + let mut cfg = ephemeral(); + + cfg.on_reverse_proxy = false; + + cfg +} + +#[must_use] +pub fn ephemeral_mode_public() -> Configuration { + let mut cfg = ephemeral(); + + cfg.mode = TrackerMode::Public; + + cfg +} + +#[must_use] +pub fn ephemeral_mode_private() -> Configuration { + let mut cfg = ephemeral(); + + cfg.mode = TrackerMode::Private; + + cfg +} + +#[must_use] +pub fn ephemeral_mode_whitelisted() -> Configuration { + let mut cfg = ephemeral(); + + cfg.mode = TrackerMode::Listed; + + cfg +} + +#[must_use] +pub fn ephemeral_mode_private_whitelisted() -> Configuration { + let mut cfg = ephemeral(); + + cfg.mode = TrackerMode::PrivateListed; + + cfg +} + +#[must_use] +pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { + let mut cfg = ephemeral(); + + cfg.external_ip = Some(ip.to_string()); + + cfg +} + +#[must_use] +pub fn ephemeral_ipv6() -> Configuration { + let mut cfg = ephemeral(); + + let ipv6 = format!("[::]:{}", 0); + + cfg.http_api.bind_address = ipv6.clone(); + cfg.http_trackers[0].bind_address = ipv6.clone(); + cfg.udp_trackers[0].bind_address = ipv6; + + cfg +} diff --git a/packages/test-helpers/src/lib.rs b/packages/test-helpers/src/lib.rs new file mode 100644 index 000000000..e0f350131 --- /dev/null +++ b/packages/test-helpers/src/lib.rs @@ -0,0 +1,2 @@ +pub mod configuration; +pub mod random; diff --git a/packages/test-helpers/src/random.rs b/packages/test-helpers/src/random.rs new file mode 100644 index 000000000..ffb2ccd6f --- /dev/null +++ b/packages/test-helpers/src/random.rs @@ -0,0 +1,7 @@ +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +/// Returns a random alphanumeric string of a certain size. +pub fn string(size: usize) -> String { + thread_rng().sample_iter(&Alphanumeric).take(size).map(char::from).collect() +} diff --git a/src/apis/middlewares/auth.rs b/src/apis/middlewares/auth.rs index 758ba1cda..f2745d42e 100644 --- a/src/apis/middlewares/auth.rs +++ b/src/apis/middlewares/auth.rs @@ -5,9 +5,9 @@ use axum::http::Request; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use serde::Deserialize; +use torrust_tracker_configuration::{Configuration, HttpApi}; use crate::apis::responses::unhandled_rejection_response; -use crate::config::{Configuration, HttpApi}; #[derive(Deserialize, Debug)] pub struct QueryParams { diff --git a/src/apis/routes.rs b/src/apis/routes.rs index 281979aa5..ecc51090c 100644 --- a/src/apis/routes.rs +++ b/src/apis/routes.rs @@ -10,7 +10,8 @@ use super::handlers::{ use super::middlewares::auth::auth; use crate::tracker::Tracker; -pub fn router(tracker: &Arc) -> Router { +#[allow(clippy::needless_pass_by_value)] +pub fn router(tracker: Arc) -> Router { Router::new() // Stats .route("/api/stats", get(get_stats_handler).with_state(tracker.clone())) diff --git a/src/apis/server.rs b/src/apis/server.rs index bbb3e5852..a283bbc54 100644 --- a/src/apis/server.rs +++ b/src/apis/server.rs @@ -1,16 +1,187 @@ use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use axum_server::Handle; +use futures::future::BoxFuture; use futures::Future; use log::info; use warp::hyper; use super::routes::router; +use crate::signals::shutdown_signal; use crate::tracker::Tracker; -pub fn start(socket_addr: SocketAddr, tracker: &Arc) -> impl Future> { +#[derive(Debug)] +pub enum Error { + Error(String), +} + +#[allow(clippy::module_name_repetitions)] +pub type StoppedApiServer = ApiServer; +#[allow(clippy::module_name_repetitions)] +pub type RunningApiServer = ApiServer; + +#[allow(clippy::module_name_repetitions)] +pub struct ApiServer { + pub cfg: torrust_tracker_configuration::HttpApi, + pub state: S, +} + +pub struct Stopped; + +pub struct Running { + pub bind_addr: SocketAddr, + task_killer: tokio::sync::oneshot::Sender, + task: tokio::task::JoinHandle<()>, +} + +impl ApiServer { + #[must_use] + pub fn new(cfg: torrust_tracker_configuration::HttpApi) -> Self { + Self { cfg, state: Stopped {} } + } + + /// # Errors + /// + /// It would return an error if no `SocketAddr` is returned after launching the server. + pub async fn start(self, tracker: Arc) -> Result, Error> { + let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::(); + let (addr_sender, addr_receiver) = tokio::sync::oneshot::channel::(); + + let configuration = self.cfg.clone(); + + let task = tokio::spawn(async move { + let (bind_addr, server) = Launcher::start(&configuration, tracker, shutdown_signal(shutdown_receiver)); + + addr_sender.send(bind_addr).expect("Could not return SocketAddr."); + + server.await; + }); + + let bind_address = addr_receiver + .await + .map_err(|_| Error::Error("Could not receive bind_address.".to_string()))?; + + Ok(ApiServer { + cfg: self.cfg, + state: Running { + bind_addr: bind_address, + task_killer: shutdown_sender, + task, + }, + }) + } +} + +impl ApiServer { + /// # Errors + /// + /// It would return an error if the channel for the task killer signal was closed. + pub async fn stop(self) -> Result, Error> { + self.state + .task_killer + .send(0) + .map_err(|_| Error::Error("Task killer channel was closed.".to_string()))?; + + let _ = self.state.task.await; + + Ok(ApiServer { + cfg: self.cfg, + state: Stopped {}, + }) + } +} + +struct Launcher; + +impl Launcher { + pub fn start( + cfg: &torrust_tracker_configuration::HttpApi, + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static, + { + let addr = SocketAddr::from_str(&cfg.bind_address).expect("bind_address is not a valid SocketAddr."); + let tcp_listener = std::net::TcpListener::bind(addr).expect("Could not bind tcp_listener to address."); + let bind_addr = tcp_listener + .local_addr() + .expect("Could not get local_addr from tcp_listener."); + + if let (true, Some(ssl_cert_path), Some(ssl_key_path)) = (&cfg.ssl_enabled, &cfg.ssl_cert_path, &cfg.ssl_key_path) { + let server = Self::start_tls_with_graceful_shutdown( + tcp_listener, + (ssl_cert_path.to_string(), ssl_key_path.to_string()), + tracker, + shutdown_signal, + ); + + (bind_addr, server) + } else { + let server = Self::start_with_graceful_shutdown(tcp_listener, tracker, shutdown_signal); + + (bind_addr, server) + } + } + + pub fn start_with_graceful_shutdown( + tcp_listener: std::net::TcpListener, + tracker: Arc, + shutdown_signal: F, + ) -> BoxFuture<'static, ()> + where + F: Future + Send + 'static, + { + let app = router(tracker); + + Box::pin(async { + axum::Server::from_tcp(tcp_listener) + .expect("Could not bind to tcp listener.") + .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown_signal) + .await + .expect("Axum server crashed."); + }) + } + + pub fn start_tls_with_graceful_shutdown( + tcp_listener: std::net::TcpListener, + (ssl_cert_path, ssl_key_path): (String, String), + tracker: Arc, + shutdown_signal: F, + ) -> BoxFuture<'static, ()> + where + F: Future + Send + 'static, + { + let app = router(tracker); + + let handle = Handle::new(); + + let cloned_handle = handle.clone(); + + tokio::task::spawn_local(async move { + shutdown_signal.await; + cloned_handle.shutdown(); + }); + + Box::pin(async { + let tls_config = RustlsConfig::from_pem_file(ssl_cert_path, ssl_key_path) + .await + .expect("Could not read tls cert."); + + axum_server::from_tcp_rustls(tcp_listener, tls_config) + .handle(handle) + .serve(app.into_make_service_with_connect_info::()) + .await + .expect("Axum server crashed."); + }) + } +} + +pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl Future> { let app = router(tracker); let server = axum::Server::bind(&socket_addr).serve(app.into_make_service()); @@ -24,7 +195,7 @@ pub fn start(socket_addr: SocketAddr, tracker: &Arc) -> impl Future, + tracker: Arc, ) -> impl Future> { let app = router(tracker); @@ -41,3 +212,36 @@ pub fn start_tls( .handle(handle) .serve(app.into_make_service()) } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use torrust_tracker_configuration::Configuration; + use torrust_tracker_test_helpers::configuration; + + use crate::apis::server::ApiServer; + use crate::tracker; + use crate::tracker::statistics; + + fn tracker_configuration() -> Arc { + Arc::new(configuration::ephemeral()) + } + + #[tokio::test] + async fn it_should_be_able_to_start_from_stopped_state_and_then_stop_again() { + let cfg = tracker_configuration(); + + let tracker = Arc::new(tracker::Tracker::new(cfg.clone(), None, statistics::Repo::new()).unwrap()); + + let stopped_api_server = ApiServer::new(cfg.http_api.clone()); + + let running_api_server_result = stopped_api_server.start(tracker).await; + + assert!(running_api_server_result.is_ok()); + + let running_api_server = running_api_server_result.unwrap(); + + assert!(running_api_server.stop().await.is_ok()); + } +} diff --git a/src/databases/driver.rs b/src/databases/driver.rs index c601f1866..4ce6ea515 100644 --- a/src/databases/driver.rs +++ b/src/databases/driver.rs @@ -1,30 +1,22 @@ -use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::DatabaseDriver; use super::error::Error; use super::mysql::Mysql; use super::sqlite::Sqlite; use super::{Builder, Database}; -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] -pub enum Driver { - Sqlite3, - MySQL, -} - -impl Driver { - /// . - /// - /// # Errors - /// - /// This function will return an error if unable to connect to the database. - pub fn build(&self, db_path: &str) -> Result, Error> { - let database = match self { - Driver::Sqlite3 => Builder::::build(db_path), - Driver::MySQL => Builder::::build(db_path), - }?; +/// . +/// +/// # Errors +/// +/// This function will return an error if unable to connect to the database. +pub fn build(driver: &DatabaseDriver, db_path: &str) -> Result, Error> { + let database = match driver { + DatabaseDriver::Sqlite3 => Builder::::build(db_path), + DatabaseDriver::MySQL => Builder::::build(db_path), + }?; - database.create_database_tables().expect("Could not create database tables."); + database.create_database_tables().expect("Could not create database tables."); - Ok(database) - } + Ok(database) } diff --git a/src/databases/error.rs b/src/databases/error.rs index 4bee82f19..68b732190 100644 --- a/src/databases/error.rs +++ b/src/databases/error.rs @@ -2,47 +2,46 @@ use std::panic::Location; use std::sync::Arc; use r2d2_mysql::mysql::UrlError; - -use super::driver::Driver; -use crate::located_error::{Located, LocatedError}; +use torrust_tracker_located_error::{Located, LocatedError}; +use torrust_tracker_primitives::DatabaseDriver; #[derive(thiserror::Error, Debug, Clone)] pub enum Error { #[error("The {driver} query unexpectedly returned nothing: {source}")] QueryReturnedNoRows { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, - driver: Driver, + driver: DatabaseDriver, }, #[error("The {driver} query was malformed: {source}")] InvalidQuery { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, - driver: Driver, + driver: DatabaseDriver, }, #[error("Unable to insert record into {driver} database, {location}")] InsertFailed { location: &'static Location<'static>, - driver: Driver, + driver: DatabaseDriver, }, #[error("Failed to remove record from {driver} database, error-code: {error_code}, {location}")] DeleteFailed { location: &'static Location<'static>, error_code: usize, - driver: Driver, + driver: DatabaseDriver, }, #[error("Failed to connect to {driver} database: {source}")] ConnectionError { source: LocatedError<'static, UrlError>, - driver: Driver, + driver: DatabaseDriver, }, #[error("Failed to create r2d2 {driver} connection pool: {source}")] ConnectionPool { source: LocatedError<'static, r2d2::Error>, - driver: Driver, + driver: DatabaseDriver, }, } @@ -52,11 +51,11 @@ impl From for Error { match err { r2d2_sqlite::rusqlite::Error::QueryReturnedNoRows => Error::QueryReturnedNoRows { source: (Arc::new(err) as Arc).into(), - driver: Driver::Sqlite3, + driver: DatabaseDriver::Sqlite3, }, _ => Error::InvalidQuery { source: (Arc::new(err) as Arc).into(), - driver: Driver::Sqlite3, + driver: DatabaseDriver::Sqlite3, }, } } @@ -68,7 +67,7 @@ impl From for Error { let e: Arc = Arc::new(err); Error::InvalidQuery { source: e.into(), - driver: Driver::MySQL, + driver: DatabaseDriver::MySQL, } } } @@ -78,14 +77,14 @@ impl From for Error { fn from(err: UrlError) -> Self { Self::ConnectionError { source: Located(err).into(), - driver: Driver::MySQL, + driver: DatabaseDriver::MySQL, } } } -impl From<(r2d2::Error, Driver)> for Error { +impl From<(r2d2::Error, DatabaseDriver)> for Error { #[track_caller] - fn from(e: (r2d2::Error, Driver)) -> Self { + fn from(e: (r2d2::Error, DatabaseDriver)) -> Self { let (err, driver) = e; Self::ConnectionPool { source: Located(err).into(), diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 4bb28f050..c8117a45c 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -7,14 +7,14 @@ use r2d2::Pool; use r2d2_mysql::mysql::prelude::Queryable; use r2d2_mysql::mysql::{params, Opts, OptsBuilder}; use r2d2_mysql::MysqlConnectionManager; +use torrust_tracker_primitives::DatabaseDriver; -use super::driver::Driver; use crate::databases::{Database, Error}; use crate::protocol::common::AUTH_KEY_LENGTH; use crate::protocol::info_hash::InfoHash; use crate::tracker::auth::{self, Key}; -const DRIVER: Driver = Driver::MySQL; +const DRIVER: DatabaseDriver = DatabaseDriver::MySQL; pub struct Mysql { pool: Pool, diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 8fac09e47..4bf2931de 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -4,14 +4,14 @@ use std::str::FromStr; use async_trait::async_trait; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; +use torrust_tracker_primitives::DatabaseDriver; -use super::driver::Driver; use crate::databases::{Database, Error}; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; use crate::tracker::auth::{self, Key}; -const DRIVER: Driver = Driver::Sqlite3; +const DRIVER: DatabaseDriver = DatabaseDriver::Sqlite3; pub struct Sqlite { pool: Pool, @@ -24,7 +24,7 @@ impl Database for Sqlite { /// Will return `r2d2::Error` if `db_path` is not able to create `SqLite` database. fn new(db_path: &str) -> Result { let cm = SqliteConnectionManager::file(db_path); - Pool::new(cm).map_or_else(|err| Err((err, Driver::Sqlite3).into()), |pool| Ok(Sqlite { pool })) + Pool::new(cm).map_or_else(|err| Err((err, DatabaseDriver::Sqlite3).into()), |pool| Ok(Sqlite { pool })) } fn create_database_tables(&self) -> Result<(), Error> { diff --git a/src/http/axum_implementation/handlers/announce.rs b/src/http/axum_implementation/handlers/announce.rs index 9a92b243d..ebb8c8586 100644 --- a/src/http/axum_implementation/handlers/announce.rs +++ b/src/http/axum_implementation/handlers/announce.rs @@ -138,54 +138,30 @@ fn map_to_aquatic_event(event: &Option) -> AnnounceEvent { #[cfg(test)] mod tests { - use std::sync::Arc; - use crate::config::{ephemeral_configuration, Configuration}; + use torrust_tracker_test_helpers::configuration; + use crate::http::axum_implementation::requests::announce::Announce; use crate::http::axum_implementation::responses; use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; use crate::protocol::info_hash::InfoHash; - use crate::tracker::mode::Mode; - use crate::tracker::statistics::Keeper; + use crate::tracker::services::common::tracker_factory; use crate::tracker::{peer, Tracker}; fn private_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Private; - tracker_factory(configuration) + tracker_factory(configuration::ephemeral_mode_private().into()) } - fn listed_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Listed; - tracker_factory(configuration) + fn whitelisted_tracker() -> Tracker { + tracker_factory(configuration::ephemeral_mode_whitelisted().into()) } fn tracker_on_reverse_proxy() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.on_reverse_proxy = true; - tracker_factory(configuration) + tracker_factory(configuration::ephemeral_with_reverse_proxy().into()) } fn tracker_not_on_reverse_proxy() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.on_reverse_proxy = false; - tracker_factory(configuration) - } - - fn tracker_factory(configuration: Configuration) -> Tracker { - // code-review: the tracker initialization is duplicated in many places. Consider make this function public. - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - match Tracker::new(&Arc::new(configuration), Some(stats_event_sender), stats_repository) { - Ok(tracker) => tracker, - Err(error) => { - panic!("{}", error) - } - } + tracker_factory(configuration::ephemeral_without_reverse_proxy().into()) } fn sample_announce_request() -> Announce { @@ -261,13 +237,13 @@ mod tests { use std::sync::Arc; - use super::{listed_tracker, sample_announce_request, sample_client_ip_sources}; + use super::{sample_announce_request, sample_client_ip_sources, whitelisted_tracker}; use crate::http::axum_implementation::handlers::announce::handle_announce; use crate::http::axum_implementation::handlers::announce::tests::assert_error_response; #[tokio::test] async fn it_should_fail_when_the_announced_torrent_is_not_whitelisted() { - let tracker = Arc::new(listed_tracker()); + let tracker = Arc::new(whitelisted_tracker()); let announce_request = sample_announce_request(); diff --git a/src/http/axum_implementation/handlers/scrape.rs b/src/http/axum_implementation/handlers/scrape.rs index ee59b80a3..fd316882d 100644 --- a/src/http/axum_implementation/handlers/scrape.rs +++ b/src/http/axum_implementation/handlers/scrape.rs @@ -96,54 +96,30 @@ fn build_response(scrape_data: ScrapeData) -> Response { mod tests { use std::net::IpAddr; use std::str::FromStr; - use std::sync::Arc; - use crate::config::{ephemeral_configuration, Configuration}; + use torrust_tracker_test_helpers::configuration; + use crate::http::axum_implementation::requests::scrape::Scrape; use crate::http::axum_implementation::responses; use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; use crate::protocol::info_hash::InfoHash; - use crate::tracker::mode::Mode; - use crate::tracker::statistics::Keeper; + use crate::tracker::services::common::tracker_factory; use crate::tracker::Tracker; fn private_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Private; - tracker_factory(configuration) + tracker_factory(configuration::ephemeral_mode_private().into()) } - fn listed_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Listed; - tracker_factory(configuration) + fn whitelisted_tracker() -> Tracker { + tracker_factory(configuration::ephemeral_mode_whitelisted().into()) } fn tracker_on_reverse_proxy() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.on_reverse_proxy = true; - tracker_factory(configuration) + tracker_factory(configuration::ephemeral_with_reverse_proxy().into()) } fn tracker_not_on_reverse_proxy() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.on_reverse_proxy = false; - tracker_factory(configuration) - } - - fn tracker_factory(configuration: Configuration) -> Tracker { - // code-review: the tracker initialization is duplicated in many places. Consider make this function public. - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - match Tracker::new(&Arc::new(configuration), Some(stats_event_sender), stats_repository) { - Ok(tracker) => tracker, - Err(error) => { - panic!("{}", error) - } - } + tracker_factory(configuration::ephemeral_without_reverse_proxy().into()) } fn sample_scrape_request() -> Scrape { @@ -212,13 +188,13 @@ mod tests { use std::sync::Arc; - use super::{listed_tracker, sample_client_ip_sources, sample_scrape_request}; + use super::{sample_client_ip_sources, sample_scrape_request, whitelisted_tracker}; use crate::http::axum_implementation::handlers::scrape::handle_scrape; use crate::tracker::ScrapeData; #[tokio::test] async fn it_should_return_zeroed_swarm_metadata_when_the_torrent_is_not_whitelisted() { - let tracker = Arc::new(listed_tracker()); + let tracker = Arc::new(whitelisted_tracker()); let scrape_request = sample_scrape_request(); diff --git a/src/http/axum_implementation/launcher.rs b/src/http/axum_implementation/launcher.rs new file mode 100644 index 000000000..a49efd11d --- /dev/null +++ b/src/http/axum_implementation/launcher.rs @@ -0,0 +1,147 @@ +use std::future::Future; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use axum_server::tls_rustls::RustlsConfig; +use axum_server::Handle; +use futures::future::BoxFuture; +use log::info; +use warp::hyper; + +use super::routes::router; +use crate::http::server::HttpServerLauncher; +use crate::tracker::Tracker; + +#[derive(Debug)] +pub enum Error { + Error(String), +} + +pub struct Launcher; + +impl Launcher { + pub fn start_from_tcp_listener_with_graceful_shutdown( + tcp_listener: std::net::TcpListener, + tracker: Arc, + shutdown_signal: F, + ) -> BoxFuture<'static, ()> + where + F: Future + Send + 'static, + { + let app = router(tracker); + + Box::pin(async { + axum::Server::from_tcp(tcp_listener) + .expect("Could not bind to tcp listener.") + .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown_signal) + .await + .expect("Axum server crashed."); + }) + } + + pub fn start_tls_from_tcp_listener_with_graceful_shutdown( + tcp_listener: std::net::TcpListener, + (ssl_cert_path, ssl_key_path): (String, String), + tracker: Arc, + shutdown_signal: F, + ) -> BoxFuture<'static, ()> + where + F: Future + Send + 'static, + { + let app = router(tracker); + + let handle = Handle::new(); + + let cloned_handle = handle.clone(); + + tokio::task::spawn_local(async move { + shutdown_signal.await; + cloned_handle.shutdown(); + }); + + Box::pin(async { + let tls_config = RustlsConfig::from_pem_file(ssl_cert_path, ssl_key_path) + .await + .expect("Could not read tls cert."); + + axum_server::from_tcp_rustls(tcp_listener, tls_config) + .handle(handle) + .serve(app.into_make_service_with_connect_info::()) + .await + .expect("Axum server crashed."); + }) + } +} + +#[async_trait] +impl HttpServerLauncher for Launcher { + fn new() -> Self { + Self {} + } + + fn start_with_graceful_shutdown( + &self, + cfg: torrust_tracker_configuration::HttpTracker, + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static, + { + let addr = SocketAddr::from_str(&cfg.bind_address).expect("bind_address is not a valid SocketAddr."); + let tcp_listener = std::net::TcpListener::bind(addr).expect("Could not bind tcp_listener to address."); + let bind_addr = tcp_listener + .local_addr() + .expect("Could not get local_addr from tcp_listener."); + + if let (true, Some(ssl_cert_path), Some(ssl_key_path)) = (cfg.ssl_enabled, &cfg.ssl_cert_path, &cfg.ssl_key_path) { + let server = Self::start_tls_from_tcp_listener_with_graceful_shutdown( + tcp_listener, + (ssl_cert_path.to_string(), ssl_key_path.to_string()), + tracker, + shutdown_signal, + ); + + (bind_addr, server) + } else { + let server = Self::start_from_tcp_listener_with_graceful_shutdown(tcp_listener, tracker, shutdown_signal); + + (bind_addr, server) + } + } +} + +pub fn start(socket_addr: std::net::SocketAddr, tracker: Arc) -> impl Future> { + let app = router(tracker); + + let server = axum::Server::bind(&socket_addr).serve(app.into_make_service_with_connect_info::()); + + server.with_graceful_shutdown(async move { + tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); + info!("Stopping Torrust HTTP tracker server on http://{} ...", socket_addr); + }) +} + +pub fn start_tls( + socket_addr: std::net::SocketAddr, + ssl_config: RustlsConfig, + tracker: Arc, +) -> impl Future> { + let app = router(tracker); + + let handle = Handle::new(); + let shutdown_handle = handle.clone(); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); + info!("Stopping Torrust HTTP tracker server on https://{} ...", socket_addr); + shutdown_handle.shutdown(); + }); + + axum_server::bind_rustls(socket_addr, ssl_config) + .handle(handle) + .serve(app.into_make_service_with_connect_info::()) +} diff --git a/src/http/axum_implementation/mod.rs b/src/http/axum_implementation/mod.rs index ecc60e1f8..79d230255 100644 --- a/src/http/axum_implementation/mod.rs +++ b/src/http/axum_implementation/mod.rs @@ -1,8 +1,8 @@ pub mod extractors; pub mod handlers; +pub mod launcher; pub mod query; pub mod requests; pub mod responses; pub mod routes; -pub mod server; pub mod services; diff --git a/src/http/axum_implementation/requests/announce.rs b/src/http/axum_implementation/requests/announce.rs index 0f9a6fbfe..6e357ea6d 100644 --- a/src/http/axum_implementation/requests/announce.rs +++ b/src/http/axum_implementation/requests/announce.rs @@ -3,11 +3,11 @@ use std::panic::Location; use std::str::FromStr; use thiserror::Error; +use torrust_tracker_located_error::{Located, LocatedError}; use crate::http::axum_implementation::query::{ParseQueryError, Query}; use crate::http::axum_implementation::responses; use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id}; -use crate::located_error::{Located, LocatedError}; use crate::protocol::info_hash::{ConversionError, InfoHash}; use crate::tracker::peer::{self, IdConversionError}; diff --git a/src/http/axum_implementation/requests/scrape.rs b/src/http/axum_implementation/requests/scrape.rs index da50d4be5..505be566e 100644 --- a/src/http/axum_implementation/requests/scrape.rs +++ b/src/http/axum_implementation/requests/scrape.rs @@ -1,11 +1,11 @@ use std::panic::Location; use thiserror::Error; +use torrust_tracker_located_error::{Located, LocatedError}; use crate::http::axum_implementation::query::Query; use crate::http::axum_implementation::responses; use crate::http::percent_encoding::percent_decode_info_hash; -use crate::located_error::{Located, LocatedError}; use crate::protocol::info_hash::{ConversionError, InfoHash}; pub type NumberOfBytes = i64; diff --git a/src/http/axum_implementation/routes.rs b/src/http/axum_implementation/routes.rs index af987ece2..a8e740f69 100644 --- a/src/http/axum_implementation/routes.rs +++ b/src/http/axum_implementation/routes.rs @@ -7,14 +7,15 @@ use axum_client_ip::SecureClientIpSource; use super::handlers::{announce, scrape}; use crate::tracker::Tracker; -pub fn router(tracker: &Arc) -> Router { +#[allow(clippy::needless_pass_by_value)] +pub fn router(tracker: Arc) -> Router { Router::new() // Announce request .route("/announce", get(announce::handle_without_key).with_state(tracker.clone())) .route("/announce/:key", get(announce::handle_with_key).with_state(tracker.clone())) // Scrape request .route("/scrape", get(scrape::handle_without_key).with_state(tracker.clone())) - .route("/scrape/:key", get(scrape::handle_with_key).with_state(tracker.clone())) + .route("/scrape/:key", get(scrape::handle_with_key).with_state(tracker)) // Add extension to get the client IP from the connection info .layer(SecureClientIpSource::ConnectInfo.into_extension()) } diff --git a/src/http/axum_implementation/server.rs b/src/http/axum_implementation/server.rs deleted file mode 100644 index 30c580af6..000000000 --- a/src/http/axum_implementation/server.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use axum_server::tls_rustls::RustlsConfig; -use axum_server::Handle; -use futures::Future; -use log::info; -use warp::hyper; - -use super::routes::router; -use crate::tracker::Tracker; - -pub fn start(socket_addr: SocketAddr, tracker: &Arc) -> impl Future> { - let app = router(tracker); - - let server = axum::Server::bind(&socket_addr).serve(app.into_make_service_with_connect_info::()); - - server.with_graceful_shutdown(async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - info!("Stopping Torrust HTTP tracker server on http://{} ...", socket_addr); - }) -} - -pub fn start_tls( - socket_addr: SocketAddr, - ssl_config: RustlsConfig, - tracker: &Arc, -) -> impl Future> { - let app = router(tracker); - - let handle = Handle::new(); - let shutdown_handle = handle.clone(); - - tokio::spawn(async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - info!("Stopping Torrust HTTP tracker server on https://{} ...", socket_addr); - shutdown_handle.shutdown(); - }); - - axum_server::bind_rustls(socket_addr, ssl_config) - .handle(handle) - .serve(app.into_make_service_with_connect_info::()) -} diff --git a/src/http/axum_implementation/services/announce.rs b/src/http/axum_implementation/services/announce.rs index 5ce0fb1d5..73d6ed468 100644 --- a/src/http/axum_implementation/services/announce.rs +++ b/src/http/axum_implementation/services/announce.rs @@ -26,36 +26,17 @@ pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut Peer) #[cfg(test)] mod tests { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::sync::Arc; use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + use torrust_tracker_test_helpers::configuration; - use crate::config::{ephemeral_configuration, Configuration}; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; - use crate::tracker::mode::Mode; - use crate::tracker::statistics::Keeper; + use crate::tracker::services::common::tracker_factory; use crate::tracker::{peer, Tracker}; fn public_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Public; - tracker_factory(configuration) - } - - fn tracker_factory(configuration: Configuration) -> Tracker { - // code-review: the tracker initialization is duplicated in many places. Consider make this function public. - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - match Tracker::new(&Arc::new(configuration), Some(stats_event_sender), stats_repository) { - Ok(tracker) => tracker, - Err(error) => { - panic!("{}", error) - } - } + tracker_factory(configuration::ephemeral_mode_public().into()) } fn sample_info_hash() -> InfoHash { @@ -93,9 +74,9 @@ mod tests { use std::sync::Arc; use mockall::predicate::eq; + use torrust_tracker_test_helpers::configuration; use super::{sample_peer_using_ipv4, sample_peer_using_ipv6}; - use crate::config::ephemeral_configuration; use crate::http::axum_implementation::services::announce::invoke; use crate::http::axum_implementation::services::announce::tests::{public_tracker, sample_info_hash, sample_peer}; use crate::tracker::peer::Peer; @@ -136,7 +117,7 @@ mod tests { let tracker = Arc::new( Tracker::new( - &Arc::new(ephemeral_configuration()), + Arc::new(configuration::ephemeral()), Some(stats_event_sender), statistics::Repo::new(), ) @@ -149,11 +130,11 @@ mod tests { } fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { - let mut configuration = ephemeral_configuration(); + let mut configuration = configuration::ephemeral(); configuration.external_ip = Some(IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)).to_string()); - Tracker::new(&Arc::new(configuration), Some(stats_event_sender), statistics::Repo::new()).unwrap() + Tracker::new(Arc::new(configuration), Some(stats_event_sender), statistics::Repo::new()).unwrap() } fn peer_with_the_ipv4_loopback_ip() -> Peer { @@ -200,7 +181,7 @@ mod tests { let tracker = Arc::new( Tracker::new( - &Arc::new(ephemeral_configuration()), + Arc::new(configuration::ephemeral()), Some(stats_event_sender), statistics::Repo::new(), ) diff --git a/src/http/mod.rs b/src/http/mod.rs index 039a2067b..b4841c0af 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; pub mod axum_implementation; pub mod percent_encoding; +pub mod server; pub mod warp_implementation; #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] diff --git a/src/http/server.rs b/src/http/server.rs new file mode 100644 index 000000000..98160777c --- /dev/null +++ b/src/http/server.rs @@ -0,0 +1,112 @@ +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; + +use futures::future::BoxFuture; + +use crate::signals::shutdown_signal; +use crate::tracker::Tracker; + +/// Trait to be implemented by a http server launcher for the tracker. +#[allow(clippy::module_name_repetitions)] +pub trait HttpServerLauncher: Sync + Send { + fn new() -> Self; + + fn start_with_graceful_shutdown( + &self, + cfg: torrust_tracker_configuration::HttpTracker, + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static; +} + +#[derive(Debug)] +pub enum Error { + Error(String), +} + +#[allow(clippy::module_name_repetitions)] +pub type StoppedHttpServer = HttpServer>; +#[allow(clippy::module_name_repetitions)] +pub type RunningHttpServer = HttpServer>; + +#[allow(clippy::module_name_repetitions)] +pub struct HttpServer { + pub cfg: torrust_tracker_configuration::HttpTracker, + pub state: S, +} + +pub struct Stopped { + launcher: I, +} + +pub struct Running { + pub bind_addr: SocketAddr, + task_killer: tokio::sync::oneshot::Sender, + task: tokio::task::JoinHandle, +} + +impl HttpServer> { + pub fn new(cfg: torrust_tracker_configuration::HttpTracker, launcher: I) -> Self { + Self { + cfg, + state: Stopped { launcher }, + } + } + + /// # Errors + /// + /// It would return an error if no `SocketAddr` is returned after launching the server. + pub async fn start(self, tracker: Arc) -> Result>, Error> { + let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::(); + let (addr_sender, addr_receiver) = tokio::sync::oneshot::channel::(); + + let configuration = self.cfg.clone(); + let launcher = self.state.launcher; + + let task = tokio::spawn(async move { + let (bind_addr, server) = + launcher.start_with_graceful_shutdown(configuration, tracker, shutdown_signal(shutdown_receiver)); + + addr_sender.send(bind_addr).expect("Could not return SocketAddr."); + + server.await; + + launcher + }); + + let bind_address = addr_receiver + .await + .map_err(|_| Error::Error("Could not receive bind_address.".to_string()))?; + + Ok(HttpServer { + cfg: self.cfg, + state: Running { + bind_addr: bind_address, + task_killer: shutdown_sender, + task, + }, + }) + } +} + +impl HttpServer> { + /// # Errors + /// + /// It would return an error if the channel for the task killer signal was closed. + pub async fn stop(self) -> Result>, Error> { + self.state + .task_killer + .send(0) + .map_err(|_| Error::Error("Task killer channel was closed.".to_string()))?; + + let launcher = self.state.task.await.map_err(|e| Error::Error(e.to_string()))?; + + Ok(HttpServer { + cfg: self.cfg, + state: Stopped { launcher }, + }) + } +} diff --git a/src/http/warp_implementation/error.rs b/src/http/warp_implementation/error.rs index f07c32f6d..55b22c27a 100644 --- a/src/http/warp_implementation/error.rs +++ b/src/http/warp_implementation/error.rs @@ -1,10 +1,9 @@ use std::panic::Location; use thiserror::Error; +use torrust_tracker_located_error::LocatedError; use warp::reject::Reject; -use crate::located_error::LocatedError; - #[derive(Error, Debug)] pub enum Error { #[error("tracker server error: {source}")] diff --git a/src/http/warp_implementation/filter_helpers.rs b/src/http/warp_implementation/filter_helpers.rs index 89188d868..583d38352 100644 --- a/src/http/warp_implementation/filter_helpers.rs +++ b/src/http/warp_implementation/filter_helpers.rs @@ -3,8 +3,7 @@ use std::panic::Location; use std::str::FromStr; use thiserror::Error; - -use crate::located_error::{Located, LocatedError}; +use torrust_tracker_located_error::{Located, LocatedError}; #[derive(Error, Debug)] pub enum XForwardedForParseError { diff --git a/src/http/warp_implementation/launcher.rs b/src/http/warp_implementation/launcher.rs new file mode 100644 index 000000000..46ec2bf3c --- /dev/null +++ b/src/http/warp_implementation/launcher.rs @@ -0,0 +1,116 @@ +use std::future::Future; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; + +use futures::future::BoxFuture; + +use super::routes; +use crate::http::server::HttpServerLauncher; +use crate::tracker; +use crate::tracker::Tracker; + +#[derive(Debug)] +pub enum Error { + Error(String), +} + +pub struct Launcher; + +impl Launcher { + pub fn start_with_graceful_shutdown( + addr: SocketAddr, + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static, + { + let (bind_addr, server) = warp::serve(routes::routes(tracker)).bind_with_graceful_shutdown(addr, shutdown_signal); + + (bind_addr, Box::pin(server)) + } + + pub fn start_tls_with_graceful_shutdown( + addr: SocketAddr, + (ssl_cert_path, ssl_key_path): (&str, &str), + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static, + { + let (bind_addr, server) = warp::serve(routes::routes(tracker)) + .tls() + .cert_path(ssl_cert_path) + .key_path(ssl_key_path) + .bind_with_graceful_shutdown(addr, shutdown_signal); + + (bind_addr, Box::pin(server)) + } +} + +impl HttpServerLauncher for Launcher { + fn new() -> Self { + Self {} + } + + fn start_with_graceful_shutdown( + &self, + cfg: torrust_tracker_configuration::HttpTracker, + tracker: Arc, + shutdown_signal: F, + ) -> (SocketAddr, BoxFuture<'static, ()>) + where + F: Future + Send + 'static, + { + let addr = SocketAddr::from_str(&cfg.bind_address).expect("bind_address is not a valid SocketAddr."); + + if let (true, Some(ssl_cert_path), Some(ssl_key_path)) = (cfg.ssl_enabled, &cfg.ssl_cert_path, &cfg.ssl_key_path) { + Self::start_tls_with_graceful_shutdown(addr, (ssl_cert_path, ssl_key_path), tracker, shutdown_signal) + } else { + Self::start_with_graceful_shutdown(addr, tracker, shutdown_signal) + } + } +} + +/// Server that listens on HTTP, needs a `tracker::TorrentTracker` +#[derive(Clone)] +pub struct Http { + tracker: Arc, +} + +impl Http { + #[must_use] + pub fn new(tracker: Arc) -> Http { + Http { tracker } + } + + /// Start the `HttpServer` + pub fn start(&self, socket_addr: SocketAddr) -> impl warp::Future { + let (_addr, server) = + warp::serve(routes::routes(self.tracker.clone())).bind_with_graceful_shutdown(socket_addr, async move { + tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); + }); + + server + } + + /// Start the `HttpServer` in TLS mode + pub fn start_tls( + &self, + socket_addr: SocketAddr, + ssl_cert_path: String, + ssl_key_path: String, + ) -> impl warp::Future { + let (_addr, server) = warp::serve(routes::routes(self.tracker.clone())) + .tls() + .cert_path(ssl_cert_path) + .key_path(ssl_key_path) + .bind_with_graceful_shutdown(socket_addr, async move { + tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); + }); + + server + } +} diff --git a/src/http/warp_implementation/mod.rs b/src/http/warp_implementation/mod.rs index 2ceda2e68..c0e046f4f 100644 --- a/src/http/warp_implementation/mod.rs +++ b/src/http/warp_implementation/mod.rs @@ -2,11 +2,11 @@ pub mod error; pub mod filter_helpers; pub mod filters; pub mod handlers; +pub mod launcher; pub mod peer_builder; pub mod request; pub mod response; pub mod routes; -pub mod server; use warp::Rejection; diff --git a/src/http/warp_implementation/server.rs b/src/http/warp_implementation/server.rs deleted file mode 100644 index 894d3e911..000000000 --- a/src/http/warp_implementation/server.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use super::routes; -use crate::tracker; - -/// Server that listens on HTTP, needs a `tracker::TorrentTracker` -#[derive(Clone)] -pub struct Http { - tracker: Arc, -} - -impl Http { - #[must_use] - pub fn new(tracker: Arc) -> Http { - Http { tracker } - } - - /// Start the `HttpServer` - pub fn start(&self, socket_addr: SocketAddr) -> impl warp::Future { - let (_addr, server) = - warp::serve(routes::routes(self.tracker.clone())).bind_with_graceful_shutdown(socket_addr, async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - }); - - server - } - - /// Start the `HttpServer` in TLS mode - pub fn start_tls( - &self, - socket_addr: SocketAddr, - ssl_cert_path: String, - ssl_key_path: String, - ) -> impl warp::Future { - let (_addr, server) = warp::serve(routes::routes(self.tracker.clone())) - .tls() - .cert_path(ssl_cert_path) - .key_path(ssl_key_path) - .bind_with_graceful_shutdown(socket_addr, async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - }); - - server - } -} diff --git a/src/jobs/http_tracker.rs b/src/jobs/http_tracker.rs index aa96af884..40caa8e88 100644 --- a/src/jobs/http_tracker.rs +++ b/src/jobs/http_tracker.rs @@ -5,10 +5,10 @@ use axum_server::tls_rustls::RustlsConfig; use log::{info, warn}; use tokio::sync::oneshot; use tokio::task::JoinHandle; +use torrust_tracker_configuration::HttpTracker; -use crate::config::HttpTracker; -use crate::http::axum_implementation::server; -use crate::http::warp_implementation::server::Http; +use crate::http::axum_implementation::launcher; +use crate::http::warp_implementation::launcher::Http; use crate::http::Version; use crate::tracker; @@ -98,7 +98,7 @@ async fn start_axum(config: &HttpTracker, tracker: Arc) -> Joi if !ssl_enabled { info!("Starting Torrust HTTP tracker server on: http://{}", bind_addr); - let handle = server::start(bind_addr, &tracker); + let handle = launcher::start(bind_addr, tracker); tx.send(ServerJobStarted()) .expect("the HTTP tracker server should not be dropped"); @@ -113,7 +113,7 @@ async fn start_axum(config: &HttpTracker, tracker: Arc) -> Joi .await .unwrap(); - let handle = server::start_tls(bind_addr, ssl_config, &tracker); + let handle = launcher::start_tls(bind_addr, ssl_config, tracker); tx.send(ServerJobStarted()) .expect("the HTTP tracker server should not be dropped"); diff --git a/src/jobs/torrent_cleanup.rs b/src/jobs/torrent_cleanup.rs index 073ceda61..4c4ed1f53 100644 --- a/src/jobs/torrent_cleanup.rs +++ b/src/jobs/torrent_cleanup.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use chrono::Utc; use log::info; use tokio::task::JoinHandle; +use torrust_tracker_configuration::Configuration; -use crate::config::Configuration; use crate::tracker; #[must_use] diff --git a/src/jobs/tracker_apis.rs b/src/jobs/tracker_apis.rs index 00e39eeba..939b58638 100644 --- a/src/jobs/tracker_apis.rs +++ b/src/jobs/tracker_apis.rs @@ -4,9 +4,9 @@ use axum_server::tls_rustls::RustlsConfig; use log::info; use tokio::sync::oneshot; use tokio::task::JoinHandle; +use torrust_tracker_configuration::HttpApi; use crate::apis::server; -use crate::config::HttpApi; use crate::tracker; #[derive(Debug)] @@ -31,7 +31,7 @@ pub async fn start_job(config: &HttpApi, tracker: Arc) -> Join if !ssl_enabled { info!("Starting Torrust APIs server on: http://{}", bind_addr); - let handle = server::start(bind_addr, &tracker); + let handle = server::start(bind_addr, tracker); tx.send(ApiServerJobStarted()).expect("the API server should not be dropped"); @@ -45,7 +45,7 @@ pub async fn start_job(config: &HttpApi, tracker: Arc) -> Join .await .unwrap(); - let handle = server::start_tls(bind_addr, ssl_config, &tracker); + let handle = server::start_tls(bind_addr, ssl_config, tracker); tx.send(ApiServerJobStarted()).expect("the API server should not be dropped"); diff --git a/src/jobs/udp_tracker.rs b/src/jobs/udp_tracker.rs index d0907c976..57232855b 100644 --- a/src/jobs/udp_tracker.rs +++ b/src/jobs/udp_tracker.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use log::{error, info, warn}; use tokio::task::JoinHandle; +use torrust_tracker_configuration::UdpTracker; -use crate::config::UdpTracker; use crate::tracker; use crate::udp::server::Udp; @@ -12,10 +12,10 @@ pub fn start_job(config: &UdpTracker, tracker: Arc) -> JoinHan let bind_addr = config.bind_address.clone(); tokio::spawn(async move { - match Udp::new(tracker, &bind_addr).await { + match Udp::new(&bind_addr).await { Ok(udp_server) => { info!("Starting UDP server on: udp://{}", bind_addr); - udp_server.start().await; + udp_server.start(tracker).await; } Err(e) => { warn!("Could not start UDP tracker on: udp://{}", bind_addr); diff --git a/src/lib.rs b/src/lib.rs index cbda2854c..f01ff0468 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,11 @@ pub mod apis; -pub mod config; pub mod databases; pub mod http; pub mod jobs; -pub mod located_error; pub mod logging; pub mod protocol; pub mod setup; +pub mod signals; pub mod stats; pub mod tracker; pub mod udp; diff --git a/src/logging.rs b/src/logging.rs index 4d16f7670..83e2c9360 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -2,8 +2,7 @@ use std::str::FromStr; use std::sync::Once; use log::{info, LevelFilter}; - -use crate::config::Configuration; +use torrust_tracker_configuration::Configuration; static INIT: Once = Once::new(); diff --git a/src/main.rs b/src/main.rs index 199e8f5c5..b0cc68b12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ use std::env; use std::sync::Arc; use log::info; -use torrust_tracker::config::Configuration; use torrust_tracker::stats::setup_statistics; use torrust_tracker::{ephemeral_instance_keys, logging, setup, static_time, tracker}; +use torrust_tracker_configuration::Configuration; #[tokio::main] async fn main() { @@ -30,7 +30,7 @@ async fn main() { let (stats_event_sender, stats_repository) = setup_statistics(config.tracker_usage_statistics); // Initialize Torrust tracker - let tracker = match tracker::Tracker::new(&config.clone(), stats_event_sender, stats_repository) { + let tracker = match tracker::Tracker::new(config.clone(), stats_event_sender, stats_repository) { Ok(tracker) => Arc::new(tracker), Err(error) => { panic!("{}", error) diff --git a/src/setup.rs b/src/setup.rs index 98d311178..5b51632a7 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use log::warn; use tokio::task::JoinHandle; +use torrust_tracker_configuration::Configuration; -use crate::config::Configuration; use crate::http::Version; use crate::jobs::{http_tracker, torrent_cleanup, tracker_apis, udp_tracker}; use crate::tracker; diff --git a/src/signals.rs b/src/signals.rs new file mode 100644 index 000000000..b5a25ded7 --- /dev/null +++ b/src/signals.rs @@ -0,0 +1,41 @@ +use log::info; + +/// Resolves on `ctrl_c` or the `terminate` signal. +pub async fn global_shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {} + } +} + +/// Resolves when the `stop_receiver` or the `global_shutdown_signal()` resolves. +pub async fn shutdown_signal(stop_receiver: tokio::sync::oneshot::Receiver) { + let stop = async { stop_receiver.await.expect("Failed to install stop signal.") }; + + tokio::select! { + _ = stop => {}, + _ = global_shutdown_signal() => {} + } +} + +/// Same as `shutdown_signal()`, but shows a message when it resolves. +pub async fn shutdown_signal_with_message(stop_receiver: tokio::sync::oneshot::Receiver, message: String) { + shutdown_signal(stop_receiver).await; + + info!("{message}"); +} diff --git a/src/tracker/auth.rs b/src/tracker/auth.rs index 84252f667..00663c383 100644 --- a/src/tracker/auth.rs +++ b/src/tracker/auth.rs @@ -10,8 +10,8 @@ use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use torrust_tracker_located_error::LocatedError; -use crate::located_error::LocatedError; use crate::protocol::clock::{Current, DurationSinceUnixEpoch, Time, TimeNow}; use crate::protocol::common::AUTH_KEY_LENGTH; diff --git a/src/tracker/error.rs b/src/tracker/error.rs index 080903da6..10ca5ec19 100644 --- a/src/tracker/error.rs +++ b/src/tracker/error.rs @@ -1,6 +1,6 @@ use std::panic::Location; -use crate::located_error::LocatedError; +use torrust_tracker_located_error::LocatedError; #[derive(thiserror::Error, Debug, Clone)] pub enum Error { diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index 2ebc4bfc3..326afbf00 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -1,6 +1,5 @@ pub mod auth; pub mod error; -pub mod mode; pub mod peer; pub mod services; pub mod statistics; @@ -15,19 +14,19 @@ use std::time::Duration; use tokio::sync::mpsc::error::SendError; use tokio::sync::{RwLock, RwLockReadGuard}; +use torrust_tracker_configuration::Configuration; +use torrust_tracker_primitives::TrackerMode; use self::auth::Key; use self::error::Error; use self::peer::Peer; use self::torrent::{SwarmMetadata, SwarmStats}; -use crate::config::Configuration; -use crate::databases::driver::Driver; use crate::databases::{self, Database}; use crate::protocol::info_hash::InfoHash; pub struct Tracker { pub config: Arc, - mode: mode::Mode, + mode: TrackerMode, keys: RwLock>, whitelist: RwLock>, torrents: RwLock>, @@ -92,15 +91,17 @@ impl Tracker { /// /// Will return a `databases::error::Error` if unable to connect to database. pub fn new( - config: &Arc, + config: Arc, stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Driver::build(&config.db_driver, &config.db_path)?; + let database = databases::driver::build(&config.db_driver, &config.db_path)?; + + let mode = config.mode; Ok(Tracker { - config: config.clone(), - mode: config.mode, + config, + mode, keys: RwLock::new(std::collections::HashMap::new()), whitelist: RwLock::new(std::collections::HashSet::new()), torrents: RwLock::new(std::collections::BTreeMap::new()), @@ -111,15 +112,15 @@ impl Tracker { } pub fn is_public(&self) -> bool { - self.mode == mode::Mode::Public + self.mode == TrackerMode::Public } pub fn is_private(&self) -> bool { - self.mode == mode::Mode::Private || self.mode == mode::Mode::PrivateListed + self.mode == TrackerMode::Private || self.mode == TrackerMode::PrivateListed } pub fn is_whitelisted(&self) -> bool { - self.mode == mode::Mode::Listed || self.mode == mode::Mode::PrivateListed + self.mode == TrackerMode::Listed || self.mode == TrackerMode::PrivateListed } pub fn requires_authentication(&self) -> bool { @@ -554,52 +555,30 @@ mod tests { use std::sync::Arc; use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + use torrust_tracker_test_helpers::configuration; - use crate::config::{ephemeral_configuration, Configuration}; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; - use crate::tracker::mode::Mode; use crate::tracker::peer::{self, Peer}; - use crate::tracker::statistics::Keeper; + use crate::tracker::services::common::tracker_factory; use crate::tracker::{TorrentsMetrics, Tracker}; - pub fn public_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Public; - tracker_factory(configuration) + fn public_tracker() -> Tracker { + tracker_factory(configuration::ephemeral_mode_public().into()) } - pub fn private_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Private; - tracker_factory(configuration) + fn private_tracker() -> Tracker { + tracker_factory(configuration::ephemeral_mode_private().into()) } - pub fn whitelisted_tracker() -> Tracker { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Listed; - tracker_factory(configuration) + fn whitelisted_tracker() -> Tracker { + tracker_factory(configuration::ephemeral_mode_whitelisted().into()) } pub fn tracker_persisting_torrents_in_database() -> Tracker { - let mut configuration = ephemeral_configuration(); + let mut configuration = configuration::ephemeral(); configuration.persistent_torrent_completed_stat = true; - tracker_factory(configuration) - } - - pub fn tracker_factory(configuration: Configuration) -> Tracker { - // code-review: the tracker initialization is duplicated in many places. Consider make this function public. - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - match Tracker::new(&Arc::new(configuration), Some(stats_event_sender), stats_repository) { - Ok(tracker) => tracker, - Err(error) => { - panic!("{}", error) - } - } + tracker_factory(Arc::new(configuration)) } fn sample_info_hash() -> InfoHash { diff --git a/src/tracker/services/common.rs b/src/tracker/services/common.rs index 8757e6a21..757725263 100644 --- a/src/tracker/services/common.rs +++ b/src/tracker/services/common.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use crate::config::Configuration; +use torrust_tracker_configuration::Configuration; + use crate::tracker::statistics::Keeper; use crate::tracker::Tracker; @@ -8,7 +9,7 @@ use crate::tracker::Tracker; /// /// Will panic if tracker cannot be instantiated. #[must_use] -pub fn tracker_factory(configuration: &Arc) -> Tracker { +pub fn tracker_factory(configuration: Arc) -> Tracker { // todo: the tracker initialization is duplicated in many places. // Initialize stats tracker diff --git a/src/tracker/services/statistics.rs b/src/tracker/services/statistics.rs index 745f5563c..28cd0b962 100644 --- a/src/tracker/services/statistics.rs +++ b/src/tracker/services/statistics.rs @@ -36,18 +36,20 @@ pub async fn get_metrics(tracker: Arc) -> TrackerMetrics { mod tests { use std::sync::Arc; - use crate::config::{ephemeral_configuration, Configuration}; + use torrust_tracker_configuration::Configuration; + use torrust_tracker_test_helpers::configuration; + use crate::tracker; use crate::tracker::services::common::tracker_factory; use crate::tracker::services::statistics::{get_metrics, TrackerMetrics}; pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) + Arc::new(configuration::ephemeral()) } #[tokio::test] async fn the_statistics_service_should_return_the_tracker_metrics() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let tracker_metrics = get_metrics(tracker.clone()).await; diff --git a/src/tracker/services/torrent.rs b/src/tracker/services/torrent.rs index e2353876e..b04b4e1dc 100644 --- a/src/tracker/services/torrent.rs +++ b/src/tracker/services/torrent.rs @@ -137,19 +137,21 @@ mod tests { use std::str::FromStr; use std::sync::Arc; - use crate::config::{ephemeral_configuration, Configuration}; + use torrust_tracker_configuration::Configuration; + use torrust_tracker_test_helpers::configuration; + use crate::protocol::info_hash::InfoHash; use crate::tracker::services::common::tracker_factory; use crate::tracker::services::torrent::tests::sample_peer; use crate::tracker::services::torrent::{get_torrent_info, Info}; pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) + Arc::new(configuration::ephemeral()) } #[tokio::test] async fn should_return_none_if_the_tracker_does_not_have_the_torrent() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let torrent_info = get_torrent_info( tracker.clone(), @@ -162,7 +164,7 @@ mod tests { #[tokio::test] async fn should_return_the_torrent_info_if_the_tracker_has_the_torrent() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); @@ -190,19 +192,21 @@ mod tests { use std::str::FromStr; use std::sync::Arc; - use crate::config::{ephemeral_configuration, Configuration}; + use torrust_tracker_configuration::Configuration; + use torrust_tracker_test_helpers::configuration; + use crate::protocol::info_hash::InfoHash; use crate::tracker::services::common::tracker_factory; use crate::tracker::services::torrent::tests::sample_peer; use crate::tracker::services::torrent::{get_torrents, BasicInfo, Pagination}; pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) + Arc::new(configuration::ephemeral()) } #[tokio::test] async fn should_return_an_empty_result_if_the_tracker_does_not_have_any_torrent() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let torrents = get_torrents(tracker.clone(), &Pagination::default()).await; @@ -211,7 +215,7 @@ mod tests { #[tokio::test] async fn should_return_a_summarized_info_for_all_torrents() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); @@ -235,7 +239,7 @@ mod tests { #[tokio::test] async fn should_allow_limiting_the_number_of_torrents_in_the_result() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash1 = InfoHash::from_str(&hash1).unwrap(); @@ -259,7 +263,7 @@ mod tests { #[tokio::test] async fn should_allow_using_pagination_in_the_result() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash1 = InfoHash::from_str(&hash1).unwrap(); @@ -292,7 +296,7 @@ mod tests { #[tokio::test] async fn should_return_torrents_ordered_by_info_hash() { - let tracker = Arc::new(tracker_factory(&tracker_configuration())); + let tracker = Arc::new(tracker_factory(tracker_configuration())); let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash1 = InfoHash::from_str(&hash1).unwrap(); diff --git a/src/udp/error.rs b/src/udp/error.rs index de66eb2bf..a6381cc78 100644 --- a/src/udp/error.rs +++ b/src/udp/error.rs @@ -1,8 +1,7 @@ use std::panic::Location; use thiserror::Error; - -use crate::located_error::LocatedError; +use torrust_tracker_located_error::LocatedError; #[derive(Error, Debug)] pub enum Error { diff --git a/src/udp/handlers.rs b/src/udp/handlers.rs index 8fda77fb4..41b1184dc 100644 --- a/src/udp/handlers.rs +++ b/src/udp/handlers.rs @@ -11,12 +11,12 @@ use log::debug; use super::connection_cookie::{check, from_connection_id, into_connection_id, make}; use crate::protocol::common::MAX_SCRAPE_TORRENTS; use crate::protocol::info_hash::InfoHash; -use crate::tracker::{self, statistics}; +use crate::tracker::{statistics, Tracker}; use crate::udp::error::Error; use crate::udp::peer_builder; use crate::udp::request::AnnounceWrapper; -pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: Arc) -> Response { +pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: &Tracker) -> Response { match Request::from_bytes(&payload[..payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| Error::InternalServer { message: format!("{e:?}"), location: Location::caller(), @@ -46,11 +46,7 @@ pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: A /// # Errors /// /// If a error happens in the `handle_request` function, it will just return the `ServerError`. -pub async fn handle_request( - request: Request, - remote_addr: SocketAddr, - tracker: Arc, -) -> Result { +pub async fn handle_request(request: Request, remote_addr: SocketAddr, tracker: &Tracker) -> Result { match request { Request::Connect(connect_request) => handle_connect(remote_addr, &connect_request, tracker).await, Request::Announce(announce_request) => handle_announce(remote_addr, &announce_request, tracker).await, @@ -61,11 +57,7 @@ pub async fn handle_request( /// # Errors /// /// This function dose not ever return an error. -pub async fn handle_connect( - remote_addr: SocketAddr, - request: &ConnectRequest, - tracker: Arc, -) -> Result { +pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, tracker: &Tracker) -> Result { let connection_cookie = make(&remote_addr); let connection_id = into_connection_id(&connection_cookie); @@ -90,7 +82,7 @@ pub async fn handle_connect( /// # Errors /// /// Will return `Error` if unable to `authenticate_request`. -pub async fn authenticate(info_hash: &InfoHash, tracker: Arc) -> Result<(), Error> { +pub async fn authenticate(info_hash: &InfoHash, tracker: &Tracker) -> Result<(), Error> { tracker .authenticate_request(info_hash, &None) .await @@ -105,7 +97,7 @@ pub async fn authenticate(info_hash: &InfoHash, tracker: Arc) pub async fn handle_announce( remote_addr: SocketAddr, announce_request: &AnnounceRequest, - tracker: Arc, + tracker: &Tracker, ) -> Result { debug!("udp announce request: {:#?}", announce_request); @@ -116,7 +108,7 @@ pub async fn handle_announce( let info_hash = wrapped_announce_request.info_hash; let remote_client_ip = remote_addr.ip(); - authenticate(&info_hash, tracker.clone()).await?; + authenticate(&info_hash, tracker).await?; let mut peer = peer_builder::from_request(&wrapped_announce_request, &remote_client_ip); @@ -182,11 +174,7 @@ pub async fn handle_announce( /// # Errors /// /// This function dose not ever return an error. -pub async fn handle_scrape( - remote_addr: SocketAddr, - request: &ScrapeRequest, - tracker: Arc, -) -> Result { +pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tracker: &Tracker) -> Result { // Convert from aquatic infohashes let mut info_hashes = vec![]; for info_hash in &request.info_hashes { @@ -250,37 +238,35 @@ mod tests { use std::sync::Arc; use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + use torrust_tracker_configuration::Configuration; + use torrust_tracker_test_helpers::configuration; - use crate::config::{ephemeral_configuration, Configuration}; use crate::protocol::clock::{Current, Time}; - use crate::tracker::{self, mode, peer, statistics}; + use crate::tracker::services::common::tracker_factory; + use crate::tracker::{self, peer}; fn tracker_configuration() -> Arc { Arc::new(default_testing_tracker_configuration()) } fn default_testing_tracker_configuration() -> Configuration { - ephemeral_configuration() + configuration::ephemeral() } - fn initialized_public_tracker() -> Arc { - let configuration = Arc::new(TrackerConfigurationBuilder::default().with_mode(mode::Mode::Public).into()); - initialized_tracker(&configuration) + fn public_tracker() -> Arc { + initialized_tracker(configuration::ephemeral_mode_public().into()) } - fn initialized_private_tracker() -> Arc { - let configuration = Arc::new(TrackerConfigurationBuilder::default().with_mode(mode::Mode::Private).into()); - initialized_tracker(&configuration) + fn private_tracker() -> Arc { + initialized_tracker(configuration::ephemeral_mode_private().into()) } - fn initialized_whitelisted_tracker() -> Arc { - let configuration = Arc::new(TrackerConfigurationBuilder::default().with_mode(mode::Mode::Listed).into()); - initialized_tracker(&configuration) + fn whitelisted_tracker() -> Arc { + initialized_tracker(configuration::ephemeral_mode_whitelisted().into()) } - fn initialized_tracker(configuration: &Arc) -> Arc { - let (stats_event_sender, stats_repository) = statistics::Keeper::new_active_instance(); - Arc::new(tracker::Tracker::new(configuration, Some(stats_event_sender), stats_repository).unwrap()) + fn initialized_tracker(configuration: Arc) -> Arc { + tracker_factory(configuration).into() } fn sample_ipv4_remote_addr() -> SocketAddr { @@ -354,11 +340,6 @@ mod tests { self } - pub fn with_mode(mut self, mode: mode::Mode) -> Self { - self.configuration.mode = mode; - self - } - pub fn into(self) -> Configuration { self.configuration } @@ -376,7 +357,7 @@ mod tests { use crate::tracker::{self, statistics}; use crate::udp::connection_cookie::{into_connection_id, make}; use crate::udp::handlers::handle_connect; - use crate::udp::handlers::tests::{initialized_public_tracker, sample_ipv4_remote_addr}; + use crate::udp::handlers::tests::{public_tracker, sample_ipv4_remote_addr}; fn sample_connect_request() -> ConnectRequest { ConnectRequest { @@ -390,7 +371,7 @@ mod tests { transaction_id: TransactionId(0i32), }; - let response = handle_connect(sample_ipv4_remote_addr(), &request, initialized_public_tracker()) + let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) .await .unwrap(); @@ -409,7 +390,7 @@ mod tests { transaction_id: TransactionId(0i32), }; - let response = handle_connect(sample_ipv4_remote_addr(), &request, initialized_public_tracker()) + let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) .await .unwrap(); @@ -435,9 +416,9 @@ mod tests { let client_socket_address = sample_ipv4_socket_address(); let torrent_tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_connect(client_socket_address, &sample_connect_request(), torrent_tracker) + handle_connect(client_socket_address, &sample_connect_request(), &torrent_tracker) .await .unwrap(); } @@ -453,9 +434,9 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let torrent_tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_connect(sample_ipv6_remote_addr(), &sample_connect_request(), torrent_tracker) + handle_connect(sample_ipv6_remote_addr(), &sample_connect_request(), &torrent_tracker) .await .unwrap(); } @@ -549,12 +530,12 @@ mod tests { use crate::udp::handlers::handle_announce; use crate::udp::handlers::tests::announce_request::AnnounceRequestBuilder; use crate::udp::handlers::tests::{ - initialized_public_tracker, sample_ipv4_socket_address, tracker_configuration, TorrentPeerBuilder, + public_tracker, sample_ipv4_socket_address, tracker_configuration, TorrentPeerBuilder, }; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let client_ip = Ipv4Addr::new(126, 0, 0, 1); let client_port = 8080; @@ -571,7 +552,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -591,11 +572,9 @@ mod tests { .with_connection_id(into_connection_id(&make(&remote_addr))) .into(); - let response = handle_announce(remote_addr, &request, initialized_public_tracker()) - .await - .unwrap(); + let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { @@ -614,7 +593,7 @@ mod tests { // From the BEP 15 (https://www.bittorrent.org/beps/bep_0015.html): // "Do note that most trackers will only honor the IP address field under limited circumstances." - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); @@ -634,7 +613,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -665,12 +644,12 @@ mod tests { .with_connection_id(into_connection_id(&make(&remote_addr))) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap() + handle_announce(remote_addr, &request, &tracker).await.unwrap() } #[tokio::test] async fn when_the_announce_request_comes_from_a_client_using_ipv4_the_response_should_not_include_peers_using_ipv6() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); add_a_torrent_peer_using_ipv6(tracker.clone()).await; @@ -696,13 +675,13 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); handle_announce( sample_ipv4_socket_address(), &AnnounceRequestBuilder::default().into(), - tracker.clone(), + &tracker, ) .await .unwrap(); @@ -717,11 +696,11 @@ mod tests { use crate::udp::connection_cookie::{into_connection_id, make}; use crate::udp::handlers::handle_announce; use crate::udp::handlers::tests::announce_request::AnnounceRequestBuilder; - use crate::udp::handlers::tests::{initialized_public_tracker, TorrentPeerBuilder}; + use crate::udp::handlers::tests::{public_tracker, TorrentPeerBuilder}; #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration_if_defined() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let client_ip = Ipv4Addr::new(127, 0, 0, 1); let client_port = 8080; @@ -738,7 +717,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -772,12 +751,12 @@ mod tests { use crate::udp::handlers::handle_announce; use crate::udp::handlers::tests::announce_request::AnnounceRequestBuilder; use crate::udp::handlers::tests::{ - initialized_public_tracker, sample_ipv6_remote_addr, tracker_configuration, TorrentPeerBuilder, + public_tracker, sample_ipv6_remote_addr, tracker_configuration, TorrentPeerBuilder, }; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let client_ip_v4 = Ipv4Addr::new(126, 0, 0, 1); let client_ip_v6 = client_ip_v4.to_ipv6_compatible(); @@ -795,7 +774,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -818,11 +797,9 @@ mod tests { .with_connection_id(into_connection_id(&make(&remote_addr))) .into(); - let response = handle_announce(remote_addr, &request, initialized_public_tracker()) - .await - .unwrap(); + let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { @@ -841,7 +818,7 @@ mod tests { // From the BEP 15 (https://www.bittorrent.org/beps/bep_0015.html): // "Do note that most trackers will only honor the IP address field under limited circumstances." - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); @@ -861,7 +838,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -895,12 +872,12 @@ mod tests { .with_connection_id(into_connection_id(&make(&remote_addr))) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap() + handle_announce(remote_addr, &request, &tracker).await.unwrap() } #[tokio::test] async fn when_the_announce_request_comes_from_a_client_using_ipv6_the_response_should_not_include_peers_using_ipv4() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); add_a_torrent_peer_using_ipv4(tracker.clone()).await; @@ -926,7 +903,7 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); let remote_addr = sample_ipv6_remote_addr(); @@ -935,9 +912,7 @@ mod tests { .with_connection_id(into_connection_id(&make(&remote_addr))) .into(); - handle_announce(remote_addr, &announce_request, tracker.clone()) - .await - .unwrap(); + handle_announce(remote_addr, &announce_request, &tracker).await.unwrap(); } mod from_a_loopback_ip { @@ -958,7 +933,7 @@ mod tests { let configuration = Arc::new(TrackerConfigurationBuilder::default().with_external_ip("::126.0.0.1").into()); let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); let tracker = - Arc::new(tracker::Tracker::new(&configuration, Some(stats_event_sender), stats_repository).unwrap()); + Arc::new(tracker::Tracker::new(configuration, Some(stats_event_sender), stats_repository).unwrap()); let loopback_ipv4 = Ipv4Addr::new(127, 0, 0, 1); let loopback_ipv6 = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1); @@ -980,7 +955,7 @@ mod tests { .with_port(client_port) .into(); - handle_announce(remote_addr, &request, tracker.clone()).await.unwrap(); + handle_announce(remote_addr, &request, &tracker).await.unwrap(); let peers = tracker.get_all_torrent_peers(&info_hash.0.into()).await; @@ -1011,7 +986,7 @@ mod tests { use crate::tracker::{self, peer}; use crate::udp::connection_cookie::{into_connection_id, make}; use crate::udp::handlers::handle_scrape; - use crate::udp::handlers::tests::{initialized_public_tracker, sample_ipv4_remote_addr}; + use crate::udp::handlers::tests::{public_tracker, sample_ipv4_remote_addr}; fn zeroed_torrent_statistics() -> TorrentScrapeStatistics { TorrentScrapeStatistics { @@ -1034,9 +1009,7 @@ mod tests { info_hashes, }; - let response = handle_scrape(remote_addr, &request, initialized_public_tracker()) - .await - .unwrap(); + let response = handle_scrape(remote_addr, &request, &public_tracker()).await.unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1081,7 +1054,7 @@ mod tests { let request = build_scrape_request(&remote_addr, &info_hash); - handle_scrape(remote_addr, &request, tracker.clone()).await.unwrap() + handle_scrape(remote_addr, &request, &tracker).await.unwrap() } fn match_scrape_response(response: Response) -> Option { @@ -1094,12 +1067,12 @@ mod tests { mod with_a_public_tracker { use aquatic_udp_protocol::{NumberOfDownloads, NumberOfPeers, TorrentScrapeStatistics}; - use crate::udp::handlers::tests::initialized_public_tracker; + use crate::udp::handlers::tests::public_tracker; use crate::udp::handlers::tests::scrape_request::{add_a_sample_seeder_and_scrape, match_scrape_response}; #[tokio::test] async fn should_return_torrent_statistics_when_the_tracker_has_the_requested_torrent() { - let tracker = initialized_public_tracker(); + let tracker = public_tracker(); let torrent_stats = match_scrape_response(add_a_sample_seeder_and_scrape(tracker.clone()).await); @@ -1121,19 +1094,18 @@ mod tests { use crate::udp::handlers::tests::scrape_request::{ add_a_sample_seeder_and_scrape, build_scrape_request, match_scrape_response, zeroed_torrent_statistics, }; - use crate::udp::handlers::tests::{initialized_private_tracker, sample_ipv4_remote_addr}; + use crate::udp::handlers::tests::{private_tracker, sample_ipv4_remote_addr}; #[tokio::test] async fn should_return_zeroed_statistics_when_the_tracker_does_not_have_the_requested_torrent() { - let tracker = initialized_private_tracker(); + let tracker = private_tracker(); let remote_addr = sample_ipv4_remote_addr(); let non_existing_info_hash = InfoHash([0u8; 20]); let request = build_scrape_request(&remote_addr, &non_existing_info_hash); - let torrent_stats = - match_scrape_response(handle_scrape(remote_addr, &request, tracker.clone()).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1143,7 +1115,7 @@ mod tests { #[tokio::test] async fn should_return_zeroed_statistics_when_the_tracker_has_the_requested_torrent_because_authenticated_requests_are_not_supported_in_udp_tracker( ) { - let tracker = initialized_private_tracker(); + let tracker = private_tracker(); let torrent_stats = match_scrape_response(add_a_sample_seeder_and_scrape(tracker.clone()).await).unwrap(); @@ -1160,11 +1132,11 @@ mod tests { use crate::udp::handlers::tests::scrape_request::{ add_a_seeder, build_scrape_request, match_scrape_response, zeroed_torrent_statistics, }; - use crate::udp::handlers::tests::{initialized_whitelisted_tracker, sample_ipv4_remote_addr}; + use crate::udp::handlers::tests::{sample_ipv4_remote_addr, whitelisted_tracker}; #[tokio::test] async fn should_return_the_torrent_statistics_when_the_requested_torrent_is_whitelisted() { - let tracker = initialized_whitelisted_tracker(); + let tracker = whitelisted_tracker(); let remote_addr = sample_ipv4_remote_addr(); let info_hash = InfoHash([0u8; 20]); @@ -1175,8 +1147,7 @@ mod tests { let request = build_scrape_request(&remote_addr, &info_hash); - let torrent_stats = - match_scrape_response(handle_scrape(remote_addr, &request, tracker.clone()).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); let expected_torrent_stats = vec![TorrentScrapeStatistics { seeders: NumberOfPeers(1), @@ -1189,7 +1160,7 @@ mod tests { #[tokio::test] async fn should_return_zeroed_statistics_when_the_requested_torrent_is_not_whitelisted() { - let tracker = initialized_whitelisted_tracker(); + let tracker = whitelisted_tracker(); let remote_addr = sample_ipv4_remote_addr(); let info_hash = InfoHash([0u8; 20]); @@ -1198,8 +1169,7 @@ mod tests { let request = build_scrape_request(&remote_addr, &info_hash); - let torrent_stats = - match_scrape_response(handle_scrape(remote_addr, &request, tracker.clone()).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1241,10 +1211,10 @@ mod tests { let remote_addr = sample_ipv4_remote_addr(); let tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), tracker.clone()) + handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) .await .unwrap(); } @@ -1273,10 +1243,10 @@ mod tests { let remote_addr = sample_ipv6_remote_addr(); let tracker = Arc::new( - tracker::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + tracker::Tracker::new(tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), tracker.clone()) + handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) .await .unwrap(); } diff --git a/src/udp/server.rs b/src/udp/server.rs index e85c81e9d..e52b8fd52 100644 --- a/src/udp/server.rs +++ b/src/udp/server.rs @@ -1,41 +1,120 @@ +use std::future::Future; use std::io::Cursor; use std::net::SocketAddr; use std::sync::Arc; use aquatic_udp_protocol::Response; +use futures::pin_mut; use log::{debug, error, info}; use tokio::net::UdpSocket; +use tokio::task::JoinHandle; -use crate::tracker; +use crate::signals::shutdown_signal; +use crate::tracker::Tracker; use crate::udp::handlers::handle_packet; use crate::udp::MAX_PACKET_SIZE; +#[derive(Debug)] +pub enum Error { + Error(String), +} + +#[allow(clippy::module_name_repetitions)] +pub type StoppedUdpServer = UdpServer; +#[allow(clippy::module_name_repetitions)] +pub type RunningUdpServer = UdpServer; + +#[allow(clippy::module_name_repetitions)] +pub struct UdpServer { + pub cfg: torrust_tracker_configuration::UdpTracker, + pub state: S, +} + +pub struct Stopped; + +pub struct Running { + pub bind_address: SocketAddr, + stop_job_sender: tokio::sync::oneshot::Sender, + job: JoinHandle<()>, +} + +impl UdpServer { + #[must_use] + pub fn new(cfg: torrust_tracker_configuration::UdpTracker) -> Self { + Self { cfg, state: Stopped {} } + } + + /// # Errors + /// + /// Will return `Err` if UDP can't bind to given bind address. + pub async fn start(self, tracker: Arc) -> Result, Error> { + let udp = Udp::new(&self.cfg.bind_address) + .await + .map_err(|e| Error::Error(e.to_string()))?; + + let bind_address = udp.socket.local_addr().map_err(|e| Error::Error(e.to_string()))?; + + let (sender, receiver) = tokio::sync::oneshot::channel::(); + + let job = tokio::spawn(async move { + udp.start_with_graceful_shutdown(tracker, shutdown_signal(receiver)).await; + }); + + let running_udp_server: UdpServer = UdpServer { + cfg: self.cfg, + state: Running { + bind_address, + stop_job_sender: sender, + job, + }, + }; + + Ok(running_udp_server) + } +} + +impl UdpServer { + /// # Errors + /// + /// Will return `Err` if the oneshot channel to send the stop signal + /// has already been called once. + pub async fn stop(self) -> Result, Error> { + self.state.stop_job_sender.send(1).map_err(|e| Error::Error(e.to_string()))?; + + let _ = self.state.job.await; + + let stopped_api_server: UdpServer = UdpServer { + cfg: self.cfg, + state: Stopped {}, + }; + + Ok(stopped_api_server) + } +} + pub struct Udp { socket: Arc, - tracker: Arc, } impl Udp { /// # Errors /// /// Will return `Err` unable to bind to the supplied `bind_address`. - pub async fn new(tracker: Arc, bind_address: &str) -> tokio::io::Result { + pub async fn new(bind_address: &str) -> tokio::io::Result { let socket = UdpSocket::bind(bind_address).await?; Ok(Udp { socket: Arc::new(socket), - tracker, }) } /// # Panics /// /// It would panic if unable to resolve the `local_addr` from the supplied ´socket´. - pub async fn start(&self) { + pub async fn start(&self, tracker: Arc) { loop { let mut data = [0; MAX_PACKET_SIZE]; let socket = self.socket.clone(); - let tracker = self.tracker.clone(); tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -49,7 +128,41 @@ impl Udp { debug!("From: {}", &remote_addr); debug!("Payload: {:?}", payload); - let response = handle_packet(remote_addr, payload, tracker).await; + let response = handle_packet(remote_addr, payload, &tracker).await; + + Udp::send_response(socket, remote_addr, response).await; + } + } + } + } + + /// # Panics + /// + /// It would panic if unable to resolve the `local_addr` from the supplied ´socket´. + async fn start_with_graceful_shutdown(&self, tracker: Arc, shutdown_signal: F) + where + F: Future, + { + // Pin the future so that it doesn't move to the first loop iteration. + pin_mut!(shutdown_signal); + + loop { + let mut data = [0; MAX_PACKET_SIZE]; + let socket = self.socket.clone(); + + tokio::select! { + _ = &mut shutdown_signal => { + info!("Stopping UDP server: {}..", self.socket.local_addr().unwrap()); + break; + } + Ok((valid_bytes, remote_addr)) = socket.recv_from(&mut data) => { + let payload = data[..valid_bytes].to_vec(); + + info!("Received {} bytes", payload.len()); + debug!("From: {}", &remote_addr); + debug!("Payload: {:?}", payload); + + let response = handle_packet(remote_addr, payload, &tracker).await; Udp::send_response(socket, remote_addr, response).await; } diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 8dd6f4c53..fcb24e491 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -5,7 +5,7 @@ use torrust_tracker::tracker::Tracker; pub mod asserts; pub mod client; pub mod connection_info; -pub mod server; +pub mod test_environment; /// It forces a database error by dropping all tables. /// That makes any query fail. diff --git a/tests/api/server.rs b/tests/api/server.rs deleted file mode 100644 index 0e23a4320..000000000 --- a/tests/api/server.rs +++ /dev/null @@ -1,78 +0,0 @@ -use core::panic; -use std::sync::Arc; - -use torrust_tracker::config::{ephemeral_configuration, Configuration}; -use torrust_tracker::jobs::tracker_apis; -use torrust_tracker::protocol::info_hash::InfoHash; -use torrust_tracker::tracker::peer::Peer; -use torrust_tracker::tracker::statistics::Keeper; -use torrust_tracker::{ephemeral_instance_keys, logging, static_time, tracker}; - -use super::connection_info::ConnectionInfo; - -pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) -} - -pub async fn start_default_api() -> Server { - let configuration = tracker_configuration(); - start_custom_api(configuration.clone()).await -} - -pub async fn start_custom_api(configuration: Arc) -> Server { - let server = start(&configuration); - tracker_apis::start_job(&configuration.http_api, server.tracker.clone()).await; - server -} - -fn start(configuration: &Arc) -> Server { - let connection_info = ConnectionInfo::authenticated( - &configuration.http_api.bind_address.clone(), - &configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(), - ); - - // Set the time of Torrust app starting - lazy_static::initialize(&static_time::TIME_AT_APP_START); - - // Initialize the Ephemeral Instance Random Seed - lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - let tracker = match tracker::Tracker::new(configuration, Some(stats_event_sender), stats_repository) { - Ok(tracker) => Arc::new(tracker), - Err(error) => { - panic!("{}", error) - } - }; - - // Initialize logging - logging::setup(configuration); - - Server { - tracker, - connection_info, - } -} - -pub struct Server { - pub tracker: Arc, - pub connection_info: ConnectionInfo, -} - -impl Server { - pub fn get_connection_info(&self) -> ConnectionInfo { - self.connection_info.clone() - } - - pub fn get_bind_address(&self) -> String { - self.connection_info.bind_address.clone() - } - - /// Add a torrent to the tracker - pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; - } -} diff --git a/tests/api/test_environment.rs b/tests/api/test_environment.rs new file mode 100644 index 000000000..b6f5ca990 --- /dev/null +++ b/tests/api/test_environment.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use torrust_tracker::apis::server::{ApiServer, RunningApiServer, StoppedApiServer}; +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker::tracker::peer::Peer; +use torrust_tracker::tracker::Tracker; + +use super::connection_info::ConnectionInfo; +use crate::common::tracker::new_tracker; + +#[allow(clippy::module_name_repetitions, dead_code)] +pub type StoppedTestEnvironment = TestEnvironment; +#[allow(clippy::module_name_repetitions)] +pub type RunningTestEnvironment = TestEnvironment; + +pub struct TestEnvironment { + pub cfg: Arc, + pub tracker: Arc, + pub state: S, +} + +#[allow(dead_code)] +pub struct Stopped { + api_server: StoppedApiServer, +} + +pub struct Running { + api_server: RunningApiServer, +} + +impl TestEnvironment { + /// Add a torrent to the tracker + pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { + self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + } +} + +impl TestEnvironment { + pub fn new_stopped(cfg: torrust_tracker_configuration::Configuration) -> Self { + let cfg = Arc::new(cfg); + + let tracker = new_tracker(cfg.clone()); + + let api_server = api_server(cfg.http_api.clone()); + + Self { + cfg, + tracker, + state: Stopped { api_server }, + } + } + + pub async fn start(self) -> TestEnvironment { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker.clone(), + state: Running { + api_server: self.state.api_server.start(self.tracker).await.unwrap(), + }, + } + } + + pub fn config_mut(&mut self) -> &mut torrust_tracker_configuration::HttpApi { + &mut self.state.api_server.cfg + } +} + +impl TestEnvironment { + pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self { + let test_env = StoppedTestEnvironment::new_stopped(cfg); + + test_env.start().await + } + + pub async fn stop(self) -> TestEnvironment { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker, + state: Stopped { + api_server: self.state.api_server.stop().await.unwrap(), + }, + } + } + + pub fn get_connection_info(&self) -> ConnectionInfo { + ConnectionInfo { + bind_address: self.state.api_server.state.bind_addr.to_string(), + api_token: self.state.api_server.cfg.access_tokens.get("admin").cloned(), + } + } +} + +#[allow(clippy::module_name_repetitions)] +pub fn stopped_test_environment(cfg: torrust_tracker_configuration::Configuration) -> StoppedTestEnvironment { + TestEnvironment::new_stopped(cfg) +} + +#[allow(clippy::module_name_repetitions)] +pub async fn running_test_environment(cfg: torrust_tracker_configuration::Configuration) -> RunningTestEnvironment { + TestEnvironment::new_running(cfg).await +} + +pub fn api_server(cfg: torrust_tracker_configuration::HttpApi) -> StoppedApiServer { + ApiServer::new(cfg) +} diff --git a/tests/common/http.rs b/tests/common/http.rs index 902752674..d682027fd 100644 --- a/tests/common/http.rs +++ b/tests/common/http.rs @@ -1,11 +1,6 @@ pub type ReqwestQuery = Vec; pub type ReqwestQueryParam = (String, String); -#[derive(Clone, Debug)] -pub struct ConnectionInfo { - pub bind_address: String, -} - /// URL Query component #[derive(Default, Debug)] pub struct Query { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b57996292..9452cc111 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,3 +1,4 @@ pub mod fixtures; pub mod http; +pub mod tracker; pub mod udp; diff --git a/tests/common/tracker.rs b/tests/common/tracker.rs new file mode 100644 index 000000000..127cfefc4 --- /dev/null +++ b/tests/common/tracker.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use torrust_tracker::tracker::services::common::tracker_factory; +use torrust_tracker::tracker::Tracker; +use torrust_tracker::{ephemeral_instance_keys, logging, static_time}; + +// TODO: Move to test-helpers crate once `Tracker` is isolated. +#[allow(clippy::module_name_repetitions)] +pub fn new_tracker(configuration: Arc) -> Arc { + // Set the time of Torrust app starting + lazy_static::initialize(&static_time::TIME_AT_APP_START); + + // Initialize the Ephemeral Instance Random Seed + lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); + + // Initialize logging + logging::setup(&configuration); + + Arc::new(tracker_factory(configuration)) +} diff --git a/tests/http/client.rs b/tests/http/client.rs index 762401078..f5cdca398 100644 --- a/tests/http/client.rs +++ b/tests/http/client.rs @@ -3,13 +3,12 @@ use std::net::IpAddr; use reqwest::{Client as ReqwestClient, Response}; use torrust_tracker::tracker::auth::Key; -use super::connection_info::ConnectionInfo; use super::requests::announce::{self, Query}; use super::requests::scrape; /// HTTP Tracker Client pub struct Client { - connection_info: ConnectionInfo, + server_addr: std::net::SocketAddr, reqwest_client: ReqwestClient, key: Option, } @@ -23,26 +22,26 @@ pub struct Client { /// base url path query /// ``` impl Client { - pub fn new(connection_info: ConnectionInfo) -> Self { + pub fn new(server_addr: std::net::SocketAddr) -> Self { Self { - connection_info, + server_addr, reqwest_client: reqwest::Client::builder().build().unwrap(), key: None, } } /// Creates the new client binding it to an specific local address - pub fn bind(connection_info: ConnectionInfo, local_address: IpAddr) -> Self { + pub fn bind(server_addr: std::net::SocketAddr, local_address: IpAddr) -> Self { Self { - connection_info, + server_addr, reqwest_client: reqwest::Client::builder().local_address(local_address).build().unwrap(), key: None, } } - pub fn authenticated(connection_info: ConnectionInfo, key: Key) -> Self { + pub fn authenticated(server_addr: std::net::SocketAddr, key: Key) -> Self { Self { - connection_info, + server_addr, reqwest_client: reqwest::Client::builder().build().unwrap(), key: Some(key), } @@ -95,6 +94,6 @@ impl Client { } fn base_url(&self) -> String { - format!("http://{}/", &self.connection_info.bind_address) + format!("http://{}/", &self.server_addr) } } diff --git a/tests/http/mod.rs b/tests/http/mod.rs index 40616025b..771145f46 100644 --- a/tests/http/mod.rs +++ b/tests/http/mod.rs @@ -1,10 +1,9 @@ pub mod asserts; pub mod asserts_warp; pub mod client; -pub mod connection_info; pub mod requests; pub mod responses; -pub mod server; +pub mod test_environment; use percent_encoding::NON_ALPHANUMERIC; diff --git a/tests/http/server.rs b/tests/http/server.rs deleted file mode 100644 index 1c8d1cb77..000000000 --- a/tests/http/server.rs +++ /dev/null @@ -1,137 +0,0 @@ -use core::panic; -use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; - -use torrust_tracker::config::{ephemeral_configuration, Configuration}; -use torrust_tracker::http::Version; -use torrust_tracker::jobs::http_tracker; -use torrust_tracker::protocol::info_hash::InfoHash; -use torrust_tracker::tracker::mode::Mode; -use torrust_tracker::tracker::peer::Peer; -use torrust_tracker::tracker::statistics::Keeper; -use torrust_tracker::{ephemeral_instance_keys, logging, static_time, tracker}; - -use super::connection_info::ConnectionInfo; - -/// Starts a HTTP tracker with mode "public" in settings -pub async fn start_public_http_tracker(version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Public; - start_custom_http_tracker(Arc::new(configuration), version).await -} - -/// Starts a HTTP tracker with mode "listed" in settings -pub async fn start_whitelisted_http_tracker(version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Listed; - start_custom_http_tracker(Arc::new(configuration), version).await -} - -/// Starts a HTTP tracker with mode "private" in settings -pub async fn start_private_http_tracker(version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - configuration.mode = Mode::Private; - start_custom_http_tracker(Arc::new(configuration), version).await -} - -/// Starts a HTTP tracker with a wildcard IPV6 address. -/// The configuration in the `config.toml` file would be like this: -/// -/// ```text -/// [[http_trackers]] -/// bind_address = "[::]:7070" -/// ``` -pub async fn start_ipv6_http_tracker(version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - - // Change socket address to "wildcard address" (unspecified address which means any IP address) - // but keeping the random port generated with the ephemeral configuration. - let socket_addr: SocketAddr = configuration.http_trackers[0].bind_address.parse().unwrap(); - let new_ipv6_socket_address = format!("[::]:{}", socket_addr.port()); - configuration.http_trackers[0].bind_address = new_ipv6_socket_address; - - start_custom_http_tracker(Arc::new(configuration), version).await -} - -/// Starts a HTTP tracker with an specific `external_ip`. -/// The configuration in the `config.toml` file would be like this: -/// -/// ```text -/// external_ip = "2.137.87.41" -/// ``` -pub async fn start_http_tracker_with_external_ip(external_ip: &IpAddr, version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - configuration.external_ip = Some(external_ip.to_string()); - start_custom_http_tracker(Arc::new(configuration), version).await -} - -/// Starts a HTTP tracker `on_reverse_proxy`. -/// The configuration in the `config.toml` file would be like this: -/// -/// ```text -/// on_reverse_proxy = true -/// ``` -pub async fn start_http_tracker_on_reverse_proxy(version: Version) -> Server { - let mut configuration = ephemeral_configuration(); - configuration.on_reverse_proxy = true; - start_custom_http_tracker(Arc::new(configuration), version).await -} - -pub async fn start_default_http_tracker(version: Version) -> Server { - let configuration = tracker_configuration(); - start_custom_http_tracker(configuration.clone(), version).await -} - -pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) -} - -pub async fn start_custom_http_tracker(configuration: Arc, version: Version) -> Server { - let server = start(&configuration); - http_tracker::start_job(&configuration.http_trackers[0], server.tracker.clone(), version).await; - server -} - -fn start(configuration: &Arc) -> Server { - let connection_info = ConnectionInfo::anonymous(&configuration.http_trackers[0].bind_address.clone()); - - // Set the time of Torrust app starting - lazy_static::initialize(&static_time::TIME_AT_APP_START); - - // Initialize the Ephemeral Instance Random Seed - lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - let tracker = match tracker::Tracker::new(configuration, Some(stats_event_sender), stats_repository) { - Ok(tracker) => Arc::new(tracker), - Err(error) => { - panic!("{}", error) - } - }; - - // Initialize logging - logging::setup(configuration); - - Server { - tracker, - connection_info, - } -} - -pub struct Server { - pub tracker: Arc, - pub connection_info: ConnectionInfo, -} - -impl Server { - pub fn get_connection_info(&self) -> ConnectionInfo { - self.connection_info.clone() - } - - pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; - } -} diff --git a/tests/http/test_environment.rs b/tests/http/test_environment.rs new file mode 100644 index 000000000..459c2fbe6 --- /dev/null +++ b/tests/http/test_environment.rs @@ -0,0 +1,120 @@ +use std::sync::Arc; + +use torrust_tracker::http::server::{HttpServer, HttpServerLauncher, RunningHttpServer, StoppedHttpServer}; +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker::tracker::peer::Peer; +use torrust_tracker::tracker::Tracker; + +use crate::common::tracker::new_tracker; + +#[allow(clippy::module_name_repetitions, dead_code)] +pub type StoppedTestEnvironment = TestEnvironment>; +#[allow(clippy::module_name_repetitions)] +pub type RunningTestEnvironment = TestEnvironment>; + +pub struct TestEnvironment { + pub cfg: Arc, + pub tracker: Arc, + pub state: S, +} + +#[allow(dead_code)] +pub struct Stopped { + http_server: StoppedHttpServer, +} + +pub struct Running { + http_server: RunningHttpServer, +} + +impl TestEnvironment { + /// Add a torrent to the tracker + pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { + self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + } +} + +impl TestEnvironment> { + #[allow(dead_code)] + pub fn new_stopped(cfg: torrust_tracker_configuration::Configuration) -> Self { + let cfg = Arc::new(cfg); + + let tracker = new_tracker(cfg.clone()); + + let http_server = http_server(cfg.http_trackers[0].clone()); + + Self { + cfg, + tracker, + state: Stopped { http_server }, + } + } + + #[allow(dead_code)] + pub async fn start(self) -> TestEnvironment> { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker.clone(), + state: Running { + http_server: self.state.http_server.start(self.tracker).await.unwrap(), + }, + } + } + + #[allow(dead_code)] + pub fn config(&self) -> &torrust_tracker_configuration::HttpTracker { + &self.state.http_server.cfg + } + + #[allow(dead_code)] + pub fn config_mut(&mut self) -> &mut torrust_tracker_configuration::HttpTracker { + &mut self.state.http_server.cfg + } +} + +impl TestEnvironment> { + pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self { + let test_env = StoppedTestEnvironment::new_stopped(cfg); + + test_env.start().await + } + + pub async fn stop(self) -> TestEnvironment> { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker, + state: Stopped { + http_server: self.state.http_server.stop().await.unwrap(), + }, + } + } + + pub fn bind_address(&self) -> &std::net::SocketAddr { + &self.state.http_server.state.bind_addr + } + + #[allow(dead_code)] + pub fn config(&self) -> &torrust_tracker_configuration::HttpTracker { + &self.state.http_server.cfg + } +} + +#[allow(clippy::module_name_repetitions, dead_code)] +pub fn stopped_test_environment( + cfg: torrust_tracker_configuration::Configuration, +) -> StoppedTestEnvironment { + TestEnvironment::new_stopped(cfg) +} + +#[allow(clippy::module_name_repetitions)] +pub async fn running_test_environment( + cfg: torrust_tracker_configuration::Configuration, +) -> RunningTestEnvironment { + TestEnvironment::new_running(cfg).await +} + +pub fn http_server(cfg: torrust_tracker_configuration::HttpTracker) -> StoppedHttpServer { + let http_server = I::new(); + + HttpServer::new(cfg, http_server) +} diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs index 4219be30a..aea8fac37 100644 --- a/tests/http_tracker.rs +++ b/tests/http_tracker.rs @@ -2,22 +2,46 @@ /// /// Warp version: /// ```text -/// cargo test `warp_http_tracker_server` -- --nocapture +/// cargo test `warp_test_env` -- --nocapture /// ``` /// /// Axum version (WIP): /// ```text -/// cargo test `warp_http_tracker_server` -- --nocapture +/// cargo test `warp_test_env` -- --nocapture /// ``` mod common; mod http; -mod warp_http_tracker_server { +pub type Axum = torrust_tracker::http::axum_implementation::launcher::Launcher; +pub type Warp = torrust_tracker::http::warp_implementation::launcher::Launcher; + +mod test_env_test_environment { + use torrust_tracker_test_helpers::configuration; + + use crate::http::test_environment::running_test_environment; + use crate::{Axum, Warp}; + + #[tokio::test] + async fn should_be_able_to_start_and_stop_a_test_environment_using_axum() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + test_env.stop().await; + } + + #[tokio::test] + async fn should_be_able_to_start_and_stop_a_test_environment_using_warp() { + let test_env = running_test_environment::(configuration::ephemeral()).await; + + test_env.stop().await; + } +} + +mod warp_test_env { mod for_all_config_modes { mod running_on_reverse_proxy { - use torrust_tracker::http::Version; + use torrust_tracker_test_helpers::configuration; use crate::http::asserts::{ assert_could_not_find_remote_address_on_xff_header_error_response, @@ -25,35 +49,38 @@ mod warp_http_tracker_server { }; use crate::http::client::Client; use crate::http::requests::announce::QueryBuilder; - use crate::http::server::start_http_tracker_on_reverse_proxy; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[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 // last IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy client. - let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let params = QueryBuilder::default().query().params(); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_could_not_find_remote_address_on_xff_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 http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let params = QueryBuilder::default().query().params(); - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") .await; assert_invalid_remote_address_on_xff_header_error_response(response).await; + + test_env.stop().await; } } @@ -75,9 +102,9 @@ mod warp_http_tracker_server { use local_ip_address::local_ip; use reqwest::Response; - use torrust_tracker::http::Version; 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::{ @@ -91,38 +118,36 @@ mod warp_http_tracker_server { use crate::http::responses; use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList}; use crate::http::responses::announce_warp::{WarpAnnounce, WarpDictionaryPeer}; - use crate::http::server::{ - start_default_http_tracker, start_http_tracker_on_reverse_proxy, start_http_tracker_with_external_ip, - start_ipv6_http_tracker, start_public_http_tracker, - }; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_respond_if_only_the_mandatory_fields_are_provided() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); params.remove_optional_params(); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; - let response = Client::new(http_tracker_server.get_connection_info()).get("announce").await; + let response = Client::new(*test_env.bind_address()).get("announce").await; assert_internal_server_error_response(response).await; } #[tokio::test] async fn should_fail_when_a_mandatory_field_is_missing() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; // Without `info_hash` param @@ -130,9 +155,7 @@ mod warp_http_tracker_server { params.info_hash = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_invalid_info_hash_error_response(response).await; @@ -142,9 +165,7 @@ mod warp_http_tracker_server { params.peer_id = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_invalid_peer_id_error_response(response).await; @@ -154,28 +175,28 @@ mod warp_http_tracker_server { params.port = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_info_hash_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_invalid_info_hash_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] @@ -185,22 +206,22 @@ mod warp_http_tracker_server { // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP if there. // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request header is tracker is running `on_reverse_proxy`. - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -209,17 +230,17 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("downloaded", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_uploaded_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -228,17 +249,17 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("uploaded", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_peer_id_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -254,17 +275,17 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("peer_id", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_invalid_peer_id_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_port_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -273,17 +294,17 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("port", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_left_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -292,19 +313,19 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("left", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_not_fail_when_the_event_param_is_invalid() { // All invalid values are ignored as if the `event` param were empty - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -321,17 +342,17 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("event", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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_not_fail_when_the_compact_param_is_invalid() { - let http_tracker_server = start_default_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -340,19 +361,19 @@ mod warp_http_tracker_server { for invalid_value in invalid_values { params.set("compact", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; assert_internal_server_error_response(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { - let http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) @@ -365,17 +386,19 @@ mod warp_http_tracker_server { &Announce { complete: 1, // the peer for this test incomplete: 0, - interval: http_tracker_server.tracker.config.announce_interval, - min_interval: http_tracker_server.tracker.config.min_announce_interval, + 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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -385,12 +408,10 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + 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(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -405,23 +426,25 @@ mod warp_http_tracker_server { &WarpAnnounce { complete: 2, incomplete: 0, - interval: http_tracker_server.tracker.config.announce_interval, - min_interval: http_tracker_server.tracker.config.min_announce_interval, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, peers: vec![WarpDictionaryPeer::from(previously_announced_peer)], }, ) .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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + 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 - http_tracker_server.add_torrent_peer(&info_hash, &peer).await; + test_env.add_torrent_peer(&info_hash, &peer).await; let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -430,11 +453,11 @@ mod warp_http_tracker_server { assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); - let response = Client::new(http_tracker_server.get_connection_info()) - .announce(&announce_query) - .await; + let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; assert_empty_announce_response(response).await; + + test_env.stop().await; } #[tokio::test] @@ -442,7 +465,7 @@ mod warp_http_tracker_server { // Tracker Returns Compact Peer Lists // https://www.bittorrent.org/beps/bep_0023.html - let http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -452,12 +475,10 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; // Announce the new Peer 2 accepting compact responses - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -476,6 +497,8 @@ mod warp_http_tracker_server { }; assert_compact_announce_response(response, &expected_response).await; + + test_env.stop().await; } #[tokio::test] @@ -483,7 +506,7 @@ mod warp_http_tracker_server { // 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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -493,14 +516,12 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + 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(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -511,6 +532,8 @@ mod warp_http_tracker_server { .await; assert!(!is_a_compact_announce_response(response).await); + + test_env.stop().await; } async fn is_a_compact_announce_response(response: Response) -> bool { @@ -521,37 +544,45 @@ mod warp_http_tracker_server { #[tokio::test] async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { - let http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_ipv6_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - Client::bind(http_tracker_server.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) @@ -559,44 +590,56 @@ mod warp_http_tracker_server { ) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_ipv6_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - Client::bind(http_tracker_server.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) @@ -604,19 +647,23 @@ mod warp_http_tracker_server { ) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Warp).await; + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -625,11 +672,13 @@ mod warp_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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] @@ -641,14 +690,16 @@ mod warp_http_tracker_server { 127.0.0.1 external_ip = "2.137.87.41" */ - let http_tracker_server = - start_http_tracker_with_external_ip(&IpAddr::from_str("2.137.87.41").unwrap(), Version::Warp).await; + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -657,11 +708,13 @@ mod warp_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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(), http_tracker_server.tracker.config.get_ext_ip().unwrap()); + 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] @@ -673,17 +726,16 @@ mod warp_http_tracker_server { ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" */ - let http_tracker_server = start_http_tracker_with_external_ip( - &IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), - Version::Warp, - ) + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -692,11 +744,13 @@ mod warp_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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(), http_tracker_server.tracker.config.get_ext_ip().unwrap()); + 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] @@ -708,11 +762,11 @@ mod warp_http_tracker_server { 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 */ - let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let client = Client::new(http_tracker_server.get_connection_info()); + let client = Client::new(*test_env.bind_address()); let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); @@ -724,10 +778,12 @@ mod warp_http_tracker_server { ) .await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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; } } @@ -744,9 +800,9 @@ mod warp_http_tracker_server { use std::net::IpAddr; use std::str::FromStr; - use torrust_tracker::http::Version; 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_internal_server_error_response, assert_scrape_response}; @@ -754,41 +810,44 @@ mod warp_http_tracker_server { use crate::http::requests; use crate::http::requests::scrape::QueryBuilder; use crate::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::http::server::{start_ipv6_http_tracker, start_public_http_tracker}; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_fail_when_the_request_is_empty() { - let http_tracker_server = start_public_http_tracker(Version::Warp).await; - let response = Client::new(http_tracker_server.get_connection_info()).get("scrape").await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let response = Client::new(*test_env.bind_address()).get("scrape").await; assert_internal_server_error_response(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_info_hash_param_is_invalid() { - let http_tracker_server = start_public_http_tracker(Version::Warp).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; // code-review: it's not returning the invalid info hash error assert_internal_server_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 http_tracker = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -798,7 +857,7 @@ mod warp_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -818,15 +877,17 @@ mod warp_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -836,7 +897,7 @@ mod warp_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -856,15 +917,17 @@ mod warp_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -873,16 +936,18 @@ mod warp_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Warp).await; + 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(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .add_info_hash(&info_hash1) @@ -897,15 +962,17 @@ mod warp_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::new(http_tracker.get_connection_info()) + Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -913,18 +980,22 @@ mod warp_http_tracker_server { ) .await; - let stats = http_tracker.tracker.get_stats().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 http_tracker = start_ipv6_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::bind(http_tracker.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -932,9 +1003,13 @@ mod warp_http_tracker_server { ) .await; - let stats = http_tracker.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; assert_eq!(stats.tcp6_scrapes_handled, 1); + + drop(stats); + + test_env.stop().await; } } } @@ -944,69 +1019,75 @@ mod warp_http_tracker_server { mod and_receiving_an_announce_request { use std::str::FromStr; - use torrust_tracker::http::Version; 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::server::start_whitelisted_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { - let http_tracker_server = start_whitelisted_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker_server.get_connection_info()) + 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 http_tracker_server = start_whitelisted_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker_server + test_env .tracker .add_torrent_to_whitelist(&info_hash) .await .expect("should add the torrent to the whitelist"); - let response = Client::new(http_tracker_server.get_connection_info()) + 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::http::Version; 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::server::start_whitelisted_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { - let http_tracker = start_whitelisted_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -1016,7 +1097,7 @@ mod warp_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -1027,15 +1108,17 @@ mod warp_http_tracker_server { 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 http_tracker = start_whitelisted_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -1045,13 +1128,13 @@ mod warp_http_tracker_server { ) .await; - http_tracker + test_env .tracker .add_torrent_to_whitelist(&info_hash) .await .expect("should add the torrent to the whitelist"); - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -1071,6 +1154,8 @@ mod warp_http_tracker_server { .build(); assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; } } } @@ -1081,9 +1166,9 @@ mod warp_http_tracker_server { use std::str::FromStr; use std::time::Duration; - use torrust_tracker::http::Version; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker::tracker::auth::Key; + use torrust_tracker_test_helpers::configuration; use crate::http::asserts::assert_is_announce_response; use crate::http::asserts_warp::{ @@ -1091,32 +1176,31 @@ mod warp_http_tracker_server { }; use crate::http::client::Client; use crate::http::requests::announce::QueryBuilder; - use crate::http::server::start_private_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_respond_to_authenticated_peers() { - let http_tracker_server = start_private_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let key = http_tracker_server - .tracker - .generate_auth_key(Duration::from_secs(60)) - .await - .unwrap(); + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(http_tracker_server.get_connection_info(), key.id()) + 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 http_tracker_server = start_private_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) .await; @@ -1125,16 +1209,18 @@ mod warp_http_tracker_server { #[tokio::test] async fn should_fail_if_the_peer_authentication_key_is_not_valid() { - let http_tracker_server = start_private_http_tracker(Version::Warp).await; + 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(http_tracker_server.get_connection_info(), unregistered_key) + let response = Client::authenticated(*test_env.bind_address(), unregistered_key) .announce(&QueryBuilder::default().query()) .await; assert_warp_invalid_authentication_key_error_response(response).await; + + test_env.stop().await; } } @@ -1143,25 +1229,26 @@ mod warp_http_tracker_server { use std::str::FromStr; use std::time::Duration; - use torrust_tracker::http::Version; 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_scrape_response; use crate::http::client::Client; use crate::http::requests; use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::server::start_private_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Warp; #[tokio::test] async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { - let http_tracker = start_private_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -1171,7 +1258,7 @@ mod warp_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -1182,15 +1269,17 @@ mod warp_http_tracker_server { 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 http_tracker = start_private_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -1200,9 +1289,9 @@ mod warp_http_tracker_server { ) .await; - let key = http_tracker.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(http_tracker.get_connection_info(), key.id()) + let response = Client::authenticated(*test_env.bind_address(), key.id()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -1222,17 +1311,19 @@ mod warp_http_tracker_server { .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 - let http_tracker = start_private_http_tracker(Version::Warp).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -1244,7 +1335,7 @@ mod warp_http_tracker_server { let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); - let response = Client::authenticated(http_tracker.get_connection_info(), false_key) + let response = Client::authenticated(*test_env.bind_address(), false_key) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -1255,6 +1346,8 @@ mod warp_http_tracker_server { 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; } } } @@ -1267,47 +1360,50 @@ mod warp_http_tracker_server { } } -mod axum_http_tracker_server { +mod axum_test_env { // WIP: migration HTTP from Warp to Axum mod for_all_config_modes { mod and_running_on_reverse_proxy { - use torrust_tracker::http::Version; + 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::server::start_http_tracker_on_reverse_proxy; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[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 http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let params = QueryBuilder::default().query().params(); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let params = QueryBuilder::default().query().params(); - let response = Client::new(http_tracker_server.get_connection_info()) + 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; } } @@ -1329,9 +1425,9 @@ mod axum_http_tracker_server { use local_ip_address::local_ip; use reqwest::Response; - use torrust_tracker::http::Version; 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::{ @@ -1344,51 +1440,53 @@ mod axum_http_tracker_server { use crate::http::requests::announce::{Compact, QueryBuilder}; use crate::http::responses; use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; - use crate::http::server::{ - start_default_http_tracker, start_http_tracker_on_reverse_proxy, start_http_tracker_with_external_ip, - start_ipv6_http_tracker, start_public_http_tracker, - }; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[tokio::test] async fn should_respond_if_only_the_mandatory_fields_are_provided() { - let http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); params.remove_optional_params(); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; - let response = Client::new(http_tracker_server.get_connection_info()).get("announce").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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let invalid_query_param = "a=b=c"; - let response = Client::new(http_tracker_server.get_connection_info()) + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; // Without `info_hash` param @@ -1396,9 +1494,7 @@ mod axum_http_tracker_server { params.info_hash = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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; @@ -1408,9 +1504,7 @@ mod axum_http_tracker_server { params.peer_id = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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; @@ -1420,28 +1514,28 @@ mod axum_http_tracker_server { params.port = None; - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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] @@ -1451,22 +1545,22 @@ mod axum_http_tracker_server { // 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1475,17 +1569,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("downloaded", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1494,17 +1588,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("uploaded", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1520,17 +1614,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("peer_id", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1539,17 +1633,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("port", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1558,17 +1652,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("left", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1585,17 +1679,17 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("event", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_default_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral()).await; let mut params = QueryBuilder::default().query().params(); @@ -1604,19 +1698,19 @@ mod axum_http_tracker_server { for invalid_value in invalid_values { params.set("compact", invalid_value); - let response = Client::new(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) @@ -1629,17 +1723,19 @@ mod axum_http_tracker_server { &Announce { complete: 1, // the peer for this test incomplete: 0, - interval: http_tracker_server.tracker.config.announce_interval, - min_interval: http_tracker_server.tracker.config.min_announce_interval, + 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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -1649,12 +1745,10 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + 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(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -1669,17 +1763,19 @@ mod axum_http_tracker_server { &Announce { complete: 2, incomplete: 0, - interval: http_tracker_server.tracker.config.announce_interval, - min_interval: http_tracker_server.tracker.config.min_announce_interval, + 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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -1688,7 +1784,7 @@ mod axum_http_tracker_server { .with_peer_id(&peer::Id(*b"-qB00000000000000001")) .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) .build(); - http_tracker_server.add_torrent_peer(&info_hash, &peer_using_ipv4).await; + test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await; // Announce a peer using IPV6 let peer_using_ipv6 = PeerBuilder::default() @@ -1698,10 +1794,10 @@ mod axum_http_tracker_server { 8080, )) .build(); - http_tracker_server.add_torrent_peer(&info_hash, &peer_using_ipv6).await; + test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; // Announce the new Peer. - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -1717,23 +1813,25 @@ mod axum_http_tracker_server { &Announce { complete: 3, incomplete: 0, - interval: http_tracker_server.tracker.config.announce_interval, - min_interval: http_tracker_server.tracker.config.min_announce_interval, + 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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + 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 - http_tracker_server.add_torrent_peer(&info_hash, &peer).await; + test_env.add_torrent_peer(&info_hash, &peer).await; let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -1742,11 +1840,11 @@ mod axum_http_tracker_server { assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); - let response = Client::new(http_tracker_server.get_connection_info()) - .announce(&announce_query) - .await; + let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; assert_empty_announce_response(response).await; + + test_env.stop().await; } #[tokio::test] @@ -1754,7 +1852,7 @@ mod axum_http_tracker_server { // Tracker Returns Compact Peer Lists // https://www.bittorrent.org/beps/bep_0023.html - let http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -1764,12 +1862,10 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; // Announce the new Peer 2 accepting compact responses - let response = Client::new(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -1788,6 +1884,8 @@ mod axum_http_tracker_server { }; assert_compact_announce_response(response, &expected_response).await; + + test_env.stop().await; } #[tokio::test] @@ -1795,7 +1893,7 @@ mod axum_http_tracker_server { // 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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); @@ -1805,14 +1903,12 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server - .add_torrent_peer(&info_hash, &previously_announced_peer) - .await; + 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(http_tracker_server.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_info_hash(&info_hash) @@ -1823,6 +1919,8 @@ mod axum_http_tracker_server { .await; assert!(!is_a_compact_announce_response(response).await); + + test_env.stop().await; } async fn is_a_compact_announce_response(response: Response) -> bool { @@ -1833,37 +1931,45 @@ mod axum_http_tracker_server { #[tokio::test] async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { - let http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_ipv6_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - Client::bind(http_tracker_server.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) @@ -1871,44 +1977,56 @@ mod axum_http_tracker_server { ) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_ipv6_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - Client::bind(http_tracker_server.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .announce(&QueryBuilder::default().query()) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(http_tracker_server.get_connection_info()) + Client::new(*test_env.bind_address()) .announce( &QueryBuilder::default() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) @@ -1916,19 +2034,23 @@ mod axum_http_tracker_server { ) .await; - let stats = http_tracker_server.tracker.get_stats().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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -1937,11 +2059,13 @@ mod axum_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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] @@ -1953,14 +2077,16 @@ mod axum_http_tracker_server { 127.0.0.1 external_ip = "2.137.87.41" */ - let http_tracker_server = - start_http_tracker_with_external_ip(&IpAddr::from_str("2.137.87.41").unwrap(), Version::Axum).await; + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -1969,11 +2095,13 @@ mod axum_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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(), http_tracker_server.tracker.config.get_ext_ip().unwrap()); + 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] @@ -1985,17 +2113,16 @@ mod axum_http_tracker_server { ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" */ - let http_tracker_server = start_http_tracker_with_external_ip( - &IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), - Version::Axum, - ) + 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(http_tracker_server.get_connection_info(), client_ip); + let client = Client::bind(*test_env.bind_address(), client_ip); let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -2004,11 +2131,13 @@ mod axum_http_tracker_server { client.announce(&announce_query).await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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(), http_tracker_server.tracker.config.get_ext_ip().unwrap()); + 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] @@ -2020,11 +2149,11 @@ mod axum_http_tracker_server { 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 */ - let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let client = Client::new(http_tracker_server.get_connection_info()); + let client = Client::new(*test_env.bind_address()); let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); @@ -2036,10 +2165,12 @@ mod axum_http_tracker_server { ) .await; - let peers = http_tracker_server.tracker.get_all_torrent_peers(&info_hash).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; } } @@ -2056,9 +2187,9 @@ mod axum_http_tracker_server { use std::net::IpAddr; use std::str::FromStr; - use torrust_tracker::http::Version; 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::{ @@ -2069,40 +2200,44 @@ mod axum_http_tracker_server { use crate::http::requests; use crate::http::requests::scrape::QueryBuilder; use crate::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::http::server::{start_ipv6_http_tracker, start_public_http_tracker}; + use crate::http::test_environment::running_test_environment; + use crate::Axum; - #[tokio::test] - async fn should_fail_when_the_url_query_component_is_empty() { - let http_tracker_server = start_public_http_tracker(Version::Axum).await; - let response = Client::new(http_tracker_server.get_connection_info()).get("scrape").await; + //#[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 http_tracker_server = start_public_http_tracker(Version::Axum).await; + 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(http_tracker_server.get_connection_info()) - .get(&format!("announce?{params}")) - .await; + 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 http_tracker = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2112,7 +2247,7 @@ mod axum_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2132,15 +2267,17 @@ mod axum_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2150,7 +2287,7 @@ mod axum_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2170,15 +2307,17 @@ mod axum_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2187,16 +2326,18 @@ mod axum_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Axum).await; + 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(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .add_info_hash(&info_hash1) @@ -2211,15 +2352,17 @@ mod axum_http_tracker_server { .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 http_tracker = start_public_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::new(http_tracker.get_connection_info()) + Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2227,18 +2370,22 @@ mod axum_http_tracker_server { ) .await; - let stats = http_tracker.tracker.get_stats().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 http_tracker = start_ipv6_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::bind(http_tracker.get_connection_info(), IpAddr::from_str("::1").unwrap()) + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2246,9 +2393,13 @@ mod axum_http_tracker_server { ) .await; - let stats = http_tracker.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; assert_eq!(stats.tcp6_scrapes_handled, 1); + + drop(stats); + + test_env.stop().await; } } } @@ -2258,68 +2409,74 @@ mod axum_http_tracker_server { mod and_receiving_an_announce_request { use std::str::FromStr; - use torrust_tracker::http::Version; 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::server::start_whitelisted_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[tokio::test] async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { - let http_tracker_server = start_whitelisted_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker_server.get_connection_info()) + 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 http_tracker_server = start_whitelisted_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker_server + test_env .tracker .add_torrent_to_whitelist(&info_hash) .await .expect("should add the torrent to the whitelist"); - let response = Client::new(http_tracker_server.get_connection_info()) + 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::http::Version; 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::server::start_whitelisted_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[tokio::test] async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { - let http_tracker = start_whitelisted_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2329,7 +2486,7 @@ mod axum_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2340,15 +2497,17 @@ mod axum_http_tracker_server { 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 http_tracker = start_whitelisted_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2358,13 +2517,13 @@ mod axum_http_tracker_server { ) .await; - http_tracker + test_env .tracker .add_torrent_to_whitelist(&info_hash) .await .expect("should add the torrent to the whitelist"); - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2384,6 +2543,8 @@ mod axum_http_tracker_server { .build(); assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; } } } @@ -2394,52 +2555,53 @@ mod axum_http_tracker_server { use std::str::FromStr; use std::time::Duration; - use torrust_tracker::http::Version; 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::server::start_private_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[tokio::test] async fn should_respond_to_authenticated_peers() { - let http_tracker_server = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let key = http_tracker_server - .tracker - .generate_auth_key(Duration::from_secs(60)) - .await - .unwrap(); + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(http_tracker_server.get_connection_info(), key.id()) + 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 http_tracker_server = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(http_tracker_server.get_connection_info()) + 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 http_tracker_server = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let invalid_key = "INVALID_KEY"; - let response = Client::new(http_tracker_server.get_connection_info()) + 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" )) @@ -2450,16 +2612,18 @@ mod axum_http_tracker_server { #[tokio::test] async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() { - let http_tracker_server = start_private_http_tracker(Version::Axum).await; + 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(http_tracker_server.get_connection_info(), unregistered_key) + 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; } } @@ -2468,25 +2632,26 @@ mod axum_http_tracker_server { use std::str::FromStr; use std::time::Duration; - use torrust_tracker::http::Version; 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::server::start_private_http_tracker; + use crate::http::test_environment::running_test_environment; + use crate::Axum; #[tokio::test] async fn should_fail_if_the_key_query_param_cannot_be_parsed() { - let http_tracker_server = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let invalid_key = "INVALID_KEY"; - let response = Client::new(http_tracker_server.get_connection_info()) + 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" )) @@ -2497,11 +2662,11 @@ mod axum_http_tracker_server { #[tokio::test] async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { - let http_tracker = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2511,7 +2676,7 @@ mod axum_http_tracker_server { ) .await; - let response = Client::new(http_tracker.get_connection_info()) + let response = Client::new(*test_env.bind_address()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2522,15 +2687,17 @@ mod axum_http_tracker_server { 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 http_tracker = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2540,9 +2707,9 @@ mod axum_http_tracker_server { ) .await; - let key = http_tracker.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(http_tracker.get_connection_info(), key.id()) + let response = Client::authenticated(*test_env.bind_address(), key.id()) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2562,6 +2729,8 @@ mod axum_http_tracker_server { .build(); assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; } #[tokio::test] @@ -2569,11 +2738,11 @@ mod axum_http_tracker_server { // There is not authentication error // code-review: should this really be this way? - let http_tracker = start_private_http_tracker(Version::Axum).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - http_tracker + test_env .add_torrent_peer( &info_hash, &PeerBuilder::default() @@ -2585,7 +2754,7 @@ mod axum_http_tracker_server { let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); - let response = Client::authenticated(http_tracker.get_connection_info(), false_key) + let response = Client::authenticated(*test_env.bind_address(), false_key) .scrape( &requests::scrape::QueryBuilder::default() .with_one_info_hash(&info_hash) @@ -2596,6 +2765,8 @@ mod axum_http_tracker_server { 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; } } } diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs index 35d9af248..dac5907c2 100644 --- a/tests/tracker_api.rs +++ b/tests/tracker_api.rs @@ -9,7 +9,6 @@ mod api; mod common; mod tracker_apis { - use crate::common::fixtures::invalid_info_hashes; // When these infohashes are used in URL path params @@ -24,77 +23,109 @@ mod tracker_apis { [String::new(), " ".to_string()].to_vec() } + mod configuration { + use torrust_tracker_test_helpers::configuration; + + use crate::api::test_environment::stopped_test_environment; + + #[tokio::test] + #[should_panic] + async fn should_fail_with_ssl_enabled_and_bad_ssl_config() { + let mut test_env = stopped_test_environment(configuration::ephemeral()); + + let cfg = test_env.config_mut(); + + cfg.ssl_enabled = true; + cfg.ssl_key_path = Some("bad key path".to_string()); + cfg.ssl_cert_path = Some("bad cert path".to_string()); + + test_env.start().await; + } + } + mod authentication { + use torrust_tracker_test_helpers::configuration; + use crate::api::asserts::{assert_token_not_valid, assert_unauthorized}; use crate::api::client::Client; - use crate::api::server::start_default_api; + use crate::api::test_environment::running_test_environment; use crate::common::http::{Query, QueryParam}; #[tokio::test] async fn should_authenticate_requests_by_using_a_token_query_param() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let token = api_server.get_connection_info().api_token.unwrap(); + let token = test_env.get_connection_info().api_token.unwrap(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec())) .await; assert_eq!(response.status(), 200); + + test_env.stop().await; } #[tokio::test] async fn should_not_authenticate_requests_when_the_token_is_missing() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_request_with_query("stats", Query::default()) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_authenticate_requests_when_the_token_is_empty() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_request_with_query("stats", Query::params([QueryParam::new("token", "")].to_vec())) .await; assert_token_not_valid(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_authenticate_requests_when_the_token_is_invalid() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_request_with_query("stats", Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec())) .await; assert_token_not_valid(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_the_token_query_param_to_be_at_any_position_in_the_url_query() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let token = api_server.get_connection_info().api_token.unwrap(); + let token = test_env.get_connection_info().api_token.unwrap(); // At the beginning of the query component - let response = Client::new(api_server.get_connection_info()) - .get_request(&format!("torrents?token={}&limit=1", &token)) + let response = Client::new(test_env.get_connection_info()) + .get_request(&format!("torrents?token={token}&limit=1")) .await; assert_eq!(response.status(), 200); // At the end of the query component - let response = Client::new(api_server.get_connection_info()) - .get_request(&format!("torrents?limit=1&token={}", &token)) + let response = Client::new(test_env.get_connection_info()) + .get_request(&format!("torrents?limit=1&token={token}")) .await; assert_eq!(response.status(), 200); + + test_env.stop().await; } } @@ -103,25 +134,26 @@ mod tracker_apis { use torrust_tracker::apis::resources::stats::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::server::start_default_api; + use crate::api::test_environment::running_test_environment; use crate::common::fixtures::PeerBuilder; #[tokio::test] async fn should_allow_getting_tracker_statistics() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - api_server + test_env .add_torrent_peer( &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), &PeerBuilder::default().into(), ) .await; - let response = Client::new(api_server.get_connection_info()).get_tracker_statistics().await; + let response = Client::new(test_env.get_connection_info()).get_tracker_statistics().await; assert_stats( response, @@ -145,23 +177,29 @@ mod tracker_apis { }, ) .await; + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .get_tracker_statistics() - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .get_tracker_statistics() + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .get_tracker_statistics() .await; assert_unauthorized(response).await; + + test_env.stop().await; } } @@ -171,6 +209,7 @@ mod tracker_apis { use torrust_tracker::apis::resources::torrent::Torrent; use torrust_tracker::apis::resources::{self, torrent}; use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker_test_helpers::configuration; use super::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; use crate::api::asserts::{ @@ -179,21 +218,19 @@ mod tracker_apis { }; use crate::api::client::Client; use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::server::start_default_api; + use crate::api::test_environment::running_test_environment; use crate::common::fixtures::PeerBuilder; use crate::common::http::{Query, QueryParam}; #[tokio::test] async fn should_allow_getting_torrents() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - api_server.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; + test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; - let response = Client::new(api_server.get_connection_info()) - .get_torrents(Query::empty()) - .await; + let response = Client::new(test_env.get_connection_info()).get_torrents(Query::empty()).await; assert_torrent_list( response, @@ -206,24 +243,22 @@ mod tracker_apis { }], ) .await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_limiting_the_torrents_in_the_result() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; // torrents are ordered alphabetically by infohashes let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - api_server - .add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()) - .await; - api_server - .add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()) - .await; + test_env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await; + test_env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec())) .await; @@ -238,24 +273,22 @@ mod tracker_apis { }], ) .await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_the_torrents_result_pagination() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; // torrents are ordered alphabetically by infohashes let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - api_server - .add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()) - .await; - api_server - .add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()) - .await; + test_env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await; + test_env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec())) .await; @@ -270,66 +303,76 @@ mod tracker_apis { }], ) .await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_getting_torrents_when_the_offset_query_parameter_cannot_be_parsed() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let invalid_offsets = [" ", "-1", "1.1", "INVALID OFFSET"]; for invalid_offset in &invalid_offsets { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("offset", invalid_offset)].to_vec())) .await; assert_bad_request(response, "Failed to deserialize query string: invalid digit found in string").await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_getting_torrents_when_the_limit_query_parameter_cannot_be_parsed() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let invalid_limits = [" ", "-1", "1.1", "INVALID LIMIT"]; for invalid_limit in &invalid_limits { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("limit", invalid_limit)].to_vec())) .await; assert_bad_request(response, "Failed to deserialize query string: invalid digit found in string").await; } + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_getting_torrents_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .get_torrents(Query::empty()) - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .get_torrents(Query::empty()) + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .get_torrents(Query::default()) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_getting_a_torrent_info() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let peer = PeerBuilder::default().into(); - api_server.add_torrent_peer(&info_hash, &peer).await; + test_env.add_torrent_peer(&info_hash, &peer).await; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrent(&info_hash.to_string()) .await; @@ -344,27 +387,31 @@ mod tracker_apis { }, ) .await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_while_getting_a_torrent_info_when_the_torrent_does_not_exist() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrent(&info_hash.to_string()) .await; assert_torrent_not_known(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_getting_a_torrent_info_when_the_provided_infohash_is_invalid() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrent(invalid_infohash) .await; @@ -372,33 +419,39 @@ mod tracker_apis { } for invalid_infohash in &invalid_infohashes_returning_not_found() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .get_torrent(invalid_infohash) .await; assert_not_found(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - api_server.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; + test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .get_torrent(&info_hash.to_string()) - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .get_torrent(&info_hash.to_string()) + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .get_torrent(&info_hash.to_string()) .await; assert_unauthorized(response).await; + + test_env.stop().await; } } @@ -406,6 +459,7 @@ mod tracker_apis { use std::str::FromStr; use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker_test_helpers::configuration; use super::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; use crate::api::asserts::{ @@ -416,82 +470,92 @@ mod tracker_apis { 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::server::start_default_api; + use crate::api::test_environment::running_test_environment; #[tokio::test] async fn should_allow_whitelisting_a_torrent() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .whitelist_a_torrent(&info_hash) .await; assert_ok(response).await; assert!( - api_server + test_env .tracker .is_info_hash_whitelisted(&InfoHash::from_str(&info_hash).unwrap()) .await ); + + test_env.stop().await; } #[tokio::test] async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let api_client = Client::new(api_server.get_connection_info()); + let api_client = Client::new(test_env.get_connection_info()); let response = api_client.whitelist_a_torrent(&info_hash).await; assert_ok(response).await; let response = api_client.whitelist_a_torrent(&info_hash).await; assert_ok(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_whitelisting_a_torrent_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .whitelist_a_torrent(&info_hash) - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .whitelist_a_torrent(&info_hash) + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .whitelist_a_torrent(&info_hash) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_torrent_cannot_be_whitelisted() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .whitelist_a_torrent(&info_hash) .await; assert_failed_to_whitelist_torrent(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_whitelisting_a_torrent_when_the_provided_infohash_is_invalid() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .whitelist_a_torrent(invalid_infohash) .await; @@ -499,49 +563,55 @@ mod tracker_apis { } for invalid_infohash in &invalid_infohashes_returning_not_found() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .whitelist_a_torrent(invalid_infohash) .await; assert_not_found(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_allow_removing_a_torrent_from_the_whitelist() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .remove_torrent_from_whitelist(&hash) .await; assert_ok(response).await; - assert!(!api_server.tracker.is_info_hash_whitelisted(&info_hash).await); + assert!(!test_env.tracker.is_info_hash_whitelisted(&info_hash).await); + + test_env.stop().await; } #[tokio::test] async fn should_not_fail_trying_to_remove_a_non_whitelisted_torrent_from_the_whitelist() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let non_whitelisted_torrent_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .remove_torrent_from_whitelist(&non_whitelisted_torrent_hash) .await; assert_ok(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_removing_a_torrent_from_the_whitelist_when_the_provided_infohash_is_invalid() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .remove_torrent_from_whitelist(invalid_infohash) .await; @@ -549,89 +619,101 @@ mod tracker_apis { } for invalid_infohash in &invalid_infohashes_returning_not_found() { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .remove_torrent_from_whitelist(invalid_infohash) .await; assert_not_found(response).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_torrent_cannot_be_removed_from_the_whitelist() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .remove_torrent_from_whitelist(&hash) .await; assert_failed_to_remove_torrent_from_whitelist(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_removing_a_torrent_from_the_whitelist_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .remove_torrent_from_whitelist(&hash) - .await; + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .remove_torrent_from_whitelist(&hash) + .await; assert_token_not_valid(response).await; - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .remove_torrent_from_whitelist(&hash) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_reload_the_whitelist_from_the_database() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - let response = Client::new(api_server.get_connection_info()).reload_whitelist().await; + let response = Client::new(test_env.get_connection_info()).reload_whitelist().await; assert_ok(response).await; /* todo: this assert fails because the whitelist has not been reloaded yet. We could add a new endpoint GET /api/whitelist/:info_hash to check if a torrent is whitelisted and use that endpoint to check if the torrent is still there after reloading. assert!( - !(api_server + !(test_env .tracker .is_info_hash_whitelisted(&InfoHash::from_str(&info_hash).unwrap()) .await) ); */ + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_whitelist_cannot_be_reloaded_from_the_database() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); - let response = Client::new(api_server.get_connection_info()).reload_whitelist().await; + let response = Client::new(test_env.get_connection_info()).reload_whitelist().await; assert_failed_to_reload_whitelist(response).await; + + test_env.stop().await; } } @@ -639,6 +721,7 @@ mod tracker_apis { use std::time::Duration; use torrust_tracker::tracker::auth::Key; + use torrust_tracker_test_helpers::configuration; use crate::api::asserts::{ assert_auth_key_utf8, assert_failed_to_delete_key, assert_failed_to_generate_key, assert_failed_to_reload_keys, @@ -648,50 +731,56 @@ mod tracker_apis { 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::server::start_default_api; + use crate::api::test_environment::running_test_environment; #[tokio::test] async fn should_allow_generating_a_new_auth_key() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .generate_auth_key(seconds_valid) .await; let auth_key_resource = assert_auth_key_utf8(response).await; // Verify the key with the tracker - assert!(api_server + assert!(test_env .tracker .verify_auth_key(&auth_key_resource.key.parse::().unwrap()) .await .is_ok()); + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .generate_auth_key(seconds_valid) - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .generate_auth_key(seconds_valid) + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .generate_auth_key(seconds_valid) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let invalid_key_durations = [ // "", it returns 404 @@ -699,50 +788,56 @@ mod tracker_apis { "-1", "text", ]; - for invalid_key_duration in &invalid_key_durations { - let response = Client::new(api_server.get_connection_info()) - .post(&format!("key/{}", &invalid_key_duration)) + for invalid_key_duration in invalid_key_durations { + let response = Client::new(test_env.get_connection_info()) + .post(&format!("key/{invalid_key_duration}")) .await; assert_invalid_key_duration_param(response, invalid_key_duration).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_auth_key_cannot_be_generated() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); let seconds_valid = 60; - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .generate_auth_key(seconds_valid) .await; assert_failed_to_generate_key(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_deleting_an_auth_key() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - let auth_key = api_server + let auth_key = test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .delete_auth_key(&auth_key.key.to_string()) .await; assert_ok(response).await; + + test_env.stop().await; } #[tokio::test] - async fn should_fail_deleting_an_auth_key_when_the_key_is_invalid() { - let api_server = start_default_api().await; + async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() { + let test_env = running_test_environment(configuration::ephemeral()).await; let invalid_auth_keys = [ // "", it returns a 404 @@ -755,123 +850,139 @@ mod tracker_apis { ]; for invalid_auth_key in &invalid_auth_keys { - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .delete_auth_key(invalid_auth_key) .await; assert_invalid_auth_key_param(response, invalid_auth_key).await; } + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_the_auth_key_cannot_be_deleted() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - let auth_key = api_server + let auth_key = test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); - let response = Client::new(api_server.get_connection_info()) + let response = Client::new(test_env.get_connection_info()) .delete_auth_key(&auth_key.key.to_string()) .await; assert_failed_to_delete_key(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_deleting_an_auth_key_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; // Generate new auth key - let auth_key = api_server + let auth_key = test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .delete_auth_key(&auth_key.key.to_string()) - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .delete_auth_key(&auth_key.key.to_string()) + .await; assert_token_not_valid(response).await; // Generate new auth key - let auth_key = api_server + let auth_key = test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .delete_auth_key(&auth_key.key.to_string()) .await; assert_unauthorized(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_allow_reloading_keys() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - api_server + test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - let response = Client::new(api_server.get_connection_info()).reload_keys().await; + let response = Client::new(test_env.get_connection_info()).reload_keys().await; assert_ok(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_fail_when_keys_cannot_be_reloaded() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - api_server + test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - force_database_error(&api_server.tracker); + force_database_error(&test_env.tracker); - let response = Client::new(api_server.get_connection_info()).reload_keys().await; + let response = Client::new(test_env.get_connection_info()).reload_keys().await; assert_failed_to_reload_keys(response).await; + + test_env.stop().await; } #[tokio::test] async fn should_not_allow_reloading_keys_for_unauthenticated_users() { - let api_server = start_default_api().await; + let test_env = running_test_environment(configuration::ephemeral()).await; let seconds_valid = 60; - api_server + test_env .tracker .generate_auth_key(Duration::from_secs(seconds_valid)) .await .unwrap(); - let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) - .reload_keys() - .await; + let response = Client::new(connection_with_invalid_token( + test_env.get_connection_info().bind_address.as_str(), + )) + .reload_keys() + .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(&api_server.get_bind_address())) + let response = Client::new(connection_with_no_token(test_env.get_connection_info().bind_address.as_str())) .reload_keys() .await; assert_unauthorized(response).await; + + test_env.stop().await; } } } diff --git a/tests/udp/client.rs b/tests/udp/client.rs index 3cb4d6134..0bec03d03 100644 --- a/tests/udp/client.rs +++ b/tests/udp/client.rs @@ -1,41 +1,54 @@ use std::io::Cursor; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; use aquatic_udp_protocol::{Request, Response}; -use rand::{thread_rng, Rng}; +use tokio::net::UdpSocket; use torrust_tracker::udp::MAX_PACKET_SIZE; -use crate::common::udp::Client as UdpClient; +use crate::udp::source_address; -/// Creates a new generic UDP client connected to a generic UDP server -pub async fn new_udp_client_connected(remote_address: &SocketAddr) -> UdpClient { - let local_address = loopback_socket_address(ephemeral_random_client_port()); - UdpClient::connected(remote_address, &local_address).await +#[allow(clippy::module_name_repetitions)] +pub struct UdpClient { + pub socket: Arc, } -/// Creates a new UDP tracker client connected to a UDP tracker server -pub async fn new_udp_tracker_client_connected(remote_address: &SocketAddr) -> Client { - let udp_client = new_udp_client_connected(remote_address).await; - Client { udp_client } -} +impl UdpClient { + pub async fn bind(local_address: &str) -> Self { + let socket = UdpSocket::bind(local_address).await.unwrap(); + Self { + socket: Arc::new(socket), + } + } -pub fn ephemeral_random_client_port() -> u16 { - // todo: this may produce random test failures because two tests can try to bind the same port. - // We could create a pool of available ports (with read/write lock) - let mut rng = thread_rng(); - rng.gen_range(49152..65535) + pub async fn connect(&self, remote_address: &str) { + self.socket.connect(remote_address).await.unwrap(); + } + + pub async fn send(&self, bytes: &[u8]) -> usize { + self.socket.writable().await.unwrap(); + self.socket.send(bytes).await.unwrap() + } + + pub async fn receive(&self, bytes: &mut [u8]) -> usize { + self.socket.readable().await.unwrap(); + self.socket.recv(bytes).await.unwrap() + } } -fn loopback_socket_address(port: u16) -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port) +/// Creates a new `UdpClient` connected to a Udp server +pub async fn new_udp_client_connected(remote_address: &str) -> UdpClient { + let port = 0; // Let OS choose an unused port. + let client = UdpClient::bind(&source_address(port)).await; + client.connect(remote_address).await; + client } -/// A UDP tracker client -pub struct Client { - pub udp_client: UdpClient, // A generic UDP client +#[allow(clippy::module_name_repetitions)] +pub struct UdpTrackerClient { + pub udp_client: UdpClient, } -impl Client { +impl UdpTrackerClient { pub async fn send(&self, request: Request) -> usize { // Write request into a buffer let request_buffer = vec![0u8; MAX_PACKET_SIZE]; @@ -63,3 +76,9 @@ impl Client { Response::from_bytes(&response_buffer[..payload_size], true).unwrap() } } + +/// Creates a new `UdpTrackerClient` connected to a Udp Tracker server +pub async fn new_udp_tracker_client_connected(remote_address: &str) -> UdpTrackerClient { + let udp_client = new_udp_client_connected(remote_address).await; + UdpTrackerClient { udp_client } +} diff --git a/tests/udp/mod.rs b/tests/udp/mod.rs index 16a77bb99..f45a4a4f9 100644 --- a/tests/udp/mod.rs +++ b/tests/udp/mod.rs @@ -1,3 +1,8 @@ pub mod asserts; pub mod client; -pub mod server; +pub mod test_environment; + +/// Generates the source address for the UDP client +fn source_address(port: u16) -> String { + format!("127.0.0.1:{port}") +} diff --git a/tests/udp/server.rs b/tests/udp/server.rs deleted file mode 100644 index 401d4cf92..000000000 --- a/tests/udp/server.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::net::SocketAddr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use tokio::task::JoinHandle; -use torrust_tracker::config::{ephemeral_configuration, Configuration}; -use torrust_tracker::jobs::udp_tracker; -use torrust_tracker::tracker::statistics::Keeper; -use torrust_tracker::{ephemeral_instance_keys, logging, static_time, tracker}; - -pub fn start_udp_tracker(configuration: &Arc) -> Server { - let mut udp_server = Server::new(); - udp_server.start(configuration); - udp_server -} - -pub fn tracker_configuration() -> Arc { - Arc::new(ephemeral_configuration()) -} -pub struct Server { - pub started: AtomicBool, - pub job: Option>, - pub bind_address: Option, -} - -impl Server { - pub fn new() -> Self { - Self { - started: AtomicBool::new(false), - job: None, - bind_address: None, - } - } - - pub fn start(&mut self, configuration: &Arc) { - if !self.started.load(Ordering::Relaxed) { - // Set the time of Torrust app starting - lazy_static::initialize(&static_time::TIME_AT_APP_START); - - // Initialize the Ephemeral Instance Random Seed - lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - let tracker = match tracker::Tracker::new(&configuration.clone(), Some(stats_event_sender), stats_repository) { - Ok(tracker) => Arc::new(tracker), - Err(error) => { - panic!("{}", error) - } - }; - - // Initialize logging - logging::setup(configuration); - - let udp_tracker_config = &configuration.udp_trackers[0]; - - // Start the UDP tracker job - self.job = Some(udp_tracker::start_job(udp_tracker_config, tracker)); - - self.bind_address = Some(udp_tracker_config.bind_address.parse().unwrap()); - - self.started.store(true, Ordering::Relaxed); - } - } -} diff --git a/tests/udp/test_environment.rs b/tests/udp/test_environment.rs new file mode 100644 index 000000000..f729777a1 --- /dev/null +++ b/tests/udp/test_environment.rs @@ -0,0 +1,100 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker::tracker::peer::Peer; +use torrust_tracker::tracker::Tracker; +use torrust_tracker::udp::server::{RunningUdpServer, StoppedUdpServer, UdpServer}; + +use crate::common::tracker::new_tracker; + +#[allow(clippy::module_name_repetitions, dead_code)] +pub type StoppedTestEnvironment = TestEnvironment; +#[allow(clippy::module_name_repetitions)] +pub type RunningTestEnvironment = TestEnvironment; + +pub struct TestEnvironment { + pub cfg: Arc, + pub tracker: Arc, + pub state: S, +} + +#[allow(dead_code)] +pub struct Stopped { + udp_server: StoppedUdpServer, +} + +pub struct Running { + udp_server: RunningUdpServer, +} + +impl TestEnvironment { + /// Add a torrent to the tracker + #[allow(dead_code)] + pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) { + self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + } +} + +impl TestEnvironment { + #[allow(dead_code)] + pub fn new_stopped(cfg: torrust_tracker_configuration::Configuration) -> Self { + let cfg = Arc::new(cfg); + + let tracker = new_tracker(cfg.clone()); + + let udp_server = udp_server(cfg.udp_trackers[0].clone()); + + Self { + cfg, + tracker, + state: Stopped { udp_server }, + } + } + + #[allow(dead_code)] + pub async fn start(self) -> TestEnvironment { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker.clone(), + state: Running { + udp_server: self.state.udp_server.start(self.tracker).await.unwrap(), + }, + } + } +} + +impl TestEnvironment { + pub async fn new_running(cfg: torrust_tracker_configuration::Configuration) -> Self { + StoppedTestEnvironment::new_stopped(cfg).start().await + } + + #[allow(dead_code)] + pub async fn stop(self) -> TestEnvironment { + TestEnvironment { + cfg: self.cfg, + tracker: self.tracker, + state: Stopped { + udp_server: self.state.udp_server.stop().await.unwrap(), + }, + } + } + + pub fn bind_address(&self) -> SocketAddr { + self.state.udp_server.state.bind_address + } +} + +#[allow(clippy::module_name_repetitions, dead_code)] +pub fn stopped_test_environment(cfg: torrust_tracker_configuration::Configuration) -> StoppedTestEnvironment { + TestEnvironment::new_stopped(cfg) +} + +#[allow(clippy::module_name_repetitions)] +pub async fn running_test_environment(cfg: torrust_tracker_configuration::Configuration) -> RunningTestEnvironment { + TestEnvironment::new_running(cfg).await +} + +pub fn udp_server(cfg: torrust_tracker_configuration::UdpTracker) -> StoppedUdpServer { + UdpServer::new(cfg) +} diff --git a/tests/udp_tracker.rs b/tests/udp_tracker.rs index 0287d01b7..0f9283a8b 100644 --- a/tests/udp_tracker.rs +++ b/tests/udp_tracker.rs @@ -17,10 +17,11 @@ mod udp_tracker_server { 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, Client}; - use crate::udp::server::{start_udp_tracker, tracker_configuration}; + 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] @@ -30,7 +31,7 @@ mod udp_tracker_server { [0; MAX_PACKET_SIZE] } - async fn send_connection_request(transaction_id: TransactionId, client: &Client) -> ConnectionId { + async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { let connect_request = ConnectRequest { transaction_id }; client.send(connect_request.into()).await; @@ -45,11 +46,9 @@ mod udp_tracker_server { #[tokio::test] async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { - let configuration = tracker_configuration(); + let test_env = running_test_environment(configuration::ephemeral()).await; - let udp_server = start_udp_tracker(&configuration); - - let client = new_udp_client_connected(&udp_server.bind_address.unwrap()).await; + let client = new_udp_client_connected(&test_env.bind_address().to_string()).await; client.send(&empty_udp_request()).await; @@ -62,18 +61,17 @@ mod udp_tracker_server { 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::server::{start_udp_tracker, tracker_configuration}; + use crate::udp::test_environment::running_test_environment; #[tokio::test] async fn should_return_a_connect_response() { - let configuration = tracker_configuration(); - - let udp_server = start_udp_tracker(&configuration); + let test_env = running_test_environment(configuration::ephemeral()).await; - let client = new_udp_tracker_client_connected(&udp_server.bind_address.unwrap()).await; + let client = new_udp_tracker_client_connected(&test_env.bind_address().to_string()).await; let connect_request = ConnectRequest { transaction_id: TransactionId(123), @@ -94,19 +92,18 @@ mod udp_tracker_server { 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::server::{start_udp_tracker, tracker_configuration}; + 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 configuration = tracker_configuration(); + let test_env = running_test_environment(configuration::ephemeral()).await; - let udp_server = start_udp_tracker(&configuration); - - let client = new_udp_tracker_client_connected(&udp_server.bind_address.unwrap()).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; @@ -137,19 +134,18 @@ mod udp_tracker_server { 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::server::{start_udp_tracker, tracker_configuration}; + 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 configuration = tracker_configuration(); - - let udp_server = start_udp_tracker(&configuration); + let test_env = running_test_environment(configuration::ephemeral()).await; - let client = new_udp_tracker_client_connected(&udp_server.bind_address.unwrap()).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;