From 90a3b8b71ab0cddeeb997c13f8800a47ab79ed84 Mon Sep 17 00:00:00 2001 From: Kent Tristan Yves Sarmiento Date: Sat, 13 Jan 2024 07:52:35 +0000 Subject: [PATCH] feat: authorize admin roles --- Cargo.lock | 2 +- link-for-later/src/auth.rs | 2 +- link-for-later/src/controller/routes/links.rs | 188 +++++--- link-for-later/src/repository.rs | 4 +- link-for-later/src/repository/inmemory.rs | 47 +- link-for-later/src/repository/mongodb.rs | 9 +- link-for-later/src/service.rs | 16 +- link-for-later/src/service/links.rs | 438 ++++++++++-------- link-for-later/tests/auth/mod.rs | 10 +- link-for-later/tests/links.rs | 164 ++++++- link-for-later/tests/users.rs | 58 ++- 11 files changed, 592 insertions(+), 346 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b09901..4ee1073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1429,7 +1429,7 @@ dependencies = [ [[package]] name = "link-for-later-types" version = "0.2.0" -source = "git+https://github.com/kentSarmiento/link-for-later-types?branch=develop#e3c1599733b1cf3da0ebea9df881ec77e17b53b4" +source = "git+https://github.com/kentSarmiento/link-for-later-types?branch=develop#6c10b435ba544dec62e4e1c30bee945a4be16c09" dependencies = [ "chrono", "serde", diff --git a/link-for-later/src/auth.rs b/link-for-later/src/auth.rs index 821949b..aed432c 100644 --- a/link-for-later/src/auth.rs +++ b/link-for-later/src/auth.rs @@ -22,7 +22,7 @@ impl Claims { &self.sub } - pub fn is_admin(&self) -> bool { + pub const fn is_admin(&self) -> bool { self.admin } } diff --git a/link-for-later/src/controller/routes/links.rs b/link-for-later/src/controller/routes/links.rs index 85c3071..8208918 100644 --- a/link-for-later/src/controller/routes/links.rs +++ b/link-for-later/src/controller/routes/links.rs @@ -25,10 +25,13 @@ pub fn router(state: AppState) -> Router { } async fn list(State(app_state): State, user: Claims) -> impl IntoResponse { - let link_query = LinkQueryBuilder::default().owner(user.id()).build(); + let query = LinkQueryBuilder::default() + .user(user.id()) + .is_from_admin(user.is_admin()) + .build(); match app_state .links_service() - .search(Box::new(app_state.links_repo().clone()), &link_query) + .search(Box::new(app_state.links_repo().clone()), &query) .await { Ok(list) => Json(list).into_response(), @@ -48,7 +51,7 @@ async fn post( } } - let link_item = LinkItemBuilder::default() + let item = LinkItemBuilder::default() .owner(user.id()) .url(payload.url()) .title(payload.title()) @@ -59,11 +62,11 @@ async fn post( .create( Box::new(app_state.analysis_service().clone()), Box::new(app_state.links_repo().clone()), - &link_item, + &item, ) .await { - Ok(link) => (StatusCode::CREATED, Json(link)).into_response(), + Ok(item) => (StatusCode::CREATED, Json(item)).into_response(), Err(e) => e.into_response(), } } @@ -73,13 +76,15 @@ async fn get( user: Claims, Path(id): Path, ) -> impl IntoResponse { - let link_query = LinkQueryBuilder::new(&id, user.id()).build(); + let query = LinkQueryBuilder::new(&id, user.id()) + .is_from_admin(user.is_admin()) + .build(); match app_state .links_service() - .get(Box::new(app_state.links_repo().clone()), &link_query) + .get(Box::new(app_state.links_repo().clone()), &query) .await { - Ok(link) => Json(link).into_response(), + Ok(item) => Json(item).into_response(), Err(e) => e.into_response(), } } @@ -97,9 +102,11 @@ async fn put( } } - let link_item = LinkItemBuilder::new(payload.url()) + let query = LinkQueryBuilder::new(&id, user.id()) + .is_from_admin(user.is_admin()) + .build(); + let item = LinkItemBuilder::new(payload.url()) .id(&id) - .owner(user.id()) .title(payload.title()) .description(payload.description()) .word_count(payload.word_count()) @@ -112,12 +119,12 @@ async fn put( .update( Box::new(app_state.analysis_service().clone()), Box::new(app_state.links_repo().clone()), - &id, - &link_item, + &query, + &item, ) .await { - Ok(link) => Json(link).into_response(), + Ok(item) => Json(item).into_response(), Err(e) => e.into_response(), } } @@ -127,10 +134,12 @@ async fn delete( user: Claims, Path(id): Path, ) -> impl IntoResponse { - let link_item = LinkItemBuilder::default().id(&id).owner(user.id()).build(); + let query = LinkQueryBuilder::new(&id, user.id()) + .is_from_admin(user.is_admin()) + .build(); match app_state .links_service() - .delete(Box::new(app_state.links_repo().clone()), &link_item) + .delete(Box::new(app_state.links_repo().clone()), &query) .await { Ok(()) => StatusCode::NO_CONTENT.into_response(), @@ -160,19 +169,24 @@ mod tests { use super::*; #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_links_empty(#[values(true, false)] is_admin: bool) { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_get_links_empty(#[case] is_admin: bool, #[case] user: &str) { + let search_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_search() - .withf(move |_, query| query == &repo_query) + .withf(move |_, query| query == &search_query) .times(1) .returning(|_, _| Ok(vec![])); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); - let response = list(State(app_state), Claims::new("user-id", is_admin, 0, 0)).await; + let response = list(State(app_state), Claims::new(user, is_admin, 0, 0)).await; let (parts, body) = response.into_response().into_parts(); assert_eq!(StatusCode::OK, parts.status); @@ -182,23 +196,28 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_links_non_empty(#[values(true, false)] is_admin: bool) { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_get_links_non_empty(#[case] is_admin: bool, #[case] user: &str) { + let search_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); let item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_search() - .withf(move |_, query| query == &repo_query) + .withf(move |_, query| query == &search_query) .times(1) .returning(move |_, _| Ok(vec![item.clone()])); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); - let response = list(State(app_state), Claims::new("user-id", is_admin, 0, 0)).await; + let response = list(State(app_state), Claims::new(user, is_admin, 0, 0)).await; let (parts, body) = response.into_response().into_parts(); assert_eq!(StatusCode::OK, parts.status); @@ -207,24 +226,29 @@ mod tests { let body = std::str::from_utf8(&body).unwrap(); let body: Vec = serde_json::from_str(body).unwrap(); assert!(body[0].id() == "1"); - assert!(body[0].owner() == "user-id"); + assert!(body[0].owner() == "user"); assert!(body[0].url() == "http://link"); } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_links_service_error(#[values(true, false)] is_admin: bool) { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_get_links_service_error(#[case] is_admin: bool, #[case] user: &str) { + let search_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_search() - .withf(move |_, query| query == &repo_query) + .withf(move |_, query| query == &search_query) .times(1) .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); - let response = list(State(app_state), Claims::new("user-id", is_admin, 0, 0)).await; + let response = list(State(app_state), Claims::new(user, is_admin, 0, 0)).await; let (parts, body) = response.into_response().into_parts(); assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, parts.status); @@ -238,10 +262,10 @@ mod tests { #[tokio::test] async fn test_post_link(#[values(true, false)] is_admin: bool) { let request = LinkItemRequest::new("http://link"); - let item_to_create = LinkItemBuilder::new("http://link").owner("user-id").build(); + let item_to_create = LinkItemBuilder::new("http://link").owner("user").build(); let created_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); let mut mock_links_service = MockLinksService::new(); @@ -254,7 +278,7 @@ mod tests { let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = post( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new("user", is_admin, 0, 0), Json(request), ) .await; @@ -266,7 +290,7 @@ mod tests { let body = std::str::from_utf8(&body).unwrap(); let body: LinkItem = serde_json::from_str(body).unwrap(); assert!(body.id() == "1"); - assert!(body.owner() == "user-id"); + assert!(body.owner() == "user"); assert!(body.url() == "http://link"); } @@ -281,7 +305,7 @@ mod tests { let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = post( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new("user", is_admin, 0, 0), Json(request), ) .await; @@ -298,7 +322,7 @@ mod tests { #[tokio::test] async fn test_post_link_service_error(#[values(true, false)] is_admin: bool) { let request = LinkItemRequest::new("http://link"); - let item_to_create = LinkItemBuilder::new("http://link").owner("user-id").build(); + let item_to_create = LinkItemBuilder::new("http://link").owner("user").build(); let mut mock_links_service = MockLinksService::new(); mock_links_service @@ -310,7 +334,7 @@ mod tests { let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = post( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new("user", is_admin, 0, 0), Json(request), ) .await; @@ -324,25 +348,29 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_link(#[values(true, false)] is_admin: bool) { - let repo_query = LinkQueryBuilder::new("1", "user-id").build(); - let retrieved_item = LinkItemBuilder::new("http://link") + async fn test_get_link(#[case] is_admin: bool, #[case] user: &str) { + let get_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_get() - .withf(move |_, query| query == &repo_query) + .withf(move |_, query| query == &get_query) .times(1) - .returning(move |_, _| Ok(retrieved_item.clone())); + .returning(move |_, _| Ok(item.clone())); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = get( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), ) .await; @@ -354,26 +382,30 @@ mod tests { let body = std::str::from_utf8(&body).unwrap(); let body: LinkItem = serde_json::from_str(body).unwrap(); assert!(body.id() == "1"); - assert!(body.owner() == "user-id"); + assert!(body.owner() == "user"); assert!(body.url() == "http://link"); } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_link_service_error(#[values(true, false)] is_admin: bool) { - let repo_query = LinkQueryBuilder::new("1", "user-id").build(); + async fn test_get_link_service_error(#[case] is_admin: bool, #[case] user: &str) { + let get_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_get() - .withf(move |_, query| query == &repo_query) + .withf(move |_, query| query == &get_query) .times(1) .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = get( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), ) .await; @@ -387,29 +419,28 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_put_link(#[values(true, false)] is_admin: bool) { + async fn test_put_link(#[case] is_admin: bool, #[case] user: &str) { let request = LinkItemRequest::new("http://link"); - let item_to_update = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") - .build(); - let updated_item = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") + let update_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); + let item_to_update = LinkItemBuilder::new("http://link").id("1").build(); + let updated_item = LinkItemBuilder::new("http://link").id("1").build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_update() - .withf(move |_, _, id, item| id == "1" && item == &item_to_update) + .withf(move |_, _, query, item| query == &update_query && item == &item_to_update) .times(1) .returning(move |_, _, _, _| Ok(updated_item.clone())); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = put( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), Json(request), ) @@ -422,7 +453,6 @@ mod tests { let body = std::str::from_utf8(&body).unwrap(); let body: LinkItem = serde_json::from_str(body).unwrap(); assert!(body.id() == "1"); - assert!(body.owner() == "user-id"); assert!(body.url() == "http://link"); } @@ -437,7 +467,7 @@ mod tests { let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = put( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new("user", is_admin, 0, 0), Path(String::from("1")), Json(request), ) @@ -452,25 +482,27 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_put_link_service_error(#[values(true, false)] is_admin: bool) { + async fn test_put_link_service_error(#[case] is_admin: bool, #[case] user: &str) { let request = LinkItemRequest::new("http://link"); - let item_to_update = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") + let update_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); + let item_to_update = LinkItemBuilder::new("http://link").id("1").build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_update() - .withf(move |_, _, id, item| id == "1" && item == &item_to_update) + .withf(move |_, _, query, item| query == &update_query && item == &item_to_update) .times(1) .returning(|_, _, _, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = put( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), Json(request), ) @@ -485,21 +517,25 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_delete_link(#[values(true, false)] is_admin: bool) { - let item_to_delete = LinkItemBuilder::default().id("1").owner("user-id").build(); + async fn test_delete_link(#[case] is_admin: bool, #[case] user: &str) { + let delete_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_delete() - .withf(move |_, item| item == &item_to_delete) + .withf(move |_, query| query == &delete_query) .times(1) .returning(move |_, _| Ok(())); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = delete( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), ) .await; @@ -509,21 +545,25 @@ mod tests { } #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_delete_link_service_error(#[values(true, false)] is_admin: bool) { - let item_to_delete = LinkItemBuilder::default().id("1").owner("user-id").build(); + async fn test_delete_link_service_error(#[case] is_admin: bool, #[case] user: &str) { + let delete_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); let mut mock_links_service = MockLinksService::new(); mock_links_service .expect_delete() - .withf(move |_, item| item == &item_to_delete) + .withf(move |_, query| query == &delete_query) .times(1) .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = delete( State(app_state), - Claims::new("user-id", is_admin, 0, 0), + Claims::new(user, is_admin, 0, 0), Path(String::from("1")), ) .await; diff --git a/link-for-later/src/repository.rs b/link-for-later/src/repository.rs index 1238a3a..df4f815 100644 --- a/link-for-later/src/repository.rs +++ b/link-for-later/src/repository.rs @@ -15,8 +15,8 @@ pub trait Links { async fn find(&self, query: &LinkQuery) -> Result>; async fn get(&self, query: &LinkQuery) -> Result; async fn create(&self, item: &LinkItem) -> Result; - async fn update(&self, id: &str, item: &LinkItem) -> Result; - async fn delete(&self, item: &LinkItem) -> Result<()>; + async fn update(&self, query: &LinkQuery, item: &LinkItem) -> Result; + async fn delete(&self, query: &LinkQuery) -> Result<()>; } #[cfg_attr(test, automock)] diff --git a/link-for-later/src/repository/inmemory.rs b/link-for-later/src/repository/inmemory.rs index 11c48d0..3ca52cd 100644 --- a/link-for-later/src/repository/inmemory.rs +++ b/link-for-later/src/repository/inmemory.rs @@ -3,8 +3,7 @@ use std::sync::Mutex; use axum::async_trait; use crate::types::{ - AppError, LinkItem, LinkItemBuilder, LinkQuery, LinkQueryBuilder, Result, UserInfo, - UserInfoBuilder, UserQuery, + AppError, LinkItem, LinkItemBuilder, LinkQuery, Result, UserInfo, UserInfoBuilder, UserQuery, }; use super::{Links as LinksRepository, Users as UsersRepository}; @@ -47,7 +46,7 @@ impl LinksRepository for LinksRepositoryProvider { .iter() .filter(|link| { (link.id() == query.id() || query.id().is_empty()) - && (link.owner() == query.owner() || query.owner().is_empty()) + && (link.owner() == query.user() || query.user().is_empty()) }) .cloned() .collect(); @@ -59,7 +58,9 @@ impl LinksRepository for LinksRepositoryProvider { .lock() .map_err(|e| AppError::Database(format!("get() {e:?}")))? .iter() - .find(|link| link.id() == query.id() && link.owner() == query.owner()) + .find(|link| { + link.id() == query.id() && (link.owner() == query.user() || query.user().is_empty()) + }) .cloned() .ok_or_else(|| AppError::LinkNotFound(query.id().to_owned())) } @@ -85,15 +86,15 @@ impl LinksRepository for LinksRepositoryProvider { Ok(link) } - async fn update(&self, id: &str, item: &LinkItem) -> Result { + async fn update(&self, query: &LinkQuery, item: &LinkItem) -> Result { self.links_data .lock() .map_err(|e| AppError::Database(format!("update() {e:?}")))? .iter() - .find(|link| link.id() == id && link.owner() == item.owner()) + .find(|link| link.id() == query.id() && link.owner() == item.owner()) .cloned() - .ok_or_else(|| AppError::LinkNotFound(id.to_owned()))?; - self.delete(item).await?; + .ok_or_else(|| AppError::LinkNotFound(query.id().to_owned()))?; + self.delete(query).await?; self.links_data .lock() .map_err(|e| AppError::Database(format!("update() {e:?}")))? @@ -101,9 +102,8 @@ impl LinksRepository for LinksRepositoryProvider { Ok(item.clone()) } - async fn delete(&self, item: &LinkItem) -> Result<()> { - let query = LinkQueryBuilder::new(item.id(), item.owner()).build(); - self.get(&query).await?; + async fn delete(&self, query: &LinkQuery) -> Result<()> { + self.get(query).await?; self.links_data .lock() .map_err(|e| AppError::Database(format!("delete() {e:?}")))? @@ -149,13 +149,13 @@ impl UsersRepository for UsersRepositoryProvider { #[cfg(test)] mod tests { - use crate::types::UserQueryBuilder; + use crate::types::{LinkQueryBuilder, UserQueryBuilder}; use super::*; #[tokio::test] async fn test_search_links_empty() { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); + let repo_query = LinkQueryBuilder::default().user("user-id").build(); let links_repository = LinksRepositoryProvider::default(); let retrieved_items = links_repository.find(&repo_query).await.unwrap(); @@ -170,7 +170,7 @@ mod tests { 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 repo_query = LinkQueryBuilder::default().user("user-id").build(); let retrieved_items = links_repository.find(&repo_query).await.unwrap(); assert!(!retrieved_items.is_empty()); assert!(retrieved_items @@ -203,10 +203,11 @@ mod tests { #[tokio::test] async fn test_update_link_not_found() { + let repo_query = LinkQueryBuilder::new("1", "user-id").build(); let item = LinkItemBuilder::new("http://link").owner("user-id").build(); let links_repository = LinksRepositoryProvider::default(); - let response = links_repository.update("1", &item).await; + let response = links_repository.update(&repo_query, &item).await; assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } @@ -218,13 +219,11 @@ mod tests { 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 item = LinkItemBuilder::from(created_item.clone()) .title("title") .build(); - let updated_item = links_repository - .update(created_item.id(), &item) - .await - .unwrap(); + let updated_item = links_repository.update(&repo_query, &item).await.unwrap(); let repo_query = LinkQueryBuilder::new(updated_item.id(), "user-id").build(); let retrieved_item = links_repository.get(&repo_query).await.unwrap(); @@ -234,13 +233,10 @@ mod tests { #[tokio::test] async fn test_delete_link_not_found() { - let item = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") - .build(); + let repo_query = LinkQueryBuilder::new("1", "user-id").build(); let links_repository = LinksRepositoryProvider::default(); - let response = links_repository.delete(&item).await; + let response = links_repository.delete(&repo_query).await; assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } @@ -252,7 +248,8 @@ mod tests { 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(); + links_repository.delete(&repo_query).await.unwrap(); let repo_query = LinkQueryBuilder::new(created_item.id(), "user-id").build(); let response = links_repository.get(&repo_query).await; diff --git a/link-for-later/src/repository/mongodb.rs b/link-for-later/src/repository/mongodb.rs index 0323db5..f57fb55 100644 --- a/link-for-later/src/repository/mongodb.rs +++ b/link-for-later/src/repository/mongodb.rs @@ -4,8 +4,7 @@ use futures::TryStreamExt; use mongodb::{options::ReplaceOptions, Collection, Database}; use crate::types::{ - AppError, LinkItem, LinkItemBuilder, LinkQuery, LinkQueryBuilder, Result, UserInfo, - UserInfoBuilder, UserQuery, + AppError, LinkItem, LinkItemBuilder, LinkQuery, Result, UserInfo, UserInfoBuilder, UserQuery, }; use super::{Links as LinksRepository, Users as UsersRepository}; @@ -87,8 +86,7 @@ impl LinksRepository for LinksRepositoryProvider { Ok(LinkItemBuilder::from(item.clone()).id(&id).build()) } - async fn update(&self, id: &str, item: &LinkItem) -> Result { - let query = LinkQueryBuilder::new(id, item.owner()).build(); + async fn update(&self, query: &LinkQuery, item: &LinkItem) -> Result { let db_query = to_document(&query).map_err(|_| AppError::Database("to_document failed".into()))?; let opts = ReplaceOptions::builder().upsert(true).build(); @@ -99,8 +97,7 @@ impl LinksRepository for LinksRepositoryProvider { Ok(item.clone()) } - async fn delete(&self, item: &LinkItem) -> Result<()> { - let query = LinkQueryBuilder::new(item.id(), item.owner()).build(); + async fn delete(&self, query: &LinkQuery) -> Result<()> { let db_query = to_document(&query).map_err(|_| AppError::Database("to_document failed".into()))?; self.links_collection diff --git a/link-for-later/src/service.rs b/link-for-later/src/service.rs index abd4ade..2bfff78 100644 --- a/link-for-later/src/service.rs +++ b/link-for-later/src/service.rs @@ -19,35 +19,31 @@ pub trait Links { async fn search( &self, links_repo: Box, - link_query: &LinkQuery, + query: &LinkQuery, ) -> Result>; async fn get( &self, links_repo: Box, - link_query: &LinkQuery, + query: &LinkQuery, ) -> Result; async fn create( &self, analysis_service: Box, links_repo: Box, - link_item: &LinkItem, + item: &LinkItem, ) -> Result; async fn update( &self, analysis_service: Box, links_repo: Box, - id: &str, - link_item: &LinkItem, + query: &LinkQuery, + item: &LinkItem, ) -> Result; - async fn delete( - &self, - links_repo: Box, - link_item: &LinkItem, - ) -> Result<()>; + async fn delete(&self, links_repo: Box, query: &LinkQuery) -> Result<()>; } #[cfg_attr(test, automock)] diff --git a/link-for-later/src/service/links.rs b/link-for-later/src/service/links.rs index 2c008f6..3da04b2 100644 --- a/link-for-later/src/service/links.rs +++ b/link-for-later/src/service/links.rs @@ -15,20 +15,25 @@ impl LinksService for ServiceProvider { async fn search( &self, links_repo: Box, - link_query: &LinkQuery, + query: &LinkQuery, ) -> Result> { - links_repo.find(link_query).await + let find_query = if query.is_from_admin() { + LinkQueryBuilder::default().user("").build() + } else { + LinkQueryBuilder::default().user(query.user()).build() + }; + links_repo.find(&find_query).await } async fn get( &self, links_repo: Box, - link_query: &LinkQuery, + query: &LinkQuery, ) -> Result { - let get_query = LinkQueryBuilder::default().id(link_query.id()).build(); + let get_query = LinkQueryBuilder::default().id(query.id()).build(); let retrieved_item = links_repo.get(&get_query).await?; - if link_query.owner() == retrieved_item.owner() { + if query.user() == retrieved_item.owner() || query.is_from_admin() { Ok(retrieved_item) } else { Err(AppError::Authorization(String::from( @@ -41,55 +46,52 @@ impl LinksService for ServiceProvider { &self, analysis_service: Box, links_repo: Box, - link_item: &LinkItem, + item: &LinkItem, ) -> Result { let now = Utc::now(); - let created_link_item = LinkItemBuilder::from(link_item.clone()) + let created_item = LinkItemBuilder::from(item.clone()) .created_at(&now) .updated_at(&now) .build(); - let created_link_item = links_repo.create(&created_link_item).await?; + let created_item = links_repo.create(&created_item).await?; - analysis_service.analyze(&created_link_item).await?; + analysis_service.analyze(&created_item).await?; - Ok(created_link_item) + Ok(created_item) } async fn update( &self, analysis_service: Box, links_repo: Box, - id: &str, - link_item: &LinkItem, + query: &LinkQuery, + item: &LinkItem, ) -> Result { - let link_query = LinkQueryBuilder::new(id, link_item.owner()).build(); - let retrieved_item = self.get(links_repo.clone(), &link_query).await?; + let retrieved_item = self.get(links_repo.clone(), query).await?; let now = Utc::now(); - let updated_link_item = LinkItemBuilder::from(link_item.clone()) + let updated_item = LinkItemBuilder::from(item.clone()) + .owner(retrieved_item.owner()) .created_at(retrieved_item.created_at()) .updated_at(&now) .build(); - let updated_link_item = links_repo.update(id, &updated_link_item).await?; + let update_query = LinkQueryBuilder::default().id(query.id()).build(); + let updated_item = links_repo.update(&update_query, &updated_item).await?; - if updated_link_item.url() != retrieved_item.url() { - analysis_service.analyze(&updated_link_item).await?; + if updated_item.url() != retrieved_item.url() { + analysis_service.analyze(&updated_item).await?; } - Ok(updated_link_item) + Ok(updated_item) } - async fn delete( - &self, - links_repo: Box, - link_item: &LinkItem, - ) -> Result<()> { - let link_query = LinkQueryBuilder::new(link_item.id(), link_item.owner()).build(); - self.get(links_repo.clone(), &link_query).await?; + async fn delete(&self, links_repo: Box, query: &LinkQuery) -> Result<()> { + self.get(links_repo.clone(), query).await?; - links_repo.delete(link_item).await + let delete_query = LinkQueryBuilder::default().id(query.id()).build(); + links_repo.delete(&delete_query).await } } @@ -98,6 +100,7 @@ mod tests { use std::sync::Arc; use mockall::Sequence; + use rstest::rstest; use crate::{ repository::MockLinks as MockLinksRepo, service::MockAnalysis as MockAnalysisService, @@ -106,48 +109,68 @@ mod tests { use super::*; + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_search_links_empty() { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); - let expected_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_search_links_empty(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); + let find_query = if is_admin { + LinkQueryBuilder::default().user("").build() + } else { + LinkQueryBuilder::default().user(user).build() + }; let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_find() - .withf(move |query| query == &expected_query) + .withf(move |query| query == &find_query) .times(1) .returning(|_| Ok(vec![])); let links_service = ServiceProvider {}; let response = links_service - .search(Box::new(Arc::new(mock_links_repo)), &repo_query) + .search(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert!(response.is_ok()); assert!(response.unwrap().is_empty()); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_search_links_non_empty() { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); - let expected_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_search_links_non_empty(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); + let find_query = if is_admin { + LinkQueryBuilder::default().user("").build() + } else { + LinkQueryBuilder::default().user(user).build() + }; let item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); let expected_items = vec![item.clone()]; let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_find() - .withf(move |query| query == &expected_query) + .withf(move |query| query == &find_query) .times(1) .returning(move |_| Ok(vec![item.clone()])); let links_service = ServiceProvider {}; let response = links_service - .search(Box::new(Arc::new(mock_links_repo)), &repo_query) + .search(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert!(response.is_ok()); @@ -159,40 +182,58 @@ mod tests { .all(|item| expected_items.contains(item))); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_search_links_repo_error() { - let repo_query = LinkQueryBuilder::default().owner("user-id").build(); - let expected_query = LinkQueryBuilder::default().owner("user-id").build(); + async fn test_search_links_repo_error(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::default() + .user(user) + .is_from_admin(is_admin) + .build(); + let find_query = if is_admin { + LinkQueryBuilder::default().user("").build() + } else { + LinkQueryBuilder::default().user(user).build() + }; let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_find() - .withf(move |query| query == &expected_query) + .withf(move |query| query == &find_query) .times(1) .returning(|_| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service - .search(Box::new(Arc::new(mock_links_repo)), &repo_query) + .search(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert_eq!(response, Err(AppError::Test)); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_link() { - let repo_query = LinkQueryBuilder::new("1", "user-id").build(); + async fn test_get_link(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") + .build(); + let response_item = LinkItemBuilder::new("http://link") + .id("1") + .owner("user") .build(); - let request_query = repo_query.clone(); - let response_item = retrieved_item.clone(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &LinkQueryBuilder::default().id("1").build()) + .withf(move |query| query == &get_query) .times(1) .returning(move |_| Ok(retrieved_item.clone())); @@ -207,17 +248,17 @@ mod tests { #[tokio::test] async fn test_get_link_unauthorized() { - let repo_query = LinkQueryBuilder::new("1", "unauthorized-user-id").build(); + let request_query = LinkQueryBuilder::new("1", "unauthorized-user").build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_query = repo_query.clone(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &LinkQueryBuilder::default().id("1").build()) + .withf(move |query| query == &get_query) .times(1) .returning(move |_| Ok(retrieved_item.clone())); @@ -234,15 +275,20 @@ mod tests { ); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_get_link_not_found() { - let repo_query = LinkQueryBuilder::new("1", "user-id").build(); - let request_query = repo_query.clone(); + async fn test_get_link_not_found(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &LinkQueryBuilder::default().id("1").build()) + .withf(move |query| query == &get_query) .times(1) .returning(|_| Err(AppError::LinkNotFound("1".into()))); @@ -256,14 +302,14 @@ mod tests { #[tokio::test] async fn test_create_link() { - let item_to_create = LinkItemBuilder::new("http://link").owner("user-id").build(); - let created_item = LinkItemBuilder::new("http://link") + let request_item = LinkItemBuilder::new("http://link").owner("user").build(); + let response_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); + let item_to_create = request_item.clone(); + let created_item = response_item.clone(); let item_to_analyze = created_item.clone(); - let request_item = item_to_create.clone(); - let response_item = created_item.clone(); let mut seq = Sequence::new(); @@ -304,8 +350,8 @@ mod tests { #[tokio::test] async fn test_create_link_repo_error() { - let item_to_create = LinkItemBuilder::new("http://link").owner("user-id").build(); - let request_item = item_to_create.clone(); + let request_item = LinkItemBuilder::new("http://link").owner("user").build(); + let item_to_create = request_item.clone(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo @@ -333,13 +379,14 @@ mod tests { #[tokio::test] async fn test_create_link_analyze_error() { - let item_to_create = LinkItemBuilder::new("http://link").owner("user-id").build(); - let created_item = LinkItemBuilder::new("http://link") + let request_item = LinkItemBuilder::new("http://link").owner("user").build(); + let response_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); + let item_to_create = request_item.clone(); + let created_item = response_item.clone(); let item_to_analyze = created_item.clone(); - let request_item = item_to_create.clone(); let mut seq = Sequence::new(); @@ -377,38 +424,47 @@ mod tests { assert_eq!(response, Err(AppError::Test)); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_update_link() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://link") - .owner("user-id") + async fn test_update_link(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let request_item = LinkItemBuilder::new("http://link") .description("sample link") .build(); - let retrieved_item = LinkItemBuilder::new("http://link") + let response_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") + .description("sample link") .build(); - let updated_item = LinkItemBuilder::new("http://link") + let get_query = LinkQueryBuilder::default().id("1").build(); + let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") + .build(); + let update_query = LinkQueryBuilder::default().id("1").build(); + let item_to_update = LinkItemBuilder::new("http://link") + .owner("user") .description("sample link") .build(); - let request_item = item_to_update.clone(); - let response_item = updated_item.clone(); + let updated_item = response_item.clone(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_update() - .withf(move |id, item| { - id == "1" + .withf(move |query, item| { + query == &update_query && item.id() == item_to_update.id() && item.url() == item_to_update.url() && item.owner() == item_to_update.owner() @@ -425,7 +481,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -434,37 +490,44 @@ mod tests { assert_eq!(response.unwrap(), response_item); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_update_link_update_url() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://updated-link") - .owner("user-id") + async fn test_update_link_update_url(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); - let retrieved_item = LinkItemBuilder::new("http://link") + let request_item = LinkItemBuilder::new("http://updated-link").build(); + let response_item = LinkItemBuilder::new("http://updated-link") .id("1") - .owner("user-id") + .owner("user") .build(); - let updated_item = LinkItemBuilder::new("http://updated-link") + let get_query = LinkQueryBuilder::default().id("1").build(); + let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); + let update_query = LinkQueryBuilder::default().id("1").build(); + let item_to_update = LinkItemBuilder::new("http://updated-link") + .owner("user") + .build(); + let updated_item = response_item.clone(); let item_to_analyze = updated_item.clone(); - let request_item = item_to_update.clone(); - let response_item = updated_item.clone(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_update() - .withf(move |id, item| { - id == "1" + .withf(move |query, item| { + query == &update_query && item.id() == item_to_update.id() && item.url() == item_to_update.url() && item.owner() == item_to_update.owner() @@ -490,7 +553,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -501,21 +564,21 @@ mod tests { #[tokio::test] async fn test_update_link_unauthorized() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://link") - .owner("unauthorized-user-id") + let request_query = LinkQueryBuilder::new("1", "unauthorized-user").build(); + let request_item = LinkItemBuilder::new("http://link") + .owner("unauthorized-user") .description("sample link") .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_item = item_to_update.clone(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo.expect_update().times(0); @@ -528,7 +591,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -541,19 +604,24 @@ mod tests { ); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_update_link_not_found() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://link") - .owner("user-id") + async fn test_update_link_not_found(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let request_item = LinkItemBuilder::new("http://link") + .owner("user") .description("sample link") .build(); - let request_item = item_to_update.clone(); + let get_query = LinkQueryBuilder::default().id("1").build(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .returning(|_| Err(AppError::LinkNotFound("1".into()))); mock_links_repo.expect_update().times(0); @@ -566,7 +634,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -574,32 +642,39 @@ mod tests { assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_update_link_repo_error() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://link") - .owner("user-id") + async fn test_update_link_repo_error(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let request_item = LinkItemBuilder::new("http://link") + .owner("user") .description("sample link") .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_item = item_to_update.clone(); + let update_query = LinkQueryBuilder::default().id("1").build(); + let item_to_update = request_item.clone(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_update() - .withf(move |id, item| { - id == "1" + .withf(move |query, item| { + query == &update_query && item.id() == item_to_update.id() && item.url() == item_to_update.url() && item.owner() == item_to_update.owner() @@ -616,7 +691,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -624,36 +699,45 @@ mod tests { assert_eq!(response, Err(AppError::Test)); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_update_link_analyze_error() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_update = LinkItemBuilder::new("http://updated-link") - .owner("user-id") + async fn test_update_link_analyze_error(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) + .build(); + let request_item = LinkItemBuilder::new("http://updated-link") + .owner("user") .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") + .build(); + let update_query = LinkQueryBuilder::default().id("1").build(); + let item_to_update = LinkItemBuilder::new("http://updated-link") + .owner("user") .build(); let updated_item = LinkItemBuilder::new("http://updated-link") .id("1") - .owner("user-id") + .owner("user") .build(); let item_to_analyze = updated_item.clone(); - let request_item = item_to_update.clone(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_update() - .withf(move |id, item| { - id == "1" + .withf(move |query, item| { + query == &update_query && item.id() == item_to_update.id() && item.url() == item_to_update.url() && item.owner() == item_to_update.owner() @@ -679,7 +763,7 @@ mod tests { .update( Box::new(Arc::new(mock_analysis_service)), Box::new(Arc::new(mock_links_repo)), - "1", + &request_query, &request_item, ) .await; @@ -687,42 +771,40 @@ mod tests { assert_eq!(response, Err(AppError::Test)); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_delete_link() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_delete = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") + async fn test_delete_link(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_item = item_to_delete.clone(); + let delete_query = LinkQueryBuilder::default().id("1").build(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_delete() - .withf(move |item| { - item.id() == item_to_delete.id() - && item.url() == item_to_delete.url() - && item.owner() == item_to_delete.owner() - }) + .withf(move |query| query == &delete_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(())); let links_service = ServiceProvider {}; let response = links_service - .delete(Box::new(Arc::new(mock_links_repo)), &request_item) + .delete(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert!(response.is_ok()); @@ -730,28 +812,24 @@ mod tests { #[tokio::test] async fn test_delete_link_unauthorized() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_delete = LinkItemBuilder::new("http://link") - .id("1") - .owner("unauthorized-user-id") - .build(); + let request_query = LinkQueryBuilder::new("1", "unauthorized-user").build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_item = item_to_delete.clone(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo.expect_delete().times(0); let links_service = ServiceProvider {}; let response = links_service - .delete(Box::new(Arc::new(mock_links_repo)), &request_item) + .delete(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert_eq!( @@ -762,74 +840,66 @@ mod tests { ); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_delete_link_not_found() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_delete = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") + async fn test_delete_link_not_found(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); - let request_item = item_to_delete.clone(); + let get_query = LinkQueryBuilder::default().id("1").build(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .returning(|_| Err(AppError::LinkNotFound("1".into()))); - mock_links_repo - .expect_delete() - .withf(move |item| { - item.id() == item_to_delete.id() - && item.url() == item_to_delete.url() - && item.owner() == item_to_delete.owner() - }) - .times(0); + mock_links_repo.expect_delete().times(0); let links_service = ServiceProvider {}; let response = links_service - .delete(Box::new(Arc::new(mock_links_repo)), &request_item) + .delete(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert_eq!(response, Err(AppError::LinkNotFound("1".into()))); } + #[rstest] + #[case(true, "admin")] + #[case(false, "user")] #[tokio::test] - async fn test_delete_link_repo_error() { - let repo_query = LinkQueryBuilder::default().id("1").build(); - let item_to_delete = LinkItemBuilder::new("http://link") - .id("1") - .owner("user-id") + async fn test_delete_link_repo_error(#[case] is_admin: bool, #[case] user: &str) { + let request_query = LinkQueryBuilder::new("1", user) + .is_from_admin(is_admin) .build(); + let get_query = LinkQueryBuilder::default().id("1").build(); let retrieved_item = LinkItemBuilder::new("http://link") .id("1") - .owner("user-id") + .owner("user") .build(); - let request_item = item_to_delete.clone(); + let delete_query = LinkQueryBuilder::default().id("1").build(); let mut seq = Sequence::new(); let mut mock_links_repo = MockLinksRepo::new(); mock_links_repo .expect_get() - .withf(move |query| query == &repo_query) + .withf(move |query| query == &get_query) .times(1) .in_sequence(&mut seq) .returning(move |_| Ok(retrieved_item.clone())); mock_links_repo .expect_delete() - .withf(move |item| { - item.id() == item_to_delete.id() - && item.url() == item_to_delete.url() - && item.owner() == item_to_delete.owner() - }) + .withf(move |query| query == &delete_query) .times(1) .in_sequence(&mut seq) .returning(|_| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service - .delete(Box::new(Arc::new(mock_links_repo)), &request_item) + .delete(Box::new(Arc::new(mock_links_repo)), &request_query) .await; assert_eq!(response, Err(AppError::Test)); diff --git a/link-for-later/tests/auth/mod.rs b/link-for-later/tests/auth/mod.rs index 18973fb..2b5adc7 100644 --- a/link-for-later/tests/auth/mod.rs +++ b/link-for-later/tests/auth/mod.rs @@ -6,15 +6,17 @@ const JWT_SECRET_KEY: &str = "JWT_SECRET"; #[derive(Debug, Serialize, Deserialize)] struct Claims { - sub: String, - iat: usize, - exp: usize, + sub: String, // email + admin: bool, // admin role + iat: usize, // creation time + exp: usize, // expiration time } -pub fn generate_token(email: &str) -> String { +pub fn generate_token(email: &str, is_admin: bool) -> String { let now = Utc::now(); let claims = Claims { sub: email.to_string(), + admin: is_admin, iat: now.timestamp() as usize, exp: 10000000000, }; diff --git a/link-for-later/tests/links.rs b/link-for-later/tests/links.rs index 1755ad3..b85fa12 100644 --- a/link-for-later/tests/links.rs +++ b/link-for-later/tests/links.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +use std::collections::HashMap; + use axum::{ body::Body, http::{Request, StatusCode}, @@ -18,13 +20,17 @@ mod auth; mod repository; #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] async fn test_get_links_empty( #[values(DatabaseType::InMemory, DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, ) { repository::new(&db_type); - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -46,12 +52,18 @@ async fn test_get_links_empty( } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_get_links_non_empty(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_get_links_non_empty( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -70,7 +82,7 @@ async fn test_get_links_non_empty(#[values(DatabaseType::MongoDb)] db_type: Data let body = response.into_body().collect().await.unwrap().to_bytes(); let body = std::str::from_utf8(&body).unwrap(); - let body: Vec = serde_json::from_str(body).unwrap(); + let body: Vec = serde_json::from_str(body).unwrap(); assert!(body.len() == 1); assert!(body[0].id() == id); assert!(body[0].owner() == "user@test.com"); @@ -78,12 +90,73 @@ async fn test_get_links_non_empty(#[values(DatabaseType::MongoDb)] db_type: Data } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_get_link_item_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_get_links_multiple_entries( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { + let repository = repository::new(&db_type); + + let mut ids: HashMap = HashMap::new(); + + let id = repository.add_link("user@test.com", "http://test1").await; + ids.insert(id, ("user@test.com".into(), "http://test1".into())); + + let second_entry_owner = if is_admin { + "another-user@test.com" + } else { + user + }; + let id = repository + .add_link(second_entry_owner, "http://test2") + .await; + ids.insert(id, (second_entry_owner.into(), "http://test2".into())); + + let token = auth::generate_token(user, is_admin); + + let response = app::new(&db_type) + .await + .oneshot( + Request::builder() + .method("GET") + .uri("/v1/links") + .header("Authorization", format!("Bearer {}", token)) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body = std::str::from_utf8(&body).unwrap(); + let body: Vec = serde_json::from_str(body).unwrap(); + + assert!(body.len() == ids.len()); + for item in body { + assert!(ids.contains_key(item.id())); + assert!(ids.get(item.id()).unwrap().0 == item.owner()); + assert!(ids.get(item.id()).unwrap().1 == item.url()); + } +} + +#[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] +#[tokio::test] +async fn test_get_link_item_found( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -109,12 +182,18 @@ async fn test_get_link_item_found(#[values(DatabaseType::MongoDb)] db_type: Data } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_get_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_get_link_item_not_found( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -138,10 +217,13 @@ async fn test_get_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: #[rstest] #[tokio::test] -async fn test_post_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_post_link( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[values(true, false)] is_admin: bool, +) { let repository = repository::new(&db_type); - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token("user@test.com", is_admin); let request = r#"{ "url": "http://test" }"#; @@ -179,10 +261,13 @@ async fn test_post_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) #[rstest] #[tokio::test] -async fn test_post_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_post_link_invalid_url( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[values(true, false)] is_admin: bool, +) { let repository = repository::new(&db_type); - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token("user@test.com", is_admin); let request = r#"{ "url": "invalid" }"#; @@ -212,12 +297,19 @@ async fn test_post_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: Da } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_put_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_put_link( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); + let request = r#"{ "url": "http://update" }"#; @@ -256,12 +348,19 @@ async fn test_put_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_put_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_put_link_invalid_url( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); + let request = r#"{ "url": "invalid" }"#; @@ -291,12 +390,19 @@ async fn test_put_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: Dat } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_put_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_put_link_item_not_found( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); + let request = r#"{ "url": "http://update" }"#; @@ -331,12 +437,18 @@ async fn test_put_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_delete_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_delete_link( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -362,12 +474,18 @@ async fn test_delete_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_delete_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_delete_link_item_not_found( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); let id = repository.add_link("user@test.com", "http://test").await; - let token = auth::generate_token("user@test.com"); + let token = auth::generate_token(user, is_admin); let response = app::new(&db_type) .await @@ -395,7 +513,7 @@ async fn test_delete_link_item_not_found(#[values(DatabaseType::MongoDb)] db_typ let db_item = repository.get_link(&id).await; assert!(db_item.id() == id); assert!(db_item.owner() == "user@test.com"); - assert!(db_item.url() == "http://test"); // not updated + assert!(db_item.url() == "http://test"); } #[rstest] diff --git a/link-for-later/tests/users.rs b/link-for-later/tests/users.rs index 323c8ff..6ebf643 100644 --- a/link-for-later/tests/users.rs +++ b/link-for-later/tests/users.rs @@ -20,14 +20,24 @@ mod auth; mod repository; #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] -async fn test_register_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_register_user( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, +) { let repository = repository::new(&db_type); - let request = r#"{ - "email": "user@test.com", - "password": "test" - }"#; + let request = format!( + r#"{{ + "email": "{}", + "password": "test", + "admin": {} + }}"#, + user, is_admin + ); let response = app::new(&db_type) .await @@ -50,20 +60,27 @@ async fn test_register_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseTy let db_count = repository.count_users().await; assert!(db_count == 1); - let db_item = repository.get_user("user@test.com").await; - assert!(db_item.email() == "user@test.com"); + let db_item = repository.get_user(user).await; + assert!(db_item.email() == user); assert!(db_item.password() != "test"); // verify password is not saved in plaintext } #[rstest] #[tokio::test] -async fn test_register_user_invalid_email(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { +async fn test_register_user_invalid_email( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[values(true, false)] is_admin: bool, +) { let repository = repository::new(&db_type); - let request = r#"{ + let request = format!( + r#"{{ "email": "user", - "password": "test" - }"#; + "password": "test", + "admin": {} + }}"#, + is_admin + ); let response = app::new(&db_type) .await @@ -89,17 +106,26 @@ async fn test_register_user_invalid_email(#[values(DatabaseType::MongoDb)] db_ty } #[rstest] +#[case(true, "admin@test.com")] +#[case(false, "user@test.com")] #[tokio::test] async fn test_register_user_already_registered( #[values(DatabaseType::MongoDb)] db_type: DatabaseType, + #[case] is_admin: bool, + #[case] user: &str, ) { let repository = repository::new(&db_type); - repository.add_user("user@test.com", "test").await; - let request = r#"{ - "email": "user@test.com", - "password": "test" - }"#; + repository.add_user(user, "test").await; + + let request = format!( + r#"{{ + "email": "{}", + "password": "test", + "admin": {} + }}"#, + user, is_admin + ); let response = app::new(&db_type) .await