diff --git a/src/app.rs b/src/app.rs index afdb8de9..b36098a9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,7 @@ use crate::config::Configuration; use crate::databases::database; use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; +use crate::services::tag::{self, DbTagRepository}; use crate::services::torrent::{ DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, @@ -58,6 +59,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati // Repositories let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); + let tag_repository = Arc::new(DbTagRepository::new(database.clone())); let user_repository = Arc::new(DbUserRepository::new(database.clone())); let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone())); let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone())); @@ -76,6 +78,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati let mailer_service = Arc::new(mailer::Service::new(configuration.clone()).await); let image_cache_service: Arc = Arc::new(ImageCacheService::new(configuration.clone()).await); let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); + let tag_service = Arc::new(tag::Service::new(tag_repository.clone(), user_repository.clone())); let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone())); let settings_service = Arc::new(settings::Service::new(configuration.clone(), user_repository.clone())); let torrent_index = Arc::new(torrent::Index::new( @@ -123,6 +126,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati mailer_service, image_cache_service, category_repository, + tag_repository, user_repository, user_authentication_repository, user_profile_repository, @@ -134,6 +138,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati torrent_listing_generator, banned_user_list, category_service, + tag_service, proxy_service, settings_service, torrent_index, diff --git a/src/common.rs b/src/common.rs index db0361e3..94e28828 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,6 +6,7 @@ use crate::config::Configuration; use crate::databases::database::Database; use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; +use crate::services::tag::{self, DbTagRepository}; use crate::services::torrent::{ DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, @@ -30,6 +31,7 @@ pub struct AppData { pub image_cache_manager: Arc, // Repositories pub category_repository: Arc, + pub tag_repository: Arc, pub user_repository: Arc, pub user_authentication_repository: Arc, pub user_profile_repository: Arc, @@ -42,6 +44,7 @@ pub struct AppData { pub banned_user_list: Arc, // Services pub category_service: Arc, + pub tag_service: Arc, pub proxy_service: Arc, pub settings_service: Arc, pub torrent_service: Arc, @@ -63,6 +66,7 @@ impl AppData { image_cache_manager: Arc, // Repositories category_repository: Arc, + tag_repository: Arc, user_repository: Arc, user_authentication_repository: Arc, user_profile_repository: Arc, @@ -75,6 +79,7 @@ impl AppData { banned_user_list: Arc, // Services category_service: Arc, + tag_service: Arc, proxy_service: Arc, settings_service: Arc, torrent_service: Arc, @@ -93,6 +98,7 @@ impl AppData { image_cache_manager, // Repositories category_repository, + tag_repository, user_repository, user_authentication_repository, user_profile_repository, @@ -105,6 +111,7 @@ impl AppData { banned_user_list, // Services category_service, + tag_service, proxy_service, settings_service, torrent_service, diff --git a/src/databases/database.rs b/src/databases/database.rs index a829424f..ea0c41a0 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -243,7 +243,7 @@ pub trait Database: Sync + Send { async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>; /// Add multiple tags to a torrent at once. - async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), Error>; + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &[TagId]) -> Result<(), Error>; /// Remove a tag from torrent. async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index fdc565d0..5ae75050 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -732,7 +732,7 @@ impl Database for Mysql { .map_err(|_| database::Error::Error) } - async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), database::Error> { + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &[TagId]) -> Result<(), database::Error> { let mut transaction = self .pool .begin() @@ -778,7 +778,7 @@ impl Database for Mysql { .bind(name) .fetch_one(&self.pool) .await - .map_err(|err| database::Error::TagNotFound) + .map_err(|_| database::Error::TagNotFound) } async fn get_tags(&self) -> Result, database::Error> { diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 39156984..81ad1763 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -722,7 +722,7 @@ impl Database for Sqlite { .map_err(|_| database::Error::Error) } - async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), database::Error> { + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &[TagId]) -> Result<(), database::Error> { let mut transaction = self .pool .begin() @@ -768,7 +768,7 @@ impl Database for Sqlite { .bind(name) .fetch_one(&self.pool) .await - .map_err(|err| database::Error::TagNotFound) + .map_err(|_| database::Error::TagNotFound) } async fn get_tags(&self) -> Result, database::Error> { diff --git a/src/errors.rs b/src/errors.rs index c625d37b..668bf3ab 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -124,14 +124,17 @@ pub enum ServiceError { FailedToSendVerificationEmail, #[display(fmt = "Category already exists.")] - CategoryExists, + CategoryAlreadyExists, #[display(fmt = "Tag already exists.")] - TagExists, + TagAlreadyExists, #[display(fmt = "Category not found.")] CategoryNotFound, + #[display(fmt = "Tag not found.")] + TagNotFound, + #[display(fmt = "Database error.")] DatabaseError, } @@ -176,14 +179,15 @@ impl ResponseError for ServiceError { ServiceError::InfoHashAlreadyExists => StatusCode::BAD_REQUEST, ServiceError::TorrentTitleAlreadyExists => StatusCode::BAD_REQUEST, ServiceError::TrackerOffline => StatusCode::INTERNAL_SERVER_ERROR, - ServiceError::CategoryExists => StatusCode::BAD_REQUEST, - ServiceError::TagExists => StatusCode::BAD_REQUEST, + ServiceError::CategoryAlreadyExists => StatusCode::BAD_REQUEST, + ServiceError::TagAlreadyExists => StatusCode::BAD_REQUEST, ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::EmailMissing => StatusCode::NOT_FOUND, ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::CategoryNotFound => StatusCode::NOT_FOUND, + ServiceError::TagNotFound => StatusCode::NOT_FOUND, } } @@ -223,9 +227,9 @@ impl From for ServiceError { database::Error::UsernameTaken => ServiceError::UsernameTaken, database::Error::EmailTaken => ServiceError::EmailTaken, database::Error::UserNotFound => ServiceError::UserNotFound, - database::Error::CategoryAlreadyExists => ServiceError::CategoryExists, + database::Error::CategoryAlreadyExists => ServiceError::CategoryAlreadyExists, database::Error::CategoryNotFound => ServiceError::InvalidCategory, - database::Error::TagAlreadyExists => ServiceError::TagExists, + database::Error::TagAlreadyExists => ServiceError::TagAlreadyExists, database::Error::TagNotFound => ServiceError::InvalidTag, database::Error::TorrentNotFound => ServiceError::TorrentNotFound, database::Error::TorrentAlreadyExists => ServiceError::InfoHashAlreadyExists, diff --git a/src/routes/tag.rs b/src/routes/tag.rs index f317d5ca..b7de7b16 100644 --- a/src/routes/tag.rs +++ b/src/routes/tag.rs @@ -2,7 +2,7 @@ use actix_web::{web, HttpRequest, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use crate::common::WebAppData; -use crate::errors::{ServiceError, ServiceResult}; +use crate::errors::ServiceResult; use crate::models::response::OkResponse; use crate::models::torrent_tag::TagId; use crate::routes::API_VERSION; @@ -24,7 +24,7 @@ pub fn init(cfg: &mut web::ServiceConfig) { /// /// This function will return an error if unable to get tags from database. pub async fn get_all(app_data: WebAppData) -> ServiceResult { - let tags = app_data.torrent_tag_repository.get_tags().await?; + let tags = app_data.tag_repository.get_all().await?; Ok(HttpResponse::Ok().json(OkResponse { data: tags })) } @@ -46,14 +46,7 @@ pub struct Create { pub async fn create(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { let user_id = app_data.auth.get_user_id_from_request(&req).await?; - let user = app_data.user_repository.get_compact(&user_id).await?; - - // check if user is administrator - if !user.administrator { - return Err(ServiceError::Unauthorized); - } - - app_data.torrent_tag_repository.add_tag(&payload.name).await?; + app_data.tag_service.add_tag(&payload.name, &user_id).await?; Ok(HttpResponse::Ok().json(OkResponse { data: payload.name.to_string(), @@ -77,14 +70,7 @@ pub struct Delete { pub async fn delete(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { let user_id = app_data.auth.get_user_id_from_request(&req).await?; - let user = app_data.user_repository.get_compact(&user_id).await?; - - // check if user is administrator - if !user.administrator { - return Err(ServiceError::Unauthorized); - } - - app_data.torrent_tag_repository.delete_tag(&payload.tag_id).await?; + app_data.tag_service.delete_tag(&payload.tag_id, &user_id).await?; Ok(HttpResponse::Ok().json(OkResponse { data: payload.tag_id })) } diff --git a/src/services/category.rs b/src/services/category.rs index 2ff56d38..dbce9023 100644 --- a/src/services/category.rs +++ b/src/services/category.rs @@ -41,13 +41,13 @@ impl Service { match self.category_repository.add(category_name).await { Ok(id) => Ok(id), Err(e) => match e { - DatabaseError::CategoryAlreadyExists => Err(ServiceError::CategoryExists), + DatabaseError::CategoryAlreadyExists => Err(ServiceError::CategoryAlreadyExists), _ => Err(ServiceError::DatabaseError), }, } } - /// Deletes a new category. + /// Deletes a category. /// /// # Errors /// diff --git a/src/services/mod.rs b/src/services/mod.rs index 79693c9c..a8886af7 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -4,5 +4,6 @@ pub mod authentication; pub mod category; pub mod proxy; pub mod settings; +pub mod tag; pub mod torrent; pub mod user; diff --git a/src/services/tag.rs b/src/services/tag.rs new file mode 100644 index 00000000..b766a14b --- /dev/null +++ b/src/services/tag.rs @@ -0,0 +1,113 @@ +//! Tag service. +use std::sync::Arc; + +use super::user::DbUserRepository; +use crate::databases::database::{Database, Error as DatabaseError, Error}; +use crate::errors::ServiceError; +use crate::models::torrent_tag::{TagId, TorrentTag}; +use crate::models::user::UserId; + +pub struct Service { + tag_repository: Arc, + user_repository: Arc, +} + +impl Service { + #[must_use] + pub fn new(tag_repository: Arc, user_repository: Arc) -> Service { + Service { + tag_repository, + user_repository, + } + } + + /// Adds a new tag. + /// + /// # Errors + /// + /// It returns an error if: + /// + /// * The user does not have the required permissions. + /// * There is a database error. + pub async fn add_tag(&self, tag_name: &str, user_id: &UserId) -> Result<(), ServiceError> { + let user = self.user_repository.get_compact(user_id).await?; + + // Check if user is administrator + // todo: extract authorization service + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + match self.tag_repository.add(tag_name).await { + Ok(_) => Ok(()), + Err(e) => match e { + DatabaseError::TagAlreadyExists => Err(ServiceError::TagAlreadyExists), + _ => Err(ServiceError::DatabaseError), + }, + } + } + + /// Deletes a tag. + /// + /// # Errors + /// + /// It returns an error if: + /// + /// * The user does not have the required permissions. + /// * There is a database error. + pub async fn delete_tag(&self, tag_id: &TagId, user_id: &UserId) -> Result<(), ServiceError> { + let user = self.user_repository.get_compact(user_id).await?; + + // Check if user is administrator + // todo: extract authorization service + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + match self.tag_repository.delete(tag_id).await { + Ok(_) => Ok(()), + Err(e) => match e { + DatabaseError::TagNotFound => Err(ServiceError::TagNotFound), + _ => Err(ServiceError::DatabaseError), + }, + } + } +} + +pub struct DbTagRepository { + database: Arc>, +} + +impl DbTagRepository { + #[must_use] + pub fn new(database: Arc>) -> Self { + Self { database } + } + + /// It adds a new tag and returns the newly created tag. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn add(&self, tag_name: &str) -> Result<(), Error> { + self.database.add_tag(tag_name).await + } + + /// It returns all the tags. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn get_all(&self) -> Result, Error> { + self.database.get_tags().await + } + + /// It removes a tag and returns it. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn delete(&self, tag_id: &TagId) -> Result<(), Error> { + self.database.delete_tag(*tag_id).await + } +} diff --git a/src/services/torrent.rs b/src/services/torrent.rs index ada162dd..d5bdaa50 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -555,15 +555,6 @@ impl DbTorrentTagRepository { Self { database } } - /// It adds a new tag and returns the newly created tag. - /// - /// # Errors - /// - /// It returns an error if there is a database error. - pub async fn add_tag(&self, tag_name: &str) -> Result<(), Error> { - self.database.add_tag(tag_name).await - } - /// It adds a new torrent tag link. /// /// # Errors @@ -578,19 +569,10 @@ impl DbTorrentTagRepository { /// # Errors /// /// It returns an error if there is a database error. - pub async fn link_torrent_to_tags(&self, torrent_id: &TorrentId, tag_ids: &Vec) -> Result<(), Error> { + pub async fn link_torrent_to_tags(&self, torrent_id: &TorrentId, tag_ids: &[TagId]) -> Result<(), Error> { self.database.add_torrent_tag_links(*torrent_id, tag_ids).await } - /// It returns all the tags. - /// - /// # Errors - /// - /// It returns an error if there is a database error. - pub async fn get_tags(&self) -> Result, Error> { - self.database.get_tags().await - } - /// It returns all the tags linked to a certain torrent ID. /// /// # Errors @@ -600,15 +582,6 @@ impl DbTorrentTagRepository { self.database.get_tags_for_torrent_id(*torrent_id).await } - /// It removes a tag and returns it. - /// - /// # Errors - /// - /// It returns an error if there is a database error. - pub async fn delete_tag(&self, tag_id: &TagId) -> Result<(), Error> { - self.database.delete_tag(*tag_id).await - } - /// It removes a torrent tag link. /// /// # Errors diff --git a/tests/common/client.rs b/tests/common/client.rs index 67bae6bc..25db78f5 100644 --- a/tests/common/client.rs +++ b/tests/common/client.rs @@ -4,6 +4,7 @@ use serde::Serialize; use super::connection_info::ConnectionInfo; use super::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; use super::contexts::settings::form::UpdateSettings; +use super::contexts::tag::forms::{AddTagForm, DeleteTagForm}; use super::contexts::torrent::forms::UpdateTorrentFrom; use super::contexts::torrent::requests::InfoHash; use super::contexts::user::forms::{LoginForm, RegistrationForm, TokenRenewalForm, TokenVerificationForm, Username}; @@ -67,6 +68,22 @@ impl Client { self.http_client.delete_with_body("/category", &delete_category_form).await } + // Context: tag + + pub async fn get_tags(&self) -> TextResponse { + // code-review: some endpoint are using plural + // (for instance, `get_categories`) and some singular. + self.http_client.get("/tags", Query::empty()).await + } + + pub async fn add_tag(&self, add_tag_form: AddTagForm) -> TextResponse { + self.http_client.post("/tag", &add_tag_form).await + } + + pub async fn delete_tag(&self, delete_tag_form: DeleteTagForm) -> TextResponse { + self.http_client.delete_with_body("/tag", &delete_tag_form).await + } + // Context: root pub async fn root(&self) -> TextResponse { diff --git a/tests/common/contexts/mod.rs b/tests/common/contexts/mod.rs index a6f14141..fa791e5f 100644 --- a/tests/common/contexts/mod.rs +++ b/tests/common/contexts/mod.rs @@ -2,5 +2,6 @@ pub mod about; pub mod category; pub mod root; pub mod settings; +pub mod tag; pub mod torrent; pub mod user; diff --git a/tests/common/contexts/tag/fixtures.rs b/tests/common/contexts/tag/fixtures.rs new file mode 100644 index 00000000..39ac3081 --- /dev/null +++ b/tests/common/contexts/tag/fixtures.rs @@ -0,0 +1,10 @@ +use rand::Rng; + +pub fn random_tag_name() -> String { + format!("category name {}", random_id()) +} + +fn random_id() -> u64 { + let mut rng = rand::thread_rng(); + rng.gen_range(0..1_000_000) +} diff --git a/tests/common/contexts/tag/forms.rs b/tests/common/contexts/tag/forms.rs new file mode 100644 index 00000000..26d1395d --- /dev/null +++ b/tests/common/contexts/tag/forms.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct AddTagForm { + pub name: String, +} + +#[derive(Serialize)] +pub struct DeleteTagForm { + pub tag_id: i64, +} diff --git a/tests/common/contexts/tag/mod.rs b/tests/common/contexts/tag/mod.rs new file mode 100644 index 00000000..6f27f51d --- /dev/null +++ b/tests/common/contexts/tag/mod.rs @@ -0,0 +1,3 @@ +pub mod fixtures; +pub mod forms; +pub mod responses; diff --git a/tests/common/contexts/tag/responses.rs b/tests/common/contexts/tag/responses.rs new file mode 100644 index 00000000..5029257e --- /dev/null +++ b/tests/common/contexts/tag/responses.rs @@ -0,0 +1,28 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct AddedTagResponse { + pub data: String, +} + +#[derive(Deserialize)] +pub struct DeletedTagResponse { + pub data: i64, // tag_id +} + +#[derive(Deserialize, Debug)] +pub struct ListResponse { + pub data: Vec, +} + +impl ListResponse { + pub fn find_tag_id(&self, tag_name: &str) -> i64 { + self.data.iter().find(|tag| tag.name == tag_name).unwrap().tag_id + } +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct ListItem { + pub tag_id: i64, + pub name: String, +} diff --git a/tests/e2e/contexts/mod.rs b/tests/e2e/contexts/mod.rs index a6f14141..fa791e5f 100644 --- a/tests/e2e/contexts/mod.rs +++ b/tests/e2e/contexts/mod.rs @@ -2,5 +2,6 @@ pub mod about; pub mod category; pub mod root; pub mod settings; +pub mod tag; pub mod torrent; pub mod user; diff --git a/tests/e2e/contexts/tag/contract.rs b/tests/e2e/contexts/tag/contract.rs new file mode 100644 index 00000000..e209758a --- /dev/null +++ b/tests/e2e/contexts/tag/contract.rs @@ -0,0 +1,183 @@ +//! API contract for `tag` context. +use torrust_index_backend::web::api; + +use crate::common::asserts::assert_json_ok; +use crate::common::client::Client; +use crate::common::contexts::tag::fixtures::random_tag_name; +use crate::common::contexts::tag::forms::{AddTagForm, DeleteTagForm}; +use crate::common::contexts::tag::responses::{AddedTagResponse, DeletedTagResponse, ListResponse}; +use crate::e2e::contexts::tag::steps::{add_random_tag, add_tag}; +use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user}; +use crate::e2e::environment::TestEnv; + +#[tokio::test] +async fn it_should_return_an_empty_tag_list_when_there_are_no_tags() { + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client.get_tags().await; + + assert_json_ok(&response); +} + +#[tokio::test] +async fn it_should_return_a_tag_list() { + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + // Add a tag + let tag_name = random_tag_name(); + let response = add_tag(&tag_name, &env).await; + assert_eq!(response.status, 200); + + let response = client.get_tags().await; + + let res: ListResponse = serde_json::from_str(&response.body).unwrap(); + + // There should be at least the tag we added. + // Since this is an E2E test that could be executed in a shred env, + // there might be more tags. + assert!(!res.data.is_empty()); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_tag_to_unauthenticated_users() { + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let response = client + .add_tag(AddTagForm { + name: "TAG NAME".to_string(), + }) + .await; + + assert_eq!(response.status, 401); +} + +#[tokio::test] +async fn it_should_not_allow_adding_a_new_tag_to_non_admins() { + let mut env = TestEnv::new(); + env.start(api::Implementation::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); + + let response = client + .add_tag(AddTagForm { + name: "TAG NAME".to_string(), + }) + .await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_allow_admins_to_add_new_tags() { + let mut env = TestEnv::new(); + env.start(api::Implementation::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); + + let tag_name = random_tag_name(); + + let response = client + .add_tag(AddTagForm { + name: tag_name.to_string(), + }) + .await; + + let res: AddedTagResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, tag_name); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_allow_adding_duplicated_tags() { + // code-review: is this an intended behavior? + + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + + // Add a tag + let random_tag_name = random_tag_name(); + let response = add_tag(&random_tag_name, &env).await; + assert_eq!(response.status, 200); + + // Try to add the same tag again + let response = add_tag(&random_tag_name, &env).await; + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_allow_adding_a_tag_with_an_empty_name() { + // code-review: is this an intended behavior? + + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + + let empty_tag_name = String::new(); + let response = add_tag(&empty_tag_name, &env).await; + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_allow_admins_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Implementation::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); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + let res: DeletedTagResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, tag_id); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +async fn it_should_not_allow_non_admins_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + + let logged_in_non_admin = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +async fn it_should_not_allow_guests_to_delete_tags() { + let mut env = TestEnv::new(); + env.start(api::Implementation::ActixWeb).await; + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let (tag_id, _tag_name) = add_random_tag(&env).await; + + let response = client.delete_tag(DeleteTagForm { tag_id }).await; + + assert_eq!(response.status, 401); +} diff --git a/tests/e2e/contexts/tag/mod.rs b/tests/e2e/contexts/tag/mod.rs new file mode 100644 index 00000000..2001efb8 --- /dev/null +++ b/tests/e2e/contexts/tag/mod.rs @@ -0,0 +1,2 @@ +pub mod contract; +pub mod steps; diff --git a/tests/e2e/contexts/tag/steps.rs b/tests/e2e/contexts/tag/steps.rs new file mode 100644 index 00000000..32bb767c --- /dev/null +++ b/tests/e2e/contexts/tag/steps.rs @@ -0,0 +1,39 @@ +use crate::common::client::Client; +use crate::common::contexts::tag::fixtures::random_tag_name; +use crate::common::contexts::tag::forms::AddTagForm; +use crate::common::contexts::tag::responses::ListResponse; +use crate::common::responses::TextResponse; +use crate::e2e::contexts::user::steps::new_logged_in_admin; +use crate::e2e::environment::TestEnv; + +pub async fn add_random_tag(env: &TestEnv) -> (i64, String) { + let tag_name = random_tag_name(); + + add_tag(&tag_name, env).await; + + let tag_id = get_tag_id(&tag_name, env).await; + + (tag_id, tag_name) +} + +pub async fn add_tag(tag_name: &str, env: &TestEnv) -> TextResponse { + let logged_in_admin = new_logged_in_admin(env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + client + .add_tag(AddTagForm { + name: tag_name.to_string(), + }) + .await +} + +pub async fn get_tag_id(tag_name: &str, env: &TestEnv) -> i64 { + let logged_in_admin = new_logged_in_admin(env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let response = client.get_tags().await; + + let res: ListResponse = serde_json::from_str(&response.body).unwrap(); + + res.find_tag_id(tag_name) +}