From 3b20d12ba1947cda487a71ac7e4e757e79aa10cc Mon Sep 17 00:00:00 2001 From: Simon Goller Date: Sat, 27 Apr 2024 00:03:15 +0200 Subject: [PATCH] Add unit tests for the services --- Cargo.lock | 86 +++++++++++++++++++++++++++++++++ dao/Cargo.toml | 2 + dao/src/lib.rs | 16 ++++--- dao_impl/Cargo.toml | 3 ++ dao_impl/src/lib.rs | 3 ++ service/Cargo.toml | 7 +++ service/src/lib.rs | 14 ++++-- service_impl/Cargo.toml | 8 ++++ service_impl/src/lib.rs | 104 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 231 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1db4dbf..55a2b25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,12 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + [[package]] name = "app" version = "0.1.0" @@ -262,6 +268,8 @@ dependencies = [ name = "dao" version = "0.1.0" dependencies = [ + "async-trait", + "mockall", "thiserror", "uuid", ] @@ -270,6 +278,7 @@ dependencies = [ name = "dao_impl" version = "0.1.0" dependencies = [ + "async-trait", "dao", "sqlx", ] @@ -303,6 +312,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.11.0" @@ -383,6 +398,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures-channel" version = "0.3.30" @@ -773,6 +794,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "nom" version = "7.1.3" @@ -964,6 +1012,32 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1157,16 +1231,22 @@ dependencies = [ name = "service" version = "0.1.0" dependencies = [ + "async-trait", "dao", + "mockall", "thiserror", + "tokio", ] [[package]] name = "service_impl" version = "0.1.0" dependencies = [ + "async-trait", "dao", + "mockall", "service", + "tokio", ] [[package]] @@ -1527,6 +1607,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.59" diff --git a/dao/Cargo.toml b/dao/Cargo.toml index 8378158..1c06167 100644 --- a/dao/Cargo.toml +++ b/dao/Cargo.toml @@ -6,5 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = "0.1.80" +mockall = "0.12.1" thiserror = "1.0.59" uuid = "1.8" diff --git a/dao/src/lib.rs b/dao/src/lib.rs index 595df05..ae439ac 100644 --- a/dao/src/lib.rs +++ b/dao/src/lib.rs @@ -1,5 +1,7 @@ -use std::{future::Future, sync::Arc}; +use std::sync::Arc; +use async_trait::async_trait; +use mockall::automock; use thiserror::Error; #[derive(Error, Debug)] @@ -8,14 +10,14 @@ pub enum DaoError { DatabaseQueryError(#[from] Box), } +#[automock] +#[async_trait] pub trait HelloDao { - fn get_hello(&self) -> impl Future, DaoError>> + Send; + async fn get_hello(&self) -> Result, DaoError>; } +#[automock] +#[async_trait] pub trait PermissionDao { - fn has_privilege( - &self, - user: &str, - privilege: &str, - ) -> impl Future> + Send; + async fn has_privilege(&self, user: &str, privilege: &str) -> Result; } diff --git a/dao_impl/Cargo.toml b/dao_impl/Cargo.toml index 4f3fb22..8cffcca 100644 --- a/dao_impl/Cargo.toml +++ b/dao_impl/Cargo.toml @@ -3,6 +3,9 @@ name = "dao_impl" version = "0.1.0" edition = "2021" +[dependencies] +async-trait = "0.1.80" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies.sqlx] diff --git a/dao_impl/src/lib.rs b/dao_impl/src/lib.rs index 08030b5..c219518 100644 --- a/dao_impl/src/lib.rs +++ b/dao_impl/src/lib.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use async_trait::async_trait; use dao::DaoError; use sqlx::{query, SqlitePool}; @@ -13,6 +14,7 @@ impl HelloDaoImpl { } } +#[async_trait] impl dao::HelloDao for HelloDaoImpl { async fn get_hello(&self) -> Result, dao::DaoError> { let result = query!(r"SELECT 'Hello, world!' as message") @@ -32,6 +34,7 @@ impl PermissionDaoImpl { Self { pool } } } +#[async_trait] impl dao::PermissionDao for PermissionDaoImpl { async fn has_privilege(&self, user: &str, privilege: &str) -> Result { let result = query!( diff --git a/service/Cargo.toml b/service/Cargo.toml index 96a092a..c0f9788 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -3,6 +3,10 @@ name = "service" version = "0.1.0" edition = "2021" +[dependencies] +async-trait = "0.1.80" +mockall = "0.12.1" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies.thiserror] @@ -10,3 +14,6 @@ version = "1.0" [dependencies.dao] path = "../dao" + +[dev-dependencies.tokio] +version = "1.37.0" diff --git a/service/src/lib.rs b/service/src/lib.rs index 32f8950..a9c22aa 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -1,3 +1,5 @@ +use async_trait::async_trait; +use mockall::automock; use std::{future::Future, sync::Arc}; use thiserror::Error; @@ -10,17 +12,19 @@ pub enum ServiceError { Forbidden, } +#[automock] pub trait HelloService { fn hello(&self) -> impl Future, ServiceError>> + Send; } +#[automock] +#[async_trait] pub trait PermissionService { - fn check_permission( - &self, - privilege: &str, - ) -> impl Future> + Send; + async fn check_permission(&self, privilege: &str) -> Result<(), ServiceError>; } +#[automock] +#[async_trait] pub trait UserService { - fn current_user(&self) -> impl Future, ServiceError>> + Send; + async fn current_user(&self) -> Result, ServiceError>; } diff --git a/service_impl/Cargo.toml b/service_impl/Cargo.toml index e0357f2..258c446 100644 --- a/service_impl/Cargo.toml +++ b/service_impl/Cargo.toml @@ -3,6 +3,10 @@ name = "service_impl" version = "0.1.0" edition = "2021" +[dependencies] +async-trait = "0.1.80" +mockall = "0.12.1" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies.service] @@ -10,3 +14,7 @@ path = "../service" [dependencies.dao] path = "../dao" + +[dev-dependencies.tokio] +version = "1.37.0" +features = ["full"] diff --git a/service_impl/src/lib.rs b/service_impl/src/lib.rs index 7c8db7a..431e083 100644 --- a/service_impl/src/lib.rs +++ b/service_impl/src/lib.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use async_trait::async_trait; + pub struct HelloServiceImpl where HelloDao: dao::HelloDao + Sync + Send, @@ -54,6 +56,7 @@ where } } +#[async_trait] impl service::PermissionService for PermissionServiceImpl where @@ -76,8 +79,109 @@ where pub struct UserServiceDev; +#[async_trait] impl service::UserService for UserServiceDev { async fn current_user(&self) -> Result, service::ServiceError> { Ok("DEVUSER".into()) } } + +#[cfg(test)] +mod tests { + use super::*; + use mockall::predicate::eq; + use service::{HelloService, MockPermissionService, PermissionService}; + use tokio; + + #[tokio::test] + async fn test_get_hello_successful() { + let mut hello_dao = dao::MockHelloDao::new(); + hello_dao + .expect_get_hello() + .times(1) + .returning(|| Ok("Hello, world!".into())); + let mut permission_service = MockPermissionService::new(); + permission_service + .expect_check_permission() + .times(1) + .returning(|_| Ok(())); + + let hello_service = + HelloServiceImpl::new(Arc::new(hello_dao), Arc::new(permission_service)); + assert_eq!( + "Hello, world!", + hello_service.hello().await.unwrap().as_ref() + ); + } + + #[tokio::test] + async fn test_get_hello_no_permission() { + let hello_dao = dao::MockHelloDao::new(); + + let mut permission_service = MockPermissionService::new(); + permission_service + .expect_check_permission() + .times(1) + .returning(|_| Err(service::ServiceError::Forbidden)); + + let hello_service = + HelloServiceImpl::new(Arc::new(hello_dao), Arc::new(permission_service)); + if let Err(service::ServiceError::Forbidden) = hello_service.hello().await { + // All good + } else { + panic!("Expected forbidden error"); + } + } + + #[tokio::test] + async fn test_check_permission() { + let mut permission_dao = dao::MockPermissionDao::new(); + permission_dao + .expect_has_privilege() + .with(eq("DEVUSER"), eq("hello")) + .returning(|_, _| Ok(true)); + + let mut user_service = service::MockUserService::new(); + user_service + .expect_current_user() + .returning(|| Ok("DEVUSER".into())); + + let permission_service = + PermissionServiceImpl::new(Arc::new(permission_dao), Arc::new(user_service)); + let result = permission_service.check_permission("hello").await; + result.expect("Expected successful authorization"); + } + + #[tokio::test] + async fn test_check_permission_denied() { + let mut permission_dao = dao::MockPermissionDao::new(); + permission_dao + .expect_has_privilege() + .with(eq("DEVUSER"), eq("hello")) + .returning(|_, _| Ok(false)); + + let mut user_service = service::MockUserService::new(); + user_service + .expect_current_user() + .returning(|| Ok("DEVUSER".into())); + + let permission_service = + PermissionServiceImpl::new(Arc::new(permission_dao), Arc::new(user_service)); + let result = permission_service.check_permission("hello").await; + if let Err(service::ServiceError::Forbidden) = result { + // All good + } else { + panic!("Expected forbidden error"); + } + } + + #[tokio::test] + async fn test_user_service_dev() { + use service::UserService; + let user_service = UserServiceDev; + assert_eq!( + "DEVUSER", + user_service.current_user().await.unwrap().as_ref() + ); + } +}