diff --git a/src/web/api/v1/contexts/category/forms.rs b/src/web/api/v1/contexts/category/forms.rs index ecddcadb..1ad7767a 100644 --- a/src/web/api/v1/contexts/category/forms.rs +++ b/src/web/api/v1/contexts/category/forms.rs @@ -1,7 +1,9 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] -pub struct CategoryForm { +pub struct AddCategoryForm { pub name: String, pub icon: Option, } + +pub type DeleteCategoryForm = AddCategoryForm; diff --git a/src/web/api/v1/contexts/category/handlers.rs b/src/web/api/v1/contexts/category/handlers.rs index 12e29532..3d09008a 100644 --- a/src/web/api/v1/contexts/category/handlers.rs +++ b/src/web/api/v1/contexts/category/handlers.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use axum::extract::{self, State}; use axum::response::Json; -use super::forms::CategoryForm; -use super::responses::added_category; +use super::forms::{AddCategoryForm, DeleteCategoryForm}; +use super::responses::{added_category, deleted_category}; use crate::common::AppData; use crate::databases::database::{self, Category}; use crate::errors::ServiceError; @@ -48,7 +48,7 @@ pub async fn get_all_handler( pub async fn add_handler( State(app_data): State>, Extract(maybe_bearer_token): Extract, - extract::Json(category_form): extract::Json, + extract::Json(category_form): extract::Json, ) -> Result>, ServiceError> { let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; @@ -57,3 +57,29 @@ pub async fn add_handler( Err(error) => Err(error), } } + +/// It deletes a category. +/// +/// # Errors +/// +/// It returns an error if: +/// +/// - The user does not have permissions to delete category. +/// - There is a database error. +#[allow(clippy::unused_async)] +pub async fn delete_handler( + State(app_data): State>, + Extract(maybe_bearer_token): Extract, + extract::Json(category_form): extract::Json, +) -> 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. + + let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?; + + match app_data.category_service.delete_category(&category_form.name, &user_id).await { + Ok(_) => Ok(deleted_category(&category_form.name)), + Err(error) => Err(error), + } +} diff --git a/src/web/api/v1/contexts/category/responses.rs b/src/web/api/v1/contexts/category/responses.rs index 97b0ebb7..cb372801 100644 --- a/src/web/api/v1/contexts/category/responses.rs +++ b/src/web/api/v1/contexts/category/responses.rs @@ -10,3 +10,10 @@ pub fn added_category(category_name: &str) -> Json> { data: category_name.to_string(), }) } + +/// Response after successfully deleting a new category. +pub fn deleted_category(category_name: &str) -> Json> { + Json(OkResponse { + data: category_name.to_string(), + }) +} diff --git a/src/web/api/v1/contexts/category/routes.rs b/src/web/api/v1/contexts/category/routes.rs index e34d2ef4..2d762c47 100644 --- a/src/web/api/v1/contexts/category/routes.rs +++ b/src/web/api/v1/contexts/category/routes.rs @@ -3,15 +3,16 @@ //! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category). use std::sync::Arc; -use axum::routing::{get, post}; +use axum::routing::{delete, get, post}; use axum::Router; -use super::handlers::{add_handler, get_all_handler}; +use super::handlers::{add_handler, delete_handler, get_all_handler}; use crate::common::AppData; /// Routes for the [`category`](crate::web::api::v1::contexts::category) API context. pub fn router(app_data: Arc) -> Router { Router::new() .route("/", get(get_all_handler).with_state(app_data.clone())) - .route("/", post(add_handler).with_state(app_data)) + .route("/", post(add_handler).with_state(app_data.clone())) + .route("/", delete(delete_handler).with_state(app_data)) } diff --git a/tests/common/contexts/category/asserts.rs b/tests/common/contexts/category/asserts.rs index 2e39d9fd..ae47bdca 100644 --- a/tests/common/contexts/category/asserts.rs +++ b/tests/common/contexts/category/asserts.rs @@ -1,5 +1,5 @@ use crate::common::asserts::assert_json_ok; -use crate::common::contexts::category::responses::AddedCategoryResponse; +use crate::common::contexts::category::responses::{AddedCategoryResponse, DeletedCategoryResponse}; use crate::common::responses::TextResponse; pub fn assert_added_category_response(response: &TextResponse, category_name: &str) { @@ -10,3 +10,12 @@ pub fn assert_added_category_response(response: &TextResponse, category_name: &s assert_json_ok(response); } + +pub fn assert_deleted_category_response(response: &TextResponse, category_name: &str) { + let deleted_category_response: DeletedCategoryResponse = serde_json::from_str(&response.body) + .unwrap_or_else(|_| panic!("response {:#?} should be a DeletedCategoryResponse", response.body)); + + assert_eq!(deleted_category_response.data, category_name); + + assert_json_ok(response); +} diff --git a/tests/common/contexts/category/responses.rs b/tests/common/contexts/category/responses.rs index a345d523..cbadb631 100644 --- a/tests/common/contexts/category/responses.rs +++ b/tests/common/contexts/category/responses.rs @@ -5,6 +5,11 @@ pub struct AddedCategoryResponse { pub data: String, } +#[derive(Deserialize)] +pub struct DeletedCategoryResponse { + pub data: String, +} + #[derive(Deserialize, Debug)] pub struct ListResponse { pub data: Vec, diff --git a/tests/e2e/contexts/category/contract.rs b/tests/e2e/contexts/category/contract.rs index 336a35c8..d1970063 100644 --- a/tests/e2e/contexts/category/contract.rs +++ b/tests/e2e/contexts/category/contract.rs @@ -215,9 +215,9 @@ mod with_axum_implementation { use crate::common::asserts::assert_json_ok; use crate::common::client::Client; - use crate::common::contexts::category::asserts::assert_added_category_response; + use crate::common::contexts::category::asserts::{assert_added_category_response, assert_deleted_category_response}; use crate::common::contexts::category::fixtures::random_category_name; - use crate::common::contexts::category::forms::AddCategoryForm; + use crate::common::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; use crate::common::contexts::category::responses::ListResponse; use crate::e2e::config::ENV_VAR_E2E_EXCLUDE_AXUM_IMPL; use crate::e2e::contexts::category::steps::{add_category, add_random_category}; @@ -379,4 +379,78 @@ mod with_axum_implementation { assert_eq!(response.status, 400); } + + #[tokio::test] + async fn it_should_allow_admins_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Implementation::Axum).await; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + let logged_in_admin = new_logged_in_admin(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_admin.token); + + let added_category_name = add_random_category(&env).await; + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_deleted_category_response(&response, &added_category_name); + } + + #[tokio::test] + async fn it_should_not_allow_non_admins_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Implementation::Axum).await; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + let added_category_name = add_random_category(&env).await; + + let logged_in_non_admin = new_logged_in_user(&env).await; + let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_in_non_admin.token); + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 403); + } + + #[tokio::test] + async fn it_should_not_allow_guests_to_delete_categories() { + let mut env = TestEnv::new(); + env.start(api::Implementation::Axum).await; + + if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() { + println!("Skipped"); + return; + } + + let client = Client::unauthenticated(&env.server_socket_addr().unwrap()); + + let added_category_name = add_random_category(&env).await; + + let response = client + .delete_category(DeleteCategoryForm { + name: added_category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 401); + } }