aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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);
+}