use crate::{StatusCode, UptrakitClient, paths};
use httpmock::{Mock, MockServer};
use serde::Serialize;
use uuid::Uuid;
pub struct MockApiServer {
server: MockServer,
}
impl MockApiServer {
pub fn start() -> Self {
Self {
server: MockServer::start(),
}
}
#[expect(
clippy::expect_used,
reason = "mock helper — client construction failure would be a programming error in test setup; panic is acceptable"
)]
pub fn client(&self) -> UptrakitClient {
UptrakitClient::with_token(&self.server.base_url(), "test-token", false, None)
.expect("mock client creation")
}
#[expect(
clippy::expect_used,
reason = "mock helper — client construction failure would be a programming error in test setup; panic is acceptable"
)]
pub fn client_unauth(&self) -> UptrakitClient {
UptrakitClient::new(&self.server.base_url(), None, false, None, None)
.expect("mock client creation")
}
pub fn server(&self) -> &MockServer {
&self.server
}
pub fn on(&self, method: &str, path: &str) -> MockEndpoint<'_> {
MockEndpoint::new(&self.server, method, path)
}
pub fn auth(&self) -> MockAuth<'_> {
MockAuth {
server: &self.server,
}
}
pub fn api_tokens(&self) -> MockApiTokens<'_> {
MockApiTokens {
server: &self.server,
}
}
pub fn enrollment_tokens(&self) -> MockEnrollmentTokens<'_> {
MockEnrollmentTokens {
server: &self.server,
}
}
pub fn health(&self) -> MockHealth<'_> {
MockHealth {
server: &self.server,
}
}
pub fn hosts(&self) -> MockHosts<'_> {
MockHosts {
server: &self.server,
}
}
pub fn oidc_auth(&self) -> MockOidcAuth<'_> {
MockOidcAuth {
server: &self.server,
}
}
pub fn oidc_providers(&self) -> MockOidcProviders<'_> {
MockOidcProviders {
server: &self.server,
}
}
pub fn pki(&self) -> MockPki<'_> {
MockPki {
server: &self.server,
}
}
pub fn plugin_configs(&self) -> MockPluginConfigs<'_> {
MockPluginConfigs {
server: &self.server,
}
}
pub fn scheduler(&self) -> MockScheduler<'_> {
MockScheduler {
server: &self.server,
}
}
pub fn services(&self) -> MockServices<'_> {
MockServices {
server: &self.server,
}
}
pub fn settings(&self) -> MockSettings<'_> {
MockSettings {
server: &self.server,
}
}
pub fn software_items(&self) -> MockSoftwareItems<'_> {
MockSoftwareItems {
server: &self.server,
}
}
pub fn system_alerts(&self) -> MockSystemAlerts<'_> {
MockSystemAlerts {
server: &self.server,
}
}
pub fn update_history(&self) -> MockUpdateHistory<'_> {
MockUpdateHistory {
server: &self.server,
}
}
}
pub struct MockEndpoint<'a> {
server: &'a MockServer,
method: String,
path: String,
}
impl<'a> MockEndpoint<'a> {
fn new(server: &'a MockServer, method: &str, path: &str) -> Self {
Self {
server,
method: method.to_uppercase(),
path: path.to_string(),
}
}
#[expect(
clippy::expect_used,
reason = "mock helper — serde_json serialization of a Serialize type is infallible for well-formed types; panic is acceptable in test setup"
)]
pub fn ok<T: Serialize>(self, body: &T) -> Mock<'a> {
let json = serde_json::to_string(body).expect("mock body serialization");
self.respond_raw(StatusCode::OK, json)
}
pub fn no_content(self) -> Mock<'a> {
let Self {
server,
method,
path,
} = self;
server.mock(move |when, then| {
when.method(method.as_str()).path(path.as_str());
then.status(StatusCode::NO_CONTENT.as_u16());
})
}
pub fn unauthorized(self) -> Mock<'a> {
self.respond_raw(StatusCode::UNAUTHORIZED, r#"{"error":"Unauthorized"}"#)
}
#[expect(
clippy::unwrap_used,
reason = "mock helper — serde_json serialization of &str is infallible; panic is acceptable in test setup"
)]
pub fn not_found(self, message: &str) -> Mock<'a> {
let json = format!(r#"{{"error":{}}}"#, serde_json::to_string(message).unwrap());
self.respond_raw(StatusCode::NOT_FOUND, json)
}
pub fn rate_limited(self, retry_after: Option<u64>) -> Mock<'a> {
let Self {
server,
method,
path,
} = self;
let retry_after_str = retry_after.map(|s| s.to_string());
server.mock(move |when, then| {
when.method(method.as_str()).path(path.as_str());
let t = then
.status(StatusCode::TOO_MANY_REQUESTS.as_u16())
.header("Content-Type", "application/json")
.body(r#"{"error":"Too many requests"}"#);
if let Some(ref secs) = retry_after_str {
t.header("Retry-After", secs.as_str());
}
})
}
#[expect(
clippy::unwrap_used,
reason = "mock helper — serde_json serialization of &str is infallible; panic is acceptable in test setup"
)]
pub fn internal_error(self, message: &str) -> Mock<'a> {
let json = format!(r#"{{"error":{}}}"#, serde_json::to_string(message).unwrap());
self.respond_raw(StatusCode::INTERNAL_SERVER_ERROR, json)
}
#[expect(
clippy::expect_used,
reason = "mock helper — serde_json serialization of a Serialize type is infallible for well-formed types; panic is acceptable in test setup"
)]
pub fn respond<T: Serialize>(self, status: StatusCode, body: &T) -> Mock<'a> {
let json = serde_json::to_string(body).expect("mock body serialization");
self.respond_raw(status, json)
}
pub fn respond_raw(self, status: StatusCode, json: impl Into<String>) -> Mock<'a> {
let Self {
server,
method,
path,
} = self;
let status = status.as_u16();
let json: String = json.into();
server.mock(move |when, then| {
when.method(method.as_str()).path(path.as_str());
then.status(status)
.header("Content-Type", "application/json")
.body(json.as_str());
})
}
}
pub struct MockAuth<'a> {
server: &'a MockServer,
}
impl<'a> MockAuth<'a> {
pub fn on_register(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::auth::REGISTER)
}
pub fn on_login(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::auth::LOGIN)
}
pub fn on_refresh(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::auth::REFRESH)
}
pub fn on_logout(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::auth::LOGOUT)
}
pub fn on_me(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::auth::ME)
}
pub fn on_auth_methods(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::auth::METHODS)
}
pub fn on_device_auth_approve(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::auth::DEVICE_APPROVE)
}
}
pub struct MockApiTokens<'a> {
server: &'a MockServer,
}
impl<'a> MockApiTokens<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::api_tokens::BASE)
}
pub fn on_create(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::api_tokens::BASE)
}
pub fn on_revoke(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::api_tokens::by_id(id))
}
}
pub struct MockEnrollmentTokens<'a> {
server: &'a MockServer,
}
impl<'a> MockEnrollmentTokens<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::enrollment_tokens::BASE)
}
pub fn on_create(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::enrollment_tokens::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::enrollment_tokens::by_id(id))
}
pub fn on_revoke(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::enrollment_tokens::by_id(id))
}
}
pub struct MockHealth<'a> {
server: &'a MockServer,
}
impl<'a> MockHealth<'a> {
pub fn on_healthz(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::health::HEALTHZ)
}
}
pub struct MockHosts<'a> {
server: &'a MockServer,
}
impl<'a> MockHosts<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::hosts::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::hosts::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::hosts::by_id(id))
}
pub fn on_deactivate(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::hosts::by_id(id))
}
}
pub struct MockOidcAuth<'a> {
server: &'a MockServer,
}
impl<'a> MockOidcAuth<'a> {
pub fn on_authorize(&self, provider_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"GET",
&paths::oidc_auth::authorize(provider_id),
)
}
pub fn on_exchange(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::oidc_auth::EXCHANGE)
}
pub fn on_link(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::oidc_auth::LINK)
}
pub fn on_complete_registration(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::oidc_auth::COMPLETE_REGISTRATION)
}
}
pub struct MockOidcProviders<'a> {
server: &'a MockServer,
}
impl<'a> MockOidcProviders<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::oidc_providers::BASE)
}
pub fn on_create(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::oidc_providers::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::oidc_providers::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::oidc_providers::by_id(id))
}
pub fn on_delete(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::oidc_providers::by_id(id))
}
pub fn on_activate(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::oidc_providers::activate(id))
}
pub fn on_deactivate(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::oidc_providers::deactivate(id))
}
}
pub struct MockPki<'a> {
server: &'a MockServer,
}
impl<'a> MockPki<'a> {
pub fn on_ca_cert(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::pki::CA_CERT)
}
pub fn on_ca_crl(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::pki::CA_CRL)
}
}
pub struct MockPluginConfigs<'a> {
server: &'a MockServer,
}
impl<'a> MockPluginConfigs<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::plugin_configs::BASE)
}
pub fn on_create(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::plugin_configs::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::plugin_configs::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::plugin_configs::by_id(id))
}
pub fn on_delete(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::plugin_configs::by_id(id))
}
}
pub struct MockScheduler<'a> {
server: &'a MockServer,
}
impl<'a> MockScheduler<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::scheduler::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::scheduler::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::scheduler::by_id(id))
}
pub fn on_trigger(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::scheduler::trigger(id))
}
}
pub struct MockServices<'a> {
server: &'a MockServer,
}
impl<'a> MockServices<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::services::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::services::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::services::by_id(id))
}
pub fn on_approve(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::services::approve(id))
}
pub fn on_reject(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::services::reject(id))
}
pub fn on_remove(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::services::by_id(id))
}
pub fn on_merge(&self, target_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::services::merge(target_id))
}
}
pub struct MockSettings<'a> {
server: &'a MockServer,
}
impl<'a> MockSettings<'a> {
pub fn on_get_combined(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::settings::COMBINED)
}
pub fn on_get_access(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::settings::ACCESS)
}
pub fn on_update_access(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", paths::settings::ACCESS)
}
pub fn on_get_agent_certificates(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::settings::AGENT_CERTIFICATES)
}
pub fn on_update_agent_certificates(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", paths::settings::AGENT_CERTIFICATES)
}
pub fn on_get_network(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::settings::NETWORK)
}
pub fn on_update_network(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", paths::settings::NETWORK)
}
pub fn on_rotate_ca(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::settings::ROTATE_CA)
}
pub fn on_renew_server_certificate(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::settings::RENEW_SERVER_CERT)
}
}
pub struct MockSoftwareItems<'a> {
server: &'a MockServer,
}
impl<'a> MockSoftwareItems<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::software_items::BASE)
}
pub fn on_create(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::software_items::BASE)
}
pub fn on_merge_preview(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::software_items::MERGE_PREVIEW)
}
pub fn on_merge_execute(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", paths::software_items::MERGE_EXECUTE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::software_items::by_id(id))
}
pub fn on_update(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "PUT", &paths::software_items::by_id(id))
}
pub fn on_delete(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "DELETE", &paths::software_items::by_id(id))
}
pub fn on_assign_hosts(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "POST", &paths::software_items::hosts(id))
}
pub fn on_update_host_assignment(&self, item_id: &Uuid, host_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"PUT",
&paths::software_items::host(item_id, host_id),
)
}
pub fn on_unassign_host(&self, item_id: &Uuid, host_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"DELETE",
&paths::software_items::host(item_id, host_id),
)
}
pub fn on_check_versions(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"POST",
&paths::software_items::check_versions(id),
)
}
pub fn on_check_versions_host(&self, item_id: &Uuid, host_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"POST",
&paths::software_items::host_check_versions(item_id, host_id),
)
}
pub fn on_trigger_update(&self, item_id: &Uuid, host_id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(
self.server,
"POST",
&paths::software_items::host_update(item_id, host_id),
)
}
}
pub struct MockSystemAlerts<'a> {
server: &'a MockServer,
}
impl<'a> MockSystemAlerts<'a> {
pub fn on_get(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::system_alerts::ALERTS)
}
}
pub struct MockUpdateHistory<'a> {
server: &'a MockServer,
}
impl<'a> MockUpdateHistory<'a> {
pub fn on_list(&self) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", paths::update_history::BASE)
}
pub fn on_get(&self, id: &Uuid) -> MockEndpoint<'_> {
MockEndpoint::new(self.server, "GET", &paths::update_history::by_id(id))
}
}