diff --git a/src/apis/context/auth_key/handlers.rs b/src/apis/context/auth_key/handlers.rs new file mode 100644 index 000000000..af78b3f4c --- /dev/null +++ b/src/apis/context/auth_key/handlers.rs @@ -0,0 +1,46 @@ +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use axum::extract::{Path, State}; +use axum::response::Response; +use serde::Deserialize; + +use super::responses::{ + auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response, +}; +use crate::apis::context::auth_key::resources::AuthKey; +use crate::apis::responses::{invalid_auth_key_param_response, ok_response}; +use crate::tracker::auth::Key; +use crate::tracker::Tracker; + +pub async fn generate_auth_key_handler(State(tracker): State>, Path(seconds_valid_or_key): Path) -> Response { + let seconds_valid = seconds_valid_or_key; + match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { + Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)), + Err(e) => failed_to_generate_key_response(e), + } +} + +#[derive(Deserialize)] +pub struct KeyParam(String); + +pub async fn delete_auth_key_handler( + State(tracker): State>, + Path(seconds_valid_or_key): Path, +) -> Response { + match Key::from_str(&seconds_valid_or_key.0) { + Err(_) => invalid_auth_key_param_response(&seconds_valid_or_key.0), + Ok(key) => match tracker.remove_auth_key(&key.to_string()).await { + Ok(_) => ok_response(), + Err(e) => failed_to_delete_key_response(e), + }, + } +} + +pub async fn reload_keys_handler(State(tracker): State>) -> Response { + match tracker.load_keys_from_database().await { + Ok(_) => ok_response(), + Err(e) => failed_to_reload_keys_response(e), + } +} diff --git a/src/apis/context/auth_key/mod.rs b/src/apis/context/auth_key/mod.rs new file mode 100644 index 000000000..746a2f064 --- /dev/null +++ b/src/apis/context/auth_key/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod resources; +pub mod responses; +pub mod routes; diff --git a/src/apis/resources/auth_key.rs b/src/apis/context/auth_key/resources.rs similarity index 100% rename from src/apis/resources/auth_key.rs rename to src/apis/context/auth_key/resources.rs diff --git a/src/apis/context/auth_key/responses.rs b/src/apis/context/auth_key/responses.rs new file mode 100644 index 000000000..8c1bf58dc --- /dev/null +++ b/src/apis/context/auth_key/responses.rs @@ -0,0 +1,35 @@ +use std::error::Error; + +use axum::http::{header, StatusCode}; +use axum::response::{IntoResponse, Response}; + +use crate::apis::context::auth_key::resources::AuthKey; +use crate::apis::responses::unhandled_rejection_response; + +/// # Panics +/// +/// Will panic if it can't convert the `AuthKey` resource to json +#[must_use] +pub fn auth_key_response(auth_key: &AuthKey) -> Response { + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/json; charset=utf-8")], + serde_json::to_string(auth_key).unwrap(), + ) + .into_response() +} + +#[must_use] +pub fn failed_to_generate_key_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to generate key: {e}")) +} + +#[must_use] +pub fn failed_to_delete_key_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to delete key: {e}")) +} + +#[must_use] +pub fn failed_to_reload_keys_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to reload keys: {e}")) +} diff --git a/src/apis/context/auth_key/routes.rs b/src/apis/context/auth_key/routes.rs new file mode 100644 index 000000000..2a4f5b9dd --- /dev/null +++ b/src/apis/context/auth_key/routes.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use axum::routing::{get, post}; +use axum::Router; + +use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; +use crate::tracker::Tracker; + +pub fn add(router: Router, tracker: Arc) -> Router { + // Keys + router + .route( + // code-review: Axum does not allow two routes with the same path but different path variable name. + // In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths: + // POST /api/key + // DELETE /api/key/:key + "/api/key/:seconds_valid_or_key", + post(generate_auth_key_handler) + .with_state(tracker.clone()) + .delete(delete_auth_key_handler) + .with_state(tracker.clone()), + ) + // Keys command + .route("/api/keys/reload", get(reload_keys_handler).with_state(tracker)) +} diff --git a/src/apis/resources/mod.rs b/src/apis/context/mod.rs similarity index 72% rename from src/apis/resources/mod.rs rename to src/apis/context/mod.rs index bf3ce273b..6d3fb7566 100644 --- a/src/apis/resources/mod.rs +++ b/src/apis/context/mod.rs @@ -1,4 +1,4 @@ pub mod auth_key; -pub mod peer; pub mod stats; pub mod torrent; +pub mod whitelist; diff --git a/src/apis/context/stats/handlers.rs b/src/apis/context/stats/handlers.rs new file mode 100644 index 000000000..e93e65996 --- /dev/null +++ b/src/apis/context/stats/handlers.rs @@ -0,0 +1,13 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::response::Json; + +use super::resources::Stats; +use super::responses::stats_response; +use crate::tracker::services::statistics::get_metrics; +use crate::tracker::Tracker; + +pub async fn get_stats_handler(State(tracker): State>) -> Json { + stats_response(get_metrics(tracker.clone()).await) +} diff --git a/src/apis/context/stats/mod.rs b/src/apis/context/stats/mod.rs new file mode 100644 index 000000000..746a2f064 --- /dev/null +++ b/src/apis/context/stats/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod resources; +pub mod responses; +pub mod routes; diff --git a/src/apis/resources/stats.rs b/src/apis/context/stats/resources.rs similarity index 100% rename from src/apis/resources/stats.rs rename to src/apis/context/stats/resources.rs diff --git a/src/apis/context/stats/responses.rs b/src/apis/context/stats/responses.rs new file mode 100644 index 000000000..ea9a2480a --- /dev/null +++ b/src/apis/context/stats/responses.rs @@ -0,0 +1,8 @@ +use axum::response::Json; + +use super::resources::Stats; +use crate::tracker::services::statistics::TrackerMetrics; + +pub fn stats_response(tracker_metrics: TrackerMetrics) -> Json { + Json(Stats::from(tracker_metrics)) +} diff --git a/src/apis/context/stats/routes.rs b/src/apis/context/stats/routes.rs new file mode 100644 index 000000000..8791ed25a --- /dev/null +++ b/src/apis/context/stats/routes.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use axum::routing::get; +use axum::Router; + +use super::handlers::get_stats_handler; +use crate::tracker::Tracker; + +pub fn add(router: Router, tracker: Arc) -> Router { + router.route("/api/stats", get(get_stats_handler).with_state(tracker)) +} diff --git a/src/apis/context/torrent/handlers.rs b/src/apis/context/torrent/handlers.rs new file mode 100644 index 000000000..1a8280e75 --- /dev/null +++ b/src/apis/context/torrent/handlers.rs @@ -0,0 +1,59 @@ +use std::fmt; +use std::str::FromStr; +use std::sync::Arc; + +use axum::extract::{Path, Query, State}; +use axum::response::{IntoResponse, Json, Response}; +use serde::{de, Deserialize, Deserializer}; + +use super::resources::torrent::ListItem; +use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response}; +use crate::apis::responses::invalid_info_hash_param_response; +use crate::apis::InfoHashParam; +use crate::protocol::info_hash::InfoHash; +use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination}; +use crate::tracker::Tracker; + +pub async fn get_torrent_handler(State(tracker): State>, Path(info_hash): Path) -> Response { + match InfoHash::from_str(&info_hash.0) { + Err(_) => invalid_info_hash_param_response(&info_hash.0), + Ok(info_hash) => match get_torrent_info(tracker.clone(), &info_hash).await { + Some(info) => torrent_info_response(info).into_response(), + None => torrent_not_known_response(), + }, + } +} + +#[derive(Deserialize)] +pub struct PaginationParams { + #[serde(default, deserialize_with = "empty_string_as_none")] + pub offset: Option, + pub limit: Option, +} + +pub async fn get_torrents_handler( + State(tracker): State>, + pagination: Query, +) -> Json> { + torrent_list_response( + &get_torrents( + tracker.clone(), + &Pagination::new_with_options(pagination.0.offset, pagination.0.limit), + ) + .await, + ) +} + +/// Serde deserialization decorator to map empty Strings to None, +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + T::Err: fmt::Display, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some), + } +} diff --git a/src/apis/context/torrent/mod.rs b/src/apis/context/torrent/mod.rs new file mode 100644 index 000000000..746a2f064 --- /dev/null +++ b/src/apis/context/torrent/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod resources; +pub mod responses; +pub mod routes; diff --git a/src/apis/context/torrent/resources/mod.rs b/src/apis/context/torrent/resources/mod.rs new file mode 100644 index 000000000..46d62aac5 --- /dev/null +++ b/src/apis/context/torrent/resources/mod.rs @@ -0,0 +1,2 @@ +pub mod peer; +pub mod torrent; diff --git a/src/apis/resources/peer.rs b/src/apis/context/torrent/resources/peer.rs similarity index 100% rename from src/apis/resources/peer.rs rename to src/apis/context/torrent/resources/peer.rs diff --git a/src/apis/resources/torrent.rs b/src/apis/context/torrent/resources/torrent.rs similarity index 96% rename from src/apis/resources/torrent.rs rename to src/apis/context/torrent/resources/torrent.rs index 3d8b2f427..1099dc923 100644 --- a/src/apis/resources/torrent.rs +++ b/src/apis/context/torrent/resources/torrent.rs @@ -74,8 +74,9 @@ mod tests { use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; - use crate::apis::resources::peer::Peer; - use crate::apis::resources::torrent::{ListItem, Torrent}; + use super::Torrent; + use crate::apis::context::torrent::resources::peer::Peer; + use crate::apis::context::torrent::resources::torrent::ListItem; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; use crate::tracker::peer; diff --git a/src/apis/context/torrent/responses.rs b/src/apis/context/torrent/responses.rs new file mode 100644 index 000000000..48e3c6e7f --- /dev/null +++ b/src/apis/context/torrent/responses.rs @@ -0,0 +1,18 @@ +use axum::response::{IntoResponse, Json, Response}; +use serde_json::json; + +use super::resources::torrent::{ListItem, Torrent}; +use crate::tracker::services::torrent::{BasicInfo, Info}; + +pub fn torrent_list_response(basic_infos: &[BasicInfo]) -> Json> { + Json(ListItem::new_vec(basic_infos)) +} + +pub fn torrent_info_response(info: Info) -> Json { + Json(Torrent::from(info)) +} + +#[must_use] +pub fn torrent_not_known_response() -> Response { + Json(json!("torrent not known")).into_response() +} diff --git a/src/apis/context/torrent/routes.rs b/src/apis/context/torrent/routes.rs new file mode 100644 index 000000000..234f17223 --- /dev/null +++ b/src/apis/context/torrent/routes.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use axum::routing::get; +use axum::Router; + +use super::handlers::{get_torrent_handler, get_torrents_handler}; +use crate::tracker::Tracker; + +pub fn add(router: Router, tracker: Arc) -> Router { + // Torrents + router + .route( + "/api/torrent/:info_hash", + get(get_torrent_handler).with_state(tracker.clone()), + ) + .route("/api/torrents", get(get_torrents_handler).with_state(tracker)) +} diff --git a/src/apis/context/whitelist/handlers.rs b/src/apis/context/whitelist/handlers.rs new file mode 100644 index 000000000..c1e90a509 --- /dev/null +++ b/src/apis/context/whitelist/handlers.rs @@ -0,0 +1,46 @@ +use std::str::FromStr; +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::response::Response; + +use super::responses::{ + failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response, +}; +use crate::apis::responses::{invalid_info_hash_param_response, ok_response}; +use crate::apis::InfoHashParam; +use crate::protocol::info_hash::InfoHash; +use crate::tracker::Tracker; + +pub async fn add_torrent_to_whitelist_handler( + State(tracker): State>, + Path(info_hash): Path, +) -> Response { + match InfoHash::from_str(&info_hash.0) { + Err(_) => invalid_info_hash_param_response(&info_hash.0), + Ok(info_hash) => match tracker.add_torrent_to_whitelist(&info_hash).await { + Ok(_) => ok_response(), + Err(e) => failed_to_whitelist_torrent_response(e), + }, + } +} + +pub async fn remove_torrent_from_whitelist_handler( + State(tracker): State>, + Path(info_hash): Path, +) -> Response { + match InfoHash::from_str(&info_hash.0) { + Err(_) => invalid_info_hash_param_response(&info_hash.0), + Ok(info_hash) => match tracker.remove_torrent_from_whitelist(&info_hash).await { + Ok(_) => ok_response(), + Err(e) => failed_to_remove_torrent_from_whitelist_response(e), + }, + } +} + +pub async fn reload_whitelist_handler(State(tracker): State>) -> Response { + match tracker.load_whitelist_from_database().await { + Ok(_) => ok_response(), + Err(e) => failed_to_reload_whitelist_response(e), + } +} diff --git a/src/apis/context/whitelist/mod.rs b/src/apis/context/whitelist/mod.rs new file mode 100644 index 000000000..f6f000f34 --- /dev/null +++ b/src/apis/context/whitelist/mod.rs @@ -0,0 +1,3 @@ +pub mod handlers; +pub mod responses; +pub mod routes; diff --git a/src/apis/context/whitelist/responses.rs b/src/apis/context/whitelist/responses.rs new file mode 100644 index 000000000..dd2727898 --- /dev/null +++ b/src/apis/context/whitelist/responses.rs @@ -0,0 +1,20 @@ +use std::error::Error; + +use axum::response::Response; + +use crate::apis::responses::unhandled_rejection_response; + +#[must_use] +pub fn failed_to_remove_torrent_from_whitelist_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to remove torrent from whitelist: {e}")) +} + +#[must_use] +pub fn failed_to_whitelist_torrent_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to whitelist torrent: {e}")) +} + +#[must_use] +pub fn failed_to_reload_whitelist_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to reload whitelist: {e}")) +} diff --git a/src/apis/context/whitelist/routes.rs b/src/apis/context/whitelist/routes.rs new file mode 100644 index 000000000..1349f8bc1 --- /dev/null +++ b/src/apis/context/whitelist/routes.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use axum::routing::{delete, get, post}; +use axum::Router; + +use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; +use crate::tracker::Tracker; + +pub fn add(router: Router, tracker: Arc) -> Router { + router + // Whitelisted torrents + .route( + "/api/whitelist/:info_hash", + post(add_torrent_to_whitelist_handler).with_state(tracker.clone()), + ) + .route( + "/api/whitelist/:info_hash", + delete(remove_torrent_from_whitelist_handler).with_state(tracker.clone()), + ) + // Whitelist commands + .route("/api/whitelist/reload", get(reload_whitelist_handler).with_state(tracker)) +} diff --git a/src/apis/handlers.rs b/src/apis/handlers.rs deleted file mode 100644 index 410def39b..000000000 --- a/src/apis/handlers.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::fmt; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - -use axum::extract::{Path, Query, State}; -use axum::response::{IntoResponse, Json, Response}; -use serde::{de, Deserialize, Deserializer}; - -use super::responses::{ - auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response, - failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response, - invalid_auth_key_param_response, invalid_info_hash_param_response, ok_response, stats_response, torrent_info_response, - torrent_list_response, torrent_not_known_response, -}; -use crate::apis::resources::auth_key::AuthKey; -use crate::apis::resources::stats::Stats; -use crate::apis::resources::torrent::ListItem; -use crate::protocol::info_hash::InfoHash; -use crate::tracker::auth::Key; -use crate::tracker::services::statistics::get_metrics; -use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination}; -use crate::tracker::Tracker; - -pub async fn get_stats_handler(State(tracker): State>) -> Json { - stats_response(get_metrics(tracker.clone()).await) -} - -#[derive(Deserialize)] -pub struct InfoHashParam(String); - -pub async fn get_torrent_handler(State(tracker): State>, Path(info_hash): Path) -> Response { - match InfoHash::from_str(&info_hash.0) { - Err(_) => invalid_info_hash_param_response(&info_hash.0), - Ok(info_hash) => match get_torrent_info(tracker.clone(), &info_hash).await { - Some(info) => torrent_info_response(info).into_response(), - None => torrent_not_known_response(), - }, - } -} - -#[derive(Deserialize)] -pub struct PaginationParams { - #[serde(default, deserialize_with = "empty_string_as_none")] - pub offset: Option, - pub limit: Option, -} - -pub async fn get_torrents_handler( - State(tracker): State>, - pagination: Query, -) -> Json> { - torrent_list_response( - &get_torrents( - tracker.clone(), - &Pagination::new_with_options(pagination.0.offset, pagination.0.limit), - ) - .await, - ) -} - -pub async fn add_torrent_to_whitelist_handler( - State(tracker): State>, - Path(info_hash): Path, -) -> Response { - match InfoHash::from_str(&info_hash.0) { - Err(_) => invalid_info_hash_param_response(&info_hash.0), - Ok(info_hash) => match tracker.add_torrent_to_whitelist(&info_hash).await { - Ok(_) => ok_response(), - Err(e) => failed_to_whitelist_torrent_response(e), - }, - } -} - -pub async fn remove_torrent_from_whitelist_handler( - State(tracker): State>, - Path(info_hash): Path, -) -> Response { - match InfoHash::from_str(&info_hash.0) { - Err(_) => invalid_info_hash_param_response(&info_hash.0), - Ok(info_hash) => match tracker.remove_torrent_from_whitelist(&info_hash).await { - Ok(_) => ok_response(), - Err(e) => failed_to_remove_torrent_from_whitelist_response(e), - }, - } -} - -pub async fn reload_whitelist_handler(State(tracker): State>) -> Response { - match tracker.load_whitelist_from_database().await { - Ok(_) => ok_response(), - Err(e) => failed_to_reload_whitelist_response(e), - } -} - -pub async fn generate_auth_key_handler(State(tracker): State>, Path(seconds_valid_or_key): Path) -> Response { - let seconds_valid = seconds_valid_or_key; - match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { - Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)), - Err(e) => failed_to_generate_key_response(e), - } -} - -#[derive(Deserialize)] -pub struct KeyParam(String); - -pub async fn delete_auth_key_handler( - State(tracker): State>, - Path(seconds_valid_or_key): Path, -) -> Response { - match Key::from_str(&seconds_valid_or_key.0) { - Err(_) => invalid_auth_key_param_response(&seconds_valid_or_key.0), - Ok(key) => match tracker.remove_auth_key(&key.to_string()).await { - Ok(_) => ok_response(), - Err(e) => failed_to_delete_key_response(e), - }, - } -} - -pub async fn reload_keys_handler(State(tracker): State>) -> Response { - match tracker.load_keys_from_database().await { - Ok(_) => ok_response(), - Err(e) => failed_to_reload_keys_response(e), - } -} - -/// Serde deserialization decorator to map empty Strings to None, -fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> -where - D: Deserializer<'de>, - T: FromStr, - T::Err: fmt::Display, -{ - let opt = Option::::deserialize(de)?; - match opt.as_deref() { - None | Some("") => Ok(None), - Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some), - } -} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index a646d5543..fd7fdb6e5 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,10 @@ -pub mod handlers; +pub mod context; pub mod middlewares; -pub mod resources; pub mod responses; pub mod routes; pub mod server; + +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct InfoHashParam(pub String); diff --git a/src/apis/responses.rs b/src/apis/responses.rs index c0a6cbcf8..4a9c39bf9 100644 --- a/src/apis/responses.rs +++ b/src/apis/responses.rs @@ -1,15 +1,6 @@ -use std::error::Error; - use axum::http::{header, StatusCode}; -use axum::response::{IntoResponse, Json, Response}; +use axum::response::{IntoResponse, Response}; use serde::Serialize; -use serde_json::json; - -use crate::apis::resources::auth_key::AuthKey; -use crate::apis::resources::stats::Stats; -use crate::apis::resources::torrent::{ListItem, Torrent}; -use crate::tracker::services::statistics::TrackerMetrics; -use crate::tracker::services::torrent::{BasicInfo, Info}; /* code-review: When Axum cannot parse a path or query param it shows a message like this: @@ -38,36 +29,6 @@ pub enum ActionStatus<'a> { Err { reason: std::borrow::Cow<'a, str> }, } -// Resource responses - -#[must_use] -pub fn stats_response(tracker_metrics: TrackerMetrics) -> Json { - Json(Stats::from(tracker_metrics)) -} - -#[must_use] -pub fn torrent_list_response(basic_infos: &[BasicInfo]) -> Json> { - Json(ListItem::new_vec(basic_infos)) -} - -#[must_use] -pub fn torrent_info_response(info: Info) -> Json { - Json(Torrent::from(info)) -} - -/// # Panics -/// -/// Will panic if it can't convert the `AuthKey` resource to json -#[must_use] -pub fn auth_key_response(auth_key: &AuthKey) -> Response { - ( - StatusCode::OK, - [(header::CONTENT_TYPE, "application/json; charset=utf-8")], - serde_json::to_string(auth_key).unwrap(), - ) - .into_response() -} - // OK response /// # Panics @@ -106,41 +67,6 @@ fn bad_request_response(body: &str) -> Response { .into_response() } -#[must_use] -pub fn torrent_not_known_response() -> Response { - Json(json!("torrent not known")).into_response() -} - -#[must_use] -pub fn failed_to_remove_torrent_from_whitelist_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to remove torrent from whitelist: {e}")) -} - -#[must_use] -pub fn failed_to_whitelist_torrent_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to whitelist torrent: {e}")) -} - -#[must_use] -pub fn failed_to_reload_whitelist_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to reload whitelist: {e}")) -} - -#[must_use] -pub fn failed_to_generate_key_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to generate key: {e}")) -} - -#[must_use] -pub fn failed_to_delete_key_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to delete key: {e}")) -} - -#[must_use] -pub fn failed_to_reload_keys_response(e: E) -> Response { - unhandled_rejection_response(format!("failed to reload keys: {e}")) -} - /// This error response is to keep backward compatibility with the old API. /// It should be a plain text or json. #[must_use] diff --git a/src/apis/routes.rs b/src/apis/routes.rs index ecc51090c..c567e50da 100644 --- a/src/apis/routes.rs +++ b/src/apis/routes.rs @@ -1,53 +1,19 @@ use std::sync::Arc; -use axum::routing::{delete, get, post}; use axum::{middleware, Router}; -use super::handlers::{ - add_torrent_to_whitelist_handler, delete_auth_key_handler, generate_auth_key_handler, get_stats_handler, get_torrent_handler, - get_torrents_handler, reload_keys_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler, -}; +use super::context::{auth_key, stats, torrent, whitelist}; use super::middlewares::auth::auth; use crate::tracker::Tracker; #[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())) - // Torrents - .route( - "/api/torrent/:info_hash", - get(get_torrent_handler).with_state(tracker.clone()), - ) - .route("/api/torrents", get(get_torrents_handler).with_state(tracker.clone())) - // Whitelisted torrents - .route( - "/api/whitelist/:info_hash", - post(add_torrent_to_whitelist_handler).with_state(tracker.clone()), - ) - .route( - "/api/whitelist/:info_hash", - delete(remove_torrent_from_whitelist_handler).with_state(tracker.clone()), - ) - // Whitelist command - .route( - "/api/whitelist/reload", - get(reload_whitelist_handler).with_state(tracker.clone()), - ) - // Keys - .route( - // code-review: Axum does not allow two routes with the same path but different path variable name. - // In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths: - // POST /api/key - // DELETE /api/key/:key - "/api/key/:seconds_valid_or_key", - post(generate_auth_key_handler) - .with_state(tracker.clone()) - .delete(delete_auth_key_handler) - .with_state(tracker.clone()), - ) - // Keys command - .route("/api/keys/reload", get(reload_keys_handler).with_state(tracker.clone())) - .layer(middleware::from_fn_with_state(tracker.config.clone(), auth)) + let router = Router::new(); + + let router = auth_key::routes::add(router, tracker.clone()); + let router = stats::routes::add(router, tracker.clone()); + let router = whitelist::routes::add(router, tracker.clone()); + let router = torrent::routes::add(router, tracker.clone()); + + router.layer(middleware::from_fn_with_state(tracker.config.clone(), auth)) } diff --git a/tests/api/asserts.rs b/tests/api/asserts.rs index 5a4abfb62..c7567e6fe 100644 --- a/tests/api/asserts.rs +++ b/tests/api/asserts.rs @@ -1,9 +1,9 @@ // code-review: should we use macros to return the exact line where the assert fails? use reqwest::Response; -use torrust_tracker::apis::resources::auth_key::AuthKey; -use torrust_tracker::apis::resources::stats::Stats; -use torrust_tracker::apis::resources::torrent::{ListItem, Torrent}; +use torrust_tracker::apis::context::auth_key::resources::AuthKey; +use torrust_tracker::apis::context::stats::resources::Stats; +use torrust_tracker::apis::context::torrent::resources::torrent::{ListItem, Torrent}; // Resource responses diff --git a/tests/api/mod.rs b/tests/api/mod.rs index fcb24e491..f59210b22 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -6,6 +6,7 @@ pub mod asserts; pub mod client; pub mod connection_info; pub mod test_environment; +pub mod tests; /// It forces a database error by dropping all tables. /// That makes any query fail. diff --git a/tests/api/tests/authentication.rs b/tests/api/tests/authentication.rs new file mode 100644 index 000000000..5183c8909 --- /dev/null +++ b/tests/api/tests/authentication.rs @@ -0,0 +1,83 @@ +use torrust_tracker_test_helpers::configuration; + +use crate::api::asserts::{assert_token_not_valid, assert_unauthorized}; +use crate::api::client::Client; +use crate::api::test_environment::running_test_environment; +use crate::common::http::{Query, QueryParam}; + +#[tokio::test] +async fn should_authenticate_requests_by_using_a_token_query_param() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let token = test_env.get_connection_info().api_token.unwrap(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let token = test_env.get_connection_info().api_token.unwrap(); + + // At the beginning of the query component + 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(test_env.get_connection_info()) + .get_request(&format!("torrents?limit=1&token={token}")) + .await; + + assert_eq!(response.status(), 200); + + test_env.stop().await; +} diff --git a/tests/api/tests/configuration.rs b/tests/api/tests/configuration.rs new file mode 100644 index 000000000..f81201191 --- /dev/null +++ b/tests/api/tests/configuration.rs @@ -0,0 +1,17 @@ +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; +} diff --git a/tests/api/tests/context/auth_key.rs b/tests/api/tests/context/auth_key.rs new file mode 100644 index 000000000..ee7121615 --- /dev/null +++ b/tests/api/tests/context/auth_key.rs @@ -0,0 +1,265 @@ +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, + assert_invalid_auth_key_param, assert_invalid_key_duration_param, assert_ok, assert_token_not_valid, assert_unauthorized, +}; +use crate::api::client::Client; +use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::api::force_database_error; +use crate::api::test_environment::running_test_environment; + +#[tokio::test] +async fn should_allow_generating_a_new_auth_key() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + + 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!(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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + + 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(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 test_env = running_test_environment(configuration::ephemeral()).await; + + let invalid_key_durations = [ + // "", it returns 404 + // " ", it returns 404 + "-1", "text", + ]; + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + force_database_error(&test_env.tracker); + + let seconds_valid = 60; + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + let auth_key = test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + 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_id_is_invalid() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let invalid_auth_keys = [ + // "", it returns a 404 + // " ", it returns a 404 + "0", + "-1", + "INVALID AUTH KEY ID", + "IrweYtVuQPGbG9Jzx1DihcPmJGGpVy8", // 32 char key cspell:disable-line + "IrweYtVuQPGbG9Jzx1DihcPmJGGpVy8zs", // 34 char key cspell:disable-line + ]; + + for invalid_auth_key in &invalid_auth_keys { + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + let auth_key = test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + force_database_error(&test_env.tracker); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + + // Generate new auth key + let auth_key = test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + 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 = test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + force_database_error(&test_env.tracker); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let seconds_valid = 60; + test_env + .tracker + .generate_auth_key(Duration::from_secs(seconds_valid)) + .await + .unwrap(); + + 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(test_env.get_connection_info().bind_address.as_str())) + .reload_keys() + .await; + + assert_unauthorized(response).await; + + test_env.stop().await; +} diff --git a/tests/api/tests/context/mod.rs b/tests/api/tests/context/mod.rs new file mode 100644 index 000000000..6d3fb7566 --- /dev/null +++ b/tests/api/tests/context/mod.rs @@ -0,0 +1,4 @@ +pub mod auth_key; +pub mod stats; +pub mod torrent; +pub mod whitelist; diff --git a/tests/api/tests/context/stats.rs b/tests/api/tests/context/stats.rs new file mode 100644 index 000000000..99ae405b7 --- /dev/null +++ b/tests/api/tests/context/stats.rs @@ -0,0 +1,71 @@ +use std::str::FromStr; + +use torrust_tracker::apis::context::stats::resources::Stats; +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker_test_helpers::configuration; + +use crate::api::asserts::{assert_stats, assert_token_not_valid, assert_unauthorized}; +use crate::api::client::Client; +use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::api::test_environment::running_test_environment; +use crate::common::fixtures::PeerBuilder; + +#[tokio::test] +async fn should_allow_getting_tracker_statistics() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + test_env + .add_torrent_peer( + &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), + &PeerBuilder::default().into(), + ) + .await; + + let response = Client::new(test_env.get_connection_info()).get_tracker_statistics().await; + + assert_stats( + response, + Stats { + torrents: 1, + seeders: 1, + completed: 0, + leechers: 0, + tcp4_connections_handled: 0, + tcp4_announces_handled: 0, + tcp4_scrapes_handled: 0, + tcp6_connections_handled: 0, + tcp6_announces_handled: 0, + tcp6_scrapes_handled: 0, + udp4_connections_handled: 0, + udp4_announces_handled: 0, + udp4_scrapes_handled: 0, + udp6_connections_handled: 0, + udp6_announces_handled: 0, + udp6_scrapes_handled: 0, + }, + ) + .await; + + test_env.stop().await; +} + +#[tokio::test] +async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() { + let test_env = running_test_environment(configuration::ephemeral()).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(test_env.get_connection_info().bind_address.as_str())) + .get_tracker_statistics() + .await; + + assert_unauthorized(response).await; + + test_env.stop().await; +} diff --git a/tests/api/tests/context/torrent.rs b/tests/api/tests/context/torrent.rs new file mode 100644 index 000000000..998c2afaf --- /dev/null +++ b/tests/api/tests/context/torrent.rs @@ -0,0 +1,249 @@ +use std::str::FromStr; + +use torrust_tracker::apis::context::torrent::resources::peer::Peer; +use torrust_tracker::apis::context::torrent::resources::torrent::{self, Torrent}; +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker_test_helpers::configuration; + +use crate::api::asserts::{ + assert_bad_request, assert_invalid_infohash_param, assert_not_found, assert_token_not_valid, assert_torrent_info, + assert_torrent_list, assert_torrent_not_known, assert_unauthorized, +}; +use crate::api::client::Client; +use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::api::test_environment::running_test_environment; +use crate::api::tests::fixtures::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; +use crate::common::fixtures::PeerBuilder; +use crate::common::http::{Query, QueryParam}; + +#[tokio::test] +async fn should_allow_getting_torrents() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; + + let response = Client::new(test_env.get_connection_info()).get_torrents(Query::empty()).await; + + assert_torrent_list( + response, + vec![torrent::ListItem { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: None, // Torrent list does not include the peer list for each torrent + }], + ) + .await; + + test_env.stop().await; +} + +#[tokio::test] +async fn should_allow_limiting_the_torrents_in_the_result() { + 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(); + + 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(test_env.get_connection_info()) + .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec())) + .await; + + assert_torrent_list( + response, + vec![torrent::ListItem { + info_hash: "0b3aea4adc213ce32295be85d3883a63bca25446".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: None, // Torrent list does not include the peer list for each torrent + }], + ) + .await; + + test_env.stop().await; +} + +#[tokio::test] +async fn should_allow_the_torrents_result_pagination() { + 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(); + + 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(test_env.get_connection_info()) + .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec())) + .await; + + assert_torrent_list( + response, + vec![torrent::ListItem { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: None, // Torrent list does not include the peer list for each torrent + }], + ) + .await; + + test_env.stop().await; +} + +#[tokio::test] +async fn should_fail_getting_torrents_when_the_offset_query_parameter_cannot_be_parsed() { + 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(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 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(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 test_env = running_test_environment(configuration::ephemeral()).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(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 test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + let peer = PeerBuilder::default().into(); + + test_env.add_torrent_peer(&info_hash, &peer).await; + + let response = Client::new(test_env.get_connection_info()) + .get_torrent(&info_hash.to_string()) + .await; + + assert_torrent_info( + response, + Torrent { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: Some(vec![Peer::from(peer)]), + }, + ) + .await; + + test_env.stop().await; +} + +#[tokio::test] +async fn should_fail_while_getting_a_torrent_info_when_the_torrent_does_not_exist() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + for invalid_infohash in &invalid_infohashes_returning_bad_request() { + let response = Client::new(test_env.get_connection_info()) + .get_torrent(invalid_infohash) + .await; + + assert_invalid_infohash_param(response, invalid_infohash).await; + } + + for invalid_infohash in &invalid_infohashes_returning_not_found() { + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).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(test_env.get_connection_info().bind_address.as_str())) + .get_torrent(&info_hash.to_string()) + .await; + + assert_unauthorized(response).await; + + test_env.stop().await; +} diff --git a/tests/api/tests/context/whitelist.rs b/tests/api/tests/context/whitelist.rs new file mode 100644 index 000000000..29ea573c0 --- /dev/null +++ b/tests/api/tests/context/whitelist.rs @@ -0,0 +1,258 @@ +use std::str::FromStr; + +use torrust_tracker::protocol::info_hash::InfoHash; +use torrust_tracker_test_helpers::configuration; + +use crate::api::asserts::{ + assert_failed_to_reload_whitelist, assert_failed_to_remove_torrent_from_whitelist, assert_failed_to_whitelist_torrent, + assert_invalid_infohash_param, assert_not_found, assert_ok, assert_token_not_valid, assert_unauthorized, +}; +use crate::api::client::Client; +use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; +use crate::api::force_database_error; +use crate::api::test_environment::running_test_environment; +use crate::api::tests::fixtures::{invalid_infohashes_returning_bad_request, invalid_infohashes_returning_not_found}; + +#[tokio::test] +async fn should_allow_whitelisting_a_torrent() { + let test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + + let response = Client::new(test_env.get_connection_info()) + .whitelist_a_torrent(&info_hash) + .await; + + assert_ok(response).await; + assert!( + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + + 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(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 test_env = running_test_environment(configuration::ephemeral()).await; + + let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + + force_database_error(&test_env.tracker); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + for invalid_infohash in &invalid_infohashes_returning_bad_request() { + let response = Client::new(test_env.get_connection_info()) + .whitelist_a_torrent(invalid_infohash) + .await; + + assert_invalid_infohash_param(response, invalid_infohash).await; + } + + for invalid_infohash in &invalid_infohashes_returning_not_found() { + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + + let response = Client::new(test_env.get_connection_info()) + .remove_torrent_from_whitelist(&hash) + .await; + + assert_ok(response).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 test_env = running_test_environment(configuration::ephemeral()).await; + + let non_whitelisted_torrent_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + for invalid_infohash in &invalid_infohashes_returning_bad_request() { + let response = Client::new(test_env.get_connection_info()) + .remove_torrent_from_whitelist(invalid_infohash) + .await; + + assert_invalid_infohash_param(response, invalid_infohash).await; + } + + for invalid_infohash in &invalid_infohashes_returning_not_found() { + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + + force_database_error(&test_env.tracker); + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + + 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; + + 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 test_env = running_test_environment(configuration::ephemeral()).await; + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + + 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!( + !(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 test_env = running_test_environment(configuration::ephemeral()).await; + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); + + force_database_error(&test_env.tracker); + + let response = Client::new(test_env.get_connection_info()).reload_whitelist().await; + + assert_failed_to_reload_whitelist(response).await; + + test_env.stop().await; +} diff --git a/tests/api/tests/fixtures.rs b/tests/api/tests/fixtures.rs new file mode 100644 index 000000000..6d147f190 --- /dev/null +++ b/tests/api/tests/fixtures.rs @@ -0,0 +1,13 @@ +use crate::common::fixtures::invalid_info_hashes; + +// When these infohashes are used in URL path params +// the response is a custom response returned in the handler +pub fn invalid_infohashes_returning_bad_request() -> Vec { + invalid_info_hashes() +} + +// When these infohashes are used in URL path params +// the response is an Axum response returned in the handler +pub fn invalid_infohashes_returning_not_found() -> Vec { + [String::new(), " ".to_string()].to_vec() +} diff --git a/tests/api/tests/mod.rs b/tests/api/tests/mod.rs new file mode 100644 index 000000000..38b4a2b37 --- /dev/null +++ b/tests/api/tests/mod.rs @@ -0,0 +1,4 @@ +pub mod authentication; +pub mod configuration; +pub mod context; +pub mod fixtures; diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs index dac5907c2..3219bc987 100644 --- a/tests/tracker_api.rs +++ b/tests/tracker_api.rs @@ -1,988 +1,7 @@ /// Integration tests for the tracker API /// /// ```text -/// cargo test tracker_apis -- --nocapture +/// cargo test --test tracker_api /// ``` -extern crate rand; - mod api; mod common; - -mod tracker_apis { - use crate::common::fixtures::invalid_info_hashes; - - // When these infohashes are used in URL path params - // the response is a custom response returned in the handler - fn invalid_infohashes_returning_bad_request() -> Vec { - invalid_info_hashes() - } - - // When these infohashes are used in URL path params - // the response is an Axum response returned in the handler - fn invalid_infohashes_returning_not_found() -> Vec { - [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::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 test_env = running_test_environment(configuration::ephemeral()).await; - - let token = test_env.get_connection_info().api_token.unwrap(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let token = test_env.get_connection_info().api_token.unwrap(); - - // At the beginning of the query component - 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(test_env.get_connection_info()) - .get_request(&format!("torrents?limit=1&token={token}")) - .await; - - assert_eq!(response.status(), 200); - - test_env.stop().await; - } - } - - mod for_stats_resources { - use std::str::FromStr; - - 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::test_environment::running_test_environment; - use crate::common::fixtures::PeerBuilder; - - #[tokio::test] - async fn should_allow_getting_tracker_statistics() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - test_env - .add_torrent_peer( - &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), - &PeerBuilder::default().into(), - ) - .await; - - let response = Client::new(test_env.get_connection_info()).get_tracker_statistics().await; - - assert_stats( - response, - Stats { - torrents: 1, - seeders: 1, - completed: 0, - leechers: 0, - tcp4_connections_handled: 0, - tcp4_announces_handled: 0, - tcp4_scrapes_handled: 0, - tcp6_connections_handled: 0, - tcp6_announces_handled: 0, - tcp6_scrapes_handled: 0, - udp4_connections_handled: 0, - udp4_announces_handled: 0, - udp4_scrapes_handled: 0, - udp6_connections_handled: 0, - udp6_announces_handled: 0, - udp6_scrapes_handled: 0, - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() { - let test_env = running_test_environment(configuration::ephemeral()).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(test_env.get_connection_info().bind_address.as_str())) - .get_tracker_statistics() - .await; - - assert_unauthorized(response).await; - - test_env.stop().await; - } - } - - mod for_torrent_resources { - use std::str::FromStr; - - 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::{ - assert_bad_request, assert_invalid_infohash_param, assert_not_found, assert_token_not_valid, assert_torrent_info, - assert_torrent_list, assert_torrent_not_known, assert_unauthorized, - }; - use crate::api::client::Client; - use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::test_environment::running_test_environment; - use crate::common::fixtures::PeerBuilder; - use crate::common::http::{Query, QueryParam}; - - #[tokio::test] - async fn should_allow_getting_torrents() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - - test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; - - let response = Client::new(test_env.get_connection_info()).get_torrents(Query::empty()).await; - - assert_torrent_list( - response, - vec![torrent::ListItem { - info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), - seeders: 1, - completed: 0, - leechers: 0, - peers: None, // Torrent list does not include the peer list for each torrent - }], - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_allow_limiting_the_torrents_in_the_result() { - 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(); - - 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(test_env.get_connection_info()) - .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec())) - .await; - - assert_torrent_list( - response, - vec![torrent::ListItem { - info_hash: "0b3aea4adc213ce32295be85d3883a63bca25446".to_string(), - seeders: 1, - completed: 0, - leechers: 0, - peers: None, // Torrent list does not include the peer list for each torrent - }], - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_allow_the_torrents_result_pagination() { - 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(); - - 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(test_env.get_connection_info()) - .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec())) - .await; - - assert_torrent_list( - response, - vec![torrent::ListItem { - info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), - seeders: 1, - completed: 0, - leechers: 0, - peers: None, // Torrent list does not include the peer list for each torrent - }], - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_getting_torrents_when_the_offset_query_parameter_cannot_be_parsed() { - 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(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 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(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 test_env = running_test_environment(configuration::ephemeral()).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(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 test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - - let peer = PeerBuilder::default().into(); - - test_env.add_torrent_peer(&info_hash, &peer).await; - - let response = Client::new(test_env.get_connection_info()) - .get_torrent(&info_hash.to_string()) - .await; - - assert_torrent_info( - response, - Torrent { - info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), - seeders: 1, - completed: 0, - leechers: 0, - peers: Some(vec![resources::peer::Peer::from(peer)]), - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_while_getting_a_torrent_info_when_the_torrent_does_not_exist() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(test_env.get_connection_info()) - .get_torrent(invalid_infohash) - .await; - - assert_invalid_infohash_param(response, invalid_infohash).await; - } - - for invalid_infohash in &invalid_infohashes_returning_not_found() { - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - - test_env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).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(test_env.get_connection_info().bind_address.as_str())) - .get_torrent(&info_hash.to_string()) - .await; - - assert_unauthorized(response).await; - - test_env.stop().await; - } - } - - mod for_whitelisted_torrent_resources { - 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::{ - assert_failed_to_reload_whitelist, assert_failed_to_remove_torrent_from_whitelist, - assert_failed_to_whitelist_torrent, assert_invalid_infohash_param, assert_not_found, assert_ok, - assert_token_not_valid, assert_unauthorized, - }; - use crate::api::client::Client; - use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::force_database_error; - use crate::api::test_environment::running_test_environment; - - #[tokio::test] - async fn should_allow_whitelisting_a_torrent() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - - let response = Client::new(test_env.get_connection_info()) - .whitelist_a_torrent(&info_hash) - .await; - - assert_ok(response).await; - assert!( - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - - 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(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 test_env = running_test_environment(configuration::ephemeral()).await; - - let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - - force_database_error(&test_env.tracker); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(test_env.get_connection_info()) - .whitelist_a_torrent(invalid_infohash) - .await; - - assert_invalid_infohash_param(response, invalid_infohash).await; - } - - for invalid_infohash in &invalid_infohashes_returning_not_found() { - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let info_hash = InfoHash::from_str(&hash).unwrap(); - test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - - let response = Client::new(test_env.get_connection_info()) - .remove_torrent_from_whitelist(&hash) - .await; - - assert_ok(response).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 test_env = running_test_environment(configuration::ephemeral()).await; - - let non_whitelisted_torrent_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(test_env.get_connection_info()) - .remove_torrent_from_whitelist(invalid_infohash) - .await; - - assert_invalid_infohash_param(response, invalid_infohash).await; - } - - for invalid_infohash in &invalid_infohashes_returning_not_found() { - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let info_hash = InfoHash::from_str(&hash).unwrap(); - test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - - force_database_error(&test_env.tracker); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let info_hash = InfoHash::from_str(&hash).unwrap(); - - 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; - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let info_hash = InfoHash::from_str(&hash).unwrap(); - test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - - 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!( - !(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 test_env = running_test_environment(configuration::ephemeral()).await; - - let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); - let info_hash = InfoHash::from_str(&hash).unwrap(); - test_env.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap(); - - force_database_error(&test_env.tracker); - - let response = Client::new(test_env.get_connection_info()).reload_whitelist().await; - - assert_failed_to_reload_whitelist(response).await; - - test_env.stop().await; - } - } - - mod for_key_resources { - 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, - assert_invalid_auth_key_param, assert_invalid_key_duration_param, assert_ok, assert_token_not_valid, - assert_unauthorized, - }; - use crate::api::client::Client; - use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; - use crate::api::force_database_error; - use crate::api::test_environment::running_test_environment; - - #[tokio::test] - async fn should_allow_generating_a_new_auth_key() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - - 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!(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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - - 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(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 test_env = running_test_environment(configuration::ephemeral()).await; - - let invalid_key_durations = [ - // "", it returns 404 - // " ", it returns 404 - "-1", "text", - ]; - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - force_database_error(&test_env.tracker); - - let seconds_valid = 60; - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - let auth_key = test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - 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_id_is_invalid() { - let test_env = running_test_environment(configuration::ephemeral()).await; - - let invalid_auth_keys = [ - // "", it returns a 404 - // " ", it returns a 404 - "0", - "-1", - "INVALID AUTH KEY ID", - "IrweYtVuQPGbG9Jzx1DihcPmJGGpVy8", // 32 char key cspell:disable-line - "IrweYtVuQPGbG9Jzx1DihcPmJGGpVy8zs", // 34 char key cspell:disable-line - ]; - - for invalid_auth_key in &invalid_auth_keys { - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - let auth_key = test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - force_database_error(&test_env.tracker); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - - // Generate new auth key - let auth_key = test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - 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 = test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - force_database_error(&test_env.tracker); - - 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 test_env = running_test_environment(configuration::ephemeral()).await; - - let seconds_valid = 60; - test_env - .tracker - .generate_auth_key(Duration::from_secs(seconds_valid)) - .await - .unwrap(); - - 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(test_env.get_connection_info().bind_address.as_str())) - .reload_keys() - .await; - - assert_unauthorized(response).await; - - test_env.stop().await; - } - } -}