diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 0850241..0f9af12 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -116,7 +116,7 @@ jobs: - name: Generate and fix code coverage run: | - cargo llvm-cov --ignore-filename-regex "main|inmemory" --lcov --output-path lcov.info + cargo llvm-cov --ignore-filename-regex "main" --lcov --output-path lcov.info ./rust-covfix lcov.info -o lcov.info env: RUST_TEST_THREADS: 1 diff --git a/Cargo.lock b/Cargo.lock index fff9350..f8b80e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1324,7 +1324,6 @@ dependencies = [ "jsonwebtoken", "mockall", "mongodb", - "once_cell", "rand", "rstest", "serde", diff --git a/link-for-later/Cargo.toml b/link-for-later/Cargo.toml index d57c91c..b55b6ee 100644 --- a/link-for-later/Cargo.toml +++ b/link-for-later/Cargo.toml @@ -15,7 +15,6 @@ futures = "0.3.29" http-body-util = "0.1.0" jsonwebtoken = "9.2.0" mongodb = "2.8.0" -once_cell = "1.19.0" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" tokio = { version = "1", features = ["macros"] } diff --git a/link-for-later/src/controller/auth.rs b/link-for-later/src/controller/auth.rs index 7627ee2..ee60fb7 100644 --- a/link-for-later/src/controller/auth.rs +++ b/link-for-later/src/controller/auth.rs @@ -20,21 +20,18 @@ where let TypedHeader(Authorization(bearer)) = TypedHeader::>::from_request_parts(parts, state) .await - .map_err(|_| AppError::AuthorizationError)?; + .map_err(|_| { + AppError::AuthorizationError(String::from("Authorization token not found")) + })?; let secret = std::env::var(JWT_SECRET_KEY).map_or_else(|_| String::default(), |secret| secret); - let token_data = match decode::( + let token_data = decode::( bearer.token(), &DecodingKey::from_secret(secret.as_bytes()), &Validation::default(), - ) { - Ok(token) => token, - Err(e) => { - tracing::error!("Error: {}", e.to_string()); - return Err(AppError::AuthorizationError); - } - }; + ) + .map_err(|e| AppError::AuthorizationError(format!("decode() {e:?}")))?; Ok(token_data.claims) } diff --git a/link-for-later/src/controller/error.rs b/link-for-later/src/controller/error.rs index 87a0bbc..37b476b 100644 --- a/link-for-later/src/controller/error.rs +++ b/link-for-later/src/controller/error.rs @@ -7,20 +7,45 @@ use serde_json::json; use crate::types::AppError; +#[allow(clippy::cognitive_complexity)] impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match self { - Self::ServerError | Self::DatabaseError(_) => { + Self::ServerError(ref e) => { + tracing::debug!("Server error: {}", e.to_string()); (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) } - Self::LinkNotFound => (StatusCode::NOT_FOUND, self.to_string()), - Self::UserAlreadyExists - | Self::UserNotFound - | Self::InvalidEmail - | Self::InvalidUrl => (StatusCode::BAD_REQUEST, self.to_string()), - Self::AuthorizationError | Self::IncorrectPassword => { + Self::DatabaseError(ref e) => { + tracing::debug!("Database error: {}", e.to_string()); + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) + } + Self::LinkNotFound(ref e) => { + tracing::debug!("Link not found: {}", e.to_string()); + (StatusCode::NOT_FOUND, self.to_string()) + } + Self::UserAlreadyExists(ref e) => { + tracing::debug!("User already exists: {}", e.to_string()); + (StatusCode::BAD_REQUEST, self.to_string()) + } + Self::UserNotFound(ref e) => { + tracing::debug!("User not found: {}", e.to_string()); + (StatusCode::BAD_REQUEST, self.to_string()) + } + Self::IncorrectPassword(ref e) => { + tracing::debug!("Incorrect password: {}", e.to_string()); + (StatusCode::UNAUTHORIZED, self.to_string()) + } + Self::AuthorizationError(ref e) => { + tracing::debug!("Authorization error: {}", e.to_string()); (StatusCode::UNAUTHORIZED, self.to_string()) } + Self::ValidationError(ref e) => { + tracing::debug!("Payload validation error: {}", e.to_string()); + (StatusCode::BAD_REQUEST, self.to_string()) + } + + #[cfg(test)] + Self::TestError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), }; let body = Json(json!({ @@ -30,3 +55,61 @@ impl IntoResponse for AppError { (status, body).into_response() } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_error_response() { + assert_eq!( + AppError::ServerError("a server operation failed".into()) + .into_response() + .status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + assert_eq!( + AppError::DatabaseError("a database operation failed".into()) + .into_response() + .status(), + StatusCode::INTERNAL_SERVER_ERROR + ); + assert_eq!( + AppError::LinkNotFound("link".into()) + .into_response() + .status(), + StatusCode::NOT_FOUND + ); + assert_eq!( + AppError::UserAlreadyExists("user".into()) + .into_response() + .status(), + StatusCode::BAD_REQUEST + ); + assert_eq!( + AppError::UserNotFound("user".into()) + .into_response() + .status(), + StatusCode::BAD_REQUEST + ); + assert_eq!( + AppError::IncorrectPassword("user".into()) + .into_response() + .status(), + StatusCode::UNAUTHORIZED + ); + assert_eq!( + AppError::AuthorizationError("an authorization error occurred".into()) + .into_response() + .status(), + StatusCode::UNAUTHORIZED + ); + assert_eq!( + AppError::ValidationError("a validation error occurred".into()) + .into_response() + .status(), + StatusCode::BAD_REQUEST + ); + } +} diff --git a/link-for-later/src/controller/links.rs b/link-for-later/src/controller/links.rs index 3333889..f058feb 100644 --- a/link-for-later/src/controller/links.rs +++ b/link-for-later/src/controller/links.rs @@ -53,8 +53,7 @@ async fn post( match payload.validate() { Ok(()) => {} Err(e) => { - tracing::error!("Error: {}", e); - return AppError::InvalidUrl.into_response(); + return AppError::ValidationError(format!("post_link() {e:?}")).into_response(); } } @@ -105,8 +104,7 @@ async fn put( match payload.validate() { Ok(()) => {} Err(e) => { - tracing::error!("Error: {}", e); - return AppError::InvalidUrl.into_response(); + return AppError::ValidationError(format!("put_link() {e:?}")).into_response(); } } @@ -231,7 +229,7 @@ mod tests { .expect_search() .withf(move |_, query| query == &repo_query) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = list(State(app_state), Claims::new("user-id", 0, 0)).await; @@ -241,7 +239,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } #[traced_test] @@ -301,7 +299,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid url"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); } #[traced_test] @@ -315,7 +313,7 @@ mod tests { .expect_create() .withf(move |_, item| item == &item_to_create) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = post( @@ -330,7 +328,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } #[traced_test] @@ -378,7 +376,7 @@ mod tests { .expect_get() .withf(move |_, query| query == &repo_query) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = get( @@ -393,7 +391,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } #[traced_test] @@ -458,7 +456,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid url"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); } #[traced_test] @@ -475,7 +473,7 @@ mod tests { .expect_update() .withf(move |_, id, item| id == "1" && item == &item_to_update) .times(1) - .returning(|_, _, _| Err(AppError::ServerError)); + .returning(|_, _, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = put( @@ -491,7 +489,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } #[traced_test] @@ -528,7 +526,7 @@ mod tests { .expect_delete() .withf(move |_, item| item == &item_to_delete) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = delete( @@ -543,7 +541,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } struct AppStateBuilder { diff --git a/link-for-later/src/controller/users.rs b/link-for-later/src/controller/users.rs index e6c5a70..851b3d3 100644 --- a/link-for-later/src/controller/users.rs +++ b/link-for-later/src/controller/users.rs @@ -31,8 +31,7 @@ async fn register( match payload.validate() { Ok(()) => {} Err(e) => { - tracing::error!("Error: {}", e); - return AppError::InvalidEmail.into_response(); + return AppError::ValidationError(format!("register() {e:?}")).into_response(); } } @@ -58,8 +57,7 @@ async fn login( match payload.validate() { Ok(()) => {} Err(e) => { - tracing::error!("Error: {}", e); - return AppError::InvalidEmail.into_response(); + return AppError::ValidationError(format!("login() {e:?}")).into_response(); } } @@ -141,7 +139,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid email"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); } #[traced_test] @@ -155,7 +153,7 @@ mod tests { .expect_register() .withf(move |_, user| user == &user_to_register) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_users_service)).build(); let response = register(State(app_state), Json(request)).await; @@ -165,7 +163,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } #[traced_test] @@ -209,7 +207,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid email"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); } #[traced_test] @@ -223,7 +221,7 @@ mod tests { .expect_login() .withf(move |_, user| user == &user_to_login) .times(1) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let app_state = AppStateBuilder::new(Arc::new(mock_users_service)).build(); let response = login(State(app_state), Json(request)).await; @@ -233,7 +231,7 @@ mod tests { let body = body.collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "server error"}).to_string()); + assert_eq!(body, json!({"error": "test error"}).to_string()); } struct AppStateBuilder { diff --git a/link-for-later/src/repository/inmemory.rs b/link-for-later/src/repository/inmemory.rs index d69599a..b47ec0e 100644 --- a/link-for-later/src/repository/inmemory.rs +++ b/link-for-later/src/repository/inmemory.rs @@ -1,7 +1,6 @@ use std::sync::Mutex; use axum::async_trait; -use once_cell::sync::Lazy; use crate::types::{ dto::{LinkQuery, LinkQueryBuilder, UserQuery}, @@ -11,22 +10,39 @@ use crate::types::{ use super::{Links as LinksRepository, Users as UsersRepository}; -#[derive(Default)] -pub struct LinksRepositoryProvider {} +pub struct LinksRepositoryProvider { + links_data: Mutex>, + links_data_counter: Mutex>, +} -static INMEMORY_LINKS_DATA: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); -static INMEMORY_LINKS_DATA_COUNTER: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); +pub struct UsersRepositoryProvider { + users_data: Mutex>, + users_data_counter: Mutex>, +} -#[derive(Default)] -pub struct UsersRepositoryProvider {} +impl Default for LinksRepositoryProvider { + fn default() -> Self { + Self { + links_data: Mutex::new(Vec::new()), + links_data_counter: Mutex::new(Vec::new()), + } + } +} -static INMEMORY_USERS_DATA: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); -static INMEMORY_USERS_DATA_COUNTER: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); +impl Default for UsersRepositoryProvider { + fn default() -> Self { + Self { + users_data: Mutex::new(Vec::new()), + users_data_counter: Mutex::new(Vec::new()), + } + } +} #[async_trait] impl LinksRepository for LinksRepositoryProvider { async fn find(&self, query: &LinkQuery) -> Result> { - let filtered_links: Vec = INMEMORY_LINKS_DATA + let filtered_links: Vec = self + .links_data .lock() .unwrap() .iter() @@ -40,42 +56,42 @@ impl LinksRepository for LinksRepositoryProvider { } async fn get(&self, query: &LinkQuery) -> Result { - INMEMORY_LINKS_DATA + self.links_data .lock() .unwrap() .iter() .find(|link| link.id() == query.id() && link.owner() == query.owner()) .cloned() - .ok_or(AppError::LinkNotFound) + .ok_or_else(|| AppError::LinkNotFound(query.id().to_owned())) } async fn create(&self, item: &LinkItem) -> Result { - let id = INMEMORY_LINKS_DATA_COUNTER.lock().unwrap().len() + 1; + let id = self.links_data_counter.lock().unwrap().len() + 1; let link = LinkItemBuilder::from(item.clone()) .id(&id.to_string()) .build(); - INMEMORY_LINKS_DATA.lock().unwrap().push(link.clone()); - INMEMORY_LINKS_DATA_COUNTER.lock().unwrap().push(id); + self.links_data.lock().unwrap().push(link.clone()); + self.links_data_counter.lock().unwrap().push(id); Ok(link) } async fn update(&self, id: &str, item: &LinkItem) -> Result { - INMEMORY_LINKS_DATA + self.links_data .lock() .unwrap() .iter() .find(|link| link.id() == id && link.owner() == item.owner()) .cloned() - .ok_or(AppError::LinkNotFound)?; + .ok_or_else(|| AppError::LinkNotFound(id.to_owned()))?; self.delete(item).await?; - INMEMORY_LINKS_DATA.lock().unwrap().push(item.clone()); + self.links_data.lock().unwrap().push(item.clone()); Ok(item.clone()) } async fn delete(&self, item: &LinkItem) -> Result<()> { let query = LinkQueryBuilder::new(item.id(), item.owner()).build(); self.get(&query).await?; - INMEMORY_LINKS_DATA + self.links_data .lock() .unwrap() .retain(|link| link.id() != query.id()); @@ -86,22 +102,163 @@ impl LinksRepository for LinksRepositoryProvider { #[async_trait] impl UsersRepository for UsersRepositoryProvider { async fn get(&self, query: &UserQuery) -> Result { - INMEMORY_USERS_DATA + self.users_data .lock() .unwrap() .iter() .find(|user| user.email() == query.email()) .cloned() - .ok_or(AppError::UserNotFound) + .ok_or_else(|| AppError::UserNotFound(query.email().to_owned())) } async fn create(&self, info: &UserInfo) -> Result { - let id = INMEMORY_USERS_DATA_COUNTER.lock().unwrap().len() + 1; + let id = self.users_data_counter.lock().unwrap().len() + 1; let user = UserInfoBuilder::from(info.clone()) .id(&id.to_string()) .build(); - INMEMORY_USERS_DATA.lock().unwrap().push(user.clone()); - INMEMORY_USERS_DATA_COUNTER.lock().unwrap().push(id); + self.users_data.lock().unwrap().push(user.clone()); + self.users_data_counter.lock().unwrap().push(id); Ok(user) } } + +#[cfg(test)] +mod tests { + + use crate::types::dto::UserQueryBuilder; + + use super::*; + + #[tokio::test] + async fn test_search_links_empty() { + let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + let links_repository = LinksRepositoryProvider::default(); + + let retrieved_items = links_repository.find(&repo_query).await.unwrap(); + assert!(retrieved_items.is_empty()); + } + + #[tokio::test] + async fn test_search_created_links() { + let item = LinkItemBuilder::new("http://link").owner("user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let created_item = links_repository.create(&item).await.unwrap(); + let expected_items = vec![created_item.clone()]; + + let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + let retrieved_items = links_repository.find(&repo_query).await.unwrap(); + assert!(!retrieved_items.is_empty()); + assert!(retrieved_items + .iter() + .all(|item| expected_items.contains(item))); + } + + #[tokio::test] + async fn test_get_link_not_found() { + let repo_query = LinkQueryBuilder::new("1", "user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let response = links_repository.get(&repo_query).await; + + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); + } + + #[tokio::test] + async fn test_get_created_link() { + let item = LinkItemBuilder::new("http://link").owner("user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let created_item = links_repository.create(&item).await.unwrap(); + + let repo_query = LinkQueryBuilder::new(created_item.id(), "user-id").build(); + let retrieved_item = links_repository.get(&repo_query).await.unwrap(); + + assert_eq!(created_item, retrieved_item); + } + + #[tokio::test] + async fn test_update_link_not_found() { + let item = LinkItemBuilder::new("http://link").owner("user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let response = links_repository.update("1", &item).await; + + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); + } + + #[tokio::test] + async fn test_update_created_link() { + let item = LinkItemBuilder::new("http://link").owner("user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let created_item = links_repository.create(&item).await.unwrap(); + + let item = LinkItemBuilder::from(created_item.clone()) + .title("title") + .build(); + let updated_item = links_repository + .update(created_item.id(), &item) + .await + .unwrap(); + + let repo_query = LinkQueryBuilder::new(updated_item.id(), "user-id").build(); + let retrieved_item = links_repository.get(&repo_query).await.unwrap(); + + assert_eq!(updated_item, retrieved_item); + } + + #[tokio::test] + async fn test_delete_link_not_found() { + let item = LinkItemBuilder::new("http://link") + .id("1") + .owner("user-id") + .build(); + + let links_repository = LinksRepositoryProvider::default(); + let response = links_repository.delete(&item).await; + + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); + } + + #[tokio::test] + async fn test_delete_created_link() { + let item = LinkItemBuilder::new("http://link").owner("user-id").build(); + + let links_repository = LinksRepositoryProvider::default(); + let created_item = links_repository.create(&item).await.unwrap(); + + links_repository.delete(&created_item).await.unwrap(); + + let repo_query = LinkQueryBuilder::new(created_item.id(), "user-id").build(); + let response = links_repository.get(&repo_query).await; + + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); + } + + #[tokio::test] + async fn test_get_user_not_found() { + let repo_query = UserQueryBuilder::new("user@test.com").build(); + + let users_repository = UsersRepositoryProvider::default(); + let response = users_repository.get(&repo_query).await; + + assert_eq!( + response, + Err(AppError::UserNotFound("user@test.com".into())) + ); + } + + #[tokio::test] + async fn test_get_created_user() { + let user = UserInfoBuilder::new("user@test.com", "test").build(); + + let users_repository = UsersRepositoryProvider::default(); + let created_user = users_repository.create(&user).await.unwrap(); + + let repo_query = UserQueryBuilder::new("user@test.com").build(); + let retrieved_user = users_repository.get(&repo_query).await.unwrap(); + + assert_eq!(created_user, retrieved_user); + } +} diff --git a/link-for-later/src/repository/mongodb.rs b/link-for-later/src/repository/mongodb.rs index 36de5bf..f92a829 100644 --- a/link-for-later/src/repository/mongodb.rs +++ b/link-for-later/src/repository/mongodb.rs @@ -65,7 +65,7 @@ impl LinksRepository for LinksRepositoryProvider { .find_one(db_query, None) .await .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; - item.ok_or(AppError::LinkNotFound) + item.ok_or_else(|| AppError::LinkNotFound(query.id().to_owned())) } async fn create(&self, item: &LinkItem) -> Result { @@ -123,7 +123,7 @@ impl UsersRepository for UsersRepositoryProvider { .find_one(db_query, None) .await .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; - item.ok_or(AppError::UserNotFound) + item.ok_or_else(|| AppError::UserNotFound(query.email().to_owned())) } async fn create(&self, info: &UserInfo) -> Result { diff --git a/link-for-later/src/service/links.rs b/link-for-later/src/service/links.rs index 3bfaec0..d5c636b 100644 --- a/link-for-later/src/service/links.rs +++ b/link-for-later/src/service/links.rs @@ -147,14 +147,14 @@ mod tests { .expect_find() .withf(move |query| query == &expected_query) .times(1) - .returning(|_| Err(AppError::ServerError)); + .returning(|_| Err(AppError::TestError)); let links_service = ServiceProvider {}; let response = links_service .search(Box::new(Arc::new(mock_links_repo)), &repo_query) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } #[tokio::test] @@ -193,14 +193,14 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::LinkNotFound)); + .returning(|_| Err(AppError::LinkNotFound("1".into()))); let links_service = ServiceProvider {}; let response = links_service .get(Box::new(Arc::new(mock_links_repo)), &request_query) .await; - assert_eq!(response, Err(AppError::LinkNotFound)); + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } #[tokio::test] @@ -239,14 +239,14 @@ mod tests { .expect_create() //.withf(move |item| item == &item_to_create) .times(1) - .returning(|_| Err(AppError::ServerError)); + .returning(|_| Err(AppError::TestError)); let links_service = ServiceProvider {}; let response = links_service .create(Box::new(Arc::new(mock_links_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } #[tokio::test] @@ -307,7 +307,7 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::LinkNotFound)); + .returning(|_| Err(AppError::LinkNotFound("1".into()))); mock_links_repo .expect_update() //.withf(move |item| item == &item_to_update) @@ -318,7 +318,7 @@ mod tests { .update(Box::new(Arc::new(mock_links_repo)), "1", &request_item) .await; - assert_eq!(response, Err(AppError::LinkNotFound)); + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } #[tokio::test] @@ -348,14 +348,14 @@ mod tests { //.withf(move |item| item == &item_to_update) .times(1) .in_sequence(&mut seq) - .returning(|_, _| Err(AppError::ServerError)); + .returning(|_, _| Err(AppError::TestError)); let links_service = ServiceProvider {}; let response = links_service .update(Box::new(Arc::new(mock_links_repo)), "1", &request_item) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } #[tokio::test] @@ -409,7 +409,7 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::LinkNotFound)); + .returning(|_| Err(AppError::LinkNotFound("1".into()))); mock_links_repo .expect_delete() //.withf(move |item| item == &item_to_delete) @@ -420,7 +420,7 @@ mod tests { .delete(Box::new(Arc::new(mock_links_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::LinkNotFound)); + assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } #[tokio::test] @@ -450,13 +450,13 @@ mod tests { //.withf(move |item| item == &item_to_delete) .times(1) .in_sequence(&mut seq) - .returning(|_| Err(AppError::ServerError)); + .returning(|_| Err(AppError::TestError)); let links_service = ServiceProvider {}; let response = links_service .delete(Box::new(Arc::new(mock_links_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } } diff --git a/link-for-later/src/service/users.rs b/link-for-later/src/service/users.rs index 09fcdf7..5fe8618 100644 --- a/link-for-later/src/service/users.rs +++ b/link-for-later/src/service/users.rs @@ -27,8 +27,8 @@ impl UsersService for ServiceProvider { ) -> Result { let user_query = UserQueryBuilder::new(user_info.email()).build(); let user_info = match users_repo.get(&user_query).await { - Ok(_) => return Err(AppError::UserAlreadyExists), - Err(AppError::UserNotFound) => user_info.clone(), + Ok(_) => return Err(AppError::UserAlreadyExists(user_info.email().to_owned())), + Err(AppError::UserNotFound(_)) => user_info.clone(), Err(e) => return Err(e), }; @@ -52,15 +52,14 @@ impl UsersService for ServiceProvider { let retrieved_user_info = users_repo.get(&user_query).await?; if retrieved_user_info.password() != user_info.password() { - tracing::info!("invalid password for user {}", &user_info.email()); - return Err(AppError::IncorrectPassword); + return Err(AppError::IncorrectPassword(user_info.email().to_owned())); } let timestamp = |timestamp: DateTime| -> Result { let timestamp: usize = timestamp .timestamp() .try_into() - .map_err(|_| AppError::ServerError)?; + .map_err(|e| AppError::ServerError(format!("timestamp() {e:?}")))?; Ok(timestamp) }; @@ -73,17 +72,12 @@ impl UsersService for ServiceProvider { let secret = std::env::var(JWT_SECRET_KEY).map_or_else(|_| String::default(), |secret| secret); - let token = match encode( + let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()), - ) { - Ok(token) => token, - Err(e) => { - tracing::error!("Error: {}", e.to_string()); - return Err(AppError::ServerError); - } - }; + ) + .map_err(|e| AppError::ServerError(format!("encode() {e:?}")))?; Ok(Token::new(&token)) } @@ -111,7 +105,7 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::UserNotFound)); + .returning(|_| Err(AppError::UserNotFound("user@test.com".into()))); mock_users_repo .expect_create() //.withf(move |user| user == &user_to_register) @@ -147,7 +141,10 @@ mod tests { .register(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::UserAlreadyExists)); + assert_eq!( + response, + Err(AppError::UserAlreadyExists("user@test.com".into())) + ); } #[tokio::test] @@ -161,7 +158,7 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::ServerError)); + .returning(|_| Err(AppError::TestError)); mock_users_repo.expect_create().times(0); let users_service = ServiceProvider {}; @@ -169,7 +166,7 @@ mod tests { .register(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } #[tokio::test] @@ -183,19 +180,19 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::UserNotFound)); + .returning(|_| Err(AppError::UserNotFound("user@test.com".into()))); mock_users_repo .expect_create() //.withf(move |user| user == &user_to_register) .times(1) - .returning(move |_| Err(AppError::ServerError)); + .returning(move |_| Err(AppError::TestError)); let users_service = ServiceProvider {}; let response = users_service .register(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::ServerError)); + assert_eq!(response, Err(AppError::TestError)); } #[tokio::test] @@ -231,14 +228,17 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(move |_| Err(AppError::UserNotFound)); + .returning(move |_| Err(AppError::UserNotFound("user@test.com".into()))); let users_service = ServiceProvider {}; let response = users_service .login(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::UserNotFound)); + assert_eq!( + response, + Err(AppError::UserNotFound("user@test.com".into())) + ); } #[tokio::test] @@ -260,6 +260,9 @@ mod tests { .login(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::IncorrectPassword)); + assert_eq!( + response, + Err(AppError::IncorrectPassword("user@test.com".into())) + ); } } diff --git a/link-for-later/src/types.rs b/link-for-later/src/types.rs index c41ea17..4133d87 100644 --- a/link-for-later/src/types.rs +++ b/link-for-later/src/types.rs @@ -1,4 +1,4 @@ -pub use self::errors::App as AppError; +pub use self::error::App as AppError; pub type Result = std::result::Result; @@ -10,4 +10,4 @@ pub enum Database { pub mod auth; pub mod dto; pub mod entity; -pub mod errors; +pub mod error; diff --git a/link-for-later/src/types/error.rs b/link-for-later/src/types/error.rs new file mode 100644 index 0000000..0ac2723 --- /dev/null +++ b/link-for-later/src/types/error.rs @@ -0,0 +1,78 @@ +use std::{error, fmt}; + +#[derive(Debug, PartialEq, Eq)] +pub enum App { + ServerError(String), + DatabaseError(String), + LinkNotFound(String), + UserAlreadyExists(String), + UserNotFound(String), + IncorrectPassword(String), + AuthorizationError(String), + ValidationError(String), + + #[cfg(test)] + TestError, +} + +impl fmt::Display for App { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ServerError(_) => write!(f, "server error"), + Self::DatabaseError(_) => write!(f, "database error"), + Self::LinkNotFound(_) => write!(f, "link item not found"), + Self::UserAlreadyExists(_) => write!(f, "user already registered"), + Self::UserNotFound(_) => write!(f, "user not found"), + Self::IncorrectPassword(_) => write!(f, "incorrect password for user"), + Self::AuthorizationError(_) => write!(f, "invalid authorization token"), + Self::ValidationError(_) => write!(f, "invalid request"), + + #[cfg(test)] + Self::TestError => write!(f, "test error"), + } + } +} + +impl error::Error for App {} + +#[cfg(test)] +mod tests { + + use super::App as AppError; + + #[test] + fn test_error_messages() { + assert_eq!( + AppError::ServerError("a server operation failed".into()).to_string(), + "server error" + ); + assert_eq!( + AppError::DatabaseError("a database operation failed".into()).to_string(), + "database error" + ); + assert_eq!( + AppError::LinkNotFound("link".into()).to_string(), + "link item not found" + ); + assert_eq!( + AppError::UserAlreadyExists("user".into()).to_string(), + "user already registered" + ); + assert_eq!( + AppError::UserNotFound("user".into()).to_string(), + "user not found" + ); + assert_eq!( + AppError::IncorrectPassword("user".into()).to_string(), + "incorrect password for user" + ); + assert_eq!( + AppError::AuthorizationError("an authorization error occurred".into()).to_string(), + "invalid authorization token" + ); + assert_eq!( + AppError::ValidationError("invalid email".into()).to_string(), + "invalid request" + ); + } +} diff --git a/link-for-later/src/types/errors.rs b/link-for-later/src/types/errors.rs deleted file mode 100644 index 1917c3b..0000000 --- a/link-for-later/src/types/errors.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{error, fmt}; - -#[derive(Debug, PartialEq, Eq)] -pub enum App { - ServerError, - DatabaseError(String), - LinkNotFound, - UserAlreadyExists, - UserNotFound, - IncorrectPassword, - AuthorizationError, - InvalidEmail, - InvalidUrl, -} - -impl fmt::Display for App { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::ServerError => write!(f, "server error"), - Self::DatabaseError(_) => write!(f, "database error"), - Self::LinkNotFound => write!(f, "link item not found"), - Self::UserAlreadyExists => write!(f, "user already regisered"), - Self::UserNotFound => write!(f, "user not found"), - Self::IncorrectPassword => write!(f, "incorrect password for user"), - Self::AuthorizationError => write!(f, "invalid authorization token"), - Self::InvalidEmail => write!(f, "invalid email"), - Self::InvalidUrl => write!(f, "invalid url"), - } - } -} - -impl error::Error for App {} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_error_messages() { - assert_eq!(App::ServerError.to_string(), "server error"); - assert_eq!( - App::DatabaseError("a database error occurred".into()).to_string(), - "database error" - ); - assert_eq!(App::ServerError.to_string(), "server error"); - assert_eq!(App::LinkNotFound.to_string(), "link item not found"); - assert_eq!(App::UserAlreadyExists.to_string(), "user already regisered"); - assert_eq!(App::UserNotFound.to_string(), "user not found"); - assert_eq!( - App::IncorrectPassword.to_string(), - "incorrect password for user" - ); - assert_eq!( - App::AuthorizationError.to_string(), - "invalid authorization token" - ); - assert_eq!(App::InvalidEmail.to_string(), "invalid email"); - assert_eq!(App::InvalidUrl.to_string(), "invalid url"); - } -} diff --git a/link-for-later/tests/links.rs b/link-for-later/tests/links.rs index a109780..2b153df 100644 --- a/link-for-later/tests/links.rs +++ b/link-for-later/tests/links.rs @@ -209,7 +209,7 @@ async fn test_post_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: Da let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid url"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); let db_count = repository.count_links().await; assert!(db_count == 0); @@ -290,7 +290,7 @@ async fn test_put_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: Dat let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid url"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); let db_count = repository.count_links().await; assert!(db_count == 1); diff --git a/link-for-later/tests/users.rs b/link-for-later/tests/users.rs index aa56887..a635263 100644 --- a/link-for-later/tests/users.rs +++ b/link-for-later/tests/users.rs @@ -80,7 +80,7 @@ async fn test_register_user_invalid_email(#[values(DatabaseType::MongoDb)] db_ty let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid email"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); let db_count = repository.count_users().await; assert!(db_count == 0); @@ -117,7 +117,10 @@ async fn test_register_user_already_registered( let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "user already regisered"}).to_string()); + assert_eq!( + body, + json!({"error": "user already registered"}).to_string() + ); let db_count = repository.count_users().await; assert!(db_count == 1); @@ -184,7 +187,7 @@ async fn test_login_user_invalid_email(#[values(DatabaseType::MongoDb)] db_type: let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - assert_eq!(body, json!({"error": "invalid email"}).to_string()); + assert_eq!(body, json!({"error": "invalid request"}).to_string()); } #[rstest]