Skip to content

Commit

Permalink
refactor(api): [#179] Axum API, category context, add category
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jun 15, 2023
1 parent bb6d9bf commit f63bf05
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 7 deletions.
7 changes: 7 additions & 0 deletions src/web/api/v1/contexts/category/forms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct CategoryForm {
pub name: String,
pub icon: Option<String>,
}
30 changes: 28 additions & 2 deletions src/web/api/v1/contexts/category/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
//! context.
use std::sync::Arc;

use axum::extract::State;
use axum::extract::{self, State};
use axum::response::Json;

use super::forms::CategoryForm;
use super::responses::added_category;
use crate::common::AppData;
use crate::databases::database::{self, Category};
use crate::web::api::v1::responses;
use crate::errors::ServiceError;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::{self, OkResponse};

/// It handles the request to get all the categories.
///
Expand All @@ -31,3 +35,25 @@ pub async fn get_all_handler(
Err(error) => Err(error),
}
}

/// It adds a new category.
///
/// # Errors
///
/// It returns an error if:
///
/// - The user does not have permissions to create a new category.
/// - There is a database error.
#[allow(clippy::unused_async)]
pub async fn add_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(category_form): extract::Json<CategoryForm>,
) -> Result<Json<OkResponse<String>>, 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 {
Ok(_) => Ok(added_category(&category_form.name)),
Err(error) => Err(error),
}
}
2 changes: 2 additions & 0 deletions src/web/api/v1/contexts/category/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,7 @@
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
//! information about the response attributes. The response contains only the
//! name of the deleted category.
pub mod forms;
pub mod handlers;
pub mod responses;
pub mod routes;
12 changes: 12 additions & 0 deletions src/web/api/v1/contexts/category/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! API responses for the the [`category`](crate::web::api::v1::contexts::category) API
//! context.
use axum::Json;

use crate::web::api::v1::responses::OkResponse;

/// Response after successfully creating a new category.
pub fn added_category(category_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
data: category_name.to_string(),
})
}
8 changes: 5 additions & 3 deletions src/web/api/v1/contexts/category/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category).
use std::sync::Arc;

use axum::routing::get;
use axum::routing::{get, post};
use axum::Router;

use super::handlers::get_all_handler;
use super::handlers::{add_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<AppData>) -> Router {
Router::new().route("/", get(get_all_handler).with_state(app_data))
Router::new()
.route("/", get(get_all_handler).with_state(app_data.clone()))
.route("/", post(add_handler).with_state(app_data))
}
12 changes: 12 additions & 0 deletions tests/common/contexts/category/asserts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::common::asserts::assert_json_ok;
use crate::common::contexts::category::responses::AddedCategoryResponse;
use crate::common::responses::TextResponse;

pub fn assert_added_category_response(response: &TextResponse, category_name: &str) {
let added_category_response: AddedCategoryResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a AddedCategoryResponse", response.body));

assert_eq!(added_category_response.data, category_name);

assert_json_ok(response);
}
1 change: 1 addition & 0 deletions tests/common/contexts/category/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod asserts;
pub mod fixtures;
pub mod forms;
pub mod responses;
153 changes: 153 additions & 0 deletions tests/e2e/contexts/category/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,19 @@ async fn it_should_not_allow_guests_to_delete_categories() {
}

mod with_axum_implementation {
use std::env;

use torrust_index_backend::web::api;

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::fixtures::random_category_name;
use crate::common::contexts::category::forms::AddCategoryForm;
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};
use crate::e2e::contexts::user::steps::{new_logged_in_admin, new_logged_in_user};
use crate::e2e::environment::TestEnv;

#[tokio::test]
Expand All @@ -226,4 +235,148 @@ mod with_axum_implementation {

assert_json_ok(&response);
}

#[tokio::test]
async fn it_should_return_a_category_list() {
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());

add_random_category(&env).await;

let response = client.get_categories().await;

let res: ListResponse = serde_json::from_str(&response.body).unwrap();

// There should be at least the category we added.
// Since this is an E2E test and it could be run in a shared test env,
// there might be more categories.
assert!(res.data.len() > 1);
if let Some(content_type) = &response.content_type {
assert_eq!(content_type, "application/json");
}
assert_eq!(response.status, 200);
}

#[tokio::test]
async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
}

let client = Client::unauthenticated(&env.server_socket_addr().unwrap());

let response = client
.add_category(AddCategoryForm {
name: "CATEGORY NAME".to_string(),
icon: None,
})
.await;

assert_eq!(response.status, 401);
}

#[tokio::test]
async fn it_should_not_allow_adding_a_new_category_to_non_admins() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
}

let logged_non_admin = new_logged_in_user(&env).await;

let client = Client::authenticated(&env.server_socket_addr().unwrap(), &logged_non_admin.token);

let response = client
.add_category(AddCategoryForm {
name: "CATEGORY NAME".to_string(),
icon: None,
})
.await;

assert_eq!(response.status, 403);
}

#[tokio::test]
async fn it_should_allow_admins_to_add_new_categories() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

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 category_name = random_category_name();

let response = client
.add_category(AddCategoryForm {
name: category_name.to_string(),
icon: None,
})
.await;

assert_added_category_response(&response, &category_name);
}

#[tokio::test]
async fn it_should_allow_adding_empty_categories() {
// code-review: this is a bit weird, is it a intended behavior?

let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

if env::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 category_name = String::new();

let response = client
.add_category(AddCategoryForm {
name: category_name.to_string(),
icon: None,
})
.await;

assert_added_category_response(&response, &category_name);
}

#[tokio::test]
async fn it_should_not_allow_adding_duplicated_categories() {
let mut env = TestEnv::new();
env.start(api::Implementation::Axum).await;

if env::var(ENV_VAR_E2E_EXCLUDE_AXUM_IMPL).is_ok() {
println!("Skipped");
return;
}

let added_category_name = add_random_category(&env).await;

// Try to add the same category again
let response = add_category(&added_category_name, &env).await;

assert_eq!(response.status, 400);
}
}
7 changes: 6 additions & 1 deletion tests/e2e/contexts/category/steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ use crate::e2e::environment::TestEnv;
/// Add a random category and return its name.
pub async fn add_random_category(env: &TestEnv) -> String {
let category_name = random_category_name();

let response = add_category(&category_name, env).await;
let res: AddedCategoryResponse = serde_json::from_str(&response.body).unwrap();

let res: AddedCategoryResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a AddedCategoryResponse", response.body));

res.data
}

pub async fn add_category(category_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
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/contexts/user/steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ pub async fn new_logged_in_admin(env: &TestEnv) -> LoggedInUserData {
.expect("Database error."),
);

let user_profile = database.get_user_profile_from_username(&user.username).await.unwrap();
let user_profile = database
.get_user_profile_from_username(&user.username)
.await
.unwrap_or_else(|_| panic!("user {user:#?} should have a profile."));

database.grant_admin_role(user_profile.user_id).await.unwrap();

Expand Down

0 comments on commit f63bf05

Please sign in to comment.