diff options
| -rw-r--r-- | crates/rocie-server/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/rocie-server/src/api/get/product.rs | 43 | ||||
| -rw-r--r-- | crates/rocie-server/tests/products/query.rs | 143 |
3 files changed, 182 insertions, 5 deletions
diff --git a/crates/rocie-server/Cargo.toml b/crates/rocie-server/Cargo.toml index d8c5332..9f09b3c 100644 --- a/crates/rocie-server/Cargo.toml +++ b/crates/rocie-server/Cargo.toml @@ -37,6 +37,7 @@ chrono = "0.4.41" clap = { version = "4.5.45", features = ["derive", "env"] } env_logger = "0.11.8" log = "0.4.27" +percent-encoding = "2.3.2" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.143" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } diff --git a/crates/rocie-server/src/api/get/product.rs b/crates/rocie-server/src/api/get/product.rs index 9356a68..55e5d91 100644 --- a/crates/rocie-server/src/api/get/product.rs +++ b/crates/rocie-server/src/api/get/product.rs @@ -1,10 +1,29 @@ -use actix_web::{HttpResponse, Responder, Result, get, web}; +use actix_web::{HttpRequest, HttpResponse, Responder, Result, get, web}; +use log::info; +use percent_encoding::percent_decode_str; use crate::{ app::App, storage::sql::product::{Product, ProductId, ProductIdStub}, }; +/// A String, that is not url-decoded on parse. +struct UrlEncodedString(String); + +impl UrlEncodedString { + /// Percent de-encode a given string + fn percent_decode(&self) -> Result<String, std::str::Utf8Error> { + percent_decode_str(self.0.replace('+', "%20").as_str()) + .decode_utf8() + .map(|s| s.to_string()) + .inspect(|s| info!("Decoded `{}` as `{s}`", self.0)) + } + + fn from_str(inner: &str) -> Self { + Self(inner.to_owned()) + } +} + /// Get Product by id #[utoipa::path( responses( @@ -40,14 +59,20 @@ pub(crate) async fn product_by_id( ("name" = String, description = "Name of the product" ), ) )] -// TODO: Html decode the name, before use. Otherwise `Milk 2` will not work, as it is send as -// `Milk+2` <2025-10-21> #[get("/product/by-name/{name}")] pub(crate) async fn product_by_name( app: web::Data<App>, + req: HttpRequest, name: web::Path<String>, ) -> Result<impl Responder> { - let name = name.into_inner(); + let _name = name; + + let name = UrlEncodedString::from_str( + req.path() + .strip_prefix("/product/by-name/") + .expect("Will always exists"), + ); + let name = name.percent_decode()?; match Product::from_name(&app, name).await? { Some(product) => Ok(HttpResponse::Ok().json(product)), @@ -68,9 +93,17 @@ pub(crate) async fn product_by_name( #[get("/product/by-part-name/{name}")] pub(crate) async fn product_suggestion_by_name( app: web::Data<App>, + req: HttpRequest, name: web::Path<String>, ) -> Result<impl Responder> { - let name = name.into_inner(); + let _name = name; + + let name = UrlEncodedString::from_str( + req.path() + .strip_prefix("/product/by-part-name/") + .expect("Will always exists"), + ); + let name = &name.percent_decode()?; let all = Product::get_all(&app).await?; diff --git a/crates/rocie-server/tests/products/query.rs b/crates/rocie-server/tests/products/query.rs new file mode 100644 index 0000000..8adfbb5 --- /dev/null +++ b/crates/rocie-server/tests/products/query.rs @@ -0,0 +1,143 @@ +use rocie_client::{ + apis::{ + api_get_product_api::{product_by_name, product_suggestion_by_name}, + api_set_unit_property_api::register_unit_property, + }, + models::UnitPropertyStub, +}; + +use crate::{ + _testenv::{TestEnv, init::function_name, log::request}, + products::create_product, +}; + +#[tokio::test] +#[expect(clippy::similar_names)] +async fn test_product_name_suggestions() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "Milk 1").await; + let milk_12_id = create_product(&env, unit_property, "Milk 12").await; + let milk_2_id = create_product(&env, unit_property, "Milk 2").await; + let milk_3_id = create_product(&env, unit_property, "Milk 3").await; + + let products = request!(env, product_suggestion_by_name("Milk")) + .into_iter() + .map(|p| p.id) + .collect::<Vec<_>>(); + + assert_eq!(products, vec![milk_1_id, milk_12_id, milk_2_id, milk_3_id]); + + let products = request!(env, product_suggestion_by_name("Milk 1")) + .into_iter() + .map(|p| p.id) + .collect::<Vec<_>>(); + + assert_eq!(products, vec![milk_1_id, milk_12_id]); +} + +#[tokio::test] +async fn test_product_name_suggest_space() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "Milk 1").await; + + let products = request!(env, product_suggestion_by_name("Milk ")) + .into_iter() + .map(|s| s.id) + .collect::<Vec<_>>(); + + assert_eq!(products, vec![milk_1_id]); +} + +#[tokio::test] +async fn test_product_name_lookup_space() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "Milk 1").await; + + let product = request!(env, product_by_name("Milk 1")); + + assert_eq!(product.id, milk_1_id); +} + +#[tokio::test] +async fn test_product_name_lookup_fancy() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "Roglič 1 (Čebule in Česen)").await; + + let product = request!(env, product_by_name("Roglič 1 (Čebule in Česen)")); + + assert_eq!(product.id, milk_1_id); +} + +#[tokio::test] +async fn test_product_name_lookup_emoji() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "👾 Milk").await; + + let product = request!(env, product_by_name("👾 Milk")); + + assert_eq!(product.id, milk_1_id); +} + +#[tokio::test] +async fn test_product_name_lookup_plus() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Voluem".to_string(), + }) + ); + + let milk_1_id = create_product(&env, unit_property, "Milk + Chocolate").await; + + let product = request!(env, product_by_name("Milk + Chocolate")); + + assert_eq!(product.id, milk_1_id); +} |
