Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 181 additions & 38 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
16 changes: 11 additions & 5 deletions src/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"] }
10 changes: 5 additions & 5 deletions src/app/src/http_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> {
fn make_response(status: u16, body: &[u8]) -> Response<Vec<u8>> {
ResponseBuilder::with_status(status)
.body(body.to_vec())
.build()
Expand All @@ -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<Info, String> = parse_json_response_any_status("test", &mut response);
assert_eq!(result.unwrap(), Info { ok: false });
}
Expand All @@ -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<Info, String> = 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<String, String> = parse_json_response_any_status("test", &mut response);
assert!(result.is_err());
assert!(result.unwrap_err().contains("JSON parse error"));
Expand Down
75 changes: 25 additions & 50 deletions src/app/src/update/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ fn post_auth_commands(model: &mut Model) -> Command<Effect, Event> {
#[cfg(test)]
mod tests {
use super::*;
use crate::EffectTestExt;

mod login {
use super::*;
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
23 changes: 7 additions & 16 deletions src/app/src/update/device/reconnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ fn advance_network_change_state(
mod tests {
use super::*;
use crate::{
EffectTestExt,
model::Model,
types::{
DeviceOperationState, HealthcheckInfo, NetworkChangeState, UpdateValidationStatus,
Expand Down Expand Up @@ -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");
});
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/app/src/update/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -122,17 +122,19 @@ 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]
fn unsubscribe_from_channels_emits_unsubscribe_all_effect() {
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));
});
}
}

Expand Down
16 changes: 8 additions & 8 deletions src/app/src/update/wifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,10 @@ fn schedule_connect_poll() -> Command<Effect, Event> {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{WifiAvailability, WifiSavedNetwork};
use crate::{
EffectTestExt,
types::{WifiAvailability, WifiSavedNetwork},
};

fn model_with_ready_state() -> Model {
Model {
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
36 changes: 12 additions & 24 deletions src/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,35 +531,23 @@ async fn run_server(
}

fn load_tls_config() -> Result<rustls::ServerConfig> {
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::<Result<Vec<_>, _>>()
.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)
}
Expand Down
4 changes: 2 additions & 2 deletions src/shared_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Loading