From d08f70eccf5b6fde7139c2413d293a1da98b49e0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 6 Jun 2023 17:28:09 +0100 Subject: [PATCH] refactor(api): [#174] Axum API scaffolding Basic changes needed to run the Axum API implementation in parallel with the current one with ActixWeb. For the time being, we will be only useinf the Auxm implementation for testing until all endpoints are migrated. --- src/app.rs | 45 ++++---------- src/bin/main.rs | 16 ++++- src/services/about.rs | 5 ++ src/web/api/actix.rs | 75 ++++++++++++++++++++++ src/web/api/axum.rs | 76 +++++++++++++++++++++++ src/web/api/mod.rs | 47 ++++++++++++++ src/web/api/v1/contexts/about/handlers.rs | 20 ++++++ src/web/api/v1/contexts/about/mod.rs | 2 + src/web/api/v1/contexts/about/routes.rs | 15 +++++ src/web/api/v1/mod.rs | 1 + src/web/api/v1/routes.rs | 22 +++++++ tests/e2e/contexts/about/contract.rs | 26 +++++++- tests/e2e/contexts/category/contract.rs | 20 +++--- tests/e2e/contexts/root/contract.rs | 4 +- tests/e2e/contexts/settings/contract.rs | 10 +-- tests/e2e/contexts/torrent/contract.rs | 44 +++++++------ tests/e2e/contexts/user/contract.rs | 18 +++--- tests/e2e/environment.rs | 6 +- tests/environments/app_starter.rs | 31 +++++---- tests/environments/isolated.rs | 9 +-- 20 files changed, 398 insertions(+), 94 deletions(-) create mode 100644 src/web/api/actix.rs create mode 100644 src/web/api/axum.rs create mode 100644 src/web/api/v1/contexts/about/handlers.rs create mode 100644 src/web/api/v1/contexts/about/routes.rs create mode 100644 src/web/api/v1/routes.rs diff --git a/src/app.rs b/src/app.rs index 3aa8e29e..a2bceb2f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,7 @@ use std::net::SocketAddr; use std::sync::Arc; -use actix_cors::Cors; -use actix_web::dev::Server; -use actix_web::{middleware, web, App, HttpServer}; -use log::info; +use tokio::task::JoinHandle; use crate::auth::Authentication; use crate::bootstrap::logging; @@ -21,16 +18,18 @@ use crate::services::torrent::{ use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; -use crate::{mailer, routes, tracker}; +use crate::web::api::{start, Implementation}; +use crate::{mailer, tracker}; pub struct Running { - pub api_server: Server, - pub socket_address: SocketAddr, + pub api_socket_addr: SocketAddr, + pub actix_web_api_server: Option>>, + pub axum_api_server: Option>>, pub tracker_data_importer_handle: tokio::task::JoinHandle<()>, } #[allow(clippy::too_many_lines)] -pub async fn run(configuration: Configuration) -> Running { +pub async fn run(configuration: Configuration, api_implementation: &Implementation) -> Running { logging::setup(); let configuration = Arc::new(configuration); @@ -42,6 +41,7 @@ pub async fn run(configuration: Configuration) -> Running { let database_connect_url = settings.database.connect_url.clone(); let torrent_info_update_interval = settings.tracker_statistics_importer.torrent_info_update_interval; + let net_ip = "0.0.0.0".to_string(); let net_port = settings.net.port; // IMPORTANT: drop settings before starting server to avoid read locks that @@ -155,33 +155,14 @@ pub async fn run(configuration: Configuration) -> Running { } }); - // Start main API server + // Start API server - // todo: get IP from settings - let ip = "0.0.0.0".to_string(); - - let server = HttpServer::new(move || { - App::new() - .wrap(Cors::permissive()) - .app_data(web::Data::new(app_data.clone())) - .wrap(middleware::Logger::default()) - .configure(routes::init) - }) - .bind((ip, net_port)) - .expect("can't bind server to socket address"); - - let socket_address = server.addrs()[0]; - - let running_server = server.run(); - - let starting_message = format!("Listening on http://{socket_address}"); - info!("{}", starting_message); - // Logging could be disabled or redirected to file. So print to stdout too. - println!("{starting_message}"); + let running_api = start(app_data, &net_ip, net_port, api_implementation).await; Running { - api_server: running_server, - socket_address, + api_socket_addr: running_api.socket_addr, + actix_web_api_server: running_api.actix_web_api_server, + axum_api_server: running_api.axum_api_server, tracker_data_importer_handle: tracker_statistics_importer_handle, } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 706c74e3..332c352d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,11 +1,21 @@ use torrust_index_backend::app; use torrust_index_backend::bootstrap::config::init_configuration; +use torrust_index_backend::web::api::Implementation; -#[actix_web::main] +#[tokio::main] async fn main() -> Result<(), std::io::Error> { let configuration = init_configuration().await; - let app = app::run(configuration).await; + // todo: we are migrating from actix-web to axum, so we need to keep both + // implementations for a while. For production we only use ActixWeb. + // Once the Axum implementation is finished and stable, we can switch to it + // and remove the ActixWeb implementation. + let api_implementation = Implementation::ActixWeb; - app.api_server.await + let app = app::run(configuration, &api_implementation).await; + + match api_implementation { + Implementation::ActixWeb => app.actix_web_api_server.unwrap().await.expect("the API server was dropped"), + Implementation::Axum => app.axum_api_server.unwrap().await.expect("the Axum API server was dropped"), + } } diff --git a/src/services/about.rs b/src/services/about.rs index b4e52a2a..b0b18c4a 100644 --- a/src/services/about.rs +++ b/src/services/about.rs @@ -3,6 +3,11 @@ use crate::routes::API_VERSION; #[must_use] pub fn index_page() -> String { + page() +} + +#[must_use] +pub fn page() -> String { format!( r#" diff --git a/src/web/api/actix.rs b/src/web/api/actix.rs new file mode 100644 index 00000000..47b5f3d6 --- /dev/null +++ b/src/web/api/actix.rs @@ -0,0 +1,75 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use actix_cors::Cors; +use actix_web::{middleware, web, App, HttpServer}; +use log::info; +use tokio::sync::oneshot::{self, Sender}; + +use super::Running; +use crate::common::AppData; +use crate::routes; +use crate::web::api::ServerStartedMessage; + +/// Starts the API server with `ActixWeb`. +/// +/// # Panics +/// +/// Panics if the API server can't be started. +pub async fn start(app_data: Arc, net_ip: &str, net_port: u16) -> Running { + let config_socket_addr: SocketAddr = format!("{net_ip}:{net_port}") + .parse() + .expect("API server socket address to be valid."); + + let (tx, rx) = oneshot::channel::(); + + // Run the API server + let join_handle = tokio::spawn(async move { + info!("Starting API server with net config: {} ...", config_socket_addr); + + let server_future = start_server(config_socket_addr, app_data.clone(), tx); + + let _ = server_future.await; + + Ok(()) + }); + + // Wait until the API server is running + let bound_addr = match rx.await { + Ok(msg) => msg.socket_addr, + Err(e) => panic!("API server start. The API server was dropped: {e}"), + }; + + info!("API server started"); + + Running { + socket_addr: bound_addr, + actix_web_api_server: Some(join_handle), + axum_api_server: None, + } +} + +fn start_server( + config_socket_addr: SocketAddr, + app_data: Arc, + tx: Sender, +) -> actix_web::dev::Server { + let server = HttpServer::new(move || { + App::new() + .wrap(Cors::permissive()) + .app_data(web::Data::new(app_data.clone())) + .wrap(middleware::Logger::default()) + .configure(routes::init) + }) + .bind(config_socket_addr) + .expect("can't bind server to socket address"); + + let bound_addr = server.addrs()[0]; + + info!("API server listening on http://{}", bound_addr); + + tx.send(ServerStartedMessage { socket_addr: bound_addr }) + .expect("the API server should not be dropped"); + + server.run() +} diff --git a/src/web/api/axum.rs b/src/web/api/axum.rs new file mode 100644 index 00000000..5371dbc9 --- /dev/null +++ b/src/web/api/axum.rs @@ -0,0 +1,76 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use futures::Future; +use log::info; +use tokio::sync::oneshot::{self, Sender}; + +use super::v1::routes::router; +use super::{Running, ServerStartedMessage}; +use crate::common::AppData; + +/// Starts the API server with `Axum`. +/// +/// # Panics +/// +/// Panics if the API server can't be started. +pub async fn start(app_data: Arc, net_ip: &str, net_port: u16) -> Running { + let config_socket_addr: SocketAddr = format!("{net_ip}:{net_port}") + .parse() + .expect("API server socket address to be valid."); + + let (tx, rx) = oneshot::channel::(); + + // Run the API server + let join_handle = tokio::spawn(async move { + info!("Starting API server with net config: {} ...", config_socket_addr); + + let handle = start_server(config_socket_addr, app_data.clone(), tx); + + if let Ok(()) = handle.await { + info!("API server stopped"); + } + + Ok(()) + }); + + // Wait until the API server is running + let bound_addr = match rx.await { + Ok(msg) => msg.socket_addr, + Err(e) => panic!("API server start. The API server was dropped: {e}"), + }; + + Running { + socket_addr: bound_addr, + actix_web_api_server: None, + axum_api_server: Some(join_handle), + } +} + +fn start_server( + config_socket_addr: SocketAddr, + app_data: Arc, + tx: Sender, +) -> impl Future> { + let tcp_listener = std::net::TcpListener::bind(config_socket_addr).expect("tcp listener to bind to a socket address"); + + let bound_addr = tcp_listener + .local_addr() + .expect("tcp listener to be bound to a socket address."); + + info!("API server listening on http://{}", bound_addr); + + let app = router(app_data); + + let server = axum::Server::from_tcp(tcp_listener) + .expect("a new server from the previously created tcp listener.") + .serve(app.into_make_service_with_connect_info::()); + + tx.send(ServerStartedMessage { socket_addr: bound_addr }) + .expect("the API server should not be dropped"); + + server.with_graceful_shutdown(async move { + tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); + info!("Stopping API server on http://{} ...", bound_addr); + }) +} diff --git a/src/web/api/mod.rs b/src/web/api/mod.rs index 8582ba66..9321f433 100644 --- a/src/web/api/mod.rs +++ b/src/web/api/mod.rs @@ -3,4 +3,51 @@ //! Currently, the API has only one version: `v1`. //! //! Refer to the [`v1`](crate::web::api::v1) module for more information. +pub mod actix; +pub mod axum; pub mod v1; + +use std::net::SocketAddr; +use std::sync::Arc; + +use tokio::task::JoinHandle; + +use crate::common::AppData; +use crate::web::api; + +/// API implementations. +pub enum Implementation { + /// API implementation with Actix Web. + ActixWeb, + /// API implementation with Axum. + Axum, +} + +/// The running API server. +pub struct Running { + /// The socket address the API server is listening on. + pub socket_addr: SocketAddr, + /// The API server when using Actix Web. + pub actix_web_api_server: Option>>, + /// The handle for the running API server task when using Axum. + pub axum_api_server: Option>>, +} + +#[must_use] +#[derive(Debug)] +pub struct ServerStartedMessage { + pub socket_addr: SocketAddr, +} + +/// Starts the API server. +/// +/// We are migrating the API server from Actix Web to Axum. While the migration +/// is in progress, we will keep both implementations, running the Axum one only +/// for testing purposes. +#[must_use] +pub async fn start(app_data: Arc, net_ip: &str, net_port: u16, implementation: &Implementation) -> api::Running { + match implementation { + Implementation::ActixWeb => actix::start(app_data, net_ip, net_port).await, + Implementation::Axum => axum::start(app_data, net_ip, net_port).await, + } +} diff --git a/src/web/api/v1/contexts/about/handlers.rs b/src/web/api/v1/contexts/about/handlers.rs new file mode 100644 index 00000000..99e4cd08 --- /dev/null +++ b/src/web/api/v1/contexts/about/handlers.rs @@ -0,0 +1,20 @@ +//! API handlers for the the [`about`](crate::web::api::v1::contexts::about) API +//! context. +use std::sync::Arc; + +use axum::extract::State; +use axum::http::{header, StatusCode}; +use axum::response::{IntoResponse, Response}; + +use crate::common::AppData; +use crate::services::about; + +#[allow(clippy::unused_async)] +pub async fn about_page_handler(State(_app_data): State>) -> Response { + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "text/html; charset=utf-8")], + about::page(), + ) + .into_response() +} diff --git a/src/web/api/v1/contexts/about/mod.rs b/src/web/api/v1/contexts/about/mod.rs index 0b12ff66..bde0696a 100644 --- a/src/web/api/v1/contexts/about/mod.rs +++ b/src/web/api/v1/contexts/about/mod.rs @@ -84,3 +84,5 @@ //! //! //! ``` +pub mod handlers; +pub mod routes; diff --git a/src/web/api/v1/contexts/about/routes.rs b/src/web/api/v1/contexts/about/routes.rs new file mode 100644 index 00000000..fe36dd92 --- /dev/null +++ b/src/web/api/v1/contexts/about/routes.rs @@ -0,0 +1,15 @@ +//! API routes for the [`about`](crate::web::api::v1::contexts::about) API context. +//! +//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::about). +use std::sync::Arc; + +use axum::routing::get; +use axum::Router; + +use super::handlers::about_page_handler; +use crate::common::AppData; + +/// It adds the routes to the router for the [`about`](crate::web::api::v1::contexts::about) API context. +pub fn add(prefix: &str, router: Router, app_data: Arc) -> Router { + router.route(&format!("{prefix}/about"), get(about_page_handler).with_state(app_data)) +} diff --git a/src/web/api/v1/mod.rs b/src/web/api/v1/mod.rs index 716030ab..9d94e076 100644 --- a/src/web/api/v1/mod.rs +++ b/src/web/api/v1/mod.rs @@ -6,3 +6,4 @@ //! information. pub mod auth; pub mod contexts; +pub mod routes; diff --git a/src/web/api/v1/routes.rs b/src/web/api/v1/routes.rs new file mode 100644 index 00000000..c980bd1e --- /dev/null +++ b/src/web/api/v1/routes.rs @@ -0,0 +1,22 @@ +//! Route initialization for the v1 API. +use std::sync::Arc; + +use axum::Router; + +use super::contexts::about; +use crate::common::AppData; + +/// Add all API routes to the router. +#[allow(clippy::needless_pass_by_value)] +pub fn router(app_data: Arc) -> Router { + let router = Router::new(); + + add(router, app_data) +} + +/// Add the routes for the v1 API. +fn add(router: Router, app_data: Arc) -> Router { + let v1_prefix = "/v1".to_string(); + + about::routes::add(&v1_prefix, router, app_data) +} diff --git a/tests/e2e/contexts/about/contract.rs b/tests/e2e/contexts/about/contract.rs index 7907c761..d50f93e5 100644 --- a/tests/e2e/contexts/about/contract.rs +++ b/tests/e2e/contexts/about/contract.rs @@ -1,4 +1,6 @@ //! 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; @@ -6,7 +8,7 @@ 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().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.about().await; @@ -18,7 +20,7 @@ async fn it_should_load_the_about_page_with_information_about_the_api() { #[tokio::test] async fn it_should_load_the_license_page_at_the_api_entrypoint() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.license().await; @@ -26,3 +28,23 @@ async fn it_should_load_the_license_page_at_the_api_entrypoint() { assert_text_ok(&response); assert_response_title(&response, "Licensing"); } + +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"); + } +} diff --git a/tests/e2e/contexts/category/contract.rs b/tests/e2e/contexts/category/contract.rs index 5d12290b..b5682327 100644 --- a/tests/e2e/contexts/category/contract.rs +++ b/tests/e2e/contexts/category/contract.rs @@ -1,4 +1,6 @@ //! API contract for `category` context. +use torrust_index_backend::web::api; + use crate::common::asserts::assert_json_ok; use crate::common::client::Client; use crate::common::contexts::category::fixtures::random_category_name; @@ -18,7 +20,7 @@ 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().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.get_categories().await; @@ -29,7 +31,7 @@ async fn it_should_return_an_empty_category_list_when_there_are_no_categories() #[tokio::test] async fn it_should_return_a_category_list() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); // Add a category @@ -53,7 +55,7 @@ async fn it_should_return_a_category_list() { #[tokio::test] async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client @@ -69,7 +71,7 @@ async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() { #[tokio::test] async fn it_should_not_allow_adding_a_new_category_to_non_admins() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_non_admin = new_logged_in_user(&env).await; @@ -88,7 +90,7 @@ async fn it_should_not_allow_adding_a_new_category_to_non_admins() { #[tokio::test] async fn it_should_allow_admins_to_add_new_categories() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_in_admin = new_logged_in_admin(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); @@ -114,7 +116,7 @@ async fn it_should_allow_admins_to_add_new_categories() { #[tokio::test] async fn it_should_not_allow_adding_duplicated_categories() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; // Add a category let random_category_name = random_category_name(); @@ -129,7 +131,7 @@ async fn it_should_not_allow_adding_duplicated_categories() { #[tokio::test] async fn it_should_allow_admins_to_delete_categories() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_in_admin = new_logged_in_admin(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); @@ -158,7 +160,7 @@ async fn it_should_allow_admins_to_delete_categories() { #[tokio::test] async fn it_should_not_allow_non_admins_to_delete_categories() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; // Add a category let category_name = random_category_name(); @@ -181,7 +183,7 @@ async fn it_should_not_allow_non_admins_to_delete_categories() { #[tokio::test] async fn it_should_not_allow_guests_to_delete_categories() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); // Add a category diff --git a/tests/e2e/contexts/root/contract.rs b/tests/e2e/contexts/root/contract.rs index 84c1fc45..10a2bf6c 100644 --- a/tests/e2e/contexts/root/contract.rs +++ b/tests/e2e/contexts/root/contract.rs @@ -1,4 +1,6 @@ //! 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; @@ -6,7 +8,7 @@ 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().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.root().await; diff --git a/tests/e2e/contexts/settings/contract.rs b/tests/e2e/contexts/settings/contract.rs index 3d87a0c7..0802512a 100644 --- a/tests/e2e/contexts/settings/contract.rs +++ b/tests/e2e/contexts/settings/contract.rs @@ -1,3 +1,5 @@ +use torrust_index_backend::web::api; + 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; @@ -6,7 +8,7 @@ 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().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.get_public_settings().await; @@ -31,7 +33,7 @@ async fn it_should_allow_guests_to_get_the_public_settings() { #[tokio::test] async fn it_should_allow_guests_to_get_the_site_name() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let response = client.get_site_name().await; @@ -48,7 +50,7 @@ async fn it_should_allow_guests_to_get_the_site_name() { #[tokio::test] async fn it_should_allow_admins_to_get_all_the_settings() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_in_admin = new_logged_in_admin(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); @@ -67,7 +69,7 @@ async fn it_should_allow_admins_to_get_all_the_settings() { #[tokio::test] async fn it_should_allow_admins_to_update_all_the_settings() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.is_isolated() { // This test can't be executed in a non-isolated environment because diff --git a/tests/e2e/contexts/torrent/contract.rs b/tests/e2e/contexts/torrent/contract.rs index 929e0cea..955f5154 100644 --- a/tests/e2e/contexts/torrent/contract.rs +++ b/tests/e2e/contexts/torrent/contract.rs @@ -16,6 +16,7 @@ Get torrent info: 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; @@ -33,7 +34,7 @@ mod for_guests { #[tokio::test] async fn it_should_allow_guests_to_get_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -56,7 +57,7 @@ mod for_guests { #[tokio::test] async fn it_should_allow_to_get_torrents_with_pagination() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -86,7 +87,7 @@ mod for_guests { #[tokio::test] async fn it_should_allow_to_limit_the_number_of_torrents_per_request() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -121,7 +122,7 @@ mod for_guests { #[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().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -152,7 +153,7 @@ mod for_guests { #[tokio::test] async fn it_should_allow_guests_to_get_torrent_details_searching_by_info_hash() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -215,7 +216,7 @@ mod for_guests { #[tokio::test] async fn it_should_allow_guests_to_download_a_torrent_file_searching_by_info_hash() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -240,7 +241,7 @@ mod for_guests { #[tokio::test] async fn it_should_return_a_not_found_trying_to_download_a_non_existing_torrent() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -260,7 +261,7 @@ mod for_guests { #[tokio::test] async fn it_should_not_allow_guests_to_delete_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -281,6 +282,7 @@ mod for_guests { mod for_authenticated_users { use torrust_index_backend::utils::parse_torrent::decode_torrent; + use torrust_index_backend::web::api; use crate::common::client::Client; use crate::common::contexts::torrent::fixtures::random_torrent; @@ -294,7 +296,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_allow_authenticated_users_to_upload_new_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -323,7 +325,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_not_allow_uploading_a_torrent_with_a_non_existing_category() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let uploader = new_logged_in_user(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token); @@ -342,7 +344,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_not_allow_uploading_a_torrent_with_a_title_that_already_exists() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -371,7 +373,7 @@ mod for_authenticated_users { #[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().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -401,7 +403,7 @@ mod for_authenticated_users { #[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().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -435,6 +437,8 @@ mod for_authenticated_users { } 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; @@ -444,7 +448,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_not_allow_non_admins_to_delete_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -464,7 +468,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_allow_non_admin_users_to_update_someone_elses_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -497,6 +501,8 @@ mod for_authenticated_users { } 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; @@ -507,7 +513,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_allow_torrent_owners_to_update_their_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -543,6 +549,8 @@ mod for_authenticated_users { } 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}; @@ -553,7 +561,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_allow_admins_to_delete_torrents_searching_by_info_hash() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); @@ -577,7 +585,7 @@ mod for_authenticated_users { #[tokio::test] async fn it_should_allow_admins_to_update_someone_elses_torrents() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; if !env.provides_a_tracker() { println!("test skipped. It requires a tracker to be running."); diff --git a/tests/e2e/contexts/user/contract.rs b/tests/e2e/contexts/user/contract.rs index 06a12f79..24abc0de 100644 --- a/tests/e2e/contexts/user/contract.rs +++ b/tests/e2e/contexts/user/contract.rs @@ -1,4 +1,6 @@ //! API contract for `user` context. +use torrust_index_backend::web::api; + use crate::common::client::Client; use crate::common::contexts::user::fixtures::random_user_registration; use crate::common::contexts::user::forms::{LoginForm, TokenRenewalForm, TokenVerificationForm}; @@ -39,7 +41,7 @@ the mailcatcher API. #[tokio::test] async fn it_should_allow_a_guest_user_to_register() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let form = random_user_registration(); @@ -56,7 +58,7 @@ async fn it_should_allow_a_guest_user_to_register() { #[tokio::test] async fn it_should_allow_a_registered_user_to_login() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let registered_user = new_registered_user(&env).await; @@ -81,7 +83,7 @@ async fn it_should_allow_a_registered_user_to_login() { #[tokio::test] async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let logged_in_user = new_logged_in_user(&env).await; @@ -104,7 +106,7 @@ async fn it_should_allow_a_logged_in_user_to_verify_an_authentication_token() { #[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().await; + env.start(api::Implementation::ActixWeb).await; let logged_in_user = new_logged_in_user(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_user.token); @@ -132,6 +134,8 @@ async fn it_should_not_allow_a_logged_in_user_to_renew_an_authentication_token_w } mod banned_user_list { + use torrust_index_backend::web::api; + use crate::common::client::Client; use crate::common::contexts::user::forms::Username; use crate::common::contexts::user::responses::BannedUserResponse; @@ -141,7 +145,7 @@ mod banned_user_list { #[tokio::test] async fn it_should_allow_an_admin_to_ban_a_user() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_in_admin = new_logged_in_admin(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); @@ -162,7 +166,7 @@ mod banned_user_list { #[tokio::test] async fn it_should_not_allow_a_non_admin_to_ban_a_user() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let logged_non_admin = new_logged_in_user(&env).await; let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token); @@ -177,7 +181,7 @@ mod banned_user_list { #[tokio::test] async fn it_should_not_allow_a_guest_to_ban_a_user() { let mut env = TestEnv::new(); - env.start().await; + env.start(api::Implementation::ActixWeb).await; let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); let registered_user = new_registered_user(&env).await; diff --git a/tests/e2e/environment.rs b/tests/e2e/environment.rs index 43eb7af3..343e5512 100644 --- a/tests/e2e/environment.rs +++ b/tests/e2e/environment.rs @@ -1,5 +1,7 @@ use std::env; +use torrust_index_backend::web::api::Implementation; + use super::config::{init_shared_env_configuration, ENV_VAR_E2E_SHARED}; use crate::common::contexts::settings::Settings; use crate::environments::{isolated, shared}; @@ -53,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) { + pub async fn start(&mut self, api_implementation: Implementation) { let e2e_shared = ENV_VAR_E2E_SHARED; // bool if let Ok(_e2e_test_env_is_shared) = env::var(e2e_shared) { @@ -64,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().await; + let isolated_env = isolated::TestEnv::running(api_implementation).await; self.isolated = Some(isolated_env); self.starting_settings = self.server_settings_for_isolated_env(); diff --git a/tests/environments/app_starter.rs b/tests/environments/app_starter.rs index 251f0481..a08f3592 100644 --- a/tests/environments/app_starter.rs +++ b/tests/environments/app_starter.rs @@ -2,7 +2,9 @@ use std::net::SocketAddr; 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::{app, config}; /// It launches the app and provides a way to stop it. @@ -25,41 +27,46 @@ impl AppStarter { } } + /// Starts the whole app with all its services. + /// /// # Panics /// /// Will panic if the app was dropped after spawning it. - pub async fn start(&mut self) { + pub async fn start(&mut self, api_implementation: Implementation) { let configuration = Configuration { settings: RwLock::new(self.configuration.clone()), config_path: self.config_path.clone(), }; // Open a channel to communicate back with this function - let (tx, rx) = oneshot::channel::(); + let (tx, rx) = oneshot::channel::(); // Launch the app in a separate task let app_handle = tokio::spawn(async move { - let app = app::run(configuration).await; + let app = app::run(configuration, &api_implementation).await; + + info!("Application started. API server listening on {}", app.api_socket_addr); // Send the socket address back to the main thread - tx.send(AppStarted { - socket_addr: app.socket_address, + tx.send(AppStartedMessage { + api_socket_addr: app.api_socket_addr, }) .expect("the app starter should not be dropped"); - app.api_server.await + match api_implementation { + Implementation::ActixWeb => app.actix_web_api_server.unwrap().await, + Implementation::Axum => app.axum_api_server.unwrap().await, + } }); // Wait until the app is started let socket_addr = match rx.await { - Ok(msg) => msg.socket_addr, + Ok(msg) => msg.api_socket_addr, Err(e) => panic!("the app was dropped: {e}"), }; let running_state = RunningState { app_handle, socket_addr }; - info!("Test environment started. Listening on {}", running_state.socket_addr); - // Update the app state self.running_state = Some(running_state); } @@ -91,13 +98,13 @@ impl AppStarter { } #[derive(Debug)] -pub struct AppStarted { - pub socket_addr: SocketAddr, +pub struct AppStartedMessage { + pub api_socket_addr: SocketAddr, } /// Stores the app state when it is running. pub struct RunningState { - app_handle: tokio::task::JoinHandle>, + app_handle: JoinHandle, tokio::task::JoinError>>, pub socket_addr: SocketAddr, } diff --git a/tests/environments/isolated.rs b/tests/environments/isolated.rs index e619e191..411a3149 100644 --- a/tests/environments/isolated.rs +++ b/tests/environments/isolated.rs @@ -1,6 +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 super::app_starter::AppStarter; use crate::common::random; @@ -15,9 +16,9 @@ pub struct TestEnv { impl TestEnv { /// Provides a running app instance for integration tests. - pub async fn running() -> Self { + pub async fn running(api_implementation: Implementation) -> Self { let mut env = Self::default(); - env.start().await; + env.start(api_implementation).await; env } @@ -39,8 +40,8 @@ impl TestEnv { } /// Starts the app. - pub async fn start(&mut self) { - self.app_starter.start().await; + pub async fn start(&mut self, api_implementation: Implementation) { + self.app_starter.start(api_implementation).await; } /// Provides the whole server configuration.