diff --git a/Cargo.lock b/Cargo.lock index 826366de..ddb17296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,6 +752,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.12.0" @@ -930,6 +936,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" version = "0.3.30" @@ -1622,6 +1634,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.66", +] + [[package]] name = "multer" version = "3.1.0" @@ -2033,6 +2072,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.85" @@ -3049,6 +3114,12 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "text-colorizer" version = "1.0.0" @@ -3304,6 +3375,7 @@ dependencies = [ "lazy_static", "lettre", "log", + "mockall", "pbkdf2", "pin-project-lite", "rand", diff --git a/Cargo.toml b/Cargo.toml index 461ca72a..64096caf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ hyper = "1" hyper-util = { version = "0.1.3", features = ["http1", "http2", "tokio"] } indexmap = "2" jsonwebtoken = "9" +mockall = "0.12.1" lazy_static = "1.4.0" lettre = { version = "0", features = [ "builder", diff --git a/src/app.rs b/src/app.rs index 761fbb3a..2272dd4f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,8 +16,8 @@ use crate::services::torrent::{ DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, }; -use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; -use crate::services::{proxy, settings, torrent}; +use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository, Repository}; +use crate::services::{authorization, proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; use crate::web::api::server::signals::Halted; use crate::web::api::server::v1::auth::Authentication; @@ -74,7 +74,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running // Repositories let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); let tag_repository = Arc::new(DbTagRepository::new(database.clone())); - let user_repository = Arc::new(DbUserRepository::new(database.clone())); + let user_repository: Arc> = Arc::new(Box::new(DbUserRepository::new(database.clone()))); let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone())); let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone())); let torrent_repository = Arc::new(DbTorrentRepository::new(database.clone())); @@ -87,15 +87,19 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running let banned_user_list = Arc::new(DbBannedUserList::new(database.clone())); // Services + let authorization_service = Arc::new(authorization::Service::new(user_repository.clone())); let tracker_service = Arc::new(tracker::service::Service::new(configuration.clone(), database.clone()).await); let tracker_statistics_importer = Arc::new(StatisticsImporter::new(configuration.clone(), tracker_service.clone(), database.clone()).await); let mailer_service = Arc::new(mailer::Service::new(configuration.clone()).await); let image_cache_service: Arc = Arc::new(ImageCacheService::new(configuration.clone()).await); - let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); - let tag_service = Arc::new(tag::Service::new(tag_repository.clone(), user_repository.clone())); + let category_service = Arc::new(category::Service::new( + category_repository.clone(), + authorization_service.clone(), + )); + let tag_service = Arc::new(tag::Service::new(tag_repository.clone(), authorization_service.clone())); let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone())); - let settings_service = Arc::new(settings::Service::new(configuration.clone(), user_repository.clone())); + let settings_service = Arc::new(settings::Service::new(configuration.clone(), authorization_service.clone())); let torrent_index = Arc::new(torrent::Index::new( configuration.clone(), tracker_statistics_importer.clone(), diff --git a/src/common.rs b/src/common.rs index 755a775b..7e08e125 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,11 +10,12 @@ use crate::services::torrent::{ DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, }; -use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; +use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, Repository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; use crate::web::api::server::v1::auth::Authentication; use crate::{mailer, tracker}; + pub type Username = String; pub struct AppData { @@ -30,7 +31,7 @@ pub struct AppData { // Repositories pub category_repository: Arc, pub tag_repository: Arc, - pub user_repository: Arc, + pub user_repository: Arc>, pub user_authentication_repository: Arc, pub user_profile_repository: Arc, pub torrent_repository: Arc, @@ -66,7 +67,7 @@ impl AppData { // Repositories category_repository: Arc, tag_repository: Arc, - user_repository: Arc, + user_repository: Arc>, user_authentication_repository: Arc, user_profile_repository: Arc, torrent_repository: Arc, diff --git a/src/services/authentication.rs b/src/services/authentication.rs index e04342a4..170cac82 100644 --- a/src/services/authentication.rs +++ b/src/services/authentication.rs @@ -5,17 +5,18 @@ use argon2::{Argon2, PasswordHash, PasswordVerifier}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use pbkdf2::Pbkdf2; -use super::user::{DbUserProfileRepository, DbUserRepository}; +use super::user::DbUserProfileRepository; use crate::config::Configuration; use crate::databases::database::{Database, Error}; use crate::errors::ServiceError; use crate::models::user::{UserAuthentication, UserClaims, UserCompact, UserId}; +use crate::services::user::Repository; use crate::utils::clock; pub struct Service { configuration: Arc, json_web_token: Arc, - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, user_authentication_repository: Arc, } @@ -24,7 +25,7 @@ impl Service { pub fn new( configuration: Arc, json_web_token: Arc, - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, user_authentication_repository: Arc, ) -> Self { diff --git a/src/services/authorization.rs b/src/services/authorization.rs new file mode 100644 index 00000000..f49c0716 --- /dev/null +++ b/src/services/authorization.rs @@ -0,0 +1,199 @@ +//! Authorization service. +use std::sync::Arc; + +use super::user::Repository; +use crate::errors::ServiceError; +use crate::models::user::{UserCompact, UserId}; + +pub enum ACTION { + AddCategory, + DeleteCategory, + GetSettings, + GetSettingsSecret, + AddTag, + DeleteTag, +} + +pub struct Service { + user_repository: Arc>, +} + +impl Service { + #[must_use] + pub fn new(user_repository: Arc>) -> Self { + Self { user_repository } + } + + /// # Errors + /// + /// Will return an error if: + /// + /// - There is not any user with the provided `UserId` (when the user id is some). + /// - The user is not authorized to perform the action. + pub async fn authorize(&self, action: ACTION, maybe_user_id: Option) -> Result<(), ServiceError> { + match action { + ACTION::AddCategory + | ACTION::DeleteCategory + | ACTION::GetSettings + | ACTION::GetSettingsSecret + | ACTION::AddTag + | ACTION::DeleteTag => match maybe_user_id { + Some(user_id) => { + let user = self.get_user(user_id).await?; + + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + Ok(()) + } + None => Err(ServiceError::Unauthorized), + }, + } + } + + async fn get_user(&self, user_id: UserId) -> Result { + self.user_repository.get_compact(&user_id).await + } +} +#[allow(unused_imports)] +#[cfg(test)] +mod test { + use std::str::FromStr; + use std::sync::Arc; + + use mockall::predicate; + + use crate::databases::database; + use crate::errors::ServiceError; + use crate::models::user::{User, UserCompact}; + use crate::services::authorization::{Service, ACTION}; + use crate::services::user::{MockRepository, Repository}; + use crate::web::api::client::v1::random::string; + + #[tokio::test] + async fn a_guest_user_should_not_be_able_to_add_categories() { + let test_user_id = 1; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(|_| Err(ServiceError::UserNotFound)); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::AddCategory, Some(test_user_id)).await, + Err(ServiceError::UserNotFound) + ); + } + + #[tokio::test] + async fn a_registered_user_should_not_be_able_to_add_categories() { + let test_user_id = 2; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "non_admin_user".to_string(), + administrator: false, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::AddCategory, Some(test_user_id)).await, + Err(ServiceError::Unauthorized) + ); + } + + #[tokio::test] + async fn an_admin_user_should_be_able_to_add_categories() { + let test_user_id = 3; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "admin_user".to_string(), + administrator: true, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!(service.authorize(ACTION::AddCategory, Some(test_user_id)).await, Ok(())); + } + + #[tokio::test] + async fn a_guest_user_should_not_be_able_to_delete_categories() { + let test_user_id = 4; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(|_| Err(ServiceError::UserNotFound)); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, + Err(ServiceError::UserNotFound) + ); + } + + #[tokio::test] + async fn a_registered_user_should_not_be_able_to_delete_categories() { + let test_user_id = 5; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "non_admin_user".to_string(), + administrator: false, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, + Err(ServiceError::Unauthorized) + ); + } + + #[tokio::test] + async fn an_admin_user_should_be_able_to_delete_categories() { + let test_user_id = 6; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "admin_user".to_string(), + administrator: true, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!(service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, Ok(())); + } +} diff --git a/src/services/category.rs b/src/services/category.rs index ec3e5ca2..b5b4523f 100644 --- a/src/services/category.rs +++ b/src/services/category.rs @@ -1,7 +1,7 @@ //! Category service. use std::sync::Arc; -use super::user::DbUserRepository; +use super::authorization::{self, ACTION}; use crate::databases::database::{Category, Database, Error as DatabaseError}; use crate::errors::ServiceError; use crate::models::category::CategoryId; @@ -9,15 +9,15 @@ use crate::models::user::UserId; pub struct Service { category_repository: Arc, - user_repository: Arc, + authorization_service: Arc, } impl Service { #[must_use] - pub fn new(category_repository: Arc, user_repository: Arc) -> Service { + pub fn new(category_repository: Arc, authorization_service: Arc) -> Service { Service { category_repository, - user_repository, + authorization_service, } } @@ -32,13 +32,9 @@ impl Service { /// * The category already exists. /// * There is a database error. pub async fn add_category(&self, category_name: &str, user_id: &UserId) -> Result { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::AddCategory, Some(*user_id)) + .await?; let trimmed_name = category_name.trim(); @@ -70,13 +66,9 @@ impl Service { /// * The user does not have the required permissions. /// * There is a database error. pub async fn delete_category(&self, category_name: &str, user_id: &UserId) -> Result<(), ServiceError> { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::DeleteCategory, Some(*user_id)) + .await?; match self.category_repository.delete(category_name).await { Ok(()) => Ok(()), diff --git a/src/services/mod.rs b/src/services/mod.rs index b2431aec..567c35a9 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,6 +1,7 @@ //! App services. pub mod about; pub mod authentication; +pub mod authorization; pub mod category; pub mod hasher; pub mod proxy; diff --git a/src/services/proxy.rs b/src/services/proxy.rs index 9ea5ef3d..7ac2a475 100644 --- a/src/services/proxy.rs +++ b/src/services/proxy.rs @@ -10,18 +10,18 @@ use std::sync::Arc; use bytes::Bytes; -use super::user::DbUserRepository; use crate::cache::image::manager::{Error, ImageCacheService}; use crate::models::user::UserId; +use crate::services::user::Repository; pub struct Service { image_cache_service: Arc, - user_repository: Arc, + user_repository: Arc>, } impl Service { #[must_use] - pub fn new(image_cache_service: Arc, user_repository: Arc) -> Self { + pub fn new(image_cache_service: Arc, user_repository: Arc>) -> Self { Self { image_cache_service, user_repository, diff --git a/src/services/settings.rs b/src/services/settings.rs index a4b0e92e..d587fb9b 100644 --- a/src/services/settings.rs +++ b/src/services/settings.rs @@ -1,22 +1,22 @@ //! Settings service. use std::sync::Arc; -use super::user::DbUserRepository; +use super::authorization::{self, ACTION}; use crate::config::{Configuration, ConfigurationPublic, Settings}; use crate::errors::ServiceError; use crate::models::user::UserId; pub struct Service { configuration: Arc, - user_repository: Arc, + authorization_service: Arc, } impl Service { #[must_use] - pub fn new(configuration: Arc, user_repository: Arc) -> Service { + pub fn new(configuration: Arc, authorization_service: Arc) -> Service { Service { configuration, - user_repository, + authorization_service, } } @@ -26,13 +26,9 @@ impl Service { /// /// It returns an error if the user does not have the required permissions. pub async fn get_all(&self, user_id: &UserId) -> Result { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::GetSettings, Some(*user_id)) + .await?; let torrust_index_configuration = self.configuration.get_all().await; @@ -45,13 +41,9 @@ impl Service { /// /// It returns an error if the user does not have the required permissions. pub async fn get_all_masking_secrets(&self, user_id: &UserId) -> Result { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::GetSettingsSecret, Some(*user_id)) + .await?; let mut torrust_index_configuration = self.configuration.get_all().await; diff --git a/src/services/tag.rs b/src/services/tag.rs index fcbf56c3..e8684b52 100644 --- a/src/services/tag.rs +++ b/src/services/tag.rs @@ -1,7 +1,7 @@ //! Tag service. use std::sync::Arc; -use super::user::DbUserRepository; +use super::authorization::{self, ACTION}; use crate::databases::database::{Database, Error as DatabaseError, Error}; use crate::errors::ServiceError; use crate::models::torrent_tag::{TagId, TorrentTag}; @@ -9,15 +9,15 @@ use crate::models::user::UserId; pub struct Service { tag_repository: Arc, - user_repository: Arc, + authorization_service: Arc, } impl Service { #[must_use] - pub fn new(tag_repository: Arc, user_repository: Arc) -> Service { + pub fn new(tag_repository: Arc, authorization_service: Arc) -> Service { Service { tag_repository, - user_repository, + authorization_service, } } @@ -30,13 +30,7 @@ impl Service { /// * The user does not have the required permissions. /// * There is a database error. pub async fn add_tag(&self, tag_name: &str, user_id: &UserId) -> Result { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service.authorize(ACTION::AddTag, Some(*user_id)).await?; let trimmed_name = tag_name.trim(); @@ -62,13 +56,9 @@ impl Service { /// * The user does not have the required permissions. /// * There is a database error. pub async fn delete_tag(&self, tag_id: &TagId, user_id: &UserId) -> Result<(), ServiceError> { - let user = self.user_repository.get_compact(user_id).await?; - - // Check if user is administrator - // todo: extract authorization service - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::DeleteTag, Some(*user_id)) + .await?; match self.tag_repository.delete(tag_id).await { Ok(()) => Ok(()), diff --git a/src/services/torrent.rs b/src/services/torrent.rs index 606f8550..b98ef90b 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -6,7 +6,6 @@ use serde_derive::{Deserialize, Serialize}; use url::Url; use super::category::DbCategoryRepository; -use super::user::DbUserRepository; use crate::config::{Configuration, TrackerMode}; use crate::databases::database::{Database, Error, Sorting}; use crate::errors::ServiceError; @@ -17,6 +16,7 @@ use crate::models::torrent::{Metadata, TorrentId, TorrentListing}; use crate::models::torrent_file::{DbTorrent, Torrent, TorrentFile}; use crate::models::torrent_tag::{TagId, TorrentTag}; use crate::models::user::UserId; +use crate::services::user::Repository; use crate::tracker::statistics_importer::StatisticsImporter; use crate::utils::parse_torrent::decode_and_validate_torrent_file; use crate::{tracker, AsCSV}; @@ -25,7 +25,7 @@ pub struct Index { configuration: Arc, tracker_statistics_importer: Arc, tracker_service: Arc, - user_repository: Arc, + user_repository: Arc>, category_repository: Arc, torrent_repository: Arc, torrent_info_hash_repository: Arc, @@ -81,7 +81,7 @@ impl Index { configuration: Arc, tracker_statistics_importer: Arc, tracker_service: Arc, - user_repository: Arc, + user_repository: Arc>, category_repository: Arc, torrent_repository: Arc, torrent_info_hash_repository: Arc, diff --git a/src/services/user.rs b/src/services/user.rs index 05f9ecfc..dc397c17 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -3,8 +3,11 @@ use std::sync::Arc; use argon2::password_hash::SaltString; use argon2::{Argon2, PasswordHasher}; +use async_trait::async_trait; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use log::{debug, info}; +#[cfg(test)] +use mockall::automock; use pbkdf2::password_hash::rand_core::OsRng; use crate::config::{Configuration, EmailOnSignup}; @@ -26,7 +29,7 @@ fn no_email() -> String { pub struct RegistrationService { configuration: Arc, mailer: Arc, - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, } @@ -35,7 +38,7 @@ impl RegistrationService { pub fn new( configuration: Arc, mailer: Arc, - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, ) -> Self { Self { @@ -184,7 +187,7 @@ impl RegistrationService { } pub struct BanService { - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, banned_user_list: Arc, } @@ -192,7 +195,7 @@ pub struct BanService { impl BanService { #[must_use] pub fn new( - user_repository: Arc, + user_repository: Arc>, user_profile_repository: Arc, banned_user_list: Arc, ) -> Self { @@ -233,6 +236,15 @@ impl BanService { } } +#[cfg_attr(test, automock)] +#[async_trait] +pub trait Repository: Sync + Send { + async fn get_compact(&self, user_id: &UserId) -> Result; + async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error>; + async fn delete(&self, user_id: &UserId) -> Result<(), Error>; + async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result; +} + pub struct DbUserRepository { database: Arc>, } @@ -242,13 +254,16 @@ impl DbUserRepository { pub fn new(database: Arc>) -> Self { Self { database } } +} +#[async_trait] +impl Repository for DbUserRepository { /// It returns the compact user. /// /// # Errors /// /// It returns an error if there is a database error. - pub async fn get_compact(&self, user_id: &UserId) -> Result { + async fn get_compact(&self, user_id: &UserId) -> Result { // todo: persistence layer should have its own errors instead of // returning a `ServiceError`. self.database @@ -262,7 +277,7 @@ impl DbUserRepository { /// # Errors /// /// It returns an error if there is a database error. - pub async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error> { + async fn grant_admin_role(&self, user_id: &UserId) -> Result<(), Error> { self.database.grant_admin_role(*user_id).await } @@ -271,7 +286,7 @@ impl DbUserRepository { /// # Errors /// /// It returns an error if there is a database error. - pub async fn delete(&self, user_id: &UserId) -> Result<(), Error> { + async fn delete(&self, user_id: &UserId) -> Result<(), Error> { self.database.delete_user(*user_id).await } @@ -280,7 +295,7 @@ impl DbUserRepository { /// # Errors /// /// It returns an error if there is a database error. - pub async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result { + async fn add(&self, username: &str, email: &str, password_hash: &str) -> Result { self.database.insert_user_and_get_id(username, email, password_hash).await } }