From 2b6c04fdf9026f50f50506aaed8da956041952da Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 25 Apr 2023 14:54:50 +0100 Subject: [PATCH] test: [#109] add more E2E tests for categories --- src/routes/category.rs | 4 + tests/e2e/client.rs | 49 ++++++- tests/e2e/contexts/category.rs | 226 ++++++++++++++++++++++++++++++++- tests/e2e/response.rs | 1 + 4 files changed, 272 insertions(+), 8 deletions(-) diff --git a/src/routes/category.rs b/src/routes/category.rs index defca8a8..823c267e 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -49,6 +49,10 @@ pub async fn delete_category( payload: web::Json, app_data: WebAppData, ) -> ServiceResult { + // 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. + // check for user let user = app_data.auth.get_user_compact_from_request(&req).await?; diff --git a/tests/e2e/client.rs b/tests/e2e/client.rs index 344177e6..e079308b 100644 --- a/tests/e2e/client.rs +++ b/tests/e2e/client.rs @@ -1,6 +1,7 @@ use reqwest::Response as ReqwestResponse; use serde::Serialize; +use super::contexts::category::{AddCategoryForm, DeleteCategoryForm}; use super::contexts::user::{LoginForm, RegistrationForm, TokenRenewalForm, TokenVerificationForm, Username}; use crate::e2e::connection_info::ConnectionInfo; use crate::e2e::http::{Query, ReqwestQuery}; @@ -34,6 +35,14 @@ impl Client { self.http_client.get("category", Query::empty()).await } + pub async fn add_category(&self, add_category_form: AddCategoryForm) -> Response { + self.http_client.post("category", &add_category_form).await + } + + pub async fn delete_category(&self, delete_category_form: DeleteCategoryForm) -> Response { + self.http_client.delete_with_body("category", &delete_category_form).await + } + // Context: root pub async fn root(&self) -> Response { @@ -82,12 +91,21 @@ impl Http { } pub async fn post(&self, path: &str, form: &T) -> Response { - let response = reqwest::Client::new() - .post(self.base_url(path).clone()) - .json(&form) - .send() - .await - .unwrap(); + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .post(self.base_url(path).clone()) + .bearer_auth(token) + .json(&form) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .post(self.base_url(path).clone()) + .json(&form) + .send() + .await + .unwrap(), + }; Response::from(response).await } @@ -108,6 +126,25 @@ impl Http { Response::from(response).await } + async fn delete_with_body(&self, path: &str, form: &T) -> Response { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .bearer_auth(token) + .json(&form) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .json(&form) + .send() + .await + .unwrap(), + }; + Response::from(response).await + } + pub async fn get_request_with_query(&self, path: &str, params: Query) -> Response { get(&self.base_url(path), Some(params)).await } diff --git a/tests/e2e/contexts/category.rs b/tests/e2e/contexts/category.rs index 61c2de7a..417b2516 100644 --- a/tests/e2e/contexts/category.rs +++ b/tests/e2e/contexts/category.rs @@ -1,6 +1,39 @@ +use serde::{Deserialize, Serialize}; + use crate::e2e::asserts::assert_json_ok; +use crate::e2e::contexts::category::fixtures::{add_category, random_category_name}; +use crate::e2e::contexts::user::fixtures::{logged_in_admin, logged_in_user}; use crate::e2e::environment::TestEnv; +// Request data + +#[derive(Serialize)] +pub struct AddCategoryForm { + pub name: String, + pub icon: Option, +} + +pub type DeleteCategoryForm = AddCategoryForm; + +// Response data + +#[derive(Deserialize)] +pub struct AddedCategoryResponse { + pub data: String, +} + +#[derive(Deserialize, Debug)] +pub struct ListResponse { + pub data: Vec, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct ListItem { + pub category_id: i64, + pub name: String, + pub num_torrents: i64, +} + #[tokio::test] #[cfg_attr(not(feature = "e2e-tests"), ignore)] async fn it_should_return_an_empty_category_list_when_there_are_no_categories() { @@ -11,10 +44,199 @@ async fn it_should_return_an_empty_category_list_when_there_are_no_categories() assert_json_ok(&response); } +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_return_a_category_list() { + // Add a category + let category_name = random_category_name(); + let response = add_category(&category_name).await; + assert_eq!(response.status, 200); + + let client = TestEnv::default().unauthenticated_client(); + + 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, 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] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_not_allow_adding_a_new_category_to_unauthenticated_users() { + let client = TestEnv::default().unauthenticated_client(); + + let response = client + .add_category(AddCategoryForm { + name: "CATEGORY NAME".to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 401); +} + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_not_allow_adding_a_new_category_to_non_admins() { + let logged_non_admin = logged_in_user().await; + let client = TestEnv::default().authenticated_client(&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] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_allow_admins_to_add_new_categories() { + let logged_in_admin = logged_in_admin().await; + let client = TestEnv::default().authenticated_client(&logged_in_admin.token); + + let category_name = random_category_name(); + + let response = client + .add_category(AddCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + let res: AddedCategoryResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, category_name); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_not_allow_adding_duplicated_categories() { + // Add a category + let random_category_name = random_category_name(); + let response = add_category(&random_category_name).await; + assert_eq!(response.status, 200); + + // Try to add the same category again + let response = add_category(&random_category_name).await; + assert_eq!(response.status, 400); +} + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_allow_admins_to_delete_categories() { + let logged_in_admin = logged_in_admin().await; + let client = TestEnv::default().authenticated_client(&logged_in_admin.token); + + // Add a category + let category_name = random_category_name(); + let response = add_category(&category_name).await; + assert_eq!(response.status, 200); + + let response = client + .delete_category(DeleteCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + let res: AddedCategoryResponse = serde_json::from_str(&response.body).unwrap(); + + assert_eq!(res.data, category_name); + if let Some(content_type) = &response.content_type { + assert_eq!(content_type, "application/json"); + } + assert_eq!(response.status, 200); +} + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_not_allow_non_admins_to_delete_categories() { + // Add a category + let category_name = random_category_name(); + let response = add_category(&category_name).await; + assert_eq!(response.status, 200); + + let logged_in_non_admin = logged_in_user().await; + let client = TestEnv::default().authenticated_client(&logged_in_non_admin.token); + + let response = client + .delete_category(DeleteCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 403); +} + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_not_allow_guests_to_delete_categories() { + // Add a category + let category_name = random_category_name(); + let response = add_category(&category_name).await; + assert_eq!(response.status, 200); + + let client = TestEnv::default().unauthenticated_client(); + + let response = client + .delete_category(DeleteCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await; + + assert_eq!(response.status, 401); +} + /* todo: - - it_should_not_allow_adding_a_new_category_to_unauthenticated_clients - it should allow adding a new category to authenticated clients - it should not allow adding a new category with an empty name - - it should not allow adding a new category with an empty icon + - it should allow adding a new category with an optional icon - ... */ + +pub mod fixtures { + + use rand::Rng; + + use super::AddCategoryForm; + use crate::e2e::contexts::user::fixtures::logged_in_admin; + use crate::e2e::environment::TestEnv; + use crate::e2e::response::Response; + + pub async fn add_category(category_name: &str) -> Response { + let logged_in_admin = logged_in_admin().await; + let client = TestEnv::default().authenticated_client(&logged_in_admin.token); + + client + .add_category(AddCategoryForm { + name: category_name.to_string(), + icon: None, + }) + .await + } + + pub fn random_category_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/e2e/response.rs b/tests/e2e/response.rs index df04680f..70261cf2 100644 --- a/tests/e2e/response.rs +++ b/tests/e2e/response.rs @@ -1,5 +1,6 @@ use reqwest::Response as ReqwestResponse; +#[derive(Debug)] pub struct Response { pub status: u16, pub content_type: Option,