From 85c249c37945f33d0dead2c14de646d7fe985639 Mon Sep 17 00:00:00 2001 From: Patrick Willner <50421879+heat1q@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:47:15 +0200 Subject: [PATCH] refactor(common): improve database trait api --- Cargo.lock | 4 + crates/common/Cargo.toml | 6 ++ crates/common/src/database/mod.rs | 51 ++++------ crates/common/src/lib.rs | 17 ++-- crates/database/Cargo.toml | 1 + crates/database/src/postgres.rs | 79 ++++++--------- crates/database/src/sqlite.rs | 117 +++++++++------------- crates/media/Cargo.toml | 3 + crates/media/src/api/router.rs | 66 ++++++------ crates/media/src/api/routes/get_media.rs | 10 +- crates/media/src/api/routes/post_media.rs | 17 ++-- crates/media/src/repository.rs | 49 +++------ src/lib.rs | 30 +++--- src/plugin/plugin_manager.rs | 11 +- 14 files changed, 208 insertions(+), 253 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c666d74..04ecb55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,9 +625,11 @@ dependencies = [ name = "common" version = "0.6.0" dependencies = [ + "anyhow", "async-trait", "axum", "http", + "mockall", "photos_network_plugin", "serde", "serde_json", @@ -883,6 +885,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" name = "database" version = "0.6.0" dependencies = [ + "anyhow", "async-trait", "common", "pretty_assertions", @@ -1790,6 +1793,7 @@ dependencies = [ name = "media" version = "0.6.0" dependencies = [ + "anyhow", "axum", "bytes", "common", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 2316765..b218d54 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -10,12 +10,16 @@ readme.workspace = true license.workspace = true edition.workspace = true +[features] +mock = ["mockall"] + [lib] name = "common" path = "src/lib.rs" doctest = false [dependencies] +anyhow.workspace = true async-trait.workspace = true axum.workspace = true http.workspace = true @@ -27,6 +31,8 @@ serde_with.workspace = true time.workspace = true tracing.workspace = true uuid = { workspace = true, features = ["serde"] } +mockall = { workspace = true, optional = true } [dev-dependencies] testdir.workspace = true +mockall = { workspace = true } diff --git a/crates/common/src/database/mod.rs b/crates/common/src/database/mod.rs index 321db81..8f4dd9a 100644 --- a/crates/common/src/database/mod.rs +++ b/crates/common/src/database/mod.rs @@ -15,8 +15,10 @@ * along with this program. If not, see . */ +use std::sync::Arc; + +use anyhow::Result; use async_trait::async_trait; -use std::error::Error; use time::OffsetDateTime; use crate::auth::user::User; @@ -30,57 +32,44 @@ pub mod reference; pub mod tag; pub mod user; +pub type ArcDynDatabase = Arc; + +#[cfg_attr(any(test, feature = "mock"), mockall::automock)] #[async_trait] pub trait Database { - /// Initialize the database and run required migrations - async fn setup(&mut self) -> Result<(), Box>; - /// List registered user accounts - async fn get_users(&self) -> Result, Box>; + async fn get_users(&self) -> Result>; /// Create a new user account - async fn create_user(&self, user: &User) -> Result<(), Box>; + async fn create_user(&self, user: &User) -> Result<()>; /// Get user by user_id - async fn get_user(&self, user_id: &str) -> Result>; + async fn get_user(&self, user_id: &str) -> Result; /// Partial update a single user account - async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box>; - async fn update_nickname(&self, nickname: &str) -> Result<(), Box>; - async fn update_names( - &self, - firstname: &str, - lastname: &str, - user_id: &str, - ) -> Result<(), Box>; + async fn update_email(&self, email: &str, user_id: &str) -> Result<()>; + async fn update_nickname(&self, nickname: &str) -> Result<()>; + async fn update_names(&self, firstname: &str, lastname: &str, user_id: &str) -> Result<()>; - async fn disable_user(&self, user_id: &str) -> Result<(), Box>; - async fn enable_user(&self, user_id: &str) -> Result<(), Box>; + async fn disable_user(&self, user_id: &str) -> Result<()>; + async fn enable_user(&self, user_id: &str) -> Result<()>; - async fn get_media_items(&self, user_id: &str) -> Result, Box>; + async fn get_media_items(&self, user_id: &str) -> Result>; async fn create_media_item( &self, user_id: &str, name: &str, date_taken: OffsetDateTime, - ) -> Result>; - async fn get_media_item(&self, media_id: &str) -> Result>; + ) -> Result; + async fn get_media_item(&self, media_id: &str) -> Result; async fn add_reference( &self, user_id: &str, media_id: &str, reference: &Reference, - ) -> Result>; + ) -> Result; - async fn update_reference( - &self, - reference_id: &str, - reference: &Reference, - ) -> Result<(), Box>; + async fn update_reference(&self, reference_id: &str, reference: &Reference) -> Result<()>; - async fn remove_reference( - &self, - media_id: &str, - reference_id: &str, - ) -> Result<(), Box>; + async fn remove_reference(&self, media_id: &str, reference_id: &str) -> Result<()>; } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 74b59d3..c500b0d 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -18,11 +18,11 @@ //! This crate offers shared data models for [Photos.network](https://photos.network) core application. //! -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use axum::Router; use config::configuration::Configuration; -use database::Database; +use database::ArcDynDatabase; use photos_network_plugin::{PluginFactoryRef, PluginId}; pub mod auth; @@ -35,18 +35,15 @@ pub mod model { /// Aggregates the applications configuration, its loaded plugins and the router for all REST APIs #[derive(Clone)] -pub struct ApplicationState { - pub config: Configuration, +pub struct ApplicationState { + pub config: Arc, pub plugins: HashMap, pub router: Option, - pub database: D, + pub database: ArcDynDatabase, } -impl ApplicationState -where - D: Database, -{ - pub fn new(config: Configuration, database: D) -> Self { +impl ApplicationState { + pub fn new(config: Arc, database: ArcDynDatabase) -> Self { Self { config, plugins: HashMap::new(), diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index b38d323..7508668 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -16,6 +16,7 @@ path = "src/lib.rs" doctest = false [dependencies] +anyhow.workspace = true common.workspace = true async-trait.workspace = true tracing.workspace = true diff --git a/crates/database/src/postgres.rs b/crates/database/src/postgres.rs index d42ba7a..70a5d26 100644 --- a/crates/database/src/postgres.rs +++ b/crates/database/src/postgres.rs @@ -17,6 +17,7 @@ //! This crate offers a database abstraction for [Photos.network](https://photos.network) core application. //! +use anyhow::Result; use async_trait::async_trait; use common::auth::user::User; use common::database::media_item::MediaItem; @@ -25,7 +26,6 @@ use common::database::Database; use sqlx::types::time::OffsetDateTime; use sqlx::PgPool; use sqlx::Row; -use std::error::Error; use tracing::info; use uuid::Uuid; @@ -35,23 +35,19 @@ pub struct PostgresDatabase { } impl PostgresDatabase { - pub async fn new(db_url: &str) -> Self { - let pool = PgPool::connect(db_url).await.unwrap(); + pub async fn new(db_url: &str) -> Result { + let pool = PgPool::connect(db_url).await?; - PostgresDatabase { pool } + // run migrations from `migrations` directory + sqlx::migrate!("./migrations").run(&pool).await?; + + Ok(PostgresDatabase { pool }) } } #[async_trait] impl Database for PostgresDatabase { - async fn setup(&mut self) -> Result<(), Box> { - // run migrations from `migrations` directory - sqlx::migrate!("./migrations").run(&self.pool).await?; - - Ok(()) - } - - async fn get_users(&self) -> Result, Box> { + async fn get_users(&self) -> Result> { let query = "SELECT uuid, email, password, lastname, firstname FROM users"; let res = sqlx::query(query); @@ -76,7 +72,7 @@ impl Database for PostgresDatabase { Ok(users) } - async fn create_user(&self, user: &User) -> Result<(), Box> { + async fn create_user(&self, user: &User) -> Result<()> { let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)"; let id = Uuid::new_v4().hyphenated().to_string(); info!("create new user with id `{}`.", id); @@ -92,11 +88,11 @@ impl Database for PostgresDatabase { Ok(()) } - async fn get_user(&self, _user_id: &str) -> Result> { - Err("Not implemented".into()) + async fn get_user(&self, _user_id: &str) -> Result { + unimplemented!() } - async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> { + async fn update_email(&self, email: &str, user_id: &str) -> Result<()> { let query = "UPDATE users SET email = $1 WHERE uuid = $2"; sqlx::query(query) @@ -108,28 +104,23 @@ impl Database for PostgresDatabase { Ok(()) } - async fn update_nickname(&self, _nickname: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_nickname(&self, _nickname: &str) -> Result<()> { + unimplemented!() } - async fn update_names( - &self, - _firstname: &str, - _lastname: &str, - _user_id: &str, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_names(&self, _firstname: &str, _lastname: &str, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn disable_user(&self, _user_id: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn disable_user(&self, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn enable_user(&self, _user_id: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn enable_user(&self, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn get_media_items(&self, _user_id: &str) -> Result, Box> { - Err("Not implemented".into()) + async fn get_media_items(&self, _user_id: &str) -> Result> { + unimplemented!() } /// Creates a new media item if it doesn't exist and returns the media_id @@ -138,7 +129,7 @@ impl Database for PostgresDatabase { user_id: &str, name: &str, date_taken: OffsetDateTime, - ) -> Result> { + ) -> Result { let query = "SELECT COUNT(*) FROM media WHERE owner is $1 and taken_at like $2"; let res = sqlx::query(query).bind(user_id).bind(date_taken); let rows = res.fetch_all(&self.pool).await?; @@ -164,31 +155,23 @@ impl Database for PostgresDatabase { Ok("".to_string()) } - async fn get_media_item(&self, _media_id: &str) -> Result> { - Err("Not implemented".into()) + async fn get_media_item(&self, _media_id: &str) -> Result { + unimplemented!() } async fn add_reference( &self, _user_id: &str, _media_id: &str, _reference: &Reference, - ) -> Result> { - Err("Not implemented".into()) + ) -> Result { + unimplemented!() } - async fn update_reference( - &self, - _reference_id: &str, - _reference: &Reference, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_reference(&self, _reference_id: &str, _reference: &Reference) -> Result<()> { + unimplemented!() } - async fn remove_reference( - &self, - _media_id: &str, - _reference_id: &str, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn remove_reference(&self, _media_id: &str, _reference_id: &str) -> Result<()> { + unimplemented!() } } diff --git a/crates/database/src/sqlite.rs b/crates/database/src/sqlite.rs index 2d39d68..72e242b 100644 --- a/crates/database/src/sqlite.rs +++ b/crates/database/src/sqlite.rs @@ -17,6 +17,7 @@ //! This crate offers a database abstraction for [Photos.network](https://photos.network) core application. //! +use anyhow::Result; use async_trait::async_trait; use common::auth::user::User; use common::database::media_item::MediaItem; @@ -26,7 +27,6 @@ use sqlx::sqlite::SqliteQueryResult; use sqlx::types::time::OffsetDateTime; use sqlx::Row; use sqlx::SqlitePool; -use std::error::Error; use std::i64; use tracing::error; use tracing::info; @@ -38,23 +38,19 @@ pub struct SqliteDatabase { } impl SqliteDatabase { - pub async fn new(db_url: &str) -> Self { - let pool = SqlitePool::connect(db_url).await.unwrap(); + pub async fn new(db_url: &str) -> Result { + let pool = SqlitePool::connect(db_url).await?; - SqliteDatabase { pool } + // run migrations from `migrations` directory + sqlx::migrate!("./migrations").run(&pool).await?; + + Ok(SqliteDatabase { pool }) } } #[async_trait] impl Database for SqliteDatabase { - async fn setup(&mut self) -> Result<(), Box> { - // run migrations from `migrations` directory - sqlx::migrate!("./migrations").run(&self.pool).await?; - - Ok(()) - } - - async fn get_users(&self) -> Result, Box> { + async fn get_users(&self) -> Result> { let query = "SELECT uuid, email, password, lastname, firstname FROM users"; let res = sqlx::query(query); @@ -79,7 +75,7 @@ impl Database for SqliteDatabase { Ok(users) } - async fn create_user(&self, user: &User) -> Result<(), Box> { + async fn create_user(&self, user: &User) -> Result<()> { let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)"; let id = Uuid::new_v4().hyphenated().to_string(); info!("create new user with id `{}`.", id); @@ -95,11 +91,11 @@ impl Database for SqliteDatabase { Ok(()) } - async fn get_user(&self, _user_id: &str) -> Result> { - Err("Not implemented".into()) + async fn get_user(&self, _user_id: &str) -> Result { + unimplemented!() } - async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> { + async fn update_email(&self, email: &str, user_id: &str) -> Result<()> { let query = "UPDATE users SET email = $1 WHERE uuid = $2"; sqlx::query(query) @@ -111,35 +107,30 @@ impl Database for SqliteDatabase { Ok(()) } - async fn update_nickname(&self, _nickname: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_nickname(&self, _nickname: &str) -> Result<()> { + unimplemented!() } - async fn update_names( - &self, - _firstname: &str, - _lastname: &str, - _user_id: &str, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_names(&self, _firstname: &str, _lastname: &str, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn disable_user(&self, _user_id: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn disable_user(&self, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn enable_user(&self, _user_id: &str) -> Result<(), Box> { - Err("Not implemented".into()) + async fn enable_user(&self, _user_id: &str) -> Result<()> { + unimplemented!() } - async fn get_media_items(&self, _user_id: &str) -> Result, Box> { - Err("Not implemented".into()) + async fn get_media_items(&self, _user_id: &str) -> Result> { + unimplemented!() } async fn create_media_item( &self, user_id: &str, name: &str, date_taken: OffsetDateTime, - ) -> Result> { + ) -> Result { struct Item { uuid: String, } @@ -188,8 +179,8 @@ impl Database for SqliteDatabase { }; } - async fn get_media_item(&self, _media_id: &str) -> Result> { - Err("Not implemented".into()) + async fn get_media_item(&self, _media_id: &str) -> Result { + unimplemented!() } async fn add_reference( @@ -197,7 +188,7 @@ impl Database for SqliteDatabase { user_id: &str, media_id: &str, reference: &Reference, - ) -> Result> { + ) -> Result { let query = "INSERT INTO reference (uuid, media, owner, filepath, filename, size) VALUES ($1, $2, $3, $4, $5, $6)"; let id = Uuid::new_v4().hyphenated().to_string(); let _res: SqliteQueryResult = sqlx::query(query) @@ -213,20 +204,12 @@ impl Database for SqliteDatabase { Ok(id) } - async fn update_reference( - &self, - _reference_id: &str, - _reference: &Reference, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn update_reference(&self, _reference_id: &str, _reference: &Reference) -> Result<()> { + unimplemented!() } - async fn remove_reference( - &self, - _media_id: &str, - _reference_id: &str, - ) -> Result<(), Box> { - Err("Not implemented".into()) + async fn remove_reference(&self, _media_id: &str, _reference_id: &str) -> Result<()> { + unimplemented!() } } @@ -238,12 +221,12 @@ mod tests { use time::format_description::well_known::Rfc3339; #[sqlx::test] - async fn create_user_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + async fn create_user_should_succeed(pool: SqlitePool) -> Result<()> { // given let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/create_user_should_succeed.sqlite", ) - .await; + .await?; // when for i in 0..3 { @@ -273,12 +256,12 @@ mod tests { } #[sqlx::test] - async fn create_already_existing_user_should_fail(pool: SqlitePool) -> sqlx::Result<()> { + async fn create_already_existing_user_should_fail(pool: SqlitePool) -> Result<()> { // given let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/create_already_existing_user_should_fail.sqlite", ) - .await; + .await?; // when let uuid = uuid::Uuid::new_v4().hyphenated().to_string(); @@ -310,7 +293,7 @@ mod tests { } #[sqlx::test] - async fn update_email_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + async fn update_email_should_succeed(pool: SqlitePool) -> Result<()> { // given sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") .bind("570DC079-664A-4496-BAA3-668C445A447") @@ -322,13 +305,13 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/update_email_should_succeed.sqlite", ) - .await; + .await?; // when let result = db .update_email( - "security@photos.network".into(), - "570DC079-664A-4496-BAA3-668C445A447".into(), + "security@photos.network", + "570DC079-664A-4496-BAA3-668C445A447", ) .await; @@ -343,7 +326,7 @@ mod tests { } #[sqlx::test] - async fn update_email_to_existing_should_fail(pool: SqlitePool) -> sqlx::Result<()> { + async fn update_email_to_existing_should_fail(pool: SqlitePool) -> Result<()> { // given sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") .bind("570DC079-664A-4496-BAA3-668C445A447") @@ -364,13 +347,13 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/update_email_to_existing_should_fail.sqlite", ) - .await; + .await?; // when let result = db .update_email( - "security@photos.network".into(), - "570DC079-664A-4496-BAA3-668C445A447".into(), + "security@photos.network", + "570DC079-664A-4496-BAA3-668C445A447", ) .await; @@ -387,7 +370,7 @@ mod tests { } #[sqlx::test] - async fn get_users_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + async fn get_users_should_succeed(pool: SqlitePool) -> Result<()> { // given sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") .bind("570DC079-664A-4496-BAA3-668C445A447") @@ -399,7 +382,7 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/get_users_should_succeed.sqlite", ) - .await; + .await?; // when let users = db.get_users().await.unwrap(); @@ -416,7 +399,7 @@ mod tests { //noinspection DuplicatedCode #[sqlx::test] - async fn create_media_item_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + async fn create_media_item_should_succeed(pool: SqlitePool) -> Result<()> { // given let user_id = "570DC079-664A-4496-BAA3-668C445A447"; // create fake user - used as FOREIGN KEY in media @@ -430,7 +413,7 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/create_media_item_should_succeed.sqlite", ) - .await; + .await?; let name = "DSC_1234"; let date_taken = OffsetDateTime::now_utc(); @@ -446,7 +429,7 @@ mod tests { //noinspection DuplicatedCode #[sqlx::test] - async fn create_media_item_should_return_existing_uuid(pool: SqlitePool) -> sqlx::Result<()> { + async fn create_media_item_should_return_existing_uuid(pool: SqlitePool) -> Result<()> { // given let user_id = "570DC079-664A-4496-BAA3-668C445A447"; @@ -476,7 +459,7 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/create_media_item_should_return_existing_uuid.sqlite", ) - .await; + .await?; // when let media_item_result = db.create_media_item(user_id, name, taken_at).await; @@ -490,7 +473,7 @@ mod tests { //noinspection DuplicatedCode #[sqlx::test] - async fn add_reference_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + async fn add_reference_should_succeed(pool: SqlitePool) -> Result<()> { // given let user_id = "570DC079-664A-4496-BAA3-668C445A447"; let media_id = "ef9ac799-02f3-4b3f-9d96-7576be0434e6"; @@ -517,7 +500,7 @@ mod tests { let db = SqliteDatabase::new( "target/sqlx/test-dbs/database/sqlite/tests/add_reference_should_succeed.sqlite", ) - .await; + .await?; let filename = "DSC_1234.jpg"; let dir: PathBuf = testdir!(); diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index b4a7b3e..ce8f6b1 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -22,6 +22,8 @@ database.workspace = true time.workspace = true +anyhow.workspace = true + tracing.workspace = true tokio = { workspace = true, features = ["full"] } @@ -49,3 +51,4 @@ mockall.workspace = true rstest.workspace = true tower = { workspace = true, features = ["util"] } testdir.workspace = true +common = { workspace = true, features = ["mock"] } diff --git a/crates/media/src/api/router.rs b/crates/media/src/api/router.rs index fa53a32..f39a4bf 100644 --- a/crates/media/src/api/router.rs +++ b/crates/media/src/api/router.rs @@ -31,18 +31,17 @@ use crate::repository::{MediaRepository, MediaRepositoryState}; use axum::routing::{delete, get, patch, post}; use axum::Router; use common::ApplicationState; -use database::sqlite::SqliteDatabase; use std::sync::Arc; pub struct MediaApi {} impl MediaApi { - pub async fn routes(state: ApplicationState) -> Router + pub async fn routes(state: &ApplicationState) -> Router where S: Send + Sync + Clone, { let media_repository: MediaRepository = - MediaRepository::new(state.database.clone(), state.config.clone()).await; + MediaRepository::new(Arc::clone(&state.database), Arc::clone(&state.config)).await; let repository_state: MediaRepositoryState = Arc::new(media_repository); Router::new() @@ -98,21 +97,24 @@ mod tests { body::Body, http::{self, Request, StatusCode}, }; - use common::config::configuration::Configuration; + use common::{config::configuration::Configuration, database::MockDatabase}; use serde_json::json; - use sqlx::SqlitePool; use tower::ServiceExt; - #[sqlx::test] - async fn get_media_with_query_success(pool: SqlitePool) { + #[tokio::test] + async fn get_media_with_query_success() { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let mut mock_db = MockDatabase::new(); + mock_db + .expect_get_media_items() + .return_once(|_| Ok(Vec::new())); + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(mock_db), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app @@ -136,16 +138,20 @@ mod tests { assert_eq!(body, "list media items. limit=100000, offset=1"); } - #[sqlx::test] - async fn get_media_without_query_success(pool: SqlitePool) { + #[tokio::test] + async fn get_media_without_query_success() { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let mut mock_db = MockDatabase::new(); + mock_db + .expect_get_media_items() + .return_once(|_| Ok(Vec::new())); + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(mock_db), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app @@ -169,16 +175,17 @@ mod tests { assert_eq!(body, "list media items. limit=1000, offset=0"); } - #[sqlx::test] - async fn post_media_without_user_fail(pool: SqlitePool) { + #[tokio::test] + async fn post_media_without_user_fail() { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let mock_db = MockDatabase::new(); + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(mock_db), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app @@ -207,17 +214,18 @@ mod tests { } // TODO: test is failing due to missing multi-part body - //#[sqlx::test] + //#[tokio::test] #[allow(dead_code)] - async fn post_media_success(pool: SqlitePool) { + async fn post_media_success() { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let mock_db = MockDatabase::new(); + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(mock_db), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app diff --git a/crates/media/src/api/routes/get_media.rs b/crates/media/src/api/routes/get_media.rs index 9060981..80dba9e 100644 --- a/crates/media/src/api/routes/get_media.rs +++ b/crates/media/src/api/routes/get_media.rs @@ -65,7 +65,7 @@ pub(crate) async fn get_media( #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{collections::HashMap, sync::Arc}; use axum::Router; use common::{config::configuration::Configuration, ApplicationState}; @@ -81,14 +81,14 @@ mod tests { #[sqlx::test] async fn get_media_unauthorized_should_not_fail(pool: SqlitePool) { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(SqliteDatabase { pool }), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app diff --git a/crates/media/src/api/routes/post_media.rs b/crates/media/src/api/routes/post_media.rs index 8a209f3..d33c60a 100644 --- a/crates/media/src/api/routes/post_media.rs +++ b/crates/media/src/api/routes/post_media.rs @@ -104,6 +104,7 @@ pub(crate) async fn post_media( mod tests { use std::collections::HashMap; use std::io; + use std::sync::Arc; use axum::Router; use common::{config::configuration::Configuration, ApplicationState}; @@ -126,14 +127,14 @@ mod tests { #[sqlx::test] async fn post_media_unauthorized_should_fail(pool: SqlitePool) { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(SqliteDatabase { pool }), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); // when let response = app @@ -155,13 +156,13 @@ mod tests { #[ignore] async fn post_media_authorized_without_name_field(pool: SqlitePool) { // given - let state: ApplicationState = ApplicationState { - config: Configuration::empty(), + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), plugins: HashMap::new(), router: None, - database: SqliteDatabase { pool }, + database: Arc::new(SqliteDatabase { pool }), }; - let app = Router::new().nest("/", MediaApi::routes(state).await); + let app = Router::new().nest("/", MediaApi::routes(&state).await); let data = media_item_form_data().await.unwrap(); // when diff --git a/crates/media/src/repository.rs b/crates/media/src/repository.rs index 9d414f0..d4500e0 100644 --- a/crates/media/src/repository.rs +++ b/crates/media/src/repository.rs @@ -17,12 +17,12 @@ use crate::data::error::DataAccessError; use crate::data::media_item::MediaItem; +use anyhow::Result; use axum::async_trait; use bytes::Bytes; use common::config::configuration::Configuration; use common::database::reference::Reference; -use common::database::Database; -use database::sqlite::SqliteDatabase; +use common::database::ArcDynDatabase; use std::fs; use std::path::Path; use std::sync::Arc; @@ -33,8 +33,8 @@ use uuid::Uuid; #[allow(dead_code)] pub struct MediaRepository { - pub(crate) database: SqliteDatabase, - pub(crate) config: Configuration, + pub(crate) database: ArcDynDatabase, + pub(crate) config: Arc, } pub type MediaRepositoryState = Arc; @@ -67,7 +67,7 @@ pub trait MediaRepositoryTrait { } impl MediaRepository { - pub async fn new(database: SqliteDatabase, config: Configuration) -> Self { + pub async fn new(database: ArcDynDatabase, config: Arc) -> Self { Self { database, config } } } @@ -175,37 +175,21 @@ impl MediaRepositoryTrait for MediaRepository { } #[allow(unused_imports)] +#[cfg(test)] mod tests { - use database::sqlite::SqliteDatabase; - use sqlx::SqlitePool; - use super::*; + use common::database::*; - //noinspection DuplicatedCode - #[sqlx::test(migrations = "../database/migrations")] - async fn get_media_items_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + #[tokio::test] + async fn get_media_items_should_succeed() -> Result<()> { // given let user_id = "605EE8BE-BAF2-4499-B8D4-BA8C74E8B242"; - sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") - .bind(user_id) - .bind("info@photos.network") - .bind("unsecure") - .bind("Stuermer") - .bind("Benjamin") - .execute(&pool).await?; - - sqlx::query("INSERT INTO media (uuid, name, owner) VALUES ($1, $2, $3)") - .bind("6A92460C-53FB-4B42-AC1B-E6760A34E169") - .bind("DSC_1234") - .bind(user_id) - .execute(&pool) - .await?; - - let db = SqliteDatabase::new( - "target/sqlx/test-dbs/media/repository/tests/get_media_items_should_succeed.sqlite", - ) - .await; - let repository = MediaRepository::new(db, Configuration::empty()).await; + let mut mock_db = MockDatabase::new(); + mock_db + .expect_get_media_items() + .return_once(|_| Ok(Vec::new())); + let repository = + MediaRepository::new(Arc::new(mock_db), Configuration::empty().into()).await; // when let result = repository @@ -213,8 +197,7 @@ mod tests { .await; // then - // TODO fix assertion - assert!(result.is_err()); + assert!(result.is_ok()); //assert_eq!(result.ok().unwrap().len(), 1); Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 3a9a7af..adbd3ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,16 +29,17 @@ use std::fs::{self, OpenOptions}; use std::net::SocketAddr; +use std::sync::Arc; use abi_stable::external_types::crossbeam_channel; use abi_stable::std_types::RResult::{RErr, ROk}; use accounts::api::router::AccountsApi; -use anyhow::Result; +use anyhow::{Context, Result}; use axum::extract::DefaultBodyLimit; use axum::routing::{get, head}; use axum::{Json, Router}; use common::auth::user::User; -use common::database::Database; +use common::database::ArcDynDatabase; use common::ApplicationState; use database::sqlite::SqliteDatabase; use media::api::router::MediaApi; @@ -95,7 +96,8 @@ pub async fn start_server() -> Result<()> { fs::create_dir_all("plugins")?; // read config file - let configuration = Configuration::new(CONFIG_PATH).expect("Could not parse configuration!"); + let configuration = + Arc::new(Configuration::new(CONFIG_PATH).context("Could not parse configuration!")?); debug!("Configuration: {}", configuration); // init database @@ -106,14 +108,11 @@ pub async fn start_server() -> Result<()> { .create_new(true) .open("data/core.sqlite3"); - let mut db = SqliteDatabase::new("data/core.sqlite3").await; + // TODO: which db? + let db: ArcDynDatabase = Arc::new(SqliteDatabase::new("data/core.sqlite3").await?); - { - let _ = db.setup().await; - } - - let users = db.get_users().await; - if users.unwrap().is_empty() { + let users = db.get_users().await?; + if users.is_empty() { info!("No user found, create a default admin user. Please check `data/credentials.txt` for details."); let default_user = "photo@photos.network"; let default_pass = "unsecure"; @@ -134,12 +133,11 @@ pub async fn start_server() -> Result<()> { updated_at: None, last_login: None, }; - let _ = db.clone().create_user(&user).await; + let _ = db.create_user(&user).await; } // init application state - //let mut app_state = ApplicationState::::new(configuration.clone(), db); - let mut app_state = ApplicationState::::new(configuration.clone(), db); + let mut app_state = ApplicationState::new(Arc::clone(&configuration), db); let cfg = ServerConfig { listen_addr: configuration.internal_url.to_owned(), @@ -168,7 +166,7 @@ pub async fn start_server() -> Result<()> { .route("/", head(status)) // Media items - .nest("/", MediaApi::routes(app_state.clone()).await) + .nest("/", MediaApi::routes(&app_state).await) // OAuth 2.0 Authentication .nest("/", AuthenticationManager::routes()) @@ -196,7 +194,7 @@ pub async fn start_server() -> Result<()> { // initialize plugin manager let mut plugin_manager = PluginManager::new( - configuration.clone(), + Arc::clone(&configuration), PLUGIN_PATH.to_string(), &mut app_state, )?; @@ -252,7 +250,7 @@ pub async fn start_server() -> Result<()> { axum::Server::bind(&addr) .serve(router.into_make_service()) .await - .unwrap(); + .context("start server")?; Ok(()) } diff --git a/src/plugin/plugin_manager.rs b/src/plugin/plugin_manager.rs index 4bb7cfe..a877b83 100644 --- a/src/plugin/plugin_manager.rs +++ b/src/plugin/plugin_manager.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use abi_stable::library::{lib_header_from_path, LibrarySuffix, RawLibrary}; @@ -23,21 +23,20 @@ use anyhow::Result; use common::{config::configuration::Configuration, ApplicationState}; use core_extensions::SelfOps; -use database::sqlite::SqliteDatabase; use photos_network_plugin::{PluginFactoryRef, PluginId}; use tracing::{debug, error, info}; pub struct PluginManager<'a> { - config: Configuration, + config: Arc, path: String, - state: &'a mut ApplicationState, + state: &'a mut ApplicationState, } impl<'a> PluginManager<'a> { pub fn new( - config: Configuration, + config: Arc, path: String, - state: &'a mut ApplicationState, + state: &'a mut ApplicationState, ) -> Result { Ok(Self { config,