about summary refs log tree commit diff stats
path: root/crates/rocie-server
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-08 11:58:49 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-08 11:58:49 +0200
commit9204e472e4f714c84237bca5ebe740080a589917 (patch)
treed17f31240b24fcb2c47eec21edf6a9f512d80123 /crates/rocie-server
parentfeat(crates/rocie-server/unit-property): Init (diff)
downloadserver-9204e472e4f714c84237bca5ebe740080a589917.zip
test(crates/rocie-server/testenv/init): Automatically choose the port and wait for server start
This avoids issues regarding a race condition between server start and
our start of requests and removes the requirement for specifying free
ports in the test files.
Diffstat (limited to 'crates/rocie-server')
-rw-r--r--crates/rocie-server/Cargo.toml1
-rw-r--r--crates/rocie-server/src/cli.rs12
-rw-r--r--crates/rocie-server/src/main.rs22
-rw-r--r--crates/rocie-server/tests/_testenv/init.rs57
-rw-r--r--crates/rocie-server/tests/_testenv/mod.rs2
-rw-r--r--crates/rocie-server/tests/products/barcode.rs163
-rw-r--r--crates/rocie-server/tests/products/mod.rs44
-rw-r--r--crates/rocie-server/tests/products/register.rs27
-rw-r--r--crates/rocie-server/tests/tests.rs1
-rw-r--r--crates/rocie-server/tests/units/mod.rs1
-rw-r--r--crates/rocie-server/tests/units/register.rs50
11 files changed, 315 insertions, 65 deletions
diff --git a/crates/rocie-server/Cargo.toml b/crates/rocie-server/Cargo.toml
index 2ea5f3d..d8c5332 100644
--- a/crates/rocie-server/Cargo.toml
+++ b/crates/rocie-server/Cargo.toml
@@ -31,6 +31,7 @@ rocie-client.workspace = true
 tokio.workspace = true
 
 [dependencies]
+actix-cors = "0.7.1"
 actix-web = "4.11.0"
 chrono = "0.4.41"
 clap = { version = "4.5.45", features = ["derive", "env"] }
diff --git a/crates/rocie-server/src/cli.rs b/crates/rocie-server/src/cli.rs
index b2ec214..80b4292 100644
--- a/crates/rocie-server/src/cli.rs
+++ b/crates/rocie-server/src/cli.rs
@@ -13,8 +13,16 @@ pub(crate) enum Command {
     /// Serve the server.
     Serve {
         /// Which port to serve the server on.
-        #[arg(short, long, default_value = "8080")]
-        port: u16,
+        ///
+        /// Leave empty to let the OS choose a free one.
+        #[arg(short, long)]
+        port: Option<u16>,
+
+        /// Print the used port as single u16 to stdout when started.
+        ///
+        /// This can be used, to determine the used port, when the `port` was left at `None`.
+        #[arg(long)]
+        print_port: bool,
 
         /// Which host to serve the server on.
         #[arg(short = 'b', long, default_value = "127.0.0.1")]
diff --git a/crates/rocie-server/src/main.rs b/crates/rocie-server/src/main.rs
index af36c9e..8e10763 100644
--- a/crates/rocie-server/src/main.rs
+++ b/crates/rocie-server/src/main.rs
@@ -1,3 +1,4 @@
+use actix_cors::Cors;
 use actix_web::{App, HttpServer, middleware::Logger, web::Data};
 use clap::Parser;
 use utoipa::OpenApi;
@@ -49,6 +50,7 @@ async fn main() -> Result<(), std::io::Error> {
             host,
             port,
             db_path,
+            print_port,
         } => {
             let data = Data::new(
                 app::App::new(db_path)
@@ -56,10 +58,10 @@ async fn main() -> Result<(), std::io::Error> {
                     .map_err(|err| std::io::Error::other(main::Error::AppInit(err)))?,
             );
 
-            eprintln!("Serving at http://{host}:{port}");
-
-            HttpServer::new(move || {
+            let srv = HttpServer::new(move || {
                 App::new()
+                    // TODO: Remove before an actual deploy <2025-09-26>
+                    .wrap(Cors::permissive())
                     .wrap(Logger::new(
                         r#"%a "%r" -> %s %b ("%{Referer}i" "%{User-Agent}i" %T s)"#,
                     ))
@@ -67,9 +69,17 @@ async fn main() -> Result<(), std::io::Error> {
                     .configure(api::get::register_paths)
                     .configure(api::set::register_paths)
             })
-            .bind((host, port))?
-            .run()
-            .await
+            .bind((host, port.unwrap_or(0)))?;
+
+            let addr = srv.addrs()[0];
+            let run = srv.run();
+
+            if print_port {
+                println!("{}", addr.port());
+            }
+            eprintln!("Serving at http://{addr}");
+
+            run.await
         }
         Command::OpenApi => {
             let openapi = ApiDoc::openapi();
diff --git a/crates/rocie-server/tests/_testenv/init.rs b/crates/rocie-server/tests/_testenv/init.rs
index 5309fea..9cb0b91 100644
--- a/crates/rocie-server/tests/_testenv/init.rs
+++ b/crates/rocie-server/tests/_testenv/init.rs
@@ -12,17 +12,29 @@ use std::{
     env,
     ffi::OsStr,
     fmt::Write,
-    fs, io, mem,
+    fs,
+    io::{self, BufRead, BufReader},
+    mem,
     path::{Path, PathBuf},
     process::{self, Stdio},
-    thread::sleep,
-    time::Duration,
 };
 
 use rocie_client::apis::configuration::Configuration;
 
 use crate::{_testenv::Paths, testenv::TestEnv};
 
+macro_rules! function_name {
+    () => {{
+        fn f() {}
+        fn type_name_of<T>(_: T) -> &'static str {
+            std::any::type_name::<T>()
+        }
+        let name = type_name_of(f);
+        name.strip_suffix("::{{closure}}::f").unwrap()
+    }};
+}
+pub(crate) use function_name;
+
 fn target_dir() -> PathBuf {
     // Tests exe is in target/debug/deps, the *rocie-server* exe is in target/debug
     env::current_exe()
@@ -36,8 +48,8 @@ fn target_dir() -> PathBuf {
         .to_path_buf()
 }
 
-fn test_dir(name: &'static str, port: u32) -> PathBuf {
-    target_dir().join("tests").join(name).join(port.to_string())
+fn test_dir(name: &'static str) -> PathBuf {
+    target_dir().join("tests").join(name)
 }
 
 fn prepare_files_and_dirs(test_dir: &Path) -> io::Result<Paths> {
@@ -83,25 +95,24 @@ fn rocie_base_path(port: &str) -> String {
     format!("http://127.0.0.1:{port}")
 }
 
-fn rocie_server_args<'a>(paths: &'a Paths, port: &'a str) -> [&'a OsStr; 5] {
+fn rocie_server_args(paths: &Paths) -> [&OsStr; 4] {
     [
         OsStr::new("serve"),
         OsStr::new("--db-path"),
         paths.db.as_os_str(),
-        OsStr::new("--port"),
-        OsStr::new(port),
+        OsStr::new("--print-port"),
     ]
 }
 
 impl TestEnv {
-    pub(crate) fn new(name: &'static str, port: u32) -> TestEnv {
-        let test_dir = test_dir(name, port);
+    pub(crate) fn new(name: &'static str) -> TestEnv {
+        let test_dir = test_dir(name);
 
         let paths = prepare_files_and_dirs(&test_dir)
             .inspect_err(|err| panic!("Error during test dir preparation: {err}"))
             .unwrap();
 
-        let server_process = {
+        let (server_process, port) = {
             let server_exe = find_server_exe();
             let mut cmd = process::Command::new(&server_exe);
 
@@ -110,15 +121,22 @@ impl TestEnv {
             cmd.stdout(Stdio::piped());
             cmd.stderr(Stdio::piped());
 
-            cmd.args(rocie_server_args(&paths, port.to_string().as_str()));
+            cmd.args(rocie_server_args(&paths));
+
+            let mut child = cmd.spawn().expect("server spawn");
+
+            let port: u16 = {
+                let mut stdout = BufReader::new(child.stdout.as_mut().expect("Was captured"));
 
-            let child = cmd.spawn().expect("server spawn");
+                let mut port = String::new();
+                assert_ne!(stdout.read_line(&mut port).expect("Works"), 0);
 
-            // Give the server time to start.
-            // TODO(@bpeetz): Use a better synchronization primitive <2025-09-11>
-            sleep(Duration::from_millis(240));
+                port.trim_end()
+                    .parse()
+                    .expect("The server should reply with a u16 port number")
+            };
 
-            child
+            (child, port)
         };
 
         let config = {
@@ -201,10 +219,7 @@ impl Drop for TestEnv {
 
         eprintln!(
             "{}",
-            format_exit(
-                rocie_server_args(&self.paths, &self.port).as_slice(),
-                &output
-            )
+            format_exit(rocie_server_args(&self.paths).as_slice(), &output)
         );
     }
 }
diff --git a/crates/rocie-server/tests/_testenv/mod.rs b/crates/rocie-server/tests/_testenv/mod.rs
index a37925e..56cea71 100644
--- a/crates/rocie-server/tests/_testenv/mod.rs
+++ b/crates/rocie-server/tests/_testenv/mod.rs
@@ -4,7 +4,7 @@ use std::{path::PathBuf, process};
 
 use rocie_client::apis::configuration::Configuration;
 
-mod init;
+pub(crate) mod init;
 pub(crate) mod log;
 
 /// Environment for the integration tests.
diff --git a/crates/rocie-server/tests/products/barcode.rs b/crates/rocie-server/tests/products/barcode.rs
index 480dcb9..8e1bf42 100644
--- a/crates/rocie-server/tests/products/barcode.rs
+++ b/crates/rocie-server/tests/products/barcode.rs
@@ -2,24 +2,28 @@ use rocie_client::{
     apis::{
         api_get_inventory_api::amount_by_id,
         api_set_barcode_api::{buy_barcode, consume_barcode},
+        api_set_product_api::{associate_barcode, register_product},
+        api_set_unit_api::register_unit,
+        api_set_unit_property_api::register_unit_property,
     },
-    models::{BarcodeId, UnitAmount},
+    models::{Barcode, BarcodeId, ProductStub, UnitAmount, UnitPropertyStub, UnitStub},
 };
 
 use crate::{
+    _testenv::init::function_name,
     products::create_associated_barcode,
     testenv::{TestEnv, log::request},
 };
 
 #[tokio::test]
 async fn test_barcode_buy() {
-    let env = TestEnv::new(module_path!(), 8087);
+    let env = TestEnv::new(function_name!());
 
     let barcode_id = 23;
     let unit_value = 1;
 
     let (unit_id, product_id) =
-        create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await;
+        create_associated_barcode(&env, "Liter", "Milk", "Volume", unit_value, barcode_id).await;
 
     request!(env, buy_barcode(BarcodeId { value: barcode_id }));
 
@@ -32,13 +36,20 @@ async fn test_barcode_buy() {
 
 #[tokio::test]
 async fn test_barcode_consume() {
-    let env = TestEnv::new(module_path!(), 8083);
+    let env = TestEnv::new(function_name!());
 
     let barcode_id = BarcodeId { value: 23 };
     let unit_value = 1;
 
-    let (unit_id, product_id) =
-        create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id.value).await;
+    let (unit_id, product_id) = create_associated_barcode(
+        &env,
+        "Liter",
+        "Milk",
+        "Volume",
+        unit_value,
+        barcode_id.value,
+    )
+    .await;
 
     request!(env, buy_barcode(barcode_id));
 
@@ -62,13 +73,20 @@ async fn test_barcode_consume() {
 
 #[tokio::test]
 async fn test_barcode_consume_error() {
-    let env = TestEnv::new(module_path!(), 8084);
+    let env = TestEnv::new(function_name!());
 
     let barcode_id = BarcodeId { value: 23 };
     let unit_value = 1;
 
-    let (unit_id, product_id) =
-        create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id.value).await;
+    let (unit_id, product_id) = create_associated_barcode(
+        &env,
+        "Liter",
+        "Milk",
+        "Volume",
+        unit_value,
+        barcode_id.value,
+    )
+    .await;
 
     request!(env, buy_barcode(barcode_id));
 
@@ -94,13 +112,20 @@ async fn test_barcode_consume_error() {
 
 #[tokio::test]
 async fn test_barcode_consume_error_other() {
-    let env = TestEnv::new(module_path!(), 8085);
+    let env = TestEnv::new(function_name!());
 
     let barcode_id = BarcodeId { value: 23 };
     let unit_value = 1;
 
-    let (unit_id, product_id) =
-        create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id.value).await;
+    let (unit_id, product_id) = create_associated_barcode(
+        &env,
+        "Liter",
+        "Milk",
+        "Volume",
+        unit_value,
+        barcode_id.value,
+    )
+    .await;
 
     request!(env, buy_barcode(barcode_id));
 
@@ -137,13 +162,20 @@ async fn test_barcode_consume_error_other() {
 
 #[tokio::test]
 async fn test_barcode_multiple_buy_and_consume() {
-    let env = TestEnv::new(module_path!(), 8086);
+    let env = TestEnv::new(function_name!());
 
     let barcode_id = BarcodeId { value: 23 };
     let unit_value = 1;
 
-    let (unit_id, product_id) =
-        create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id.value).await;
+    let (unit_id, product_id) = create_associated_barcode(
+        &env,
+        "Liter",
+        "Milk",
+        "Volume",
+        unit_value,
+        barcode_id.value,
+    )
+    .await;
 
     request!(env, buy_barcode(barcode_id));
 
@@ -183,3 +215,104 @@ async fn test_barcode_multiple_buy_and_consume() {
     assert_eq!(product_amount.amount.unit, unit_id);
     assert_eq!(product_amount.amount.value, 0);
 }
+
+#[tokio::test]
+async fn test_barcode_fill_up() {
+    let env = TestEnv::new(function_name!());
+
+    let milk_id = BarcodeId { value: 23 };
+    let bread_id = BarcodeId { value: 24 };
+    let nuts_id = BarcodeId { value: 25 };
+
+    let _ = create_associated_barcode(&env, "Liter", "Milk", "Volume", 2, milk_id.value).await;
+    let _ = create_associated_barcode(&env, "Kilogram", "Bread", "Mass", 2, bread_id.value).await;
+    let _ = create_associated_barcode(&env, "Piece", "Nut", "Quantity", 2, nuts_id.value).await;
+
+    request!(env, buy_barcode(milk_id));
+    request!(env, buy_barcode(bread_id));
+    request!(env, buy_barcode(nuts_id));
+}
+
+#[tokio::test]
+async fn test_barcode_associate_false_unit() {
+    let env = TestEnv::new(function_name!());
+
+    let unit_property_mass = request!(
+        env,
+        register_unit_property(UnitPropertyStub {
+            description: None,
+            name: "Mass".to_owned()
+        })
+    );
+    let unit_property_volume = request!(
+        env,
+        register_unit_property(UnitPropertyStub {
+            description: None,
+            name: "Volume".to_owned()
+        })
+    );
+
+    let product = request!(
+        env,
+        register_product(ProductStub {
+            description: None,
+            name: "Milk".to_owned(),
+            parent: None,
+            unit_property: unit_property_mass,
+        })
+    );
+
+    let unit_mass = request!(
+        env,
+        register_unit(UnitStub {
+            description: None,
+            full_name_plural: "Grams".to_owned(),
+            full_name_singular: "Gram".to_owned(),
+            short_name: "g".to_owned(),
+            unit_property: unit_property_mass
+        })
+    );
+    let unit_volume = request!(
+        env,
+        register_unit(UnitStub {
+            description: None,
+            full_name_plural: "Liters".to_owned(),
+            full_name_singular: "Liter".to_owned(),
+            short_name: "L".to_owned(),
+            unit_property: unit_property_volume
+        })
+    );
+
+    let barcode_id_1 = BarcodeId { value: 23 };
+    let barcode_id_2 = BarcodeId { value: 24 };
+    let value = 1;
+
+    request!(
+        env,
+        associate_barcode(
+            product,
+            Barcode {
+                amount: UnitAmount {
+                    unit: unit_mass,
+                    value,
+                },
+                id: barcode_id_1,
+            },
+        )
+    );
+
+    request!(@expect_error
+        "Should not work, as we registered the base product with unit_property_mass"
+        env,
+        associate_barcode(
+            product,
+            Barcode {
+                amount: UnitAmount {
+                    unit: unit_volume,
+                    value,
+                },
+                id: barcode_id_2,
+            },
+        )
+    );
+}
diff --git a/crates/rocie-server/tests/products/mod.rs b/crates/rocie-server/tests/products/mod.rs
index 2ae52fa..b39a07c 100644
--- a/crates/rocie-server/tests/products/mod.rs
+++ b/crates/rocie-server/tests/products/mod.rs
@@ -1,10 +1,13 @@
 use rocie_client::{
     apis::{
-        api_get_product_api::product_by_id,
         api_set_product_api::{associate_barcode, register_product},
         api_set_unit_api::register_unit,
+        api_set_unit_property_api::register_unit_property,
+    },
+    models::{
+        Barcode, BarcodeId, ProductId, ProductStub, UnitAmount, UnitId, UnitPropertyId,
+        UnitPropertyStub, UnitStub,
     },
-    models::{Barcode, BarcodeId, Product, ProductId, ProductStub, UnitAmount, UnitId, UnitStub},
 };
 
 use crate::testenv::{TestEnv, log::request};
@@ -12,17 +15,24 @@ use crate::testenv::{TestEnv, log::request};
 mod barcode;
 mod register;
 
-async fn create_product(env: &TestEnv, name: &str) -> ProductId {
-    request!(
+async fn create_product(
+    env: &TestEnv,
+    unit_property: UnitPropertyId,
+    name: &str,
+) -> (ProductId, UnitPropertyId) {
+    let product_id = request!(
         env,
         register_product(ProductStub {
             description: Some(None),
             name: name.to_owned(),
             parent: None,
-        },)
-    )
+            unit_property
+        })
+    );
+
+    (product_id, unit_property)
 }
-async fn create_unit(env: &TestEnv, name: &str) -> UnitId {
+async fn create_unit(env: &TestEnv, name: &str, unit_property: UnitPropertyId) -> UnitId {
     request!(
         env,
         register_unit(UnitStub {
@@ -30,7 +40,8 @@ async fn create_unit(env: &TestEnv, name: &str) -> UnitId {
             full_name_plural: name.to_owned(),
             full_name_singular: name.to_owned(),
             short_name: name.to_owned(),
-        },)
+            unit_property
+        })
     )
 }
 
@@ -38,11 +49,20 @@ async fn create_associated_barcode(
     env: &TestEnv,
     unit_name: &str,
     product_name: &str,
+    unit_property_name: &str,
     barcode_value: u32,
     barcode_id: u32,
 ) -> (UnitId, ProductId) {
-    let unit_id = create_unit(env, unit_name).await;
-    let product_id = create_product(env, product_name).await;
+    let unit_property = request!(
+        env,
+        register_unit_property(UnitPropertyStub {
+            description: None,
+            name: unit_property_name.to_owned()
+        })
+    );
+
+    let (product_id, unit_property) = create_product(env, unit_property, product_name).await;
+    let unit_id = create_unit(env, unit_name, unit_property).await;
     let barcode_id = BarcodeId { value: barcode_id };
 
     request!(
@@ -61,7 +81,3 @@ async fn create_associated_barcode(
 
     (unit_id, product_id)
 }
-
-async fn get_product(env: &TestEnv, id: ProductId) -> Product {
-    product_by_id(&env.config, id).await.unwrap()
-}
diff --git a/crates/rocie-server/tests/products/register.rs b/crates/rocie-server/tests/products/register.rs
index 15abec4..4284bd1 100644
--- a/crates/rocie-server/tests/products/register.rs
+++ b/crates/rocie-server/tests/products/register.rs
@@ -1,13 +1,27 @@
 use rocie_client::{
-    apis::{api_get_product_api::product_by_id, api_set_product_api::register_product},
-    models::ProductStub,
+    apis::{
+        api_get_product_api::product_by_id, api_set_product_api::register_product,
+        api_set_unit_property_api::register_unit_property,
+    },
+    models::{ProductStub, UnitPropertyStub},
 };
 
-use crate::testenv::{TestEnv, log::request};
+use crate::{
+    _testenv::init::function_name,
+    testenv::{TestEnv, log::request},
+};
 
 #[tokio::test]
 async fn test_product_register_roundtrip() {
-    let env = TestEnv::new(module_path!(), 8081);
+    let env = TestEnv::new(function_name!());
+
+    let unit_property = request!(
+        env,
+        register_unit_property(UnitPropertyStub {
+            description: Some(Some("The total mass of a product".to_owned())),
+            name: "Mass".to_owned()
+        })
+    );
 
     let id = request!(
         env,
@@ -15,7 +29,8 @@ async fn test_product_register_roundtrip() {
             description: Some(Some("A soy based alternative to milk".to_owned())),
             name: "Soy drink".to_owned(),
             parent: None,
-        },)
+            unit_property,
+        })
     );
 
     let product = request!(env, product_by_id(id));
@@ -25,7 +40,7 @@ async fn test_product_register_roundtrip() {
         product.description,
         Some(Some("A soy based alternative to milk".to_owned()))
     );
-    assert_eq!(product.id, id,);
+    assert_eq!(product.id, id);
     // assert_eq!(
     //     product.parent,
     //     None,
diff --git a/crates/rocie-server/tests/tests.rs b/crates/rocie-server/tests/tests.rs
index 34395a6..cf34156 100644
--- a/crates/rocie-server/tests/tests.rs
+++ b/crates/rocie-server/tests/tests.rs
@@ -4,3 +4,4 @@ mod _testenv;
 pub(crate) use _testenv as testenv;
 
 mod products;
+mod units;
diff --git a/crates/rocie-server/tests/units/mod.rs b/crates/rocie-server/tests/units/mod.rs
new file mode 100644
index 0000000..5518167
--- /dev/null
+++ b/crates/rocie-server/tests/units/mod.rs
@@ -0,0 +1 @@
+mod register;
diff --git a/crates/rocie-server/tests/units/register.rs b/crates/rocie-server/tests/units/register.rs
new file mode 100644
index 0000000..5367b55
--- /dev/null
+++ b/crates/rocie-server/tests/units/register.rs
@@ -0,0 +1,50 @@
+use rocie_client::{
+    apis::{
+        api_get_unit_api::unit_by_id, api_set_unit_api::register_unit,
+        api_set_unit_property_api::register_unit_property,
+    },
+    models::{UnitPropertyStub, UnitStub},
+};
+
+use crate::{
+    _testenv::init::function_name,
+    testenv::{TestEnv, log::request},
+};
+
+#[tokio::test]
+async fn test_unit_register_roundtrip() {
+    let env = TestEnv::new(function_name!());
+
+    let unit_property = request!(
+        env,
+        register_unit_property(UnitPropertyStub {
+            description: Some(Some("The total mass of a product".to_owned())),
+            name: "Mass".to_owned()
+        })
+    );
+
+    let id = request!(
+        env,
+        register_unit(UnitStub {
+            description: Some(Some("Fancy new unit".to_owned())),
+            full_name_plural: "Grams".to_owned(),
+            full_name_singular: "Gram".to_owned(),
+            short_name: "g".to_owned(),
+            unit_property,
+        })
+    );
+
+    let unit = request!(env, unit_by_id(id));
+
+    assert_eq!(&unit.short_name, "g");
+    assert_eq!(&unit.full_name_plural, "Grams");
+    assert_eq!(&unit.full_name_singular, "Gram");
+    assert_eq!(unit.description, Some(Some("Fancy new unit".to_owned())));
+    assert_eq!(unit.id, id);
+}
+
+#[tokio::test]
+async fn test_unit_conversions_register_error() {
+    // TODO test if a conversion between two units from different universes actually returns an
+    // error.
+}