From 717cdaae4c7e01ff2db88654fe7e07826fbd19dd Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 20 Jun 2023 14:49:07 +0100 Subject: [PATCH] refactor(api): [#208] use API impementation enum for API versioning We've removed the ActixWeb implementation for the API. We can use the enum for implementations for versioning the API. Currently there is only one version `v1` but i'ts ready to add the verion `v2`. --- src/app.rs | 10 +- src/bin/main.rs | 10 +- src/mailer.rs | 4 +- src/services/about.rs | 6 +- src/web/api/mod.rs | 19 +- src/web/api/{axum.rs => server.rs} | 4 +- src/web/api/v1/routes.rs | 4 +- tests/e2e/contexts/about/contract.rs | 32 - tests/e2e/contexts/category/contract.rs | 207 ------ tests/e2e/contexts/proxy/contract.rs | 6 - tests/e2e/contexts/root/contract.rs | 21 - tests/e2e/contexts/settings/contract.rs | 96 --- tests/e2e/contexts/tag/contract.rs | 179 ----- tests/e2e/contexts/torrent/contract.rs | 630 ------------------ tests/e2e/contexts/user/contract.rs | 180 ----- tests/e2e/environment.rs | 6 +- tests/e2e/mod.rs | 2 +- tests/e2e/web/api/mod.rs | 1 + .../e2e/web/api/v1/contexts/about/contract.rs | 31 + .../{ => web/api/v1}/contexts/about/mod.rs | 0 .../web/api/v1/contexts/category/contract.rs | 205 ++++++ .../{ => web/api/v1}/contexts/category/mod.rs | 0 .../api/v1}/contexts/category/steps.rs | 2 +- tests/e2e/{ => web/api/v1}/contexts/mod.rs | 0 .../e2e/web/api/v1/contexts/proxy/contract.rs | 3 + .../{ => web/api/v1}/contexts/proxy/mod.rs | 0 .../e2e/web/api/v1/contexts/root/contract.rs | 20 + .../e2e/{ => web/api/v1}/contexts/root/mod.rs | 0 .../web/api/v1/contexts/settings/contract.rs | 94 +++ .../{ => web/api/v1}/contexts/settings/mod.rs | 0 tests/e2e/web/api/v1/contexts/tag/contract.rs | 177 +++++ .../e2e/{ => web/api/v1}/contexts/tag/mod.rs | 0 .../{ => web/api/v1}/contexts/tag/steps.rs | 2 +- .../api/v1}/contexts/torrent/asserts.rs | 0 .../web/api/v1/contexts/torrent/contract.rs | 627 +++++++++++++++++ .../{ => web/api/v1}/contexts/torrent/mod.rs | 0 .../api/v1}/contexts/torrent/steps.rs | 0 .../e2e/web/api/v1/contexts/user/contract.rs | 176 +++++ .../e2e/{ => web/api/v1}/contexts/user/mod.rs | 0 .../{ => web/api/v1}/contexts/user/steps.rs | 0 tests/e2e/web/api/v1/mod.rs | 1 + tests/e2e/web/mod.rs | 1 + tests/environments/app_starter.rs | 10 +- tests/environments/isolated.rs | 10 +- 44 files changed, 1380 insertions(+), 1396 deletions(-) rename src/web/api/{axum.rs => server.rs} (96%) delete mode 100644 tests/e2e/contexts/about/contract.rs delete mode 100644 tests/e2e/contexts/category/contract.rs delete mode 100644 tests/e2e/contexts/proxy/contract.rs delete mode 100644 tests/e2e/contexts/root/contract.rs delete mode 100644 tests/e2e/contexts/settings/contract.rs delete mode 100644 tests/e2e/contexts/tag/contract.rs delete mode 100644 tests/e2e/contexts/torrent/contract.rs delete mode 100644 tests/e2e/contexts/user/contract.rs create mode 100644 tests/e2e/web/api/mod.rs create mode 100644 tests/e2e/web/api/v1/contexts/about/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/about/mod.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/category/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/category/mod.rs (100%) rename tests/e2e/{ => web/api/v1}/contexts/category/steps.rs (93%) rename tests/e2e/{ => web/api/v1}/contexts/mod.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/proxy/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/proxy/mod.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/root/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/root/mod.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/settings/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/settings/mod.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/tag/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/tag/mod.rs (100%) rename tests/e2e/{ => web/api/v1}/contexts/tag/steps.rs (94%) rename tests/e2e/{ => web/api/v1}/contexts/torrent/asserts.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/torrent/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/torrent/mod.rs (100%) rename tests/e2e/{ => web/api/v1}/contexts/torrent/steps.rs (100%) create mode 100644 tests/e2e/web/api/v1/contexts/user/contract.rs rename tests/e2e/{ => web/api/v1}/contexts/user/mod.rs (100%) rename tests/e2e/{ => web/api/v1}/contexts/user/steps.rs (100%) create mode 100644 tests/e2e/web/api/v1/mod.rs create mode 100644 tests/e2e/web/mod.rs diff --git a/src/app.rs b/src/app.rs index c1d89e3c..e0e263ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,17 +19,17 @@ use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbU use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; use crate::web::api::v1::auth::Authentication; -use crate::web::api::{start, Implementation}; +use crate::web::api::{start, Version}; use crate::{mailer, tracker}; pub struct Running { pub api_socket_addr: SocketAddr, - pub axum_api_server: Option>>, + pub api_server: Option>>, pub tracker_data_importer_handle: tokio::task::JoinHandle<()>, } #[allow(clippy::too_many_lines)] -pub async fn run(configuration: Configuration, api_implementation: &Implementation) -> Running { +pub async fn run(configuration: Configuration, api_version: &Version) -> Running { let log_level = configuration.settings.read().await.log_level.clone(); logging::setup(&log_level); @@ -166,11 +166,11 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati // Start API server - let running_api = start(app_data, &net_ip, net_port, api_implementation).await; + let running_api = start(app_data, &net_ip, net_port, api_version).await; Running { api_socket_addr: running_api.socket_addr, - axum_api_server: running_api.axum_api_server, + api_server: running_api.api_server, tracker_data_importer_handle: tracker_statistics_importer_handle, } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 3772d321..5660be68 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,16 +1,16 @@ use torrust_index_backend::app; use torrust_index_backend::bootstrap::config::init_configuration; -use torrust_index_backend::web::api::Implementation; +use torrust_index_backend::web::api::Version; #[tokio::main] async fn main() -> Result<(), std::io::Error> { let configuration = init_configuration().await; - let api_implementation = Implementation::Axum; + let api_version = Version::V1; - let app = app::run(configuration, &api_implementation).await; + let app = app::run(configuration, &api_version).await; - match api_implementation { - Implementation::Axum => app.axum_api_server.unwrap().await.expect("the Axum API server was dropped"), + match api_version { + Version::V1 => app.api_server.unwrap().await.expect("the API server was dropped"), } } diff --git a/src/mailer.rs b/src/mailer.rs index bf4c7a30..e55f26f9 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::config::Configuration; use crate::errors::ServiceError; use crate::utils::clock; -use crate::web::api::API_VERSION; +use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; pub struct Service { cfg: Arc, @@ -147,7 +147,7 @@ impl Service { base_url = cfg_base_url; } - format!("{base_url}/{API_VERSION}/user/email/verify/{token}") + format!("{base_url}/{API_VERSION_URL_PREFIX}/user/email/verify/{token}") } } diff --git a/src/services/about.rs b/src/services/about.rs index fed3d973..100822d8 100644 --- a/src/services/about.rs +++ b/src/services/about.rs @@ -1,5 +1,5 @@ //! Templates for "about" static pages. -use crate::web::api::API_VERSION; +use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; #[must_use] pub fn index_page() -> String { @@ -22,7 +22,7 @@ pub fn page() -> String {

Hi! This is a running torrust-index-backend.

"# @@ -55,7 +55,7 @@ pub fn license_page() -> String {

If you want to read more about all the licenses and how they apply please refer to the contributor agreement.

"# diff --git a/src/web/api/mod.rs b/src/web/api/mod.rs index 31c38487..46ffe2b7 100644 --- a/src/web/api/mod.rs +++ b/src/web/api/mod.rs @@ -3,7 +3,7 @@ //! Currently, the API has only one version: `v1`. //! //! Refer to the [`v1`](crate::web::api::v1) module for more information. -pub mod axum; +pub mod server; pub mod v1; use std::net::SocketAddr; @@ -14,20 +14,17 @@ use tokio::task::JoinHandle; use crate::common::AppData; use crate::web::api; -pub const API_VERSION: &str = "v1"; - -/// API implementations. -pub enum Implementation { - /// API implementation with Axum. - Axum, +/// API versions. +pub enum Version { + V1, } /// The running API server. pub struct Running { /// The socket address the API server is listening on. pub socket_addr: SocketAddr, - /// The handle for the running API server task when using Axum. - pub axum_api_server: Option>>, + /// The handle for the running API server. + pub api_server: Option>>, } #[must_use] @@ -38,8 +35,8 @@ pub struct ServerStartedMessage { /// Starts the API server. #[must_use] -pub async fn start(app_data: Arc, net_ip: &str, net_port: u16, implementation: &Implementation) -> api::Running { +pub async fn start(app_data: Arc, net_ip: &str, net_port: u16, implementation: &Version) -> api::Running { match implementation { - Implementation::Axum => axum::start(app_data, net_ip, net_port).await, + Version::V1 => server::start(app_data, net_ip, net_port).await, } } diff --git a/src/web/api/axum.rs b/src/web/api/server.rs similarity index 96% rename from src/web/api/axum.rs rename to src/web/api/server.rs index c281381c..8fa1e704 100644 --- a/src/web/api/axum.rs +++ b/src/web/api/server.rs @@ -9,7 +9,7 @@ use super::v1::routes::router; use super::{Running, ServerStartedMessage}; use crate::common::AppData; -/// Starts the API server with `Axum`. +/// Starts the API server. /// /// # Panics /// @@ -42,7 +42,7 @@ pub async fn start(app_data: Arc, net_ip: &str, net_port: u16) -> Runni Running { socket_addr: bound_addr, - axum_api_server: Some(join_handle), + api_server: Some(join_handle), } } diff --git a/src/web/api/v1/routes.rs b/src/web/api/v1/routes.rs index f012d68c..586ec2d7 100644 --- a/src/web/api/v1/routes.rs +++ b/src/web/api/v1/routes.rs @@ -10,6 +10,8 @@ use super::contexts::{about, proxy, settings, tag, torrent}; use super::contexts::{category, user}; use crate::common::AppData; +pub const API_VERSION_URL_PREFIX: &str = "v1"; + /// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] pub fn router(app_data: Arc) -> Router { @@ -30,7 +32,7 @@ pub fn router(app_data: Arc) -> Router { Router::new() .route("/", get(about_page_handler).with_state(app_data)) - .nest("/v1", v1_api_routes) + .nest(&format!("/{API_VERSION_URL_PREFIX}"), v1_api_routes) // For development purposes only. // //.layer(CorsLayer::permissive()) // Uncomment this line and the `use` import. diff --git a/tests/e2e/contexts/about/contract.rs b/tests/e2e/contexts/about/contract.rs deleted file mode 100644 index 52d7efed..00000000 --- a/tests/e2e/contexts/about/contract.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! API contract for `about` context. -mod with_axum_implementation { - use torrust_index_backend::web::api; - - use crate::common::asserts::{assert_response_title, assert_text_ok}; - use crate::common::client::Client; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_load_the_about_page_with_information_about_the_api() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.about().await; - - assert_text_ok(&response); - assert_response_title(&response, "About"); - } - - #[tokio::test] - async fn it_should_load_the_license_page_at_the_api_entrypoint() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.license().await; - - assert_text_ok(&response); - assert_response_title(&response, "Licensing"); - } -} diff --git a/tests/e2e/contexts/category/contract.rs b/tests/e2e/contexts/category/contract.rs deleted file mode 100644 index eb5f2d94..00000000 --- a/tests/e2e/contexts/category/contract.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! API contract for `category` context. -mod with_axum_implementation { - - use torrust_index_backend::web::api; - - use crate::common::asserts::assert_json_ok_response; - use crate::common::client::Client; - use crate::common::contexts::category::asserts::{assert_added_category_response, assert_deleted_category_response}; - use crate::common::contexts::category::fixtures::random_category_name; - use crate::common::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; - use crate::common::contexts::category::responses::ListResponse; - use crate::e2e::contexts::category::steps::{add_category, add_random_category}; - use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_return_an_empty_category_list_when_there_are_no_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.get_categories().await; - - assert_json_ok_response(&response); - } - - #[tokio::test] - async fn it_should_return_a_category_list() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - add_random_category(&env).await; - - let response = client.get_categories().await; - - let res: ListResponse = serde_json::from_str(&response.body).unwrap(); - - // There should be at least the category we added. - // Since this is an E2E test and it could be run in a shared test env, - // there might be more categories. - assert!(res.data.len() > 1); - if let Some(content_type) = &response.content_type { - assert_eq!(content_type, "application/json"); - } - assert_eq!(response.status, 200); - } - - #[tokio::test] - async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client - .add_category(AddCategoryForm { - name: "CATEGORY NAME".to_string(), - icon: None, - }) - .await; - - assert_eq!(response.status, 401); - } - - #[tokio::test] - async fn it_should_not_allow_adding_a_new_category_to_non_admins() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_non_admin = new_logged_in_user(&env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); - - let response = client - .add_category(AddCategoryForm { - name: "CATEGORY NAME".to_string(), - icon: None, - }) - .await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_allow_admins_to_add_new_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let category_name = random_category_name(); - - let response = client - .add_category(AddCategoryForm { - name: category_name.to_string(), - icon: None, - }) - .await; - - assert_added_category_response(&response, &category_name); - } - - #[tokio::test] - async fn it_should_allow_adding_empty_categories() { - // code-review: this is a bit weird, is it a intended behavior? - - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if env.is_shared() { - // This test cannot be run in a shared test env because it will fail - // when the empty category already exits - println!("Skipped"); - return; - } - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let category_name = String::new(); - - let response = client - .add_category(AddCategoryForm { - name: category_name.to_string(), - icon: None, - }) - .await; - - assert_added_category_response(&response, &category_name); - } - - #[tokio::test] - async fn it_should_not_allow_adding_duplicated_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let added_category_name = add_random_category(&env).await; - - // Try to add the same category again - let response = add_category(&added_category_name, &env).await; - - assert_eq!(response.status, 400); - } - - #[tokio::test] - async fn it_should_allow_admins_to_delete_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let added_category_name = add_random_category(&env).await; - - let response = client - .delete_category(DeleteCategoryForm { - name: added_category_name.to_string(), - icon: None, - }) - .await; - - assert_deleted_category_response(&response, &added_category_name); - } - - #[tokio::test] - async fn it_should_not_allow_non_admins_to_delete_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let added_category_name = add_random_category(&env).await; - - let logged_in_non_admin = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); - - let response = client - .delete_category(DeleteCategoryForm { - name: added_category_name.to_string(), - icon: None, - }) - .await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_not_allow_guests_to_delete_categories() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let added_category_name = add_random_category(&env).await; - - let response = client - .delete_category(DeleteCategoryForm { - name: added_category_name.to_string(), - icon: None, - }) - .await; - - assert_eq!(response.status, 401); - } -} diff --git a/tests/e2e/contexts/proxy/contract.rs b/tests/e2e/contexts/proxy/contract.rs deleted file mode 100644 index 46c8b8a9..00000000 --- a/tests/e2e/contexts/proxy/contract.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! API contract for `proxy` context. - -mod with_axum_implementation { - - // todo -} diff --git a/tests/e2e/contexts/root/contract.rs b/tests/e2e/contexts/root/contract.rs deleted file mode 100644 index e66661d2..00000000 --- a/tests/e2e/contexts/root/contract.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! API contract for `root` context. -mod with_axum_implementation { - use torrust_index_backend::web::api; - - use crate::common::asserts::{assert_response_title, assert_text_ok}; - use crate::common::client::Client; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_load_the_about_page_at_the_api_entrypoint() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.root().await; - - assert_text_ok(&response); - assert_response_title(&response, "About"); - } -} diff --git a/tests/e2e/contexts/settings/contract.rs b/tests/e2e/contexts/settings/contract.rs deleted file mode 100644 index ac4cbd49..00000000 --- a/tests/e2e/contexts/settings/contract.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! API contract for `settings` context. -mod with_axum_implementation { - - use torrust_index_backend::web::api; - - use crate::common::asserts::assert_json_ok_response; - use crate::common::client::Client; - use crate::common::contexts::settings::responses::{AllSettingsResponse, Public, PublicSettingsResponse, SiteNameResponse}; - use crate::e2e::contexts::user::steps::new_logged_in_admin; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_guests_to_get_the_public_settings() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.get_public_settings().await; - - let res: PublicSettingsResponse = serde_json::from_str(&response.body) - .unwrap_or_else(|_| panic!("response {:#?} should be a PublicSettingsResponse", response.body)); - - assert_eq!( - res.data, - Public { - website_name: env.server_settings().unwrap().website.name, - tracker_url: env.server_settings().unwrap().tracker.url, - tracker_mode: env.server_settings().unwrap().tracker.mode, - email_on_signup: env.server_settings().unwrap().auth.email_on_signup, - } - ); - - assert_json_ok_response(&response); - } - - #[tokio::test] - async fn it_should_allow_guests_to_get_the_site_name() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.get_site_name().await; - - let res: SiteNameResponse = serde_json::from_str(&response.body).unwrap(); - - assert_eq!(res.data, "Torrust"); - - assert_json_ok_response(&response); - } - - #[tokio::test] - async fn it_should_allow_admins_to_get_all_the_settings() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let response = client.get_settings().await; - - let res: AllSettingsResponse = serde_json::from_str(&response.body).unwrap(); - - assert_eq!(res.data, env.server_settings().unwrap()); - - assert_json_ok_response(&response); - } - - #[tokio::test] - async fn it_should_allow_admins_to_update_all_the_settings() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.is_isolated() { - // This test can't be executed in a non-isolated environment because - // it will change the settings for all the other tests. - return; - } - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let mut new_settings = env.server_settings().unwrap(); - - new_settings.website.name = "UPDATED NAME".to_string(); - - let response = client.update_settings(&new_settings).await; - - let res: AllSettingsResponse = serde_json::from_str(&response.body).unwrap(); - - assert_eq!(res.data, new_settings); - - assert_json_ok_response(&response); - } -} diff --git a/tests/e2e/contexts/tag/contract.rs b/tests/e2e/contexts/tag/contract.rs deleted file mode 100644 index 775b53dc..00000000 --- a/tests/e2e/contexts/tag/contract.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! API contract for `tag` context. -mod with_axum_implementation { - - use torrust_index_backend::web::api; - - use crate::common::asserts::assert_json_ok_response; - use crate::common::client::Client; - use crate::common::contexts::tag::asserts::{assert_added_tag_response, assert_deleted_tag_response}; - use crate::common::contexts::tag::fixtures::random_tag_name; - use crate::common::contexts::tag::forms::{AddTagForm, DeleteTagForm}; - use crate::common::contexts::tag::responses::ListResponse; - use crate::e2e::contexts::tag::steps::{add_random_tag, add_tag}; - use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_return_an_empty_tag_list_when_there_are_no_tags() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client.get_tags().await; - - assert_json_ok_response(&response); - } - - #[tokio::test] - async fn it_should_return_a_tag_list() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - // Add a tag - let tag_name = random_tag_name(); - let response = add_tag(&tag_name, &env).await; - assert_eq!(response.status, 200); - - let response = client.get_tags().await; - - let res: ListResponse = serde_json::from_str(&response.body).unwrap(); - - // There should be at least the tag we added. - // Since this is an E2E test that could be executed in a shred env, - // there might be more tags. - assert!(!res.data.is_empty()); - if let Some(content_type) = &response.content_type { - assert_eq!(content_type, "application/json"); - } - assert_eq!(response.status, 200); - } - - #[tokio::test] - async fn it_should_not_allow_adding_a_new_tag_to_unauthenticated_users() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let response = client - .add_tag(AddTagForm { - name: "TAG NAME".to_string(), - }) - .await; - - assert_eq!(response.status, 401); - } - - #[tokio::test] - async fn it_should_not_allow_adding_a_new_tag_to_non_admins() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_non_admin = new_logged_in_user(&env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); - - let response = client - .add_tag(AddTagForm { - name: "TAG NAME".to_string(), - }) - .await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_allow_admins_to_add_new_tags() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let tag_name = random_tag_name(); - - let response = client - .add_tag(AddTagForm { - name: tag_name.to_string(), - }) - .await; - - assert_added_tag_response(&response, &tag_name); - } - - #[tokio::test] - async fn it_should_allow_adding_duplicated_tags() { - // code-review: is this an intended behavior? - - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - // Add a tag - let random_tag_name = random_tag_name(); - let response = add_tag(&random_tag_name, &env).await; - assert_eq!(response.status, 200); - - // Try to add the same tag again - let response = add_tag(&random_tag_name, &env).await; - assert_eq!(response.status, 200); - } - - #[tokio::test] - async fn it_should_allow_adding_a_tag_with_an_empty_name() { - // code-review: is this an intended behavior? - - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let empty_tag_name = String::new(); - let response = add_tag(&empty_tag_name, &env).await; - assert_eq!(response.status, 200); - } - - #[tokio::test] - async fn it_should_allow_admins_to_delete_tags() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let (tag_id, _tag_name) = add_random_tag(&env).await; - - let response = client.delete_tag(DeleteTagForm { tag_id }).await; - - assert_deleted_tag_response(&response, tag_id); - } - - #[tokio::test] - async fn it_should_not_allow_non_admins_to_delete_tags() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_non_admin = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); - - let (tag_id, _tag_name) = add_random_tag(&env).await; - - let response = client.delete_tag(DeleteTagForm { tag_id }).await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_not_allow_guests_to_delete_tags() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let (tag_id, _tag_name) = add_random_tag(&env).await; - - let response = client.delete_tag(DeleteTagForm { tag_id }).await; - - assert_eq!(response.status, 401); - } -} diff --git a/tests/e2e/contexts/torrent/contract.rs b/tests/e2e/contexts/torrent/contract.rs deleted file mode 100644 index a01cf5d3..00000000 --- a/tests/e2e/contexts/torrent/contract.rs +++ /dev/null @@ -1,630 +0,0 @@ -//! API contract for `torrent` context. - -/* -todo: - -Delete torrent: - -- After deleting a torrent, it should be removed from the tracker whitelist - -Get torrent info: - -- The torrent info: - - should contain the magnet link with the trackers from the torrent file - - should contain realtime seeders and leechers from the tracker -*/ - -mod with_axum_implementation { - - mod for_guests { - - use torrust_index_backend::utils::parse_torrent::decode_torrent; - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::category::fixtures::software_predefined_category_id; - use crate::common::contexts::torrent::asserts::assert_expected_torrent_details; - use crate::common::contexts::torrent::requests::InfoHash; - use crate::common::contexts::torrent::responses::{ - Category, File, TorrentDetails, TorrentDetailsResponse, TorrentListResponse, - }; - use crate::common::http::{Query, QueryParam}; - use crate::e2e::contexts::torrent::asserts::expected_torrent; - use crate::e2e::contexts::torrent::steps::upload_random_torrent_to_index; - use crate::e2e::contexts::user::steps::new_logged_in_user; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_guests_to_get_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let uploader = new_logged_in_user(&env).await; - let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let response = client.get_torrents(Query::empty()).await; - - let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); - - assert!(torrent_list_response.data.total > 0); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_allow_to_get_torrents_with_pagination() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - - // Given we insert two torrents - let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - // When we request only one torrent per page - let response = client - .get_torrents(Query::with_params([QueryParam::new("page_size", "1")].to_vec())) - .await; - - let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); - - // Then we should have only one torrent per page - assert_eq!(torrent_list_response.data.results.len(), 1); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_allow_to_limit_the_number_of_torrents_per_request() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - - let max_torrent_page_size = 30; - - // Given we insert one torrent more than the page size limit - for _ in 0..max_torrent_page_size { - let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - // When we request more torrents than the page size limit - let response = client - .get_torrents(Query::with_params( - [QueryParam::new("page_size", &format!("{}", (max_torrent_page_size + 1)))].to_vec(), - )) - .await; - - let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); - - // Then we should get only the page size limit - assert_eq!(torrent_list_response.data.results.len(), max_torrent_page_size); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_return_a_default_amount_of_torrents_per_request_if_no_page_size_is_provided() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - - let default_torrent_page_size = 10; - - // Given we insert one torrent more than the default page size - for _ in 0..default_torrent_page_size { - let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - // When we request more torrents than the default page size limit - let response = client.get_torrents(Query::empty()).await; - - let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); - - // Then we should get only the default number of torrents per page - assert_eq!(torrent_list_response.data.results.len(), default_torrent_page_size); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_allow_guests_to_get_torrent_details_searching_by_info_hash() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let response = client.get_torrent(&test_torrent.info_hash()).await; - - let torrent_details_response: TorrentDetailsResponse = serde_json::from_str(&response.body).unwrap(); - - let tracker_url = env.server_settings().unwrap().tracker.url; - let encoded_tracker_url = urlencoding::encode(&tracker_url); - - let expected_torrent = TorrentDetails { - torrent_id: uploaded_torrent.torrent_id, - uploader: uploader.username, - info_hash: test_torrent.file_info.info_hash.to_uppercase(), - title: test_torrent.index_info.title.clone(), - description: test_torrent.index_info.description, - category: Category { - category_id: software_predefined_category_id(), - name: test_torrent.index_info.category, - num_torrents: 19, // Ignored in assertion - }, - upload_date: "2023-04-27 07:56:08".to_string(), // Ignored in assertion - file_size: test_torrent.file_info.content_size, - seeders: 0, - leechers: 0, - files: vec![File { - path: vec![test_torrent.file_info.files[0].clone()], - // Using one file torrent for testing: content_size = first file size - length: test_torrent.file_info.content_size, - md5sum: None, - }], - // code-review: why is this duplicated? It seems that is adding the - // same tracker twice because first ti adds all trackers and then - // it adds the tracker with the personal announce url, if the user - // is logged in. If the user is not logged in, it adds the default - // tracker again, and it ends up with two trackers. - trackers: vec![tracker_url.clone(), tracker_url.clone()], - magnet_link: format!( - // cspell:disable-next-line - "magnet:?xt=urn:btih:{}&dn={}&tr={}&tr={}", - test_torrent.file_info.info_hash.to_uppercase(), - urlencoding::encode(&test_torrent.index_info.title), - encoded_tracker_url, - encoded_tracker_url - ), - }; - - assert_expected_torrent_details(&torrent_details_response.data, &expected_torrent); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_allow_guests_to_download_a_torrent_file_searching_by_info_hash() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _torrent_listed_in_index) = upload_random_torrent_to_index(&uploader, &env).await; - - let response = client.download_torrent(&test_torrent.info_hash()).await; - - let torrent = decode_torrent(&response.bytes).expect("could not decode downloaded torrent"); - let uploaded_torrent = - decode_torrent(&test_torrent.index_info.torrent_file.contents).expect("could not decode uploaded torrent"); - let expected_torrent = expected_torrent(uploaded_torrent, &env, &None).await; - assert_eq!(torrent, expected_torrent); - assert!(response.is_bittorrent_and_ok()); - } - - #[tokio::test] - async fn it_should_return_a_not_found_trying_to_download_a_non_existing_torrent() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let non_existing_info_hash: InfoHash = "443c7602b4fde83d1154d6d9da48808418b181b6".to_string(); - - let response = client.download_torrent(&non_existing_info_hash).await; - - // code-review: should this be 404? - assert_eq!(response.status, 400); - } - - #[tokio::test] - async fn it_should_not_allow_guests_to_delete_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let response = client.delete_torrent(&test_torrent.info_hash()).await; - - assert_eq!(response.status, 401); - } - } - - mod for_authenticated_users { - - use torrust_index_backend::utils::parse_torrent::decode_torrent; - use torrust_index_backend::web::api; - - use crate::common::asserts::assert_json_error_response; - use crate::common::client::Client; - use crate::common::contexts::torrent::fixtures::random_torrent; - use crate::common::contexts::torrent::forms::UploadTorrentMultipartForm; - use crate::common::contexts::torrent::responses::UploadedTorrentResponse; - use crate::e2e::contexts::torrent::asserts::{build_announce_url, get_user_tracker_key}; - use crate::e2e::contexts::torrent::steps::upload_random_torrent_to_index; - use crate::e2e::contexts::user::steps::new_logged_in_user; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_authenticated_users_to_upload_new_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - let test_torrent = random_torrent(); - let info_hash = test_torrent.info_hash().clone(); - - let form: UploadTorrentMultipartForm = test_torrent.index_info.into(); - - let response = client.upload_torrent(form.into()).await; - - let uploaded_torrent_response: UploadedTorrentResponse = serde_json::from_str(&response.body).unwrap(); - - assert_eq!( - uploaded_torrent_response.data.info_hash.to_lowercase(), - info_hash.to_lowercase() - ); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_not_allow_uploading_a_torrent_with_a_non_existing_category() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let uploader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - let mut test_torrent = random_torrent(); - - test_torrent.index_info.category = "non-existing-category".to_string(); - - let form: UploadTorrentMultipartForm = test_torrent.index_info.into(); - - let response = client.upload_torrent(form.into()).await; - - assert_eq!(response.status, 400); - } - - #[tokio::test] - async fn it_should_not_allow_uploading_a_torrent_with_a_title_that_already_exists() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - // Upload the first torrent - let first_torrent = random_torrent(); - let first_torrent_title = first_torrent.index_info.title.clone(); - let form: UploadTorrentMultipartForm = first_torrent.index_info.into(); - let _response = client.upload_torrent(form.into()).await; - - // Upload the second torrent with the same title as the first one - let mut second_torrent = random_torrent(); - second_torrent.index_info.title = first_torrent_title; - let form: UploadTorrentMultipartForm = second_torrent.index_info.into(); - let response = client.upload_torrent(form.into()).await; - - assert_json_error_response(&response, "This torrent title has already been used."); - } - - #[tokio::test] - async fn it_should_not_allow_uploading_a_torrent_with_a_info_hash_that_already_exists() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - // Upload the first torrent - let first_torrent = random_torrent(); - let mut first_torrent_clone = first_torrent.clone(); - let first_torrent_title = first_torrent.index_info.title.clone(); - let form: UploadTorrentMultipartForm = first_torrent.index_info.into(); - let _response = client.upload_torrent(form.into()).await; - - // Upload the second torrent with the same info-hash as the first one. - // We need to change the title otherwise the torrent will be rejected - // because of the duplicate title. - first_torrent_clone.index_info.title = format!("{first_torrent_title}-clone"); - let form: UploadTorrentMultipartForm = first_torrent_clone.index_info.into(); - let response = client.upload_torrent(form.into()).await; - - assert_eq!(response.status, 400); - } - - #[tokio::test] - async fn it_should_allow_authenticated_users_to_download_a_torrent_with_a_personal_announce_url() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - // Given a previously uploaded torrent - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _torrent_listed_in_index) = upload_random_torrent_to_index(&uploader, &env).await; - - // And a logged in user who is going to download the torrent - let downloader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &downloader.token); - - // When the user downloads the torrent - let response = client.download_torrent(&test_torrent.info_hash()).await; - - let torrent = decode_torrent(&response.bytes).expect("could not decode downloaded torrent"); - - // Then the torrent should have the personal announce URL - let tracker_key = get_user_tracker_key(&downloader, &env) - .await - .expect("uploader should have a valid tracker key"); - - let tracker_url = env.server_settings().unwrap().tracker.url; - - assert_eq!( - torrent.announce.unwrap(), - build_announce_url(&tracker_url, &Some(tracker_key)) - ); - } - - mod and_non_admins { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::torrent::forms::UpdateTorrentFrom; - use crate::e2e::contexts::torrent::steps::upload_random_torrent_to_index; - use crate::e2e::contexts::user::steps::new_logged_in_user; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_not_allow_non_admins_to_delete_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - let response = client.delete_torrent(&test_torrent.info_hash()).await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_allow_non_admin_users_to_update_someone_elses_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - // Given a users uploads a torrent - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - // Then another non admin user should not be able to update the torrent - let not_the_uploader = new_logged_in_user(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), ¬_the_uploader.token); - - let new_title = format!("{}-new-title", test_torrent.index_info.title); - let new_description = format!("{}-new-description", test_torrent.index_info.description); - - let response = client - .update_torrent( - &test_torrent.info_hash(), - UpdateTorrentFrom { - title: Some(new_title.clone()), - description: Some(new_description.clone()), - }, - ) - .await; - - assert_eq!(response.status, 403); - } - } - - mod and_torrent_owners { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::torrent::forms::UpdateTorrentFrom; - use crate::common::contexts::torrent::responses::UpdatedTorrentResponse; - use crate::e2e::contexts::torrent::steps::upload_random_torrent_to_index; - use crate::e2e::contexts::user::steps::new_logged_in_user; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_torrent_owners_to_update_their_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); - - let new_title = format!("{}-new-title", test_torrent.index_info.title); - let new_description = format!("{}-new-description", test_torrent.index_info.description); - - let response = client - .update_torrent( - &test_torrent.info_hash(), - UpdateTorrentFrom { - title: Some(new_title.clone()), - description: Some(new_description.clone()), - }, - ) - .await; - - let updated_torrent_response: UpdatedTorrentResponse = serde_json::from_str(&response.body).unwrap(); - - let torrent = updated_torrent_response.data; - - assert_eq!(torrent.title, new_title); - assert_eq!(torrent.description, new_description); - assert!(response.is_json_and_ok()); - } - } - - mod and_admins { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::torrent::forms::UpdateTorrentFrom; - use crate::common::contexts::torrent::responses::{DeletedTorrentResponse, UpdatedTorrentResponse}; - use crate::e2e::contexts::torrent::steps::upload_random_torrent_to_index; - use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_admins_to_delete_torrents_searching_by_info_hash() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &admin.token); - - let response = client.delete_torrent(&test_torrent.info_hash()).await; - - let deleted_torrent_response: DeletedTorrentResponse = serde_json::from_str(&response.body).unwrap(); - - assert_eq!(deleted_torrent_response.data.torrent_id, uploaded_torrent.torrent_id); - assert!(response.is_json_and_ok()); - } - - #[tokio::test] - async fn it_should_allow_admins_to_update_someone_elses_torrents() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - if !env.provides_a_tracker() { - println!("test skipped. It requires a tracker to be running."); - return; - } - - let uploader = new_logged_in_user(&env).await; - let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let new_title = format!("{}-new-title", test_torrent.index_info.title); - let new_description = format!("{}-new-description", test_torrent.index_info.description); - - let response = client - .update_torrent( - &test_torrent.info_hash(), - UpdateTorrentFrom { - title: Some(new_title.clone()), - description: Some(new_description.clone()), - }, - ) - .await; - - let updated_torrent_response: UpdatedTorrentResponse = serde_json::from_str(&response.body).unwrap(); - - let torrent = updated_torrent_response.data; - - assert_eq!(torrent.title, new_title); - assert_eq!(torrent.description, new_description); - assert!(response.is_json_and_ok()); - } - } - } -} diff --git a/tests/e2e/contexts/user/contract.rs b/tests/e2e/contexts/user/contract.rs deleted file mode 100644 index e1e66d56..00000000 --- a/tests/e2e/contexts/user/contract.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! API contract for `user` context. - -/* - -This test suite is not complete. It's just a starting point to show how to -write E2E tests. Anyway, the goal is not to fully cover all the app features -with E2E tests. The goal is to cover the most important features and to -demonstrate how to write E2E tests. Some important pending tests could be: - -todo: - -- It should allow renewing a token one week before it expires. -- It should allow verifying user registration via email. - -The first one requires to mock the time. Consider extracting the mod - into -an independent crate. - -The second one requires: -- To call the mailcatcher API to get the verification URL. -- To enable email verification in the configuration. -- To fix current tests to verify the email for newly created users. -- To find out which email is the one that contains the verification URL for a -given test. That maybe done using the email recipient if that's possible with -the mailcatcher API. - -*/ - -mod with_axum_implementation { - - mod registration { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::user::asserts::assert_added_user_response; - use crate::common::contexts::user::fixtures::random_user_registration_form; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_a_guest_user_to_register() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let form = random_user_registration_form(); - - let response = client.register_user(form).await; - - assert_added_user_response(&response); - } - } - - mod authentication { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::user::asserts::{ - assert_successful_login_response, assert_token_renewal_response, assert_token_verified_response, - }; - use crate::common::contexts::user::forms::{LoginForm, TokenRenewalForm, TokenVerificationForm}; - use crate::e2e::contexts::user::steps::{new_logged_in_user, new_registered_user}; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_a_registered_user_to_login() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let registered_user = new_registered_user(&env).await; - - let response = client - .login_user(LoginForm { - login: registered_user.username.clone(), - password: registered_user.password.clone(), - }) - .await; - - assert_successful_login_response(&response, ®istered_user); - } - - #[tokio::test] - async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let logged_in_user = new_logged_in_user(&env).await; - - let response = client - .verify_token(TokenVerificationForm { - token: logged_in_user.token.clone(), - }) - .await; - - assert_token_verified_response(&response); - } - - #[tokio::test] - async fn it_should_not_allow_a_logged_in_user_to_renew_an_authentication_token_which_is_still_valid_for_more_than_one_week( - ) { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_user = new_logged_in_user(&env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_user.token); - - let response = client - .renew_token(TokenRenewalForm { - token: logged_in_user.token.clone(), - }) - .await; - - assert_token_renewal_response(&response, &logged_in_user); - } - } - - mod banned_user_list { - - use torrust_index_backend::web::api; - - use crate::common::client::Client; - use crate::common::contexts::user::asserts::assert_banned_user_response; - use crate::common::contexts::user::forms::Username; - use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user, new_registered_user}; - use crate::e2e::environment::TestEnv; - - #[tokio::test] - async fn it_should_allow_an_admin_to_ban_a_user() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_in_admin = new_logged_in_admin(&env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); - - let registered_user = new_registered_user(&env).await; - - let response = client.ban_user(Username::new(registered_user.username.clone())).await; - - assert_banned_user_response(&response, ®istered_user); - } - - #[tokio::test] - async fn it_should_not_allow_a_non_admin_to_ban_a_user() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let logged_non_admin = new_logged_in_user(&env).await; - - let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); - - let registered_user = new_registered_user(&env).await; - - let response = client.ban_user(Username::new(registered_user.username.clone())).await; - - assert_eq!(response.status, 403); - } - - #[tokio::test] - async fn it_should_not_allow_a_guest_to_ban_a_user() { - let mut env = TestEnv::new(); - env.start(api::Implementation::Axum).await; - - let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); - - let registered_user = new_registered_user(&env).await; - - let response = client.ban_user(Username::new(registered_user.username.clone())).await; - - assert_eq!(response.status, 401); - } - } -} diff --git a/tests/e2e/environment.rs b/tests/e2e/environment.rs index 343e5512..4684fd82 100644 --- a/tests/e2e/environment.rs +++ b/tests/e2e/environment.rs @@ -1,6 +1,6 @@ use std::env; -use torrust_index_backend::web::api::Implementation; +use torrust_index_backend::web::api::Version; use super::config::{init_shared_env_configuration, ENV_VAR_E2E_SHARED}; use crate::common::contexts::settings::Settings; @@ -55,7 +55,7 @@ impl TestEnv { /// It starts the test environment. It can be a shared or isolated test /// environment depending on the value of the `ENV_VAR_E2E_SHARED` env var. - pub async fn start(&mut self, api_implementation: Implementation) { + pub async fn start(&mut self, api_version: Version) { let e2e_shared = ENV_VAR_E2E_SHARED; // bool if let Ok(_e2e_test_env_is_shared) = env::var(e2e_shared) { @@ -66,7 +66,7 @@ impl TestEnv { self.starting_settings = self.server_settings_for_shared_env().await; } else { // Using an isolated test env. - let isolated_env = isolated::TestEnv::running(api_implementation).await; + let isolated_env = isolated::TestEnv::running(api_version).await; self.isolated = Some(isolated_env); self.starting_settings = self.server_settings_for_isolated_env(); diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index 71386b0f..2b909fd9 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -37,5 +37,5 @@ //! documentation. See for more //! information. pub mod config; -pub mod contexts; pub mod environment; +pub mod web; diff --git a/tests/e2e/web/api/mod.rs b/tests/e2e/web/api/mod.rs new file mode 100644 index 00000000..a3a6d96c --- /dev/null +++ b/tests/e2e/web/api/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/tests/e2e/web/api/v1/contexts/about/contract.rs b/tests/e2e/web/api/v1/contexts/about/contract.rs new file mode 100644 index 00000000..edbe2d2c --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/about/contract.rs @@ -0,0 +1,31 @@ +//! API contract for `about` context. + +use torrust_index_backend::web::api; + +use crate::common::asserts::{assert_response_title, assert_text_ok}; +use crate::common::client::Client; +use crate::e2e::environment::TestEnv; + +#[tokio::test] +async fn it_should_load_the_about_page_with_information_about_the_api() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.about().await; + + assert_text_ok(&response); + assert_response_title(&response, "About"); +} + +#[tokio::test] +async fn it_should_load_the_license_page_at_the_api_entrypoint() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.license().await; + + assert_text_ok(&response); + assert_response_title(&response, "Licensing"); +} diff --git a/tests/e2e/contexts/about/mod.rs b/tests/e2e/web/api/v1/contexts/about/mod.rs similarity index 100% rename from tests/e2e/contexts/about/mod.rs rename to tests/e2e/web/api/v1/contexts/about/mod.rs diff --git a/tests/e2e/web/api/v1/contexts/category/contract.rs b/tests/e2e/web/api/v1/contexts/category/contract.rs new file mode 100644 index 00000000..0ec559c9 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/category/contract.rs @@ -0,0 +1,205 @@ +//! API contract for `category` context. + +use torrust_index_backend::web::api; + +use crate::common::asserts::assert_json_ok_response; +use crate::common::client::Client; +use crate::common::contexts::category::asserts::{assert_added_category_response, assert_deleted_category_response}; +use crate::common::contexts::category::fixtures::random_category_name; +use crate::common::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; +use crate::common::contexts::category::responses::ListResponse; +use crate::e2e::environment::TestEnv; +use crate::e2e::web::api::v1::contexts::category::steps::{add_category, add_random_category}; +use crate::e2e::web::api::v1::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; + +#[tokio::test] +async fn it_should_return_an_empty_category_list_when_there_are_no_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.get_categories().await; + + assert_json_ok_response(&response); +} + +#[tokio::test] +async fn it_should_return_a_category_list() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + add_random_category(&env).await; + + let response = client.get_categories().await; + + let res: ListResponse = serde_json::from_str(&response.body).unwrap(); + + // There should be at least the category we added. + // Since this is an E2E test and it could be run in a shared test env, + // there might be more categories. + assert!(res.data.len() > 1); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client + .add_category(AddCategoryForm { + name: "CATEGORY NAME".to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 401); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_category_to_non_admins() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_non_admin = new_logged_in_user(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); + + let response = client + .add_category(AddCategoryForm { + name: "CATEGORY NAME".to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_allow_admins_to_add_new_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let category_name = random_category_name(); + + let response = client + .add_category(AddCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + assert_added_category_response(&response, &category_name); +} + +#[tokio::test] +async fn it_should_allow_adding_empty_categories() { + // code-review: this is a bit weird, is it a intended behavior? + + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if env.is_shared() { + // This test cannot be run in a shared test env because it will fail + // when the empty category already exits + println!("Skipped"); + return; + } + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let category_name = String::new(); + + let response = client + .add_category(AddCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + assert_added_category_response(&response, &category_name); +} + +#[tokio::test] +async fn it_should_not_allow_adding_duplicated_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let added_category_name = add_random_category(&env).await; + + // Try to add the same category again + let response = add_category(&added_category_name, &env).await; + + assert_eq!(response.status, 400); +} + +#[tokio::test] +async fn it_should_allow_admins_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let added_category_name = add_random_category(&env).await; + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_deleted_category_response(&response, &added_category_name); +} + +#[tokio::test] +async fn it_should_not_allow_non_admins_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let added_category_name = add_random_category(&env).await; + + let logged_in_non_admin = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_not_allow_guests_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let added_category_name = add_random_category(&env).await; + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 401); +} diff --git a/tests/e2e/contexts/category/mod.rs b/tests/e2e/web/api/v1/contexts/category/mod.rs similarity index 100% rename from tests/e2e/contexts/category/mod.rs rename to tests/e2e/web/api/v1/contexts/category/mod.rs diff --git a/tests/e2e/contexts/category/steps.rs b/tests/e2e/web/api/v1/contexts/category/steps.rs similarity index 93% rename from tests/e2e/contexts/category/steps.rs rename to tests/e2e/web/api/v1/contexts/category/steps.rs index ab000dab..cca5b8ae 100644 --- a/tests/e2e/contexts/category/steps.rs +++ b/tests/e2e/web/api/v1/contexts/category/steps.rs @@ -3,8 +3,8 @@ use crate::common::contexts::category::fixtures::random_category_name; use crate::common::contexts::category::forms::AddCategoryForm; use crate::common::contexts::category::responses::AddedCategoryResponse; use crate::common::responses::TextResponse; -use crate::e2e::contexts::user::steps::new_logged_in_admin; use crate::e2e::environment::TestEnv; +use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_admin; /// Add a random category and return its name. pub async fn add_random_category(env: &TestEnv) -> String { diff --git a/tests/e2e/contexts/mod.rs b/tests/e2e/web/api/v1/contexts/mod.rs similarity index 100% rename from tests/e2e/contexts/mod.rs rename to tests/e2e/web/api/v1/contexts/mod.rs diff --git a/tests/e2e/web/api/v1/contexts/proxy/contract.rs b/tests/e2e/web/api/v1/contexts/proxy/contract.rs new file mode 100644 index 00000000..0b63dfc4 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/proxy/contract.rs @@ -0,0 +1,3 @@ +//! API contract for `proxy` context. + +// todo diff --git a/tests/e2e/contexts/proxy/mod.rs b/tests/e2e/web/api/v1/contexts/proxy/mod.rs similarity index 100% rename from tests/e2e/contexts/proxy/mod.rs rename to tests/e2e/web/api/v1/contexts/proxy/mod.rs diff --git a/tests/e2e/web/api/v1/contexts/root/contract.rs b/tests/e2e/web/api/v1/contexts/root/contract.rs new file mode 100644 index 00000000..24d763df --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/root/contract.rs @@ -0,0 +1,20 @@ +//! API contract for `root` context. + +use torrust_index_backend::web::api; + +use crate::common::asserts::{assert_response_title, assert_text_ok}; +use crate::common::client::Client; +use crate::e2e::environment::TestEnv; + +#[tokio::test] +async fn it_should_load_the_about_page_at_the_api_entrypoint() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.root().await; + + assert_text_ok(&response); + assert_response_title(&response, "About"); +} diff --git a/tests/e2e/contexts/root/mod.rs b/tests/e2e/web/api/v1/contexts/root/mod.rs similarity index 100% rename from tests/e2e/contexts/root/mod.rs rename to tests/e2e/web/api/v1/contexts/root/mod.rs diff --git a/tests/e2e/web/api/v1/contexts/settings/contract.rs b/tests/e2e/web/api/v1/contexts/settings/contract.rs new file mode 100644 index 00000000..5bd1c420 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/settings/contract.rs @@ -0,0 +1,94 @@ +//! API contract for `settings` context. + +use torrust_index_backend::web::api; + +use crate::common::asserts::assert_json_ok_response; +use crate::common::client::Client; +use crate::common::contexts::settings::responses::{AllSettingsResponse, Public, PublicSettingsResponse, SiteNameResponse}; +use crate::e2e::environment::TestEnv; +use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_admin; + +#[tokio::test] +async fn it_should_allow_guests_to_get_the_public_settings() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.get_public_settings().await; + + let res: PublicSettingsResponse = serde_json::from_str(&response.body) + .unwrap_or_else(|_| panic!("response {:#?} should be a PublicSettingsResponse", response.body)); + + assert_eq!( + res.data, + Public { + website_name: env.server_settings().unwrap().website.name, + tracker_url: env.server_settings().unwrap().tracker.url, + tracker_mode: env.server_settings().unwrap().tracker.mode, + email_on_signup: env.server_settings().unwrap().auth.email_on_signup, + } + ); + + assert_json_ok_response(&response); +} + +#[tokio::test] +async fn it_should_allow_guests_to_get_the_site_name() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.get_site_name().await; + + let res: SiteNameResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, "Torrust"); + + assert_json_ok_response(&response); +} + +#[tokio::test] +async fn it_should_allow_admins_to_get_all_the_settings() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let response = client.get_settings().await; + + let res: AllSettingsResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, env.server_settings().unwrap()); + + assert_json_ok_response(&response); +} + +#[tokio::test] +async fn it_should_allow_admins_to_update_all_the_settings() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.is_isolated() { + // This test can't be executed in a non-isolated environment because + // it will change the settings for all the other tests. + return; + } + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let mut new_settings = env.server_settings().unwrap(); + + new_settings.website.name = "UPDATED NAME".to_string(); + + let response = client.update_settings(&new_settings).await; + + let res: AllSettingsResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, new_settings); + + assert_json_ok_response(&response); +} diff --git a/tests/e2e/contexts/settings/mod.rs b/tests/e2e/web/api/v1/contexts/settings/mod.rs similarity index 100% rename from tests/e2e/contexts/settings/mod.rs rename to tests/e2e/web/api/v1/contexts/settings/mod.rs diff --git a/tests/e2e/web/api/v1/contexts/tag/contract.rs b/tests/e2e/web/api/v1/contexts/tag/contract.rs new file mode 100644 index 00000000..bfb7b1b8 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/tag/contract.rs @@ -0,0 +1,177 @@ +//! API contract for `tag` context. + +use torrust_index_backend::web::api; + +use crate::common::asserts::assert_json_ok_response; +use crate::common::client::Client; +use crate::common::contexts::tag::asserts::{assert_added_tag_response, assert_deleted_tag_response}; +use crate::common::contexts::tag::fixtures::random_tag_name; +use crate::common::contexts::tag::forms::{AddTagForm, DeleteTagForm}; +use crate::common::contexts::tag::responses::ListResponse; +use crate::e2e::environment::TestEnv; +use crate::e2e::web::api::v1::contexts::tag::steps::{add_random_tag, add_tag}; +use crate::e2e::web::api::v1::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; + +#[tokio::test] +async fn it_should_return_an_empty_tag_list_when_there_are_no_tags() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.get_tags().await; + + assert_json_ok_response(&response); +} + +#[tokio::test] +async fn it_should_return_a_tag_list() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + // Add a tag + let tag_name = random_tag_name(); + let response = add_tag(&tag_name, &env).await; + assert_eq!(response.status, 200); + + let response = client.get_tags().await; + + let res: ListResponse = serde_json::from_str(&response.body).unwrap(); + + // There should be at least the tag we added. + // Since this is an E2E test that could be executed in a shred env, + // there might be more tags. + assert!(!res.data.is_empty()); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_tag_to_unauthenticated_users() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client + .add_tag(AddTagForm { + name: "TAG NAME".to_string(), + }) + .await; + + assert_eq!(response.status, 401); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_tag_to_non_admins() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_non_admin = new_logged_in_user(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); + + let response = client + .add_tag(AddTagForm { + name: "TAG NAME".to_string(), + }) + .await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_allow_admins_to_add_new_tags() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let tag_name = random_tag_name(); + + let response = client + .add_tag(AddTagForm { + name: tag_name.to_string(), + }) + .await; + + assert_added_tag_response(&response, &tag_name); +} + +#[tokio::test] +async fn it_should_allow_adding_duplicated_tags() { + // code-review: is this an intended behavior? + + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + // Add a tag + let random_tag_name = random_tag_name(); + let response = add_tag(&random_tag_name, &env).await; + assert_eq!(response.status, 200); + + // Try to add the same tag again + let response = add_tag(&random_tag_name, &env).await; + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_allow_adding_a_tag_with_an_empty_name() { + // code-review: is this an intended behavior? + + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let empty_tag_name = String::new(); + let response = add_tag(&empty_tag_name, &env).await; + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_allow_admins_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + assert_deleted_tag_response(&response, tag_id); +} + +#[tokio::test] +async fn it_should_not_allow_non_admins_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_non_admin = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_not_allow_guests_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + assert_eq!(response.status, 401); +} diff --git a/tests/e2e/contexts/tag/mod.rs b/tests/e2e/web/api/v1/contexts/tag/mod.rs similarity index 100% rename from tests/e2e/contexts/tag/mod.rs rename to tests/e2e/web/api/v1/contexts/tag/mod.rs diff --git a/tests/e2e/contexts/tag/steps.rs b/tests/e2e/web/api/v1/contexts/tag/steps.rs similarity index 94% rename from tests/e2e/contexts/tag/steps.rs rename to tests/e2e/web/api/v1/contexts/tag/steps.rs index 32bb767c..0e59d0ec 100644 --- a/tests/e2e/contexts/tag/steps.rs +++ b/tests/e2e/web/api/v1/contexts/tag/steps.rs @@ -3,8 +3,8 @@ use crate::common::contexts::tag::fixtures::random_tag_name; use crate::common::contexts::tag::forms::AddTagForm; use crate::common::contexts::tag::responses::ListResponse; use crate::common::responses::TextResponse; -use crate::e2e::contexts::user::steps::new_logged_in_admin; use crate::e2e::environment::TestEnv; +use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_admin; pub async fn add_random_tag(env: &TestEnv) -> (i64, String) { let tag_name = random_tag_name(); diff --git a/tests/e2e/contexts/torrent/asserts.rs b/tests/e2e/web/api/v1/contexts/torrent/asserts.rs similarity index 100% rename from tests/e2e/contexts/torrent/asserts.rs rename to tests/e2e/web/api/v1/contexts/torrent/asserts.rs diff --git a/tests/e2e/web/api/v1/contexts/torrent/contract.rs b/tests/e2e/web/api/v1/contexts/torrent/contract.rs new file mode 100644 index 00000000..69ad61a5 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/torrent/contract.rs @@ -0,0 +1,627 @@ +//! API contract for `torrent` context. + +/* +todo: + +Delete torrent: + +- After deleting a torrent, it should be removed from the tracker whitelist + +Get torrent info: + +- The torrent info: + - should contain the magnet link with the trackers from the torrent file + - should contain realtime seeders and leechers from the tracker +*/ + +mod for_guests { + + use torrust_index_backend::utils::parse_torrent::decode_torrent; + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::category::fixtures::software_predefined_category_id; + use crate::common::contexts::torrent::asserts::assert_expected_torrent_details; + use crate::common::contexts::torrent::requests::InfoHash; + use crate::common::contexts::torrent::responses::{ + Category, File, TorrentDetails, TorrentDetailsResponse, TorrentListResponse, + }; + use crate::common::http::{Query, QueryParam}; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::torrent::asserts::expected_torrent; + use crate::e2e::web::api::v1::contexts::torrent::steps::upload_random_torrent_to_index; + use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_user; + + #[tokio::test] + async fn it_should_allow_guests_to_get_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let uploader = new_logged_in_user(&env).await; + let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let response = client.get_torrents(Query::empty()).await; + + let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); + + assert!(torrent_list_response.data.total > 0); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_allow_to_get_torrents_with_pagination() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + + // Given we insert two torrents + let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + // When we request only one torrent per page + let response = client + .get_torrents(Query::with_params([QueryParam::new("page_size", "1")].to_vec())) + .await; + + let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); + + // Then we should have only one torrent per page + assert_eq!(torrent_list_response.data.results.len(), 1); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_allow_to_limit_the_number_of_torrents_per_request() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + + let max_torrent_page_size = 30; + + // Given we insert one torrent more than the page size limit + for _ in 0..max_torrent_page_size { + let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + // When we request more torrents than the page size limit + let response = client + .get_torrents(Query::with_params( + [QueryParam::new("page_size", &format!("{}", (max_torrent_page_size + 1)))].to_vec(), + )) + .await; + + let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); + + // Then we should get only the page size limit + assert_eq!(torrent_list_response.data.results.len(), max_torrent_page_size); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_return_a_default_amount_of_torrents_per_request_if_no_page_size_is_provided() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + + let default_torrent_page_size = 10; + + // Given we insert one torrent more than the default page size + for _ in 0..default_torrent_page_size { + let (_test_torrent, _indexed_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + // When we request more torrents than the default page size limit + let response = client.get_torrents(Query::empty()).await; + + let torrent_list_response: TorrentListResponse = serde_json::from_str(&response.body).unwrap(); + + // Then we should get only the default number of torrents per page + assert_eq!(torrent_list_response.data.results.len(), default_torrent_page_size); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_allow_guests_to_get_torrent_details_searching_by_info_hash() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let response = client.get_torrent(&test_torrent.info_hash()).await; + + let torrent_details_response: TorrentDetailsResponse = serde_json::from_str(&response.body).unwrap(); + + let tracker_url = env.server_settings().unwrap().tracker.url; + let encoded_tracker_url = urlencoding::encode(&tracker_url); + + let expected_torrent = TorrentDetails { + torrent_id: uploaded_torrent.torrent_id, + uploader: uploader.username, + info_hash: test_torrent.file_info.info_hash.to_uppercase(), + title: test_torrent.index_info.title.clone(), + description: test_torrent.index_info.description, + category: Category { + category_id: software_predefined_category_id(), + name: test_torrent.index_info.category, + num_torrents: 19, // Ignored in assertion + }, + upload_date: "2023-04-27 07:56:08".to_string(), // Ignored in assertion + file_size: test_torrent.file_info.content_size, + seeders: 0, + leechers: 0, + files: vec![File { + path: vec![test_torrent.file_info.files[0].clone()], + // Using one file torrent for testing: content_size = first file size + length: test_torrent.file_info.content_size, + md5sum: None, + }], + // code-review: why is this duplicated? It seems that is adding the + // same tracker twice because first ti adds all trackers and then + // it adds the tracker with the personal announce url, if the user + // is logged in. If the user is not logged in, it adds the default + // tracker again, and it ends up with two trackers. + trackers: vec![tracker_url.clone(), tracker_url.clone()], + magnet_link: format!( + // cspell:disable-next-line + "magnet:?xt=urn:btih:{}&dn={}&tr={}&tr={}", + test_torrent.file_info.info_hash.to_uppercase(), + urlencoding::encode(&test_torrent.index_info.title), + encoded_tracker_url, + encoded_tracker_url + ), + }; + + assert_expected_torrent_details(&torrent_details_response.data, &expected_torrent); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_allow_guests_to_download_a_torrent_file_searching_by_info_hash() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _torrent_listed_in_index) = upload_random_torrent_to_index(&uploader, &env).await; + + let response = client.download_torrent(&test_torrent.info_hash()).await; + + let torrent = decode_torrent(&response.bytes).expect("could not decode downloaded torrent"); + let uploaded_torrent = + decode_torrent(&test_torrent.index_info.torrent_file.contents).expect("could not decode uploaded torrent"); + let expected_torrent = expected_torrent(uploaded_torrent, &env, &None).await; + assert_eq!(torrent, expected_torrent); + assert!(response.is_bittorrent_and_ok()); + } + + #[tokio::test] + async fn it_should_return_a_not_found_trying_to_download_a_non_existing_torrent() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let non_existing_info_hash: InfoHash = "443c7602b4fde83d1154d6d9da48808418b181b6".to_string(); + + let response = client.download_torrent(&non_existing_info_hash).await; + + // code-review: should this be 404? + assert_eq!(response.status, 400); + } + + #[tokio::test] + async fn it_should_not_allow_guests_to_delete_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let response = client.delete_torrent(&test_torrent.info_hash()).await; + + assert_eq!(response.status, 401); + } +} + +mod for_authenticated_users { + + use torrust_index_backend::utils::parse_torrent::decode_torrent; + use torrust_index_backend::web::api; + + use crate::common::asserts::assert_json_error_response; + use crate::common::client::Client; + use crate::common::contexts::torrent::fixtures::random_torrent; + use crate::common::contexts::torrent::forms::UploadTorrentMultipartForm; + use crate::common::contexts::torrent::responses::UploadedTorrentResponse; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::torrent::asserts::{build_announce_url, get_user_tracker_key}; + use crate::e2e::web::api::v1::contexts::torrent::steps::upload_random_torrent_to_index; + use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_user; + + #[tokio::test] + async fn it_should_allow_authenticated_users_to_upload_new_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + let test_torrent = random_torrent(); + let info_hash = test_torrent.info_hash().clone(); + + let form: UploadTorrentMultipartForm = test_torrent.index_info.into(); + + let response = client.upload_torrent(form.into()).await; + + let uploaded_torrent_response: UploadedTorrentResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!( + uploaded_torrent_response.data.info_hash.to_lowercase(), + info_hash.to_lowercase() + ); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_not_allow_uploading_a_torrent_with_a_non_existing_category() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let uploader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + let mut test_torrent = random_torrent(); + + test_torrent.index_info.category = "non-existing-category".to_string(); + + let form: UploadTorrentMultipartForm = test_torrent.index_info.into(); + + let response = client.upload_torrent(form.into()).await; + + assert_eq!(response.status, 400); + } + + #[tokio::test] + async fn it_should_not_allow_uploading_a_torrent_with_a_title_that_already_exists() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + // Upload the first torrent + let first_torrent = random_torrent(); + let first_torrent_title = first_torrent.index_info.title.clone(); + let form: UploadTorrentMultipartForm = first_torrent.index_info.into(); + let _response = client.upload_torrent(form.into()).await; + + // Upload the second torrent with the same title as the first one + let mut second_torrent = random_torrent(); + second_torrent.index_info.title = first_torrent_title; + let form: UploadTorrentMultipartForm = second_torrent.index_info.into(); + let response = client.upload_torrent(form.into()).await; + + assert_json_error_response(&response, "This torrent title has already been used."); + } + + #[tokio::test] + async fn it_should_not_allow_uploading_a_torrent_with_a_info_hash_that_already_exists() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + // Upload the first torrent + let first_torrent = random_torrent(); + let mut first_torrent_clone = first_torrent.clone(); + let first_torrent_title = first_torrent.index_info.title.clone(); + let form: UploadTorrentMultipartForm = first_torrent.index_info.into(); + let _response = client.upload_torrent(form.into()).await; + + // Upload the second torrent with the same info-hash as the first one. + // We need to change the title otherwise the torrent will be rejected + // because of the duplicate title. + first_torrent_clone.index_info.title = format!("{first_torrent_title}-clone"); + let form: UploadTorrentMultipartForm = first_torrent_clone.index_info.into(); + let response = client.upload_torrent(form.into()).await; + + assert_eq!(response.status, 400); + } + + #[tokio::test] + async fn it_should_allow_authenticated_users_to_download_a_torrent_with_a_personal_announce_url() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + // Given a previously uploaded torrent + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _torrent_listed_in_index) = upload_random_torrent_to_index(&uploader, &env).await; + + // And a logged in user who is going to download the torrent + let downloader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &downloader.token); + + // When the user downloads the torrent + let response = client.download_torrent(&test_torrent.info_hash()).await; + + let torrent = decode_torrent(&response.bytes).expect("could not decode downloaded torrent"); + + // Then the torrent should have the personal announce URL + let tracker_key = get_user_tracker_key(&downloader, &env) + .await + .expect("uploader should have a valid tracker key"); + + let tracker_url = env.server_settings().unwrap().tracker.url; + + assert_eq!( + torrent.announce.unwrap(), + build_announce_url(&tracker_url, &Some(tracker_key)) + ); + } + + mod and_non_admins { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::torrent::forms::UpdateTorrentFrom; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::torrent::steps::upload_random_torrent_to_index; + use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_user; + + #[tokio::test] + async fn it_should_not_allow_non_admins_to_delete_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + let response = client.delete_torrent(&test_torrent.info_hash()).await; + + assert_eq!(response.status, 403); + } + + #[tokio::test] + async fn it_should_allow_non_admin_users_to_update_someone_elses_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + // Given a users uploads a torrent + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + // Then another non admin user should not be able to update the torrent + let not_the_uploader = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), ¬_the_uploader.token); + + let new_title = format!("{}-new-title", test_torrent.index_info.title); + let new_description = format!("{}-new-description", test_torrent.index_info.description); + + let response = client + .update_torrent( + &test_torrent.info_hash(), + UpdateTorrentFrom { + title: Some(new_title.clone()), + description: Some(new_description.clone()), + }, + ) + .await; + + assert_eq!(response.status, 403); + } + } + + mod and_torrent_owners { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::torrent::forms::UpdateTorrentFrom; + use crate::common::contexts::torrent::responses::UpdatedTorrentResponse; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::torrent::steps::upload_random_torrent_to_index; + use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_user; + + #[tokio::test] + async fn it_should_allow_torrent_owners_to_update_their_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); + + let new_title = format!("{}-new-title", test_torrent.index_info.title); + let new_description = format!("{}-new-description", test_torrent.index_info.description); + + let response = client + .update_torrent( + &test_torrent.info_hash(), + UpdateTorrentFrom { + title: Some(new_title.clone()), + description: Some(new_description.clone()), + }, + ) + .await; + + let updated_torrent_response: UpdatedTorrentResponse = serde_json::from_str(&response.body).unwrap(); + + let torrent = updated_torrent_response.data; + + assert_eq!(torrent.title, new_title); + assert_eq!(torrent.description, new_description); + assert!(response.is_json_and_ok()); + } + } + + mod and_admins { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::torrent::forms::UpdateTorrentFrom; + use crate::common::contexts::torrent::responses::{DeletedTorrentResponse, UpdatedTorrentResponse}; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::torrent::steps::upload_random_torrent_to_index; + use crate::e2e::web::api::v1::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; + + #[tokio::test] + async fn it_should_allow_admins_to_delete_torrents_searching_by_info_hash() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &admin.token); + + let response = client.delete_torrent(&test_torrent.info_hash()).await; + + let deleted_torrent_response: DeletedTorrentResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(deleted_torrent_response.data.torrent_id, uploaded_torrent.torrent_id); + assert!(response.is_json_and_ok()); + } + + #[tokio::test] + async fn it_should_allow_admins_to_update_someone_elses_torrents() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + if !env.provides_a_tracker() { + println!("test skipped. It requires a tracker to be running."); + return; + } + + let uploader = new_logged_in_user(&env).await; + let (test_torrent, _uploaded_torrent) = upload_random_torrent_to_index(&uploader, &env).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let new_title = format!("{}-new-title", test_torrent.index_info.title); + let new_description = format!("{}-new-description", test_torrent.index_info.description); + + let response = client + .update_torrent( + &test_torrent.info_hash(), + UpdateTorrentFrom { + title: Some(new_title.clone()), + description: Some(new_description.clone()), + }, + ) + .await; + + let updated_torrent_response: UpdatedTorrentResponse = serde_json::from_str(&response.body).unwrap(); + + let torrent = updated_torrent_response.data; + + assert_eq!(torrent.title, new_title); + assert_eq!(torrent.description, new_description); + assert!(response.is_json_and_ok()); + } + } +} diff --git a/tests/e2e/contexts/torrent/mod.rs b/tests/e2e/web/api/v1/contexts/torrent/mod.rs similarity index 100% rename from tests/e2e/contexts/torrent/mod.rs rename to tests/e2e/web/api/v1/contexts/torrent/mod.rs diff --git a/tests/e2e/contexts/torrent/steps.rs b/tests/e2e/web/api/v1/contexts/torrent/steps.rs similarity index 100% rename from tests/e2e/contexts/torrent/steps.rs rename to tests/e2e/web/api/v1/contexts/torrent/steps.rs diff --git a/tests/e2e/web/api/v1/contexts/user/contract.rs b/tests/e2e/web/api/v1/contexts/user/contract.rs new file mode 100644 index 00000000..12c4e146 --- /dev/null +++ b/tests/e2e/web/api/v1/contexts/user/contract.rs @@ -0,0 +1,176 @@ +//! API contract for `user` context. + +/* + +This test suite is not complete. It's just a starting point to show how to +write E2E tests. Anyway, the goal is not to fully cover all the app features +with E2E tests. The goal is to cover the most important features and to +demonstrate how to write E2E tests. Some important pending tests could be: + +todo: + +- It should allow renewing a token one week before it expires. +- It should allow verifying user registration via email. + +The first one requires to mock the time. Consider extracting the mod + into +an independent crate. + +The second one requires: +- To call the mailcatcher API to get the verification URL. +- To enable email verification in the configuration. +- To fix current tests to verify the email for newly created users. +- To find out which email is the one that contains the verification URL for a +given test. That maybe done using the email recipient if that's possible with +the mailcatcher API. + +*/ + +mod registration { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::user::asserts::assert_added_user_response; + use crate::common::contexts::user::fixtures::random_user_registration_form; + use crate::e2e::environment::TestEnv; + + #[tokio::test] + async fn it_should_allow_a_guest_user_to_register() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let form = random_user_registration_form(); + + let response = client.register_user(form).await; + + assert_added_user_response(&response); + } +} + +mod authentication { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::user::asserts::{ + assert_successful_login_response, assert_token_renewal_response, assert_token_verified_response, + }; + use crate::common::contexts::user::forms::{LoginForm, TokenRenewalForm, TokenVerificationForm}; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::user::steps::{new_logged_in_user, new_registered_user}; + + #[tokio::test] + async fn it_should_allow_a_registered_user_to_login() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let registered_user = new_registered_user(&env).await; + + let response = client + .login_user(LoginForm { + login: registered_user.username.clone(), + password: registered_user.password.clone(), + }) + .await; + + assert_successful_login_response(&response, ®istered_user); + } + + #[tokio::test] + async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let logged_in_user = new_logged_in_user(&env).await; + + let response = client + .verify_token(TokenVerificationForm { + token: logged_in_user.token.clone(), + }) + .await; + + assert_token_verified_response(&response); + } + + #[tokio::test] + async fn it_should_not_allow_a_logged_in_user_to_renew_an_authentication_token_which_is_still_valid_for_more_than_one_week() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_user = new_logged_in_user(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_user.token); + + let response = client + .renew_token(TokenRenewalForm { + token: logged_in_user.token.clone(), + }) + .await; + + assert_token_renewal_response(&response, &logged_in_user); + } +} + +mod banned_user_list { + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::user::asserts::assert_banned_user_response; + use crate::common::contexts::user::forms::Username; + use crate::e2e::environment::TestEnv; + use crate::e2e::web::api::v1::contexts::user::steps::{new_logged_in_admin, new_logged_in_user, new_registered_user}; + + #[tokio::test] + async fn it_should_allow_an_admin_to_ban_a_user() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_in_admin = new_logged_in_admin(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let registered_user = new_registered_user(&env).await; + + let response = client.ban_user(Username::new(registered_user.username.clone())).await; + + assert_banned_user_response(&response, ®istered_user); + } + + #[tokio::test] + async fn it_should_not_allow_a_non_admin_to_ban_a_user() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let logged_non_admin = new_logged_in_user(&env).await; + + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); + + let registered_user = new_registered_user(&env).await; + + let response = client.ban_user(Username::new(registered_user.username.clone())).await; + + assert_eq!(response.status, 403); + } + + #[tokio::test] + async fn it_should_not_allow_a_guest_to_ban_a_user() { + let mut env = TestEnv::new(); + env.start(api::Version::V1).await; + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let registered_user = new_registered_user(&env).await; + + let response = client.ban_user(Username::new(registered_user.username.clone())).await; + + assert_eq!(response.status, 401); + } +} diff --git a/tests/e2e/contexts/user/mod.rs b/tests/e2e/web/api/v1/contexts/user/mod.rs similarity index 100% rename from tests/e2e/contexts/user/mod.rs rename to tests/e2e/web/api/v1/contexts/user/mod.rs diff --git a/tests/e2e/contexts/user/steps.rs b/tests/e2e/web/api/v1/contexts/user/steps.rs similarity index 100% rename from tests/e2e/contexts/user/steps.rs rename to tests/e2e/web/api/v1/contexts/user/steps.rs diff --git a/tests/e2e/web/api/v1/mod.rs b/tests/e2e/web/api/v1/mod.rs new file mode 100644 index 00000000..0f9779b8 --- /dev/null +++ b/tests/e2e/web/api/v1/mod.rs @@ -0,0 +1 @@ +pub mod contexts; diff --git a/tests/e2e/web/mod.rs b/tests/e2e/web/mod.rs new file mode 100644 index 00000000..e5fdf85e --- /dev/null +++ b/tests/e2e/web/mod.rs @@ -0,0 +1 @@ +pub mod api; diff --git a/tests/environments/app_starter.rs b/tests/environments/app_starter.rs index 47aef6e2..9999f4d3 100644 --- a/tests/environments/app_starter.rs +++ b/tests/environments/app_starter.rs @@ -4,7 +4,7 @@ use log::info; use tokio::sync::{oneshot, RwLock}; use tokio::task::JoinHandle; use torrust_index_backend::config::Configuration; -use torrust_index_backend::web::api::Implementation; +use torrust_index_backend::web::api::Version; use torrust_index_backend::{app, config}; /// It launches the app and provides a way to stop it. @@ -32,7 +32,7 @@ impl AppStarter { /// # Panics /// /// Will panic if the app was dropped after spawning it. - pub async fn start(&mut self, api_implementation: Implementation) { + pub async fn start(&mut self, api_version: Version) { let configuration = Configuration { settings: RwLock::new(self.configuration.clone()), config_path: self.config_path.clone(), @@ -43,7 +43,7 @@ impl AppStarter { // Launch the app in a separate task let app_handle = tokio::spawn(async move { - let app = app::run(configuration, &api_implementation).await; + let app = app::run(configuration, &api_version).await; info!("Application started. API server listening on {}", app.api_socket_addr); @@ -53,8 +53,8 @@ impl AppStarter { }) .expect("the app starter should not be dropped"); - match api_implementation { - Implementation::Axum => app.axum_api_server.unwrap().await, + match api_version { + Version::V1 => app.api_server.unwrap().await, } }); diff --git a/tests/environments/isolated.rs b/tests/environments/isolated.rs index a4b1ae45..96a7179b 100644 --- a/tests/environments/isolated.rs +++ b/tests/environments/isolated.rs @@ -1,7 +1,7 @@ use tempfile::TempDir; use torrust_index_backend::config; use torrust_index_backend::config::FREE_PORT; -use torrust_index_backend::web::api::Implementation; +use torrust_index_backend::web::api::Version; use super::app_starter::AppStarter; use crate::common::random; @@ -16,9 +16,9 @@ pub struct TestEnv { impl TestEnv { /// Provides a running app instance for integration tests. - pub async fn running(api_implementation: Implementation) -> Self { + pub async fn running(api_version: Version) -> Self { let mut env = Self::default(); - env.start(api_implementation).await; + env.start(api_version).await; env } @@ -40,8 +40,8 @@ impl TestEnv { } /// Starts the app. - pub async fn start(&mut self, api_implementation: Implementation) { - self.app_starter.start(api_implementation).await; + pub async fn start(&mut self, api_version: Version) { + self.app_starter.start(api_version).await; } /// Provides the whole server configuration.