diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-09-23 08:33:06 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-09-23 08:34:45 +0200 |
| commit | 2dc74d621399be454abbbff892fb46204ddc6e7b (patch) | |
| tree | f9525527fc09c465d4e2e4a4f665bfd444b889f8 /crates/rocie-server/tests | |
| parent | feat: Provide basic barcode handling support (diff) | |
| download | server-2dc74d621399be454abbbff892fb46204ddc6e7b.zip | |
feat(treewide): Add tests and barcode buying/consuming
Diffstat (limited to 'crates/rocie-server/tests')
| -rw-r--r-- | crates/rocie-server/tests/_testenv/init.rs | 210 | ||||
| -rw-r--r-- | crates/rocie-server/tests/_testenv/log.rs | 72 | ||||
| -rw-r--r-- | crates/rocie-server/tests/_testenv/mod.rs | 23 | ||||
| -rw-r--r-- | crates/rocie-server/tests/products/barcode.rs | 185 | ||||
| -rw-r--r-- | crates/rocie-server/tests/products/mod.rs | 69 | ||||
| -rw-r--r-- | crates/rocie-server/tests/products/register.rs | 33 | ||||
| -rw-r--r-- | crates/rocie-server/tests/tests.rs | 6 |
7 files changed, 598 insertions, 0 deletions
diff --git a/crates/rocie-server/tests/_testenv/init.rs b/crates/rocie-server/tests/_testenv/init.rs new file mode 100644 index 0000000..5309fea --- /dev/null +++ b/crates/rocie-server/tests/_testenv/init.rs @@ -0,0 +1,210 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{ + env, + ffi::OsStr, + fmt::Write, + fs, io, mem, + path::{Path, PathBuf}, + process::{self, Stdio}, + thread::sleep, + time::Duration, +}; + +use rocie_client::apis::configuration::Configuration; + +use crate::{_testenv::Paths, testenv::TestEnv}; + +fn target_dir() -> PathBuf { + // Tests exe is in target/debug/deps, the *rocie-server* exe is in target/debug + env::current_exe() + .expect("./target/debug/deps/rocie-server-*") + .parent() + .expect("./target/debug/deps") + .parent() + .expect("./target/debug") + .parent() + .expect("./target") + .to_path_buf() +} + +fn test_dir(name: &'static str, port: u32) -> PathBuf { + target_dir().join("tests").join(name).join(port.to_string()) +} + +fn prepare_files_and_dirs(test_dir: &Path) -> io::Result<Paths> { + fs::create_dir_all(test_dir)?; + + let db_path = test_dir.join("database.sqlite"); + + { + // Remove all files, so that the test run stays pure + for entry in fs::read_dir(test_dir).unwrap() { + let entry = entry.unwrap(); + let entry_ft = entry.file_type().unwrap(); + + if entry_ft.is_dir() { + fs::remove_dir_all(entry.path())?; + } else if entry_ft.is_file() { + fs::remove_file(entry.path())?; + } else { + panic!("Unknown file: {} ({entry_ft:#?})", entry.path().display()); + } + } + } + + Ok(Paths { + db: db_path, + test_dir: test_dir.to_owned(), + }) +} + +fn find_server_exe() -> PathBuf { + let target = target_dir().join("debug"); + + let exe_name = if cfg!(windows) { + "rocie-server.exe" + } else { + "rocie-server" + }; + + target.join(exe_name) +} + +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] { + [ + OsStr::new("serve"), + OsStr::new("--db-path"), + paths.db.as_os_str(), + OsStr::new("--port"), + OsStr::new(port), + ] +} + +impl TestEnv { + pub(crate) fn new(name: &'static str, port: u32) -> TestEnv { + let test_dir = test_dir(name, port); + + let paths = prepare_files_and_dirs(&test_dir) + .inspect_err(|err| panic!("Error during test dir preparation: {err}")) + .unwrap(); + + let server_process = { + let server_exe = find_server_exe(); + let mut cmd = process::Command::new(&server_exe); + + cmd.current_dir(&paths.test_dir); + + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + cmd.args(rocie_server_args(&paths, port.to_string().as_str())); + + let child = cmd.spawn().expect("server spawn"); + + // Give the server time to start. + // TODO(@bpeetz): Use a better synchronization primitive <2025-09-11> + sleep(Duration::from_millis(240)); + + child + }; + + let config = { + let mut inner = Configuration::new(); + inner.base_path = rocie_base_path(port.to_string().as_str()); + inner.user_agent = Some(String::from("Rocie test driver")); + inner + }; + + let me = TestEnv { + name, + test_dir, + paths, + server_process: Some(server_process), + config, + port: port.to_string(), + }; + + me.log(format!("Starting test `{name}` on port `{port}`")); + + me + } +} + +impl Drop for TestEnv { + fn drop(&mut self) { + /// Format an error message for when the server did not exit successfully. + fn format_exit(args: &[&OsStr], output: &process::Output) -> String { + let mut base = String::new(); + + { + let args = args + .iter() + .map(|s| s.to_str().unwrap()) + .collect::<Vec<_>>() + .join(" "); + + if output.status.success() { + writeln!(base, "`rocie-server {args}` did exit successfully.") + .expect("In-memory"); + } else { + writeln!(base, "`rocie-server {args}` did not exit successfully.") + .expect("In-memory"); + } + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !stdout.is_empty() { + writeln!(base, "Stdout:\n---\n{stdout}\n---").expect("In-memory"); + } + if !stderr.is_empty() { + writeln!(base, "Stderr:\n---\n{stderr}\n---").expect("In-memory"); + } + + base + } + + { + // Stop the server process via SIGTERM. + let mut kill = process::Command::new("kill") + .args([ + "-s", + "TERM", + &self.server_process.as_ref().unwrap().id().to_string(), + ]) + .spawn() + .unwrap(); + + eprintln!("Killing the server process"); + + kill.wait().unwrap(); + } + + let output = mem::take(&mut self.server_process) + .expect("Is some at this point") + .wait_with_output() + .expect("server exit output"); + + eprintln!( + "{}", + format_exit( + rocie_server_args(&self.paths, &self.port).as_slice(), + &output + ) + ); + } +} diff --git a/crates/rocie-server/tests/_testenv/log.rs b/crates/rocie-server/tests/_testenv/log.rs new file mode 100644 index 0000000..9a07e78 --- /dev/null +++ b/crates/rocie-server/tests/_testenv/log.rs @@ -0,0 +1,72 @@ +use crate::testenv::TestEnv; + +macro_rules! request { + ($env:expr, $fn:ident ($($arg:expr),* $(,)?)) => {{ + match request!(@call, $env, $fn, $($arg),*) { + Ok(ok) => { + $env.log(format!("> {ok:?}")); + + ok + }, + + Err(err) => { + $env.log(format!(">! {err:?}")); + + panic!("Server request failed."); + } + } + + }}; + + (@expect_error $reason:literal $env:expr, $fn:ident ($($arg:expr),* $(,)?)) => {{ + match request!(@call, $env, $fn, $($arg),*) { + Err(err) => { + $env.log(format!("> `{err}` ({})", $reason)); + + err + }, + + Ok(ok) => { + $env.log(format!(">? {ok:?}")); + + panic!("Server request succeeded, but should fail."); + } + } + }}; + + + (@call, $env:expr, $fn:ident, $($arg:expr),*) => {{ + $env.log( + format!( + "< {}({})", + stringify!($fn), + request!(@format, $fn, $($arg),*) + ) + ); + + $fn(&$env.config, $($arg),*).await + }}; + + (@format, $fn:ident, $($arg:expr),*) => {{ + use std::fmt::Write; + + let mut base = String::new(); + $( + write!(base, "{:?}, ", $arg) + .expect("In memory write"); + )* + + base.trim().trim_end_matches(',').to_owned() + }}; +} + +pub(crate) use request; + +impl TestEnv { + pub(crate) fn log<A>(&self, message: A) + where + A: AsRef<str>, + { + eprintln!("{}: {}", self.name, message.as_ref()); + } +} diff --git a/crates/rocie-server/tests/_testenv/mod.rs b/crates/rocie-server/tests/_testenv/mod.rs new file mode 100644 index 0000000..a37925e --- /dev/null +++ b/crates/rocie-server/tests/_testenv/mod.rs @@ -0,0 +1,23 @@ +//! This code was taken from *fd* at 30-06-2025. + +use std::{path::PathBuf, process}; + +use rocie_client::apis::configuration::Configuration; + +mod init; +pub(crate) mod log; + +/// Environment for the integration tests. +pub(crate) struct TestEnv { + pub(crate) name: &'static str, + pub(crate) port: String, + pub(crate) test_dir: PathBuf, + pub(crate) paths: Paths, + pub(crate) server_process: Option<process::Child>, + pub(crate) config: Configuration, +} + +pub(crate) struct Paths { + pub(crate) db: PathBuf, + pub(crate) test_dir: PathBuf, +} diff --git a/crates/rocie-server/tests/products/barcode.rs b/crates/rocie-server/tests/products/barcode.rs new file mode 100644 index 0000000..c267006 --- /dev/null +++ b/crates/rocie-server/tests/products/barcode.rs @@ -0,0 +1,185 @@ +use rocie_client::{ + apis::{ + api_get_inventory_api::amount_by_id, + api_set_barcode_api::{buy_barcode, consume_barcode}, + }, + models::UnitAmount, +}; + +use crate::{ + products::create_associated_barcode, + testenv::{TestEnv, log::request}, +}; + +#[tokio::test] +async fn test_barcode_buy() { + let env = TestEnv::new(module_path!(), 8087); + + let barcode_id = 23; + let unit_value = 1; + + let (unit_id, product_id) = + create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await; + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + let product_amount = request!(env, amount_by_id(product_id.to_string().as_str())); + + assert_eq!(product_amount.product_id, product_id); + assert_eq!(product_amount.amount.unit, unit_id); + assert_eq!(product_amount.amount.value, i64::from(unit_value)); +} + +#[tokio::test] +async fn test_barcode_consume() { + let env = TestEnv::new(module_path!(), 8083); + + let barcode_id = 23; + let unit_value = 1; + + let (unit_id, product_id) = + create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await; + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + request!( + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: 1, + }, + ) + ); + + let product_amount = request!(env, amount_by_id(product_id.to_string().as_str())); + + assert_eq!(product_amount.product_id, product_id); + assert_eq!(product_amount.amount.unit, unit_id); + assert_eq!(product_amount.amount.value, 0); +} + +#[tokio::test] +async fn test_barcode_consume_error() { + let env = TestEnv::new(module_path!(), 8084); + + let barcode_id = 23; + let unit_value = 1; + + let (unit_id, product_id) = + create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await; + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + request!( + @expect_error "We should not be able to consume more than available." + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: i64::from(unit_value + 1), + }, + ) + ); + + // Test, that the error does not actually go into the db. + let product_amount = request!(env, amount_by_id(product_id.to_string().as_str())); + + assert_eq!(product_amount.product_id, product_id); + assert_eq!(product_amount.amount.unit, unit_id); + assert_eq!(product_amount.amount.value, i64::from(unit_value)); +} + +#[tokio::test] +async fn test_barcode_consume_error_other() { + let env = TestEnv::new(module_path!(), 8085); + + let barcode_id = 23; + let unit_value = 1; + + let (unit_id, product_id) = + create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await; + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + request!( + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: 1, + }, + ) + ); + + request!( + @expect_error "We already consumed everything, we bought" + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: 1, + }, + ) + ); + + // Test, that the error does not actually go into the db. + let product_amount = request!(env, amount_by_id(product_id.to_string().as_str())); + + assert_eq!(product_amount.product_id, product_id); + assert_eq!(product_amount.amount.unit, unit_id); + assert_eq!(product_amount.amount.value, i64::from(0)); +} + +#[tokio::test] +async fn test_barcode_multiple_buy_and_consume() { + let env = TestEnv::new(module_path!(), 8086); + + let barcode_id = 23; + let unit_value = 1; + + let (unit_id, product_id) = + create_associated_barcode(&env, "Liter", "Milk", unit_value, barcode_id).await; + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + request!(env, buy_barcode(i32::try_from(barcode_id).unwrap())); + + env.log("Bought both barcodes"); + + request!( + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: 1, + }, + ) + ); + + env.log("Consumed first barcode"); + + request!( + env, + consume_barcode( + i32::try_from(barcode_id).unwrap(), + UnitAmount { + unit: unit_id, + value: 1, + }, + ) + ); + + env.log("Consumed second barcode"); + + let product_amount = request!(env, amount_by_id(product_id.to_string().as_str())); + + assert_eq!(product_amount.product_id, product_id); + assert_eq!(product_amount.amount.unit, unit_id); + assert_eq!(product_amount.amount.value, i64::from(0)); +} diff --git a/crates/rocie-server/tests/products/mod.rs b/crates/rocie-server/tests/products/mod.rs new file mode 100644 index 0000000..e96ecc9 --- /dev/null +++ b/crates/rocie-server/tests/products/mod.rs @@ -0,0 +1,69 @@ +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, + }, + models::{Barcode, Product, ProductStub, UnitAmount, UnitStub}, +}; +use uuid::Uuid; + +use crate::testenv::{TestEnv, log::request}; + +mod barcode; +mod register; + +async fn create_product(env: &TestEnv, name: &str) -> Uuid { + request!( + env, + register_product(ProductStub { + description: Some(None), + name: name.to_owned(), + parent: None, + },) + ) +} +async fn create_unit(env: &TestEnv, name: &str) -> Uuid { + request!( + env, + register_unit(UnitStub { + description: Some(None), + full_name_plural: name.to_owned(), + full_name_singular: name.to_owned(), + short_name: name.to_owned(), + },) + ) +} + +async fn create_associated_barcode( + env: &TestEnv, + unit_name: &str, + product_name: &str, + barcode_value: u32, + barcode_id: u32, +) -> (Uuid, Uuid) { + let unit_id = create_unit(env, unit_name).await; + let product_id = create_product(env, product_name).await; + + request!( + env, + associate_barcode( + product_id.to_string().as_str(), + Barcode { + amount: Box::new(UnitAmount { + unit: unit_id, + value: i64::from(barcode_value), + }), + id: i32::try_from(barcode_id).unwrap(), + }, + ) + ); + + (unit_id, product_id) +} + +async fn get_product(env: &TestEnv, id: Uuid) -> Product { + product_by_id(&env.config, id.to_string().as_str()) + .await + .unwrap() +} diff --git a/crates/rocie-server/tests/products/register.rs b/crates/rocie-server/tests/products/register.rs new file mode 100644 index 0000000..34f7b5c --- /dev/null +++ b/crates/rocie-server/tests/products/register.rs @@ -0,0 +1,33 @@ +use rocie_client::{ + apis::{api_get_product_api::product_by_id, api_set_product_api::register_product}, + models::ProductStub, +}; + +use crate::testenv::{TestEnv, log::request}; + +#[tokio::test] +async fn test_product_register_roundtrip() { + let env = TestEnv::new(module_path!(), 8081); + + let id = request!( + env, + register_product(ProductStub { + description: Some(Some("A soy based alternative to milk".to_owned())), + name: "Soy drink".to_owned(), + parent: None, + },) + ); + + let product = request!(env, product_by_id(id.to_string().as_str())); + + assert_eq!(&product.name, "Soy drink"); + assert_eq!( + product.description, + Some(Some("A soy based alternative to milk".to_owned())) + ); + 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 new file mode 100644 index 0000000..34395a6 --- /dev/null +++ b/crates/rocie-server/tests/tests.rs @@ -0,0 +1,6 @@ +#![allow(unused_crate_dependencies)] + +mod _testenv; +pub(crate) use _testenv as testenv; + +mod products; |
