about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/rocie-server/Cargo.toml1
-rw-r--r--crates/rocie-server/src/api/get/product.rs43
-rw-r--r--crates/rocie-server/tests/products/query.rs143
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);
+}