From e1ab023d1f9ecbba83817893a3f611a815bf7d5d Mon Sep 17 00:00:00 2001 From: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:04:11 +0200 Subject: [PATCH 1/3] fix(security): resolve cargo audit advisories in backend deps Summary: - Update quinn-proto 0.11.14 -> 0.11.16 (RUSTSEC-2026-0185, high: remote memory exhaustion via unbounded out-of-order stream reassembly) - Update anyhow 1.0.102 -> 1.0.103 (RUSTSEC-2026-0190: unsound Error::downcast_mut()) - Replace unmaintained rustls-pemfile (RUSTSEC-2025-0134) with the rustls-pki-types pem API in load_tls_config; from_pem_file auto-detects PKCS#1/PKCS#8/SEC1 keys, collapsing the previous key-type match - Bump workspace version 1.2.4 -> 1.2.5 Reason: - cargo audit reported one high vulnerability, one unsoundness advisory and an unmaintained direct dependency. Frontend (crux) transitive warnings (bincode, proc-macro-error) and the no-fix rsa advisory (via jsonwebtoken) are out of scope. Verification: - cargo build, cargo clippy --features mock (pedantic+nursery clean), cargo test --features mock (all pass), cargo +nightly fmt - cargo audit: rustls-pemfile no longer in dependency tree Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> --- Cargo.lock | 41 ++++++++++++++++++++++------------------- Cargo.toml | 2 +- src/backend/Cargo.toml | 2 +- src/backend/src/main.rs | 36 ++++++++++++------------------------ 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c15a7f2..7ea78f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,9 +390,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "argon2" @@ -1537,11 +1537,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -2364,7 +2366,7 @@ dependencies = [ [[package]] name = "omnect-ui" -version = "1.2.4" +version = "1.2.5" dependencies = [ "actix-files", "actix-http", @@ -2391,7 +2393,7 @@ dependencies = [ "reqwest", "rust-ini", "rustls", - "rustls-pemfile", + "rustls-pki-types", "semver", "serde", "serde_json", @@ -2407,7 +2409,7 @@ dependencies = [ [[package]] name = "omnect-ui-core" -version = "1.2.4" +version = "1.2.5" dependencies = [ "base64 0.22.1", "console_log", @@ -2795,15 +2797,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.2", "lru-slab", - "rand 0.9.4", + "rand 0.10.1", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -2926,6 +2929,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3122,15 +3134,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3457,7 +3460,7 @@ dependencies = [ [[package]] name = "shared_types" -version = "1.2.4" +version = "1.2.5" dependencies = [ "anyhow", "crux_core", diff --git a/Cargo.toml b/Cargo.toml index 5e45927..7e2b5fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,4 +66,4 @@ edition = "2024" homepage = "https://www.omnect.io/home" license = "MIT OR Apache-2.0" repository = "git@github.com:omnect/omnect-ui.git" -version = "1.2.4" +version = "1.2.5" diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml index 6543b19..aa2fa5a 100644 --- a/src/backend/Cargo.toml +++ b/src/backend/Cargo.toml @@ -58,7 +58,7 @@ rustls = { version = "0.23", default-features = false, features = [ "std", "tls12", ] } -rustls-pemfile = { version = "2.2", default-features = false, features = [ +rustls-pki-types = { version = "1.14", default-features = false, features = [ "std", ] } semver = { version = "1.0", default-features = false } diff --git a/src/backend/src/main.rs b/src/backend/src/main.rs index 127856a..e53fe12 100644 --- a/src/backend/src/main.rs +++ b/src/backend/src/main.rs @@ -531,35 +531,23 @@ async fn run_server( } fn load_tls_config() -> Result { - let paths = &AppConfig::get().certificate; - - let mut tls_certs = std::io::BufReader::new( - std::fs::File::open(&paths.cert_path).context("failed to open certificate file")?, - ); + use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; - let mut tls_key = std::io::BufReader::new( - std::fs::File::open(&paths.key_path).context("failed to open key file")?, - ); + let paths = &AppConfig::get().certificate; - let tls_certs = rustls_pemfile::certs(&mut tls_certs) + let tls_certs = CertificateDer::pem_file_iter(&paths.cert_path) + .context("failed to open certificate file")? .collect::, _>>() .context("failed to parse certificate pem")?; - let key_item = rustls_pemfile::read_one(&mut tls_key) - .context("failed to read key pem file")? - .context("no valid key found in pem file")?; - - let config = match key_item { - rustls_pemfile::Item::Pkcs1Key(key) => rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(tls_certs, rustls::pki_types::PrivateKeyDer::Pkcs1(key)) - .context("failed to create tls config with pkcs1 key")?, - rustls_pemfile::Item::Pkcs8Key(key) => rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(tls_certs, rustls::pki_types::PrivateKeyDer::Pkcs8(key)) - .context("failed to create tls config with pkcs8 key")?, - _ => anyhow::bail!("unexpected key type in pem file"), - }; + // from_pem_file auto-detects the key format (PKCS#1, PKCS#8, SEC1) + let tls_key = + PrivateKeyDer::from_pem_file(&paths.key_path).context("failed to parse private key pem")?; + + let config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(tls_certs, tls_key) + .context("failed to create tls config")?; Ok(config) } From 5f9801693fe4167c93410179c368b5ebef637d59 Mon Sep 17 00:00:00 2001 From: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:21:14 +0200 Subject: [PATCH 2/3] chore(deps): bump crux to 0.19 (core/http/macros/time) Summary: - crux_core 0.17 -> 0.19, crux_http 0.16 -> 0.19, crux_macros 0.8 -> 0.10, crux_time 0.15 -> 0.17 (in app + shared_types) - The crux testing helpers (Command/Effect expect_*, ResponseBuilder) moved behind feature flags in 0.19. Enable them as dev-dependencies (crux_core "testing", crux_http "http-types") so they stay out of the WASM/production build. - Adapt http_helpers tests: ResponseBuilder::with_status now takes u16, and crux_http re-exports http::StatusCode (OK/SERVICE_UNAVAILABLE). Reason: - Keep the Crux core current. Note: this does not clear the bincode / proc-macro-error unmaintained warnings; crux 0.19 still pulls both in. Verification: - cargo build, cargo clippy --features mock (clean), cargo test --features mock (368 pass), cargo +nightly fmt - wasm-pack build (production, testing feature absent) clean - TypeScript bindings regenerated; full e2e suite 133/133 pass Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> --- Cargo.lock | 178 ++++++++++++++++++++++++++++++++---- src/app/Cargo.toml | 16 +++- src/app/src/http_helpers.rs | 10 +- src/shared_types/Cargo.toml | 4 +- 4 files changed, 177 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ea78f7..7e8fd57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ dependencies = [ "bytestring", "derive_more 2.1.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "h2", "http 0.2.12", @@ -246,7 +246,7 @@ dependencies = [ "cookie", "derive_more 2.1.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -382,6 +382,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstyle" version = "1.0.14" @@ -680,6 +686,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + [[package]] name = "const-oid" version = "0.9.6" @@ -802,16 +814,17 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crux_core" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68328a703adba9b705c7b2de3e0fe434f4a310974fc6944c280cd4d15651485c" +checksum = "159720bb44c1a09e0089459217c8ea6c71a78d868c5bf55d5b41f8781e92af7a" dependencies = [ "anyhow", "bincode", "crossbeam-channel", "crux_macros", - "facet", + "facet 0.44.5", "futures", + "include_dir 0.7.4", "serde", "serde-generate", "serde-reflection", @@ -822,18 +835,21 @@ dependencies = [ [[package]] name = "crux_http" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0aa92500155f78911382f2c81f04191f51bc05c2b89246833e308e5e8a7654" +checksum = "cb5aec17a4618dc5834f201aaa28f0ea4cad5a5f593714e74a43ff95cf171313" dependencies = [ "anyhow", "async-trait", "crux_core", "derive_builder", "encoding_rs", - "facet", + "facet 0.44.5", + "facet-generate-attrs", "futures-util", + "http 1.4.2", "http-types-red-badger-temporary-fork", + "mime", "pin-project-lite", "serde", "serde_bytes", @@ -846,9 +862,9 @@ dependencies = [ [[package]] name = "crux_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c2549bcdfd872810d0c6a704ce1bd0e7b2007a8078370f02aa7f45ccade686" +checksum = "952c0b238735c79e917b908c140c7c5f6f6921924ebee21171203fe5fb5fc3f3" dependencies = [ "darling 0.23.0", "heck 0.5.0", @@ -860,12 +876,12 @@ dependencies = [ [[package]] name = "crux_time" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c57ba9be867f4678049e7329859ac2d4b0758964d2f0b98a9384dee068c4a3" +checksum = "a2dda3b405568dc1e0fc7a46cc913e6324e918c66fd7cd19d64cc28759d71a58" dependencies = [ "crux_core", - "facet", + "facet 0.44.5", "futures", "serde", "thiserror 2.0.18", @@ -1281,11 +1297,22 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d643309d7c46d073b6c51e29901ed9bc3d858dc09aa21f5b32b424491e1bbef" dependencies = [ - "facet-core", - "facet-macros", + "facet-core 0.31.8", + "facet-macros 0.31.8", "static_assertions", ] +[[package]] +name = "facet" +version = "0.44.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c78066af2cc259674a54fef24323f6081ae54a5f3a3fb53b995af7a867041c81" +dependencies = [ + "autocfg", + "facet-core 0.44.4", + "facet-macros 0.44.4", +] + [[package]] name = "facet-core" version = "0.31.8" @@ -1296,16 +1323,68 @@ dependencies = [ "impls", ] +[[package]] +name = "facet-core" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04625a816bc27bb757b5f62ec17a228b90c08631f579ee0b36eccd6c426df207" +dependencies = [ + "autocfg", + "const-fnv1a-hash", + "iddqd", + "impls", +] + +[[package]] +name = "facet-generate-attrs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b430b495d59ffcf07dabe103a796d8d3ba809d12bb25a5960b605883835b3de" +dependencies = [ + "facet 0.44.5", +] + +[[package]] +name = "facet-macro-parse" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a288cd230608b4d7e89b307f85be2029f265e27dcdc565a2ae5358d0c4e53f57" +dependencies = [ + "facet-macro-types", + "proc-macro2", + "quote", +] + +[[package]] +name = "facet-macro-types" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c805b7f1ad4bba14e1dba5c67ef810bf9eef2373364fe3916af1d08a5411296" +dependencies = [ + "proc-macro2", + "quote", + "unsynn", +] + [[package]] name = "facet-macros" version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391fcf43b68a76ba4a494710629a7b6fb430345a5623a6a87f7c263bfadb1643" dependencies = [ - "facet-core", + "facet-core 0.31.8", "facet-macros-emit", ] +[[package]] +name = "facet-macros" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cc46a94a2725e82f2c0095bac16665057c24d7f4f11169a7a879dbcba81d8f" +dependencies = [ + "facet-macros-impl", +] + [[package]] name = "facet-macros-emit" version = "0.31.8" @@ -1316,6 +1395,20 @@ dependencies = [ "quote", ] +[[package]] +name = "facet-macros-impl" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1af13fd5dfa728cc48418cf33cbcfc0d5cfdbaaf22a3d0f5229da4b1e5669fa5" +dependencies = [ + "facet-macro-parse", + "facet-macro-types", + "proc-macro2", + "quote", + "strsim", + "unsynn", +] + [[package]] name = "facet-macros-parse" version = "0.31.8" @@ -1367,6 +1460,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1610,7 +1709,16 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", ] [[package]] @@ -1726,7 +1834,7 @@ dependencies = [ "anyhow", "async-channel", "base64 0.22.1", - "facet", + "facet 0.31.8", "futures-lite", "infer", "pin-project-lite", @@ -1905,6 +2013,19 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "iddqd" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616230c7d641ef971a3a5bfcc654c6b7524ab9c9fb665693c6a397dba9a14aca" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "rustc-hash", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1955,6 +2076,15 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + [[package]] name = "include_dir_impl" version = "0.6.2" @@ -1968,6 +2098,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -3279,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8309fc8d475cf5884e92a463b3a003d433684335be19c3f739af0451b027254b" dependencies = [ "heck 0.3.3", - "include_dir", + "include_dir 0.6.2", "phf", "serde", "serde-reflection", diff --git a/src/app/Cargo.toml b/src/app/Cargo.toml index 15adddf..00df397 100644 --- a/src/app/Cargo.toml +++ b/src/app/Cargo.toml @@ -23,10 +23,10 @@ workspace = true [dependencies] base64 = { version = "0.22", default-features = false, features = ["alloc"] } console_log = { version = "1.0", default-features = false } -crux_core = { version = "0.17", default-features = false } -crux_http = { version = "0.16", default-features = false } -crux_macros = { version = "0.8", default-features = false } -crux_time = { version = "0.15", default-features = false } +crux_core = { version = "0.19", default-features = false } +crux_http = { version = "0.19", default-features = false } +crux_macros = { version = "0.10", default-features = false } +crux_time = { version = "0.17", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } hmac = { version = "0.13", default-features = false } log = { version = "0.4", default-features = false } @@ -44,5 +44,11 @@ getrandom = { version = "0.3", features = ["wasm_js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] getrandom = { version = "0.3" } +[dev-dependencies] +# Enable the crux testing helpers (expect_*, ResponseBuilder). These moved +# behind feature flags in crux 0.19 and must not leak into the WASM build. +crux_core = { version = "0.19", default-features = false, features = ["testing"] } +crux_http = { version = "0.19", default-features = false, features = ["http-types"] } + [build-dependencies] -crux_core = { version = "0.17.0", default-features = false, features = ["typegen"] } +crux_core = { version = "0.19.0", default-features = false, features = ["typegen"] } diff --git a/src/app/src/http_helpers.rs b/src/app/src/http_helpers.rs index 7c4c111..e9bc200 100644 --- a/src/app/src/http_helpers.rs +++ b/src/app/src/http_helpers.rs @@ -200,9 +200,9 @@ where #[cfg(test)] mod tests { use super::*; - use crux_http::{http::StatusCode, testing::ResponseBuilder}; + use crux_http::testing::ResponseBuilder; - fn make_response(status: StatusCode, body: &[u8]) -> Response> { + fn make_response(status: u16, body: &[u8]) -> Response> { ResponseBuilder::with_status(status) .body(body.to_vec()) .build() @@ -220,7 +220,7 @@ mod tests { ok: bool, } - let mut response = make_response(StatusCode::ServiceUnavailable, b"{\"ok\":false}"); + let mut response = make_response(503, b"{\"ok\":false}"); let result: Result = parse_json_response_any_status("test", &mut response); assert_eq!(result.unwrap(), Info { ok: false }); } @@ -232,14 +232,14 @@ mod tests { value: u32, } - let mut response = make_response(StatusCode::Ok, b"{\"value\":42}"); + let mut response = make_response(200, b"{\"value\":42}"); let result: Result = parse_json_response_any_status("test", &mut response); assert_eq!(result.unwrap(), Info { value: 42 }); } #[test] fn parse_json_response_any_status_returns_error_on_invalid_json() { - let mut response = make_response(StatusCode::Ok, b"not json"); + let mut response = make_response(200, b"not json"); let result: Result = parse_json_response_any_status("test", &mut response); assert!(result.is_err()); assert!(result.unwrap_err().contains("JSON parse error")); diff --git a/src/shared_types/Cargo.toml b/src/shared_types/Cargo.toml index 4513cb1..0a27f2b 100644 --- a/src/shared_types/Cargo.toml +++ b/src/shared_types/Cargo.toml @@ -15,6 +15,6 @@ workspace = true [build-dependencies] anyhow = "1.0" -crux_core = { version = "0.17", features = ["typegen"] } -crux_http = { version = "0.16", features = ["typegen"] } +crux_core = { version = "0.19", features = ["typegen"] } +crux_http = { version = "0.19", features = ["typegen"] } omnect-ui-core = { path = "../app", features = ["typegen"] } From 61ab9c5e0b9b4e3fe79a4392a70e2d2ed733ac9f Mon Sep 17 00:00:00 2001 From: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:54:08 +0200 Subject: [PATCH 3/3] test(app): adopt crux 0.19 fluent effect assertions Summary: - Convert single- and known-order effect assertions to the fluent TestExt API added in crux 0.18 (expect_only_web_socket_with, expect_only_http_with, expect_render().expect_http_with, expect_http_with). - Removes the manual expect_effect()/find_map/split() boilerplate in auth, websocket, wifi and the reconnection healthcheck test. - Keep the position-independent timer helpers (find_time_effect, collect_time_effects): the fluent API is order-based and does not fit find-among-many / multi-timer membership checks. Reason: - The 0.19 bump exposed the fluent assertion API; using it makes the effect tests shorter and read top-to-bottom in effect order. Verification: - cargo test --features mock (368 pass), cargo clippy --features mock --all-targets (clean), cargo +nightly fmt - Test-only changes (#[cfg(test)]): WASM/production build and e2e unaffected Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com> --- src/app/src/update/auth.rs | 75 ++++++++--------------- src/app/src/update/device/reconnection.rs | 23 +++---- src/app/src/update/websocket.rs | 12 ++-- src/app/src/update/wifi.rs | 16 ++--- 4 files changed, 47 insertions(+), 79 deletions(-) diff --git a/src/app/src/update/auth.rs b/src/app/src/update/auth.rs index d4d9468..74a019c 100644 --- a/src/app/src/update/auth.rs +++ b/src/app/src/update/auth.rs @@ -123,6 +123,7 @@ fn post_auth_commands(model: &mut Model) -> Command { #[cfg(test)] mod tests { use super::*; + use crate::EffectTestExt; mod login { use super::*; @@ -138,25 +139,16 @@ mod tests { ); // Login produces render + http effects - let effects = [cmd.expect_effect(), cmd.expect_effect()]; - let http_req = effects - .into_iter() - .find_map(|e| match e { - Effect::Http(req) => Some(req), - _ => None, - }) - .expect("Expected Http effect"); - let (http_request, _) = http_req.split(); - - assert_eq!(http_request.url, "https://relative/token/login"); - assert_eq!(http_request.method, "POST"); - assert!( - http_request - .headers - .iter() - .any(|h| h.name.eq_ignore_ascii_case("authorization") - && h.value.starts_with("Basic ")) - ); + cmd.expect_render().expect_http_with(|op| { + assert_eq!(op.url, "https://relative/token/login"); + assert_eq!(op.method, "POST"); + assert!( + op.headers + .iter() + .any(|h| h.name.eq_ignore_ascii_case("authorization") + && h.value.starts_with("Basic ")) + ); + }); } #[test] @@ -242,25 +234,16 @@ mod tests { }; let mut cmd = handle(AuthEvent::Logout, &mut model); - let effects = [cmd.expect_effect(), cmd.expect_effect()]; - let http_req = effects - .into_iter() - .find_map(|e| match e { - Effect::Http(req) => Some(req), - _ => None, - }) - .expect("Expected Http effect"); - let (http_request, _) = http_req.split(); - - assert_eq!(http_request.url, "https://relative/logout"); - assert_eq!(http_request.method, "POST"); - assert!( - http_request - .headers - .iter() - .any(|h| h.name.eq_ignore_ascii_case("authorization") - && h.value.starts_with("Bearer ")) - ); + cmd.expect_render().expect_http_with(|op| { + assert_eq!(op.url, "https://relative/logout"); + assert_eq!(op.method, "POST"); + assert!( + op.headers + .iter() + .any(|h| h.name.eq_ignore_ascii_case("authorization") + && h.value.starts_with("Bearer ")) + ); + }); } #[test] @@ -470,18 +453,10 @@ mod tests { let mut model = Model::default(); let mut cmd = handle(AuthEvent::CheckRequiresPasswordSet, &mut model); - let effects = [cmd.expect_effect(), cmd.expect_effect()]; - let http_req = effects - .into_iter() - .find_map(|e| match e { - Effect::Http(req) => Some(req), - _ => None, - }) - .expect("Expected Http effect"); - let (http_request, _) = http_req.split(); - - assert_eq!(http_request.url, "https://relative/require-set-password"); - assert_eq!(http_request.method, "GET"); + cmd.expect_render().expect_http_with(|op| { + assert_eq!(op.url, "https://relative/require-set-password"); + assert_eq!(op.method, "GET"); + }); } #[test] diff --git a/src/app/src/update/device/reconnection.rs b/src/app/src/update/device/reconnection.rs index 3a60f56..499bde3 100644 --- a/src/app/src/update/device/reconnection.rs +++ b/src/app/src/update/device/reconnection.rs @@ -254,6 +254,7 @@ fn advance_network_change_state( mod tests { use super::*; use crate::{ + EffectTestExt, model::Model, types::{ DeviceOperationState, HealthcheckInfo, NetworkChangeState, UpdateValidationStatus, @@ -339,22 +340,12 @@ mod tests { }; let mut cmd = handle_reconnection_check_tick(&mut model); - // Command::all([http_get!(...), schedule_poll()]) produces Http + Time effects; - // find the Http one. - let http_effect = cmd - .effects() - .find_map(|e| { - if let Effect::Http(_) = e { - Some(e.expect_http()) - } else { - None - } - }) - .expect("expected Http effect"); - let (http_request, _) = http_effect.split(); - - assert_eq!(http_request.url, "https://relative/healthcheck"); - assert_eq!(http_request.method, "GET"); + // Command::all([http_get!(...), schedule_poll()]) produces [Http, Time]; + // the Http effect comes first. + cmd.expect_http_with(|op| { + assert_eq!(op.url, "https://relative/healthcheck"); + assert_eq!(op.method, "GET"); + }); } } diff --git a/src/app/src/update/websocket.rs b/src/app/src/update/websocket.rs index e975a9a..cd56b00 100644 --- a/src/app/src/update/websocket.rs +++ b/src/app/src/update/websocket.rs @@ -106,7 +106,7 @@ fn sync_network_form_from_status(m: &mut Model) { mod tests { use super::*; use crate::{ - WebSocketOperation, + EffectTestExt, WebSocketOperation, types::{ FactoryReset, FactoryResetStatus, OnlineStatus, OsInfo, SystemInfo, UpdateValidationStatus, @@ -122,8 +122,9 @@ mod tests { let mut cmd = handle(WebSocketEvent::SubscribeToChannels, &mut model); // SubscribeToChannels produces a single WebSocket effect (no render wrapper) - let (operation, _) = cmd.expect_one_effect().expect_web_socket().split(); - assert!(matches!(operation, WebSocketOperation::SubscribeAll)); + cmd.expect_only_web_socket_with(|op| { + assert!(matches!(op, WebSocketOperation::SubscribeAll)); + }); } #[test] @@ -131,8 +132,9 @@ mod tests { let mut model = Model::default(); let mut cmd = handle(WebSocketEvent::UnsubscribeFromChannels, &mut model); - let (operation, _) = cmd.expect_one_effect().expect_web_socket().split(); - assert!(matches!(operation, WebSocketOperation::UnsubscribeAll)); + cmd.expect_only_web_socket_with(|op| { + assert!(matches!(op, WebSocketOperation::UnsubscribeAll)); + }); } } diff --git a/src/app/src/update/wifi.rs b/src/app/src/update/wifi.rs index cac0a3e..7142bb4 100644 --- a/src/app/src/update/wifi.rs +++ b/src/app/src/update/wifi.rs @@ -507,7 +507,10 @@ fn schedule_connect_poll() -> Command { #[cfg(test)] mod tests { use super::*; - use crate::types::{WifiAvailability, WifiSavedNetwork}; + use crate::{ + EffectTestExt, + types::{WifiAvailability, WifiSavedNetwork}, + }; fn model_with_ready_state() -> Model { Model { @@ -536,13 +539,10 @@ mod tests { let mut cmd = handle(WifiEvent::CheckAvailability, &mut model); // Silent http_get! produces a single Http effect (no loading, no render). - let Effect::Http(http_req) = cmd.expect_one_effect() else { - panic!("Expected Http effect"); - }; - let (http_request, _) = http_req.split(); - - assert_eq!(http_request.url, "https://relative/wifi/available"); - assert_eq!(http_request.method, "GET"); + cmd.expect_only_http_with(|op| { + assert_eq!(op.url, "https://relative/wifi/available"); + assert_eq!(op.method, "GET"); + }); // The silent check must not set the global loading flag. assert!(!model.is_loading); }