From 538ebcc18a28f698b85227a677358943a81141fe Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 15 Jun 2023 16:58:41 +0100 Subject: [PATCH] refactor(api): [#182] Axum API, torrent context, upload torrent file --- src/errors.rs | 5 + src/web/api/v1/contexts/category/handlers.rs | 10 +- src/web/api/v1/contexts/category/responses.rs | 10 +- src/web/api/v1/contexts/settings/handlers.rs | 18 +- src/web/api/v1/contexts/tag/handlers.rs | 10 +- src/web/api/v1/contexts/tag/responses.rs | 10 +- src/web/api/v1/contexts/torrent/handlers.rs | 131 ++++ src/web/api/v1/contexts/torrent/mod.rs | 3 + src/web/api/v1/contexts/torrent/responses.rs | 22 + src/web/api/v1/contexts/torrent/routes.rs | 15 + src/web/api/v1/contexts/user/handlers.rs | 16 +- src/web/api/v1/contexts/user/responses.rs | 14 +- src/web/api/v1/responses.rs | 36 +- src/web/api/v1/routes.rs | 5 +- tests/common/asserts.rs | 17 +- tests/common/contexts/category/asserts.rs | 6 +- tests/common/contexts/tag/asserts.rs | 6 +- tests/common/contexts/user/asserts.rs | 12 +- tests/e2e/contexts/category/contract.rs | 8 +- tests/e2e/contexts/settings/contract.rs | 10 +- tests/e2e/contexts/tag/contract.rs | 8 +- tests/e2e/contexts/torrent/contract.rs | 736 ++++++++++++++++++ 22 files changed, 1030 insertions(+), 78 deletions(-) create mode 100644 src/web/api/v1/contexts/torrent/handlers.rs create mode 100644 src/web/api/v1/contexts/torrent/responses.rs create mode 100644 src/web/api/v1/contexts/torrent/routes.rs diff --git a/src/errors.rs b/src/errors.rs index 6f880162..02404896 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -139,6 +139,9 @@ pub enum ServiceError { DatabaseError, } +// Begin ActixWeb error handling +// todo: remove after migration to Axum + #[derive(Serialize, Deserialize)] pub struct ErrorToResponse { pub error: String, @@ -156,6 +159,8 @@ impl ResponseError for ServiceError { } } +// End ActixWeb error handling + impl From for ServiceError { fn from(e: sqlx::Error) -> Self { eprintln!("{e:?}"); diff --git a/src/web/api/v1/contexts/category/handlers.rs b/src/web/api/v1/contexts/category/handlers.rs index 3d09008a..981c4aad 100644 --- a/src/web/api/v1/contexts/category/handlers.rs +++ b/src/web/api/v1/contexts/category/handlers.rs @@ -11,7 +11,7 @@ use crate::common::AppData; use crate::databases::database::{self, Category}; use crate::errors::ServiceError; use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::{self, OkResponse}; +use crate::web::api::v1::responses::{self, OkResponseData}; /// It handles the request to get all the categories. /// @@ -29,9 +29,9 @@ use crate::web::api::v1::responses::{self, OkResponse}; #[allow(clippy::unused_async)] pub async fn get_all_handler( State(app_data): State>, -) -> Result>>, database::Error> { +) -> Result>>, database::Error> { match app_data.category_repository.get_all().await { - Ok(categories) => Ok(Json(responses::OkResponse { data: categories })), + Ok(categories) => Ok(Json(responses::OkResponseData { data: categories })), Err(error) => Err(error), } } @@ -49,7 +49,7 @@ pub async fn add_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, extract::Json(category_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; match app_data.category_service.add_category(&category_form.name, &user_id).await { @@ -71,7 +71,7 @@ pub async fn delete_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, extract::Json(category_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { // code-review: why do we need to send the whole category object to delete it? // And we should use the ID instead of the name, because the name could change // or we could add support for multiple languages. diff --git a/src/web/api/v1/contexts/category/responses.rs b/src/web/api/v1/contexts/category/responses.rs index cb372801..b1e20d19 100644 --- a/src/web/api/v1/contexts/category/responses.rs +++ b/src/web/api/v1/contexts/category/responses.rs @@ -2,18 +2,18 @@ //! context. use axum::Json; -use crate::web::api::v1::responses::OkResponse; +use crate::web::api::v1::responses::OkResponseData; /// Response after successfully creating a new category. -pub fn added_category(category_name: &str) -> Json> { - Json(OkResponse { +pub fn added_category(category_name: &str) -> Json> { + Json(OkResponseData { data: category_name.to_string(), }) } /// Response after successfully deleting a new category. -pub fn deleted_category(category_name: &str) -> Json> { - Json(OkResponse { +pub fn deleted_category(category_name: &str) -> Json> { + Json(OkResponseData { data: category_name.to_string(), }) } diff --git a/src/web/api/v1/contexts/settings/handlers.rs b/src/web/api/v1/contexts/settings/handlers.rs index 17144c63..feca203a 100644 --- a/src/web/api/v1/contexts/settings/handlers.rs +++ b/src/web/api/v1/contexts/settings/handlers.rs @@ -9,7 +9,7 @@ use crate::common::AppData; use crate::config::{ConfigurationPublic, TorrustBackend}; use crate::errors::ServiceError; use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::{self, OkResponse}; +use crate::web::api::v1::responses::{self, OkResponseData}; /// Get all settings. /// @@ -21,28 +21,28 @@ use crate::web::api::v1::responses::{self, OkResponse}; pub async fn get_all_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; let all_settings = app_data.settings_service.get_all(&user_id).await?; - Ok(Json(responses::OkResponse { data: all_settings })) + Ok(Json(responses::OkResponseData { data: all_settings })) } /// Get public Settings. #[allow(clippy::unused_async)] -pub async fn get_public_handler(State(app_data): State>) -> Json> { +pub async fn get_public_handler(State(app_data): State>) -> Json> { let public_settings = app_data.settings_service.get_public().await; - Json(responses::OkResponse { data: public_settings }) + Json(responses::OkResponseData { data: public_settings }) } /// Get website name. #[allow(clippy::unused_async)] -pub async fn get_site_name_handler(State(app_data): State>) -> Json> { +pub async fn get_site_name_handler(State(app_data): State>) -> Json> { let site_name = app_data.settings_service.get_site_name().await; - Json(responses::OkResponse { data: site_name }) + Json(responses::OkResponseData { data: site_name }) } /// Update all the settings. @@ -59,10 +59,10 @@ pub async fn update_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, extract::Json(torrust_backend): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; let new_settings = app_data.settings_service.update_all(torrust_backend, &user_id).await?; - Ok(Json(responses::OkResponse { data: new_settings })) + Ok(Json(responses::OkResponseData { data: new_settings })) } diff --git a/src/web/api/v1/contexts/tag/handlers.rs b/src/web/api/v1/contexts/tag/handlers.rs index 507e80a9..7944c4a4 100644 --- a/src/web/api/v1/contexts/tag/handlers.rs +++ b/src/web/api/v1/contexts/tag/handlers.rs @@ -12,7 +12,7 @@ use crate::databases::database; use crate::errors::ServiceError; use crate::models::torrent_tag::TorrentTag; use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::{self, OkResponse}; +use crate::web::api::v1::responses::{self, OkResponseData}; /// It handles the request to get all the tags. /// @@ -30,9 +30,9 @@ use crate::web::api::v1::responses::{self, OkResponse}; #[allow(clippy::unused_async)] pub async fn get_all_handler( State(app_data): State>, -) -> Result>>, database::Error> { +) -> Result>>, database::Error> { match app_data.tag_repository.get_all().await { - Ok(tags) => Ok(Json(responses::OkResponse { data: tags })), + Ok(tags) => Ok(Json(responses::OkResponseData { data: tags })), Err(error) => Err(error), } } @@ -50,7 +50,7 @@ pub async fn add_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, extract::Json(add_tag_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; match app_data.tag_service.add_tag(&add_tag_form.name, &user_id).await { @@ -72,7 +72,7 @@ pub async fn delete_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, extract::Json(delete_tag_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; match app_data.tag_service.delete_tag(&delete_tag_form.tag_id, &user_id).await { diff --git a/src/web/api/v1/contexts/tag/responses.rs b/src/web/api/v1/contexts/tag/responses.rs index 7b4d4120..3b44d51d 100644 --- a/src/web/api/v1/contexts/tag/responses.rs +++ b/src/web/api/v1/contexts/tag/responses.rs @@ -3,18 +3,18 @@ use axum::Json; use crate::models::torrent_tag::TagId; -use crate::web::api::v1::responses::OkResponse; +use crate::web::api::v1::responses::OkResponseData; /// Response after successfully creating a new tag. -pub fn added_tag(tag_name: &str) -> Json> { - Json(OkResponse { +pub fn added_tag(tag_name: &str) -> Json> { + Json(OkResponseData { data: tag_name.to_string(), }) } /// Response after successfully deleting a tag. -pub fn deleted_tag(tag_id: TagId) -> Json> { - Json(OkResponse { +pub fn deleted_tag(tag_id: TagId) -> Json> { + Json(OkResponseData { data: tag_id.to_string(), }) } diff --git a/src/web/api/v1/contexts/torrent/handlers.rs b/src/web/api/v1/contexts/torrent/handlers.rs new file mode 100644 index 00000000..794895f6 --- /dev/null +++ b/src/web/api/v1/contexts/torrent/handlers.rs @@ -0,0 +1,131 @@ +//! API handlers for the [`torrent`](crate::web::api::v1::contexts::torrent) API +//! context. +use std::io::{Cursor, Write}; +use std::sync::Arc; + +use axum::extract::{Multipart, State}; +use axum::response::{IntoResponse, Response}; + +use super::responses::new_torrent_response; +use crate::common::AppData; +use crate::errors::ServiceError; +use crate::models::torrent::TorrentRequest; +use crate::models::torrent_tag::TagId; +use crate::routes::torrent::Create; +use crate::utils::parse_torrent; +use crate::web::api::v1::extractors::bearer_token::Extract; + +/// Upload a new torrent file to the Index +/// +/// # Errors +/// +/// This function will return an error if +/// +/// - The user does not have permission to upload the torrent file. +/// - The submitted torrent file is not a valid torrent file. +#[allow(clippy::unused_async)] +pub async fn upload_torrent_handler( + State(app_data): State>, + Extract(maybe_bearer_token): Extract, + multipart: Multipart, +) -> Response { + let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await { + Ok(user_id) => user_id, + Err(err) => return err.into_response(), + }; + + let torrent_request = match get_torrent_request_from_payload(multipart).await { + Ok(torrent_request) => torrent_request, + Err(err) => return err.into_response(), + }; + + let info_hash = torrent_request.torrent.info_hash().clone(); + + match app_data.torrent_service.add_torrent(torrent_request, user_id).await { + Ok(torrent_id) => new_torrent_response(torrent_id, &info_hash).into_response(), + Err(error) => error.into_response(), + } +} + +/// Extracts the [`TorrentRequest`] from the multipart form payload. +async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result { + let torrent_buffer = vec![0u8]; + let mut torrent_cursor = Cursor::new(torrent_buffer); + + let mut title = String::new(); + let mut description = String::new(); + let mut category = String::new(); + let mut tags: Vec = vec![]; + + while let Some(mut field) = payload.next_field().await.unwrap() { + let name = field.name().unwrap().clone(); + + match name { + "title" => { + let data = field.bytes().await.unwrap(); + if data.is_empty() { + continue; + }; + title = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?; + } + "description" => { + let data = field.bytes().await.unwrap(); + if data.is_empty() { + continue; + }; + description = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?; + } + "category" => { + let data = field.bytes().await.unwrap(); + if data.is_empty() { + continue; + }; + category = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?; + } + "tags" => { + let data = field.bytes().await.unwrap(); + if data.is_empty() { + continue; + }; + let string_data = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?; + tags = serde_json::from_str(&string_data).map_err(|_| ServiceError::BadRequest)?; + } + "torrent" => { + let content_type = field.content_type().unwrap().clone(); + + if content_type != "application/x-bittorrent" { + return Err(ServiceError::InvalidFileType); + } + + while let Some(chunk) = field.chunk().await.map_err(|_| (ServiceError::BadRequest))? { + torrent_cursor.write_all(&chunk)?; + } + } + _ => {} + } + } + + let fields = Create { + title, + description, + category, + tags, + }; + + fields.verify()?; + + let position = usize::try_from(torrent_cursor.position()).map_err(|_| ServiceError::InvalidTorrentFile)?; + let inner = torrent_cursor.get_ref(); + + let torrent = parse_torrent::decode_torrent(&inner[..position]).map_err(|_| ServiceError::InvalidTorrentFile)?; + + // Make sure that the pieces key has a length that is a multiple of 20 + // code-review: I think we could put this inside the service. + if let Some(pieces) = torrent.info.pieces.as_ref() { + if pieces.as_ref().len() % 20 != 0 { + return Err(ServiceError::InvalidTorrentPiecesLength); + } + } + + Ok(TorrentRequest { fields, torrent }) +} diff --git a/src/web/api/v1/contexts/torrent/mod.rs b/src/web/api/v1/contexts/torrent/mod.rs index 77d08b8a..3e533153 100644 --- a/src/web/api/v1/contexts/torrent/mod.rs +++ b/src/web/api/v1/contexts/torrent/mod.rs @@ -328,3 +328,6 @@ //! //! Refer to the [`DeletedTorrentResponse`](crate::models::response::DeletedTorrentResponse) //! struct for more information about the response attributes. +pub mod handlers; +pub mod responses; +pub mod routes; diff --git a/src/web/api/v1/contexts/torrent/responses.rs b/src/web/api/v1/contexts/torrent/responses.rs new file mode 100644 index 00000000..9e338f32 --- /dev/null +++ b/src/web/api/v1/contexts/torrent/responses.rs @@ -0,0 +1,22 @@ +use axum::Json; +use serde::{Deserialize, Serialize}; + +use crate::models::torrent::TorrentId; +use crate::web::api::v1::responses::OkResponseData; + +#[allow(clippy::module_name_repetitions)] +#[derive(Serialize, Deserialize, Debug)] +pub struct NewTorrentResponseData { + pub torrent_id: TorrentId, + pub info_hash: String, +} + +/// Response after successfully uploading a new torrent. +pub fn new_torrent_response(torrent_id: TorrentId, info_hash: &str) -> Json> { + Json(OkResponseData { + data: NewTorrentResponseData { + torrent_id, + info_hash: info_hash.to_owned(), + }, + }) +} diff --git a/src/web/api/v1/contexts/torrent/routes.rs b/src/web/api/v1/contexts/torrent/routes.rs new file mode 100644 index 00000000..344fd995 --- /dev/null +++ b/src/web/api/v1/contexts/torrent/routes.rs @@ -0,0 +1,15 @@ +//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context. +//! +//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag). +use std::sync::Arc; + +use axum::routing::post; +use axum::Router; + +use super::handlers::upload_torrent_handler; +use crate::common::AppData; + +/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context. +pub fn router_for_single_resources(app_data: Arc) -> Router { + Router::new().route("/upload", post(upload_torrent_handler).with_state(app_data)) +} diff --git a/src/web/api/v1/contexts/user/handlers.rs b/src/web/api/v1/contexts/user/handlers.rs index 543b87f9..0ab4e995 100644 --- a/src/web/api/v1/contexts/user/handlers.rs +++ b/src/web/api/v1/contexts/user/handlers.rs @@ -11,7 +11,7 @@ use super::responses::{self, NewUser, TokenResponse}; use crate::common::AppData; use crate::errors::ServiceError; use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::OkResponse; +use crate::web::api::v1::responses::OkResponseData; // Registration @@ -25,7 +25,7 @@ pub async fn registration_handler( State(app_data): State>, Host(host_from_header): Host, extract::Json(registration_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { let api_base_url = app_data .cfg .get_api_base_url() @@ -68,7 +68,7 @@ pub async fn email_verification_handler(State(app_data): State>, Pa pub async fn login_handler( State(app_data): State>, extract::Json(login_form): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { match app_data .authentication_service .login(&login_form.login, &login_form.password) @@ -91,9 +91,9 @@ pub async fn login_handler( pub async fn verify_token_handler( State(app_data): State>, extract::Json(token): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { match app_data.json_web_token.verify(&token.token).await { - Ok(_) => Ok(axum::Json(OkResponse { + Ok(_) => Ok(axum::Json(OkResponseData { data: "Token is valid.".to_string(), })), Err(error) => Err(error), @@ -115,7 +115,7 @@ pub struct UsernameParam(pub String); pub async fn renew_token_handler( State(app_data): State>, extract::Json(token): extract::Json, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { match app_data.authentication_service.renew_token(&token.token).await { Ok((token, user_compact)) => Ok(responses::renewed_token(token, user_compact)), Err(error) => Err(error), @@ -135,13 +135,13 @@ pub async fn ban_handler( State(app_data): State>, Path(to_be_banned_username): Path, Extract(maybe_bearer_token): Extract, -) -> Result>, ServiceError> { +) -> Result>, ServiceError> { // todo: add reason and `date_expiry` parameters to request let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; match app_data.ban_service.ban_user(&to_be_banned_username.0, &user_id).await { - Ok(_) => Ok(axum::Json(OkResponse { + Ok(_) => Ok(axum::Json(OkResponseData { data: format!("Banned user: {}", to_be_banned_username.0), })), Err(error) => Err(error), diff --git a/src/web/api/v1/contexts/user/responses.rs b/src/web/api/v1/contexts/user/responses.rs index 731db068..17a06bdf 100644 --- a/src/web/api/v1/contexts/user/responses.rs +++ b/src/web/api/v1/contexts/user/responses.rs @@ -2,7 +2,7 @@ use axum::Json; use serde::{Deserialize, Serialize}; use crate::models::user::{UserCompact, UserId}; -use crate::web::api::v1::responses::OkResponse; +use crate::web::api::v1::responses::OkResponseData; // Registration @@ -12,8 +12,8 @@ pub struct NewUser { } /// Response after successfully creating a new user. -pub fn added_user(user_id: i64) -> Json> { - Json(OkResponse { +pub fn added_user(user_id: i64) -> Json> { + Json(OkResponseData { data: NewUser { user_id }, }) } @@ -28,8 +28,8 @@ pub struct TokenResponse { } /// Response after successfully logging in a user. -pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json> { - Json(OkResponse { +pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json> { + Json(OkResponseData { data: TokenResponse { token, username: user_compact.username, @@ -39,8 +39,8 @@ pub fn logged_in_user(token: String, user_compact: UserCompact) -> Json Json> { - Json(OkResponse { +pub fn renewed_token(token: String, user_compact: UserCompact) -> Json> { + Json(OkResponseData { data: TokenResponse { token, username: user_compact.username, diff --git a/src/web/api/v1/responses.rs b/src/web/api/v1/responses.rs index de9701c0..3adb7442 100644 --- a/src/web/api/v1/responses.rs +++ b/src/web/api/v1/responses.rs @@ -1,25 +1,49 @@ //! Generic responses for the API. use axum::response::{IntoResponse, Response}; +use hyper::{header, StatusCode}; use serde::{Deserialize, Serialize}; +use serde_json::json; use crate::databases::database; use crate::errors::{http_status_code_for_service_error, map_database_error_to_service_error, ServiceError}; #[derive(Serialize, Deserialize, Debug)] -pub struct OkResponse { +pub struct OkResponseData { pub data: T, } +#[derive(Serialize, Deserialize, Debug)] +pub struct ErrorResponseData { + pub error: String, +} + +impl IntoResponse for ServiceError { + fn into_response(self) -> Response { + json_error_response( + http_status_code_for_service_error(&self), + &ErrorResponseData { error: self.to_string() }, + ) + } +} + impl IntoResponse for database::Error { fn into_response(self) -> Response { let service_error = map_database_error_to_service_error(&self); - (http_status_code_for_service_error(&service_error), service_error.to_string()).into_response() + json_error_response( + http_status_code_for_service_error(&service_error), + &ErrorResponseData { + error: service_error.to_string(), + }, + ) } } -impl IntoResponse for ServiceError { - fn into_response(self) -> Response { - (http_status_code_for_service_error(&self), self.to_string()).into_response() - } +fn json_error_response(status_code: StatusCode, error_response_data: &ErrorResponseData) -> Response { + ( + status_code, + [(header::CONTENT_TYPE, "application/json")], + json!(error_response_data).to_string(), + ) + .into_response() } diff --git a/src/web/api/v1/routes.rs b/src/web/api/v1/routes.rs index 008722c9..10c27d20 100644 --- a/src/web/api/v1/routes.rs +++ b/src/web/api/v1/routes.rs @@ -6,7 +6,7 @@ use axum::Router; use super::contexts::about::handlers::about_page_handler; //use tower_http::cors::CorsLayer; -use super::contexts::{about, settings, tag}; +use super::contexts::{about, settings, tag, torrent}; use super::contexts::{category, user}; use crate::common::AppData; @@ -23,7 +23,8 @@ pub fn router(app_data: Arc) -> Router { .nest("/category", category::routes::router(app_data.clone())) .nest("/tag", tag::routes::router_for_single_resources(app_data.clone())) .nest("/tags", tag::routes::router_for_multiple_resources(app_data.clone())) - .nest("/settings", settings::routes::router(app_data.clone())); + .nest("/settings", settings::routes::router(app_data.clone())) + .nest("/torrent", torrent::routes::router_for_single_resources(app_data.clone())); Router::new() .route("/", get(about_page_handler).with_state(app_data)) diff --git a/tests/common/asserts.rs b/tests/common/asserts.rs index 60df0956..cd326d5f 100644 --- a/tests/common/asserts.rs +++ b/tests/common/asserts.rs @@ -1,5 +1,7 @@ // Text responses +use torrust_index_backend::web::api::v1::responses::ErrorResponseData; + use super::responses::TextResponse; pub fn assert_response_title(response: &TextResponse, title: &str) { @@ -27,9 +29,22 @@ pub fn _assert_text_bad_request(response: &TextResponse) { // JSON responses -pub fn assert_json_ok(response: &TextResponse) { +pub fn assert_json_ok_response(response: &TextResponse) { + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } assert_eq!(response.status, 200); +} + +pub fn assert_json_error_response(response: &TextResponse, error: &str) { + assert_eq!(response.body, "{\"error\":\"This torrent title has already been used.\"}"); + + let error_response_data: ErrorResponseData = serde_json::from_str(&response.body) + .unwrap_or_else(|_| panic!("response {:#?} should be a ErrorResponseData", response.body)); + + assert_eq!(error_response_data.error, error); if let Some(content_type) = &response.content_type { assert_eq!(content_type, "application/json"); } + assert_eq!(response.status, 400); } diff --git a/tests/common/contexts/category/asserts.rs b/tests/common/contexts/category/asserts.rs index ae47bdca..2568531d 100644 --- a/tests/common/contexts/category/asserts.rs +++ b/tests/common/contexts/category/asserts.rs @@ -1,4 +1,4 @@ -use crate::common::asserts::assert_json_ok; +use crate::common::asserts::assert_json_ok_response; use crate::common::contexts::category::responses::{AddedCategoryResponse, DeletedCategoryResponse}; use crate::common::responses::TextResponse; @@ -8,7 +8,7 @@ pub fn assert_added_category_response(response: &TextResponse, category_name: &s assert_eq!(added_category_response.data, category_name); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_deleted_category_response(response: &TextResponse, category_name: &str) { @@ -17,5 +17,5 @@ pub fn assert_deleted_category_response(response: &TextResponse, category_name: assert_eq!(deleted_category_response.data, category_name); - assert_json_ok(response); + assert_json_ok_response(response); } diff --git a/tests/common/contexts/tag/asserts.rs b/tests/common/contexts/tag/asserts.rs index cd796e91..3d1347df 100644 --- a/tests/common/contexts/tag/asserts.rs +++ b/tests/common/contexts/tag/asserts.rs @@ -1,6 +1,6 @@ use torrust_index_backend::models::torrent_tag::TagId; -use crate::common::asserts::assert_json_ok; +use crate::common::asserts::assert_json_ok_response; use crate::common::contexts::tag::responses::{AddedTagResponse, DeletedTagResponse}; use crate::common::responses::TextResponse; @@ -10,7 +10,7 @@ pub fn assert_added_tag_response(response: &TextResponse, tag_name: &str) { assert_eq!(added_tag_response.data, tag_name); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_deleted_tag_response(response: &TextResponse, tag_id: TagId) { @@ -19,5 +19,5 @@ pub fn assert_deleted_tag_response(response: &TextResponse, tag_id: TagId) { assert_eq!(deleted_tag_response.data, tag_id); - assert_json_ok(response); + assert_json_ok_response(response); } diff --git a/tests/common/contexts/user/asserts.rs b/tests/common/contexts/user/asserts.rs index bcf92f5f..dfa5352b 100644 --- a/tests/common/contexts/user/asserts.rs +++ b/tests/common/contexts/user/asserts.rs @@ -1,6 +1,6 @@ use super::forms::RegistrationForm; use super::responses::LoggedInUserData; -use crate::common::asserts::assert_json_ok; +use crate::common::asserts::assert_json_ok_response; use crate::common::contexts::user::responses::{ AddedUserResponse, BannedUserResponse, SuccessfulLoginResponse, TokenRenewalData, TokenRenewalResponse, TokenVerifiedResponse, }; @@ -9,7 +9,7 @@ use crate::common::responses::TextResponse; pub fn assert_added_user_response(response: &TextResponse) { let _added_user_response: AddedUserResponse = serde_json::from_str(&response.body) .unwrap_or_else(|_| panic!("response {:#?} should be a AddedUserResponse", response.body)); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_successful_login_response(response: &TextResponse, registered_user: &RegistrationForm) { @@ -20,7 +20,7 @@ pub fn assert_successful_login_response(response: &TextResponse, registered_user assert_eq!(logged_in_user.username, registered_user.username); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_token_verified_response(response: &TextResponse) { @@ -29,7 +29,7 @@ pub fn assert_token_verified_response(response: &TextResponse) { assert_eq!(token_verified_response.data, "Token is valid."); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_token_renewal_response(response: &TextResponse, logged_in_user: &LoggedInUserData) { @@ -45,7 +45,7 @@ pub fn assert_token_renewal_response(response: &TextResponse, logged_in_user: &L } ); - assert_json_ok(response); + assert_json_ok_response(response); } pub fn assert_banned_user_response(response: &TextResponse, registered_user: &RegistrationForm) { @@ -57,5 +57,5 @@ pub fn assert_banned_user_response(response: &TextResponse, registered_user: &Re format!("Banned user: {}", registered_user.username) ); - assert_json_ok(response); + assert_json_ok_response(response); } diff --git a/tests/e2e/contexts/category/contract.rs b/tests/e2e/contexts/category/contract.rs index d1970063..de6b57e6 100644 --- a/tests/e2e/contexts/category/contract.rs +++ b/tests/e2e/contexts/category/contract.rs @@ -1,7 +1,7 @@ //! API contract for `category` context. use torrust_index_backend::web::api; -use crate::common::asserts::assert_json_ok; +use crate::common::asserts::assert_json_ok_response; use crate::common::client::Client; use crate::common::contexts::category::fixtures::random_category_name; use crate::common::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; @@ -18,7 +18,7 @@ async fn it_should_return_an_empty_category_list_when_there_are_no_categories() let response = client.get_categories().await; - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] @@ -213,7 +213,7 @@ mod with_axum_implementation { use torrust_index_backend::web::api; - use crate::common::asserts::assert_json_ok; + 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; @@ -233,7 +233,7 @@ mod with_axum_implementation { let response = client.get_categories().await; - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] diff --git a/tests/e2e/contexts/settings/contract.rs b/tests/e2e/contexts/settings/contract.rs index 174ae6c1..97e6082e 100644 --- a/tests/e2e/contexts/settings/contract.rs +++ b/tests/e2e/contexts/settings/contract.rs @@ -100,7 +100,7 @@ mod with_axum_implementation { use torrust_index_backend::web::api; - use crate::common::asserts::assert_json_ok; + 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::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; @@ -134,7 +134,7 @@ mod with_axum_implementation { } ); - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] @@ -155,7 +155,7 @@ mod with_axum_implementation { assert_eq!(res.data, "Torrust"); - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] @@ -177,7 +177,7 @@ mod with_axum_implementation { assert_eq!(res.data, env.server_settings().unwrap()); - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] @@ -209,6 +209,6 @@ mod with_axum_implementation { assert_eq!(res.data, new_settings); - assert_json_ok(&response); + assert_json_ok_response(&response); } } diff --git a/tests/e2e/contexts/tag/contract.rs b/tests/e2e/contexts/tag/contract.rs index 53b4356d..a64d7359 100644 --- a/tests/e2e/contexts/tag/contract.rs +++ b/tests/e2e/contexts/tag/contract.rs @@ -1,7 +1,7 @@ //! API contract for `tag` context. use torrust_index_backend::web::api; -use crate::common::asserts::assert_json_ok; +use crate::common::asserts::assert_json_ok_response; use crate::common::client::Client; use crate::common::contexts::tag::fixtures::random_tag_name; use crate::common::contexts::tag::forms::{AddTagForm, DeleteTagForm}; @@ -18,7 +18,7 @@ async fn it_should_return_an_empty_tag_list_when_there_are_no_tags() { let response = client.get_tags().await; - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] @@ -187,7 +187,7 @@ mod with_axum_implementation { use torrust_index_backend::web::api; - use crate::common::asserts::assert_json_ok; + 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; @@ -212,7 +212,7 @@ mod with_axum_implementation { let response = client.get_tags().await; - assert_json_ok(&response); + assert_json_ok_response(&response); } #[tokio::test] diff --git a/tests/e2e/contexts/torrent/contract.rs b/tests/e2e/contexts/torrent/contract.rs index 955f5154..d6fb197e 100644 --- a/tests/e2e/contexts/torrent/contract.rs +++ b/tests/e2e/contexts/torrent/contract.rs @@ -621,3 +621,739 @@ mod for_authenticated_users { } } } + +mod with_axum_implementation { + + mod for_guests { + /* + + use std::env; + + 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::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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 std::env; + + //use torrust_index_backend::utils::parse_torrent::decode_torrent; + use torrust_index_backend::web::api; + + //use crate::e2e::contexts::torrent::asserts::{build_announce_url, get_user_tracker_key}; + 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::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; + //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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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 std::env; + + use torrust_index_backend::web::api; + + use crate::common::client::Client; + use crate::common::contexts::torrent::forms::UpdateTorrentFrom; + use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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 std::env; + + 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::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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 std::env; + + 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::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + 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()); + } + + */ + } + } +}