diff --git a/Cargo.lock b/Cargo.lock index 3ca115b..4c742e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,6 +1807,7 @@ dependencies = [ "rstest", "serde", "serde_json", + "serde_qs", "sqlx", "tempfile", "testdir", @@ -2843,6 +2844,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "axum", + "futures", + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 27a6801..91d4dca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ rsa = "0.9.2" reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "stream", "multipart"] } serde = "1.0.183" +serde_qs = "0.12.0" serde_json = { version = "1.0.104", features = ["raw_value"] } serde_with = "3.3.0" serde_urlencoded = "0.7.1" diff --git a/crates/common/src/database/mod.rs b/crates/common/src/database/mod.rs index c35ffa5..ad588bf 100644 --- a/crates/common/src/database/mod.rs +++ b/crates/common/src/database/mod.rs @@ -56,7 +56,12 @@ pub trait Database { 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>; + async fn get_media_items( + &self, + user_id: &str, + years: Vec, + months: Vec, + ) -> Result>; async fn create_media_item( &self, user_id: &str, diff --git a/crates/database/src/postgres.rs b/crates/database/src/postgres.rs index 452c249..2ccfbbf 100644 --- a/crates/database/src/postgres.rs +++ b/crates/database/src/postgres.rs @@ -119,7 +119,12 @@ impl Database for PostgresDatabase { unimplemented!() } - async fn get_media_items(&self, _user_id: &str) -> Result> { + async fn get_media_items( + &self, + _user_id: &str, + _years: Vec, + _months: Vec, + ) -> Result> { unimplemented!() } diff --git a/crates/database/src/sqlite.rs b/crates/database/src/sqlite.rs index f8e1af4..30cc2c0 100644 --- a/crates/database/src/sqlite.rs +++ b/crates/database/src/sqlite.rs @@ -122,8 +122,13 @@ impl Database for SqliteDatabase { unimplemented!() } - async fn get_media_items(&self, _user_id: &str) -> Result> { - unimplemented!() + async fn get_media_items( + &self, + _user_id: &str, + years: Vec, + months: Vec, + ) -> Result> { + Ok(vec![]) } async fn create_media_item( &self, diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index ce8f6b1..235eb64 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -37,6 +37,7 @@ hyper = { workspace = true, features = ["full"] } tower-http.workspace = true mime.workspace = true bytes.workspace = true +serde_qs = { workspace = true, features = ["axum"] } # persistency uuid = { workspace = true, features = ["serde"] } diff --git a/crates/media/src/api/router.rs b/crates/media/src/api/router.rs index f39a4bf..77b65ae 100644 --- a/crates/media/src/api/router.rs +++ b/crates/media/src/api/router.rs @@ -120,7 +120,7 @@ mod tests { let response = app .oneshot( Request::builder() - .uri("/media?limit=100000&offset=1") + .uri("/media") .method("GET") .header("Authorization", "FakeAuth") .body(Body::empty()) @@ -135,7 +135,7 @@ mod tests { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let body: String = serde_json::from_slice(&body).unwrap(); - assert_eq!(body, "list media items. limit=100000, offset=1"); + assert_eq!(body, "list media items. limit=, offset="); } #[tokio::test] @@ -172,7 +172,7 @@ mod tests { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let body: String = serde_json::from_slice(&body).unwrap(); - assert_eq!(body, "list media items. limit=1000, offset=0"); + assert_eq!(body, "list media items. limit=, offset="); } #[tokio::test] diff --git a/crates/media/src/api/routes/get_media.rs b/crates/media/src/api/routes/get_media.rs index 80dba9e..f372935 100644 --- a/crates/media/src/api/routes/get_media.rs +++ b/crates/media/src/api/routes/get_media.rs @@ -18,9 +18,10 @@ //! Returns a list of owned media items for current user //! use axum::extract::State; -use axum::{extract::Query, http::StatusCode, Json}; +use axum::{http::StatusCode, Json}; use common::auth::user::User; use serde::{Deserialize, Serialize}; +use serde_qs::axum::QsQuery; use std::result::Result; use tracing::error; use uuid::Uuid; @@ -31,17 +32,21 @@ use crate::repository::MediaRepositoryState; #[derive(Serialize, Deserialize)] pub(crate) struct MediaListQuery { - offset: Option, - limit: Option, + years: Option>, + months: Option>, } pub(crate) async fn get_media( State(repo): State, user: User, - Query(query): Query, + QsQuery(query): QsQuery, //Query(query): Query, ) -> Result, StatusCode> { let items: Result, DataAccessError> = repo - .get_media_items_for_user(Uuid::parse_str(user.uuid.as_str()).unwrap()) + .get_media_items_for_user( + Uuid::parse_str(user.uuid.as_str()).unwrap(), + query.years.unwrap_or(vec![]), + query.months.unwrap_or(vec![]), + ) .await; match items { Ok(i) => { @@ -51,15 +56,11 @@ pub(crate) async fn get_media( error!("Failed to get media items!"); } } + // TODO: read list from persistency // TODO: return list Ok(Json( - format!( - "list media items. limit={}, offset={}", - query.limit.unwrap_or(1000), - query.offset.unwrap_or(0) - ) - .to_owned(), + format!("list media items. limit=, offset=").to_owned(), )) } @@ -105,4 +106,126 @@ mod tests { // then assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + + #[sqlx::test] + async fn get_media_with_multiple_years_only_should_return_only_requested_years( + pool: SqlitePool, + ) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), + plugins: HashMap::new(), + router: None, + database: Arc::new(SqliteDatabase { pool }), + }; + + let app = Router::new().nest("/", MediaApi::routes(&state).await); + + // when + let response = app + .oneshot( + Request::builder() + .method("GET") + .header(hyper::header::AUTHORIZATION, "FakeAuth") + .uri(format!("/media?years=[1990,1991]")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::OK); + } + + #[sqlx::test] + async fn get_media_with_multiple_years_and_months_should_return_requested_years_and_months( + pool: SqlitePool, + ) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), + plugins: HashMap::new(), + router: None, + database: Arc::new(SqliteDatabase { pool }), + }; + + let app = Router::new().nest("/", MediaApi::routes(&state).await); + + // when + let response = app + .oneshot( + Request::builder() + .method("GET") + .header(hyper::header::AUTHORIZATION, "FakeAuth") + .uri(format!("/media?years=[1990,1991]&months=[3,4]")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::OK); + } + + #[sqlx::test] + async fn get_media_with_multiple_month_only_should_return_requested_months_for_all_years( + pool: SqlitePool, + ) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), + plugins: HashMap::new(), + router: None, + database: Arc::new(SqliteDatabase { pool }), + }; + + let app = Router::new().nest("/", MediaApi::routes(&state).await); + + // when + let response = app + .oneshot( + Request::builder() + .method("GET") + .header(hyper::header::AUTHORIZATION, "FakeAuth") + .uri(format!("/media?years=[1990,1991]")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::OK); + } + + #[sqlx::test] + async fn get_media_without_parameters_should_return_all(pool: SqlitePool) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty().into(), + plugins: HashMap::new(), + router: None, + database: Arc::new(SqliteDatabase { pool }), + }; + + let app = Router::new().nest("/", MediaApi::routes(&state).await); + + // when + let response = app + .oneshot( + Request::builder() + .method("GET") + .header(hyper::header::AUTHORIZATION, "FakeAuth") + .uri(format!("/media")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::OK); + } } diff --git a/crates/media/src/repository.rs b/crates/media/src/repository.rs index c114c0c..ff262b4 100644 --- a/crates/media/src/repository.rs +++ b/crates/media/src/repository.rs @@ -46,6 +46,8 @@ pub trait MediaRepositoryTrait { async fn get_media_items_for_user( &self, user_id: Uuid, + years: Vec, + months: Vec, ) -> Result, DataAccessError>; /// Create a new media item for the given user @@ -76,12 +78,17 @@ impl MediaRepositoryTrait for MediaRepository { async fn get_media_items_for_user( &self, user_id: Uuid, + years: Vec, + months: Vec, ) -> Result, DataAccessError> { - info!("get items for user {}", user_id); + info!( + "get items for user:{}, years: {:?}, month: {:?}", + user_id, years, months + ); let items_result = &self .database - .get_media_items(user_id.hyphenated().to_string().as_str()) + .get_media_items(user_id.hyphenated().to_string().as_str(), years, months) .await; return match items_result { Ok(items) => { @@ -208,7 +215,7 @@ mod tests { // when let result = repository - .get_media_items_for_user(Uuid::parse_str(user_id).unwrap()) + .get_media_items_for_user(Uuid::parse_str(user_id).unwrap(), Vec::new(), Vec::new()) .await; // then