diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index abb4458..0850241 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|repository" --lcov --output-path lcov.info + cargo llvm-cov --ignore-filename-regex "main|inmemory" --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 5c076f2..fff9350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.30" @@ -1320,6 +1326,7 @@ dependencies = [ "mongodb", "once_cell", "rand", + "rstest", "serde", "serde_json", "tokio", @@ -2050,6 +2057,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2074,6 +2087,35 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.0", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.0", + "syn 2.0.43", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/link-for-later/Cargo.toml b/link-for-later/Cargo.toml index e69d55f..d57c91c 100644 --- a/link-for-later/Cargo.toml +++ b/link-for-later/Cargo.toml @@ -26,5 +26,6 @@ validator = { version = "0.16.1", features = ["derive"] } [dev-dependencies] mockall = "0.12.0" rand = "0.8.5" +rstest = "0.18.2" tracing-test = "0.2.4" diff --git a/link-for-later/src/app.rs b/link-for-later/src/app.rs index a24991f..b15ae44 100644 --- a/link-for-later/src/app.rs +++ b/link-for-later/src/app.rs @@ -15,12 +15,14 @@ pub fn new(db: Database) -> Router { let users_service = Arc::new(service::users::ServiceProvider {}) as DynUsersService; let (links_repo, users_repo) = match db { Database::MongoDb(db) => ( - Arc::new(repository::mongodb::LinksDb::new(&db)) as DynLinksRepository, - Arc::new(repository::mongodb::UsersDb::new(&db)) as DynUsersRepository, + Arc::new(repository::mongodb::LinksRepositoryProvider::new(&db)) as DynLinksRepository, + Arc::new(repository::mongodb::UsersRepositoryProvider::new(&db)) as DynUsersRepository, ), Database::InMemory => ( - Arc::new(repository::inmemory::LinksDb::default()) as DynLinksRepository, - Arc::new(repository::inmemory::UsersDb::default()) as DynUsersRepository, + Arc::new(repository::inmemory::LinksRepositoryProvider::default()) + as DynLinksRepository, + Arc::new(repository::inmemory::UsersRepositoryProvider::default()) + as DynUsersRepository, ), }; diff --git a/link-for-later/src/controller/error.rs b/link-for-later/src/controller/error.rs index f2ebd7e..87a0bbc 100644 --- a/link-for-later/src/controller/error.rs +++ b/link-for-later/src/controller/error.rs @@ -10,7 +10,7 @@ use crate::types::AppError; impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match self { - Self::ServerError | Self::DatabaseError => { + Self::ServerError | Self::DatabaseError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) } Self::LinkNotFound => (StatusCode::NOT_FOUND, self.to_string()), diff --git a/link-for-later/src/repository/inmemory.rs b/link-for-later/src/repository/inmemory.rs index 19fded3..d69599a 100644 --- a/link-for-later/src/repository/inmemory.rs +++ b/link-for-later/src/repository/inmemory.rs @@ -12,19 +12,19 @@ use crate::types::{ use super::{Links as LinksRepository, Users as UsersRepository}; #[derive(Default)] -pub struct LinksDb {} +pub struct LinksRepositoryProvider {} static INMEMORY_LINKS_DATA: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); static INMEMORY_LINKS_DATA_COUNTER: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); #[derive(Default)] -pub struct UsersDb {} +pub struct UsersRepositoryProvider {} static INMEMORY_USERS_DATA: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); static INMEMORY_USERS_DATA_COUNTER: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); #[async_trait] -impl LinksRepository for LinksDb { +impl LinksRepository for LinksRepositoryProvider { async fn find(&self, query: &LinkQuery) -> Result> { let filtered_links: Vec = INMEMORY_LINKS_DATA .lock() @@ -84,7 +84,7 @@ impl LinksRepository for LinksDb { } #[async_trait] -impl UsersRepository for UsersDb { +impl UsersRepository for UsersRepositoryProvider { async fn get(&self, query: &UserQuery) -> Result { INMEMORY_USERS_DATA .lock() diff --git a/link-for-later/src/repository/mongodb.rs b/link-for-later/src/repository/mongodb.rs index bde2e89..36de5bf 100644 --- a/link-for-later/src/repository/mongodb.rs +++ b/link-for-later/src/repository/mongodb.rs @@ -1,12 +1,10 @@ -use std::str::FromStr; - use axum::async_trait; -use bson::{doc, Bson}; +use bson::{doc, to_document}; use futures::TryStreamExt; use mongodb::{options::ReplaceOptions, Collection, Database}; use crate::types::{ - dto::{LinkQuery, UserQuery}, + dto::{LinkQuery, LinkQueryBuilder, UserQuery}, entity::LinkItem, entity::{LinkItemBuilder, UserInfo, UserInfoBuilder}, AppError, Result, @@ -20,15 +18,15 @@ const LINKS_COLLECTION_NAME_DEFAULT: &str = "v1/links"; const USERS_COLLECTION_NAME_KEY: &str = "USERS_COLLECTION_NAME"; const USERS_COLLECTION_NAME_DEFAULT: &str = "v1/users"; -pub struct LinksDb { +pub struct LinksRepositoryProvider { links_collection: Collection, } -pub struct UsersDb { +pub struct UsersRepositoryProvider { users_collection: Collection, } -impl LinksDb { +impl LinksRepositoryProvider { pub fn new(db: &Database) -> Self { let collection_name = std::env::var(LINKS_COLLECTION_NAME_KEY) .unwrap_or_else(|_| LINKS_COLLECTION_NAME_DEFAULT.to_string()); @@ -37,7 +35,7 @@ impl LinksDb { } } -impl UsersDb { +impl UsersRepositoryProvider { pub fn new(db: &Database) -> Self { let collection_name = std::env::var(USERS_COLLECTION_NAME_KEY) .unwrap_or_else(|_| USERS_COLLECTION_NAME_DEFAULT.to_string()); @@ -47,149 +45,105 @@ impl UsersDb { } #[async_trait] -impl LinksRepository for LinksDb { +impl LinksRepository for LinksRepositoryProvider { async fn find(&self, query: &LinkQuery) -> Result> { - let mut db_query = doc! {}; - if !query.id().is_empty() { - let Ok(id) = bson::oid::ObjectId::from_str(query.id()) else { - tracing::error!("Error: {} cannot be converted to Bson ObjectId", query.id()); - return Err(AppError::LinkNotFound); - }; - db_query.insert("_id", id); - } - if !query.owner().is_empty() { - db_query.insert("owner", query.owner()); - } - match self.links_collection.find(db_query, None).await { - Ok(result) => Ok(result.try_collect().await.unwrap_or_else(|_| vec![])), - Err(e) => { - tracing::error!("Error: find(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let db_query = + to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + let result = self + .links_collection + .find(db_query, None) + .await + .map_err(|e| AppError::DatabaseError(format!("find() {e:?}")))?; + Ok(result.try_collect().await.unwrap_or_else(|_| vec![])) } async fn get(&self, query: &LinkQuery) -> Result { - let mut db_query = doc! {}; - if !query.id().is_empty() { - let Ok(id) = bson::oid::ObjectId::from_str(query.id()) else { - tracing::error!("Error: {} cannot be converted to Bson ObjectId", query.id()); - return Err(AppError::LinkNotFound); - }; - db_query.insert("_id", id); - } - if !query.owner().is_empty() { - db_query.insert("owner", query.owner()); - } - match self.links_collection.find_one(db_query, None).await { - Ok(item) => item.map_or(Err(AppError::LinkNotFound), |item| { - let returned_item = LinkItemBuilder::from(item).id(query.id()).build(); - Ok(returned_item) - }), - Err(e) => { - tracing::error!("Error: find_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let db_query = + to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + let item = self + .links_collection + .find_one(db_query, None) + .await + .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; + item.ok_or(AppError::LinkNotFound) } async fn create(&self, item: &LinkItem) -> Result { - match self.links_collection.insert_one(item, None).await { - Ok(result) => { - let id = if let Bson::ObjectId(id) = result.inserted_id { - id.to_hex() - } else { - tracing::error!("Error: unexpected inserted_id: {}", result.inserted_id); - return Err(AppError::DatabaseError); - }; - let query = doc! {"_id": result.inserted_id}; - let update = doc! {"$set": doc! { "id": &id } }; - self.links_collection - .update_one(query, update, None) - .await - .unwrap(); - - let returned_item = LinkItemBuilder::from(item.clone()).id(&id).build(); - Ok(returned_item) - } - Err(e) => { - tracing::error!("Error: insert_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let result = self + .links_collection + .insert_one(item, None) + .await + .map_err(|e| AppError::DatabaseError(format!("insert_one() {e:?}")))?; + + let id = result.inserted_id.as_object_id().map_or_else( + || Err(AppError::DatabaseError("unexpected inserted_id()".into())), + |id| Ok(id.to_hex()), + )?; + let query = doc! {"_id": result.inserted_id}; + let update = doc! {"$set": doc! { "id": &id } }; + self.links_collection + .update_one(query, update, None) + .await + .map_err(|e| AppError::DatabaseError(format!("update_one() {e:?}")))?; + + Ok(LinkItemBuilder::from(item.clone()).id(&id).build()) } async fn update(&self, id: &str, item: &LinkItem) -> Result { - let Ok(id) = bson::oid::ObjectId::from_str(id) else { - tracing::error!("Error: {id} cannot be converted to Bson ObjectId"); - return Err(AppError::LinkNotFound); - }; - let query = doc! {"_id": id, "owner": item.owner()}; + let query = LinkQueryBuilder::new(id, item.owner()).build(); + let db_query = to_document(&query) + .map_err(|_| AppError::DatabaseError("to_document failed".into()))?; let opts = ReplaceOptions::builder().upsert(true).build(); - match self - .links_collection - .replace_one(query, item, Some(opts)) + self.links_collection + .replace_one(db_query, item, Some(opts)) .await - { - Ok(_) => Ok(item.clone()), - Err(e) => { - tracing::error!("Error: replace_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + .map_err(|e| AppError::DatabaseError(format!("replace_one() {e:?}")))?; + Ok(item.clone()) } async fn delete(&self, item: &LinkItem) -> Result<()> { - let Ok(id) = bson::oid::ObjectId::from_str(item.id()) else { - tracing::error!("Error: {} cannot be converted to Bson ObjectId", item.id()); - return Err(AppError::LinkNotFound); - }; - let query = doc! {"_id": id, "owner": item.owner()}; - match self.links_collection.delete_one(query, None).await { - Ok(_) => Ok(()), - Err(e) => { - tracing::error!("Error: delete_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let query = LinkQueryBuilder::new(item.id(), item.owner()).build(); + let db_query = to_document(&query) + .map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + self.links_collection + .delete_one(db_query, None) + .await + .map_err(|e| AppError::DatabaseError(format!("delete_one() {e:?}")))?; + Ok(()) } } #[async_trait] -impl UsersRepository for UsersDb { +impl UsersRepository for UsersRepositoryProvider { async fn get(&self, query: &UserQuery) -> Result { - let query = doc! {"email": query.email()}; - match self.users_collection.find_one(query, None).await { - Ok(item) => item.ok_or(AppError::UserNotFound), - Err(e) => { - tracing::error!("Error: find_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let db_query = + to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + let item = self + .users_collection + .find_one(db_query, None) + .await + .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; + item.ok_or(AppError::UserNotFound) } async fn create(&self, info: &UserInfo) -> Result { - match self.users_collection.insert_one(info, None).await { - Ok(result) => { - let id = if let Bson::ObjectId(id) = result.inserted_id { - id.to_hex() - } else { - tracing::error!("Error: unexpected inserted_id: {}", result.inserted_id); - return Err(AppError::DatabaseError); - }; - let query = doc! {"_id": result.inserted_id}; - let update = doc! {"$set": doc! { "id": &id } }; - self.users_collection - .update_one(query, update, None) - .await - .unwrap(); - let returned_info = UserInfoBuilder::from(info.clone()).id(&id).build(); - Ok(returned_info) - } - Err(e) => { - tracing::error!("Error: insert_one(): {e:?}"); - Err(AppError::DatabaseError) - } - } + let result = self + .users_collection + .insert_one(info, None) + .await + .map_err(|e| AppError::DatabaseError(format!("insert_one() {e:?}")))?; + + let id = result.inserted_id.as_object_id().map_or_else( + || Err(AppError::DatabaseError("unexpected inserted_id()".into())), + |id| Ok(id.to_hex()), + )?; + let query = doc! {"_id": result.inserted_id}; + let update = doc! {"$set": doc! { "id": &id } }; + self.users_collection + .update_one(query, update, None) + .await + .map_err(|e| AppError::DatabaseError(format!("update_one() {e:?}")))?; + + Ok(UserInfoBuilder::from(info.clone()).id(&id).build()) } } diff --git a/link-for-later/src/types/dto.rs b/link-for-later/src/types/dto.rs index b614070..1a586db 100644 --- a/link-for-later/src/types/dto.rs +++ b/link-for-later/src/types/dto.rs @@ -33,9 +33,11 @@ impl LinkItemRequest { } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct LinkQuery { + #[serde(skip_serializing_if = "String::is_empty")] id: String, + #[serde(skip_serializing_if = "String::is_empty")] owner: String, } @@ -101,7 +103,7 @@ impl UserInfoRequest { } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct UserQuery { email: String, } diff --git a/link-for-later/src/types/errors.rs b/link-for-later/src/types/errors.rs index 71af195..1917c3b 100644 --- a/link-for-later/src/types/errors.rs +++ b/link-for-later/src/types/errors.rs @@ -3,7 +3,7 @@ use std::{error, fmt}; #[derive(Debug, PartialEq, Eq)] pub enum App { ServerError, - DatabaseError, + DatabaseError(String), LinkNotFound, UserAlreadyExists, UserNotFound, @@ -17,7 +17,7 @@ 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::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"), @@ -30,3 +30,32 @@ impl fmt::Display for App { } 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/app/mod.rs b/link-for-later/tests/app/mod.rs index 06de4da..3a96f78 100644 --- a/link-for-later/tests/app/mod.rs +++ b/link-for-later/tests/app/mod.rs @@ -1,9 +1,14 @@ #![allow(dead_code)] use axum::Router; -use crate::repository; +use crate::repository::{mongodb, DatabaseType}; -pub async fn new() -> Router { - let db = repository::database().await; - link_for_later::app::new(link_for_later::DatabaseType::MongoDb(db)) +pub async fn new(db_type: &DatabaseType) -> Router { + match db_type { + DatabaseType::InMemory => link_for_later::app::new(link_for_later::DatabaseType::InMemory), + DatabaseType::MongoDb => { + let db = mongodb::database().await; + link_for_later::app::new(link_for_later::DatabaseType::MongoDb(db)) + } + } } diff --git a/link-for-later/tests/links.rs b/link-for-later/tests/links.rs index 4ec3f1c..a109780 100644 --- a/link-for-later/tests/links.rs +++ b/link-for-later/tests/links.rs @@ -3,25 +3,29 @@ use axum::{ http::{Request, StatusCode}, }; use http_body_util::BodyExt; +use rstest::rstest; use serde_json::json; use tower::ServiceExt; use tracing_test::traced_test; -use crate::entity::LinkItem; +use crate::{entity::LinkItem, repository::DatabaseType}; mod app; mod auth; mod entity; mod repository; +#[rstest] #[traced_test] #[tokio::test] -async fn test_get_links_empty() { - repository::setup(); +async fn test_get_links_empty( + #[values(DatabaseType::InMemory, DatabaseType::MongoDb)] db_type: DatabaseType, +) { + repository::new(&db_type); let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -40,15 +44,16 @@ async fn test_get_links_empty() { assert_eq!(&body[..], b"[]"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_get_links_non_empty() { - repository::setup(); +async fn test_get_links_non_empty(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -72,15 +77,16 @@ async fn test_get_links_non_empty() { assert!(body[0].url == "http://test"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_get_link_item_found() { - repository::setup(); +async fn test_get_link_item_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -103,15 +109,16 @@ async fn test_get_link_item_found() { assert!(body.url == "http://test"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_get_link_item_not_found() { - repository::setup(); +async fn test_get_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - repository::add_link("user@test.com", "http://test").await; + repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -131,17 +138,18 @@ async fn test_get_link_item_not_found() { assert_eq!(body, json!({"error": "link item not found"}).to_string()); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_post_link() { - repository::setup(); +async fn test_post_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); let token = auth::generate_token("user@test.com"); let request = r#"{ "url": "http://test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -164,25 +172,26 @@ async fn test_post_link() { assert!(body.owner == "user@test.com"); assert!(body.url == "http://test"); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 1); - let db_item = repository::get_link(&body.id).await; + let db_item = repository.get_link(&body.id).await; assert!(db_item.owner == "user@test.com"); assert!(db_item.url == "http://test"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_post_link_invalid_url() { - repository::setup(); +async fn test_post_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); let token = auth::generate_token("user@test.com"); let request = r#"{ "url": "invalid" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -202,22 +211,23 @@ async fn test_post_link_invalid_url() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "invalid url"}).to_string()); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 0); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_put_link() { - repository::setup(); +async fn test_put_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); let request = r#"{ "url": "http://update" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -241,27 +251,28 @@ async fn test_put_link() { assert!(body.owner == "user@test.com"); assert!(body.url == "http://update"); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 1); - let db_item = repository::get_link(&id).await; + 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://update"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_put_link_invalid_url() { - repository::setup(); +async fn test_put_link_invalid_url(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); let request = r#"{ "url": "invalid" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -281,22 +292,23 @@ async fn test_put_link_invalid_url() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "invalid url"}).to_string()); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 1); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_put_link_item_not_found() { - repository::setup(); +async fn test_put_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); let request = r#"{ "url": "http://update" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -316,24 +328,25 @@ async fn test_put_link_item_not_found() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "link item not found"}).to_string()); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 1); - let db_item = repository::get_link(&id).await; + 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 } +#[rstest] #[traced_test] #[tokio::test] -async fn test_delete_link() { - repository::setup(); +async fn test_delete_link(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -352,19 +365,20 @@ async fn test_delete_link() { let body = response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(&body[..], b""); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 0); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_delete_link_item_not_found() { - repository::setup(); +async fn test_delete_link_item_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - let id = repository::add_link("user@test.com", "http://test").await; + let id = repository.add_link("user@test.com", "http://test").await; let token = auth::generate_token("user@test.com"); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -384,21 +398,24 @@ async fn test_delete_link_item_not_found() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "link item not found"}).to_string()); - let db_count = repository::count_links().await; + let db_count = repository.count_links().await; assert!(db_count == 1); - let db_item = repository::get_link(&id).await; + 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 } +#[rstest] #[traced_test] #[tokio::test] -async fn test_unauthorized_access_to_links_no_token() { - repository::setup(); +async fn test_unauthorized_access_to_links_no_token( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, +) { + repository::new(&db_type); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -420,12 +437,15 @@ async fn test_unauthorized_access_to_links_no_token() { ); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_unauthorized_access_to_links_invalid_token() { - repository::setup(); +async fn test_unauthorized_access_to_links_invalid_token( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, +) { + repository::new(&db_type); - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() diff --git a/link-for-later/tests/repository/inmemory.rs b/link-for-later/tests/repository/inmemory.rs new file mode 100644 index 0000000..35b9965 --- /dev/null +++ b/link-for-later/tests/repository/inmemory.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +use axum::async_trait; + +use crate::entity::{LinkItem, UserInfo}; + +#[derive(Default)] +pub struct RepositoryProvider {} + +#[async_trait] +impl super::Repository for RepositoryProvider { + async fn count_links(&self) -> u64 { + unimplemented!() + } + + async fn get_link(&self, _id: &str) -> LinkItem { + unimplemented!() + } + + async fn add_link(&self, _owner: &str, _url: &str) -> String { + unimplemented!() + } + + async fn count_users(&self) -> u64 { + unimplemented!() + } + + async fn get_user(&self, _email: &str) -> UserInfo { + unimplemented!() + } + + async fn add_user(&self, _email: &str, _password: &str) -> String { + unimplemented!() + } +} diff --git a/link-for-later/tests/repository/mod.rs b/link-for-later/tests/repository/mod.rs index 941729a..0192fd7 100644 --- a/link-for-later/tests/repository/mod.rs +++ b/link-for-later/tests/repository/mod.rs @@ -1,106 +1,35 @@ #![allow(dead_code)] -use std::str::FromStr; -use bson::{doc, oid::ObjectId, Bson}; -use mongodb::{options::ClientOptions, Client, Database}; -use rand::Rng; +use axum::async_trait; use crate::entity::{LinkItem, UserInfo}; -const MONGODB_URI_KEY: &str = "MONGODB_URI"; -const MONGODB_DATABASE_NAME_KEY: &str = "MONGODB_DATABASE_NAME"; -const LINKS_COLLECTION_NAME_KEY: &str = "LINKS_COLLECTION_NAME"; -const USERS_COLLECTION_NAME_KEY: &str = "USERS_COLLECTION_NAME"; - -pub fn setup() { - let mut rng = rand::thread_rng(); - let id = rng.gen::(); - - let links_collection = format!("{}/links", id); - std::env::set_var(LINKS_COLLECTION_NAME_KEY, links_collection); - - let users_collection = format!("{}/users", id); - std::env::set_var(USERS_COLLECTION_NAME_KEY, users_collection); -} - -pub async fn database() -> Database { - let uri = std::env::var(MONGODB_URI_KEY).unwrap(); - let database_name = std::env::var(MONGODB_DATABASE_NAME_KEY).unwrap(); - - let client_options = ClientOptions::parse(uri).await.unwrap(); - let client = Client::with_options(client_options).unwrap(); - client.database(&database_name) -} - -pub async fn count_links() -> u64 { - let collection = std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection::(&collection); - - collection.count_documents(None, None).await.unwrap() -} - -pub async fn get_link(id: &str) -> LinkItem { - let collection = std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection(&collection); - - let id = if let Ok(id) = bson::oid::ObjectId::from_str(id) { - id - } else { - ObjectId::default() - }; - let db_query = doc! {"_id": id}; - collection.find_one(db_query, None).await.unwrap().unwrap() -} - -pub async fn add_link(owner: &str, url: &str) -> String { - let collection = std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection(&collection); - - let document = doc! {"id": "1", "owner": owner, "url": url, "title": "", "description": "", "created_at": "", "updated_at": ""}; - let result = collection.insert_one(document, None).await.unwrap(); - - let id = if let Bson::ObjectId(id) = result.inserted_id { - id.to_hex() - } else { - String::default() - }; - let query = doc! {"_id": result.inserted_id.clone()}; - let update = doc! {"$set": doc! { "id": &id } }; - collection.update_one(query, update, None).await.unwrap(); - - id -} - -pub async fn count_users() -> u64 { - let collection = std::env::var(USERS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection::(&collection); - - collection.count_documents(None, None).await.unwrap() -} - -pub async fn get_user(email: &str) -> UserInfo { - let collection = std::env::var(USERS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection(&collection); - - let db_query = doc! {"email": email}; - collection.find_one(db_query, None).await.unwrap().unwrap() -} - -pub async fn add_user(email: &str, password: &str) -> String { - let collection = std::env::var(USERS_COLLECTION_NAME_KEY).unwrap(); - let collection = database().await.collection(&collection); - - let document = doc! {"id": "1", "email": email, "password": password, "verified": true, "created_at": "", "updated_at": ""}; - let result = collection.insert_one(document, None).await.unwrap(); - - let id = if let Bson::ObjectId(id) = result.inserted_id { - id.to_hex() - } else { - String::default() - }; - let query = doc! {"_id": result.inserted_id.clone()}; - let update = doc! {"$set": doc! { "id": &id } }; - collection.update_one(query, update, None).await.unwrap(); - - id +pub mod inmemory; +pub mod mongodb; + +#[derive(Clone)] +pub enum DatabaseType { + MongoDb, + InMemory, +} + +#[async_trait] +pub trait Repository { + async fn count_links(&self) -> u64; + async fn get_link(&self, id: &str) -> LinkItem; + async fn add_link(&self, owner: &str, url: &str) -> String; + async fn count_users(&self) -> u64; + async fn get_user(&self, email: &str) -> UserInfo; + async fn add_user(&self, email: &str, password: &str) -> String; +} + +pub fn new(db_type: &DatabaseType) -> Box { + match db_type { + DatabaseType::InMemory => Box::::default(), + DatabaseType::MongoDb => { + let repository = mongodb::RepositoryProvider::default(); + repository.setup(); + Box::new(repository) + } + } } diff --git a/link-for-later/tests/repository/mongodb.rs b/link-for-later/tests/repository/mongodb.rs new file mode 100644 index 0000000..d809b9e --- /dev/null +++ b/link-for-later/tests/repository/mongodb.rs @@ -0,0 +1,110 @@ +#![allow(dead_code)] +use std::str::FromStr; + +use axum::async_trait; +use bson::doc; +use mongodb::{options::ClientOptions, Client, Database}; +use rand::Rng; + +use crate::entity::{LinkItem, UserInfo}; + +const MONGODB_URI_KEY: &str = "MONGODB_URI"; +const MONGODB_DATABASE_NAME_KEY: &str = "MONGODB_DATABASE_NAME"; + +const LINKS_COLLECTION_NAME_KEY: &str = "LINKS_COLLECTION_NAME"; +const USERS_COLLECTION_NAME_KEY: &str = "USERS_COLLECTION_NAME"; + +#[derive(Default)] +pub struct RepositoryProvider {} + +#[async_trait] +impl super::Repository for RepositoryProvider { + async fn count_links(&self) -> u64 { + database() + .await + .collection::(&std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap()) + .count_documents(None, None) + .await + .unwrap() + } + + async fn get_link(&self, id: &str) -> LinkItem { + let db_query = doc! {"_id": bson::oid::ObjectId::from_str(id).unwrap()}; + database() + .await + .collection(&std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap()) + .find_one(db_query, None) + .await + .unwrap() + .unwrap() + } + + async fn add_link(&self, owner: &str, url: &str) -> String { + let collection = database() + .await + .collection(&std::env::var(LINKS_COLLECTION_NAME_KEY).unwrap()); + + let document = doc! {"id": "1", "owner": owner, "url": url, "title": "", "description": "", "created_at": "", "updated_at": ""}; + let result = collection.insert_one(document.clone(), None).await.unwrap(); + + let id = result.inserted_id.as_object_id().unwrap().to_hex(); + let update = doc! {"$set": doc! { "id": &id } }; + collection.update_one(document, update, None).await.unwrap(); + + id + } + + async fn count_users(&self) -> u64 { + database() + .await + .collection::(&std::env::var(USERS_COLLECTION_NAME_KEY).unwrap()) + .count_documents(None, None) + .await + .unwrap() + } + + async fn get_user(&self, email: &str) -> UserInfo { + let db_query = doc! {"email": email}; + database() + .await + .collection(&std::env::var(USERS_COLLECTION_NAME_KEY).unwrap()) + .find_one(db_query, None) + .await + .unwrap() + .unwrap() + } + + async fn add_user(&self, email: &str, password: &str) -> String { + let collection = database() + .await + .collection(&std::env::var(USERS_COLLECTION_NAME_KEY).unwrap()); + + let document = doc! {"id": "1", "email": email, "password": password, "verified": true, "created_at": "", "updated_at": ""}; + let result = collection.insert_one(document.clone(), None).await.unwrap(); + + let id = result.inserted_id.as_object_id().unwrap().to_hex(); + let update = doc! {"$set": doc! { "id": &id } }; + collection.update_one(document, update, None).await.unwrap(); + + id + } +} + +impl RepositoryProvider { + pub fn setup(&self) { + let mut rng = rand::thread_rng(); + let id = rng.gen::(); + + std::env::set_var(LINKS_COLLECTION_NAME_KEY, format!("v{}/links", id)); + std::env::set_var(USERS_COLLECTION_NAME_KEY, format!("v{}/users", id)); + } +} + +pub async fn database() -> Database { + let uri = std::env::var(MONGODB_URI_KEY).unwrap(); + let database_name = std::env::var(MONGODB_DATABASE_NAME_KEY).unwrap(); + + let client_options = ClientOptions::parse(uri).await.unwrap(); + let client = Client::with_options(client_options).unwrap(); + client.database(&database_name) +} diff --git a/link-for-later/tests/users.rs b/link-for-later/tests/users.rs index 2c714bd..aa56887 100644 --- a/link-for-later/tests/users.rs +++ b/link-for-later/tests/users.rs @@ -3,26 +3,30 @@ use axum::{ http::{Request, StatusCode}, }; use http_body_util::BodyExt; +use rstest::rstest; use serde_json::{json, Value}; use tower::ServiceExt; use tracing_test::traced_test; +use crate::repository::DatabaseType; + mod app; mod auth; mod entity; mod repository; +#[rstest] #[traced_test] #[tokio::test] -async fn test_register_user() { - repository::setup(); +async fn test_register_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); let request = r#"{ "email": "user@test.com", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -40,25 +44,26 @@ async fn test_register_user() { let body = response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(&body[..], b""); - let db_count = repository::count_users().await; + let db_count = repository.count_users().await; assert!(db_count == 1); - let db_item = repository::get_user("user@test.com").await; + let db_item = repository.get_user("user@test.com").await; assert!(db_item.email == "user@test.com"); assert!(db_item.password == "test"); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_register_user_invalid_email() { - repository::setup(); +async fn test_register_user_invalid_email(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); let request = r#"{ "email": "user", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -77,22 +82,25 @@ async fn test_register_user_invalid_email() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "invalid email"}).to_string()); - let db_count = repository::count_users().await; + let db_count = repository.count_users().await; assert!(db_count == 0); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_register_user_already_registered() { - repository::setup(); +async fn test_register_user_already_registered( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, +) { + let repository = repository::new(&db_type); - repository::add_user("user@test.com", "test").await; + repository.add_user("user@test.com", "test").await; let request = r#"{ "email": "user@test.com", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -111,22 +119,23 @@ async fn test_register_user_already_registered() { let body = std::str::from_utf8(&body).unwrap(); assert_eq!(body, json!({"error": "user already regisered"}).to_string()); - let db_count = repository::count_users().await; + let db_count = repository.count_users().await; assert!(db_count == 1); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_login_user() { - repository::setup(); +async fn test_login_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - repository::add_user("user@test.com", "test").await; + repository.add_user("user@test.com", "test").await; let request = r#"{ "email": "user@test.com", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -147,17 +156,18 @@ async fn test_login_user() { assert!(!body["token"].to_string().is_empty()); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_login_user_invalid_email() { - repository::setup(); +async fn test_login_user_invalid_email(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + repository::new(&db_type); let request = r#"{ "email": "user", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -177,18 +187,19 @@ async fn test_login_user_invalid_email() { assert_eq!(body, json!({"error": "invalid email"}).to_string()); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_login_user_not_found() { - repository::setup(); +async fn test_login_user_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { + let repository = repository::new(&db_type); - repository::add_user("user@test.com", "test").await; + repository.add_user("user@test.com", "test").await; let request = r#"{ "email": "user2@test.com", "password": "test" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder() @@ -208,18 +219,21 @@ async fn test_login_user_not_found() { assert_eq!(body, json!({"error": "user not found"}).to_string()); } +#[rstest] #[traced_test] #[tokio::test] -async fn test_login_user_incorrect_password() { - repository::setup(); +async fn test_login_user_incorrect_password( + #[values(DatabaseType::MongoDb)] db_type: DatabaseType, +) { + let repository = repository::new(&db_type); - repository::add_user("user@test.com", "test").await; + repository.add_user("user@test.com", "test").await; let request = r#"{ "email": "user@test.com", "password": "incorrect" }"#; - let response = app::new() + let response = app::new(&db_type) .await .oneshot( Request::builder()