aboutsummaryrefslogtreecommitdiffstats
path: root/crates
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
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 '')
-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.
+}