use async_trait::async_trait;
use crate::{
http::HttpClient,
lock::{
LockLogEntry,
transport::{
AuditItem, AuditKind, AuditTransport, TransportResult,
mapping::{self, LockServerHealthEntry, LockServerLogEntry, LockServerMetricsEntry},
},
},
log::{LogWriter, LoggerWeak},
};
pub(crate) struct RestTransport {
client: HttpClient,
logger: Option<LoggerWeak>,
}
impl RestTransport {
pub(crate) fn new(client: HttpClient, logger: Option<LoggerWeak>) -> Self {
Self { client, logger }
}
}
#[cfg_attr(not(any(target_arch = "wasm32", target_arch = "wasm64")), async_trait)]
#[cfg_attr(any(target_arch = "wasm32", target_arch = "wasm64"), async_trait(?Send))]
impl AuditTransport for RestTransport {
async fn send(&self, entries: &[AuditItem], audit_kind: &AuditKind) -> TransportResult<()> {
if entries.is_empty() {
return Ok(());
}
let warn = |msg| self.logger.log_any(LockLogEntry::warn(msg));
match audit_kind {
AuditKind::Log(url) => {
let payloads: Vec<LockServerLogEntry> = mapping::map_entries(entries, warn)?;
self.client
.raw_client
.post(url.as_str())
.json(&payloads)
.send()
.await?
.error_for_status()?;
},
AuditKind::Telemetry(url) => {
let payloads: Vec<LockServerMetricsEntry> = mapping::map_entries(entries, warn)?;
self.client
.raw_client
.post(url.as_str())
.json(&payloads)
.send()
.await?
.error_for_status()?;
},
AuditKind::Health(url) => {
let payloads: Vec<LockServerHealthEntry> = mapping::map_entries(entries, warn)?;
self.client
.raw_client
.post(url.as_str())
.json(&payloads)
.send()
.await?
.error_for_status()?;
},
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::{HttpClientConfig, lock::transport::TransportError};
use super::*;
use mockito::{Server, ServerGuard};
use url::Url;
use crate::lock::transport::test_utils::{
malformed_log_item, sample_health_item, sample_log_item, sample_metric_item,
};
fn create_test_client() -> HttpClient {
HttpClient::new(HttpClientConfig::default()).expect("Http client should be initialized")
}
fn mock_log_endpoint(server: &mut ServerGuard) -> mockito::Mock {
server
.mock("POST", "/audit/log/bulk")
.match_header("content-type", "application/json")
.with_status(201)
.create()
}
fn mock_telemetry_endpoint(server: &mut ServerGuard) -> mockito::Mock {
server
.mock("POST", "/audit/telemetry/bulk")
.match_header("content-type", "application/json")
.with_status(201)
.create()
}
#[tokio::test]
async fn test_send_logs_success() {
let mut server = Server::new_async().await;
let mock = mock_log_endpoint(&mut server);
let endpoint: Url = format!("{}/audit/log/bulk", server.url()).parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_log_item()];
transport
.send(&entries, &AuditKind::Log(endpoint))
.await
.expect("logs should be sent successfully");
mock.assert();
}
#[tokio::test]
async fn test_send_logs_empty() {
let server = Server::new_async().await;
let endpoint: Url = format!("{}/audit/log/bulk", server.url()).parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
transport
.send(&[], &AuditKind::Log(endpoint))
.await
.expect("logs should be sent successfully");
}
#[tokio::test]
async fn test_send_logs_server_error_500() {
let mut server = Server::new_async().await;
let mock = server
.mock("POST", "/audit/log/bulk")
.with_status(500)
.with_body("Internal Server Error")
.create();
let endpoint: Url = format!("{}/audit/log/bulk", server.url()).parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_log_item()];
let error = transport
.send(&entries, &AuditKind::Log(endpoint))
.await
.expect_err("this should cause a server error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
mock.assert();
}
#[tokio::test]
async fn test_send_logs_client_error_400() {
let mut server = Server::new_async().await;
let mock = server
.mock("POST", "/audit/log/bulk")
.with_status(400)
.with_body("Bad Request")
.create();
let endpoint: Url = format!("{}/audit/log/bulk", server.url()).parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_log_item()];
let error = transport
.send(&entries, &AuditKind::Log(endpoint))
.await
.expect_err("this should cause a server error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
mock.assert();
}
#[tokio::test]
async fn test_send_logs_network_failure() {
let endpoint: Url = "http://localhost:1/invalid".parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_log_item()];
let error = transport
.send(&entries, &AuditKind::Log(endpoint))
.await
.expect_err("this should cause a network error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
}
#[tokio::test]
async fn test_send_logs_all_invalid_dropped() {
let endpoint: Url = "http://localhost:8080/audit/log/bulk".parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![malformed_log_item()];
let result = transport.send(&entries, &AuditKind::Log(endpoint)).await;
assert!(matches!(
result.unwrap_err(),
TransportError::Serialization(_)
));
}
#[tokio::test]
async fn test_send_logs_large_batch() {
let mut server = Server::new_async().await;
let mock = mock_log_endpoint(&mut server);
let endpoint: Url = format!("{}/audit/log/bulk", server.url()).parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = (0..1000).map(|_| sample_log_item()).collect::<Vec<_>>();
transport
.send(&entries, &AuditKind::Log(endpoint))
.await
.expect("logs should be sent successfully");
mock.assert();
}
#[tokio::test]
async fn test_send_telemetry_success() {
let mut server = Server::new_async().await;
let mock = mock_telemetry_endpoint(&mut server);
let endpoint: Url = format!("{}/audit/telemetry/bulk", server.url())
.parse()
.expect("valid telemetry endpoint URL");
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_metric_item()];
transport
.send(&entries, &AuditKind::Telemetry(endpoint))
.await
.expect("telemetry should be sent successfully");
mock.assert();
}
fn mock_health_endpoint(server: &mut ServerGuard) -> mockito::Mock {
server
.mock("POST", "/audit/health/bulk")
.match_header("content-type", "application/json")
.with_status(200)
.create()
}
#[tokio::test]
async fn test_send_health_success() {
let mut server = Server::new_async().await;
let mock = mock_health_endpoint(&mut server);
let endpoint: Url = format!("{}/audit/health/bulk", server.url())
.parse()
.expect("valid health endpoint URL");
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_health_item()];
transport
.send(&entries, &AuditKind::Health(endpoint))
.await
.expect("health check should be sent successfully");
mock.assert();
}
#[tokio::test]
async fn test_send_health_empty() {
let server = Server::new_async().await;
let endpoint: Url = format!("{}/audit/health/bulk", server.url())
.parse()
.unwrap();
let transport = RestTransport::new(create_test_client(), None);
transport
.send(&[], &AuditKind::Health(endpoint))
.await
.expect("health check should be sent successfully");
}
#[tokio::test]
async fn test_send_health_server_error_500() {
let mut server = Server::new_async().await;
let mock = server
.mock("POST", "/audit/health/bulk")
.with_status(500)
.with_body("Internal Server Error")
.create();
let endpoint: Url = format!("{}/audit/health/bulk", server.url())
.parse()
.unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_health_item()];
let error = transport
.send(&entries, &AuditKind::Health(endpoint))
.await
.expect_err("this should cause a server error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
mock.assert();
}
#[tokio::test]
async fn test_send_health_client_error_400() {
let mut server = Server::new_async().await;
let mock = server
.mock("POST", "/audit/health/bulk")
.with_status(400)
.with_body("Bad Request")
.create();
let endpoint: Url = format!("{}/audit/health/bulk", server.url())
.parse()
.unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_health_item()];
let error = transport
.send(&entries, &AuditKind::Health(endpoint))
.await
.expect_err("this should cause a client error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
mock.assert();
}
#[tokio::test]
async fn test_send_health_network_failure() {
let endpoint: Url = "http://localhost:1/invalid".parse().unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = vec![sample_health_item()];
let error = transport
.send(&entries, &AuditKind::Health(endpoint))
.await
.expect_err("this should cause a network error");
assert!(
matches!(error, TransportError::Rest(_)),
"expected reqwest error, got {error:?}"
);
}
#[tokio::test]
async fn test_send_health_large_batch() {
let mut server = Server::new_async().await;
let mock = mock_health_endpoint(&mut server);
let endpoint: Url = format!("{}/audit/health/bulk", server.url())
.parse()
.unwrap();
let transport = RestTransport::new(create_test_client(), None);
let entries = (0..1000).map(|_| sample_health_item()).collect::<Vec<_>>();
transport
.send(&entries, &AuditKind::Health(endpoint))
.await
.expect("health checks should be sent successfully");
mock.assert();
}
}