use std::sync::Arc;
use async_trait::async_trait;
use crate::mock::dispatch::dispatch;
use crate::mock::fault_injection::{FaultInjector, PendingFault};
use crate::mock::state::MockState;
use crate::mock::{auth, helpers};
use crate::transport::{Transport, TransportError};
const MOCK_BASE: &str = "http://mock";
#[derive(Clone)]
pub struct MockTransport {
state: Arc<MockState>,
faults: Arc<FaultInjector>,
enforce_auth: bool,
}
impl MockTransport {
pub fn new() -> Self {
Self {
state: Arc::new(MockState::new()),
faults: Arc::new(FaultInjector::new()),
enforce_auth: false,
}
}
pub fn with_state(state: MockState) -> Self {
Self {
state: Arc::new(state),
faults: Arc::new(FaultInjector::new()),
enforce_auth: false,
}
}
pub fn with_auth(mut self) -> Self {
self.enforce_auth = true;
self
}
pub fn device(&self) -> &MockState {
&self.state
}
pub fn inject_fault(
&self,
action_suffix: impl Into<String>,
code: impl Into<String>,
reason: impl Into<String>,
) {
self.faults.inject(PendingFault {
action_suffix: action_suffix.into(),
code: code.into(),
reason: reason.into(),
});
}
pub fn clear_faults(&self) {
self.faults.clear_all();
}
}
impl Default for MockTransport {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Transport for MockTransport {
async fn soap_post(
&self,
_url: &str,
action: &str,
body: String,
) -> Result<String, TransportError> {
if let Some(f) = self.faults.take_for_action(action) {
return Ok(helpers::resp_soap_fault(&f.code, &f.reason));
}
if self.enforce_auth && auth::requires_auth(action) {
if let Err(reason) = auth::validate_ws_security(&body, &self.state) {
return Ok(auth::auth_fault(&reason));
}
}
Ok(dispatch(action, MOCK_BASE, &self.state, &body))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::OnvifClient;
fn client_with(t: MockTransport) -> OnvifClient {
OnvifClient::new("http://mock").with_transport(Arc::new(t))
}
#[tokio::test]
async fn get_device_info_roundtrips_without_credentials() {
let c = client_with(MockTransport::new());
let info = c.get_device_info().await.unwrap();
assert_eq!(info.manufacturer, "oxvif-mock");
assert_eq!(info.model, "MockCam-1080p");
}
#[tokio::test]
async fn set_then_get_hostname_roundtrips() {
let c = client_with(MockTransport::new());
c.set_hostname("lab-cam").await.unwrap();
let h = c.get_hostname().await.unwrap();
assert_eq!(h.name.as_deref(), Some("lab-cam"));
}
#[tokio::test]
async fn injected_fault_surfaces_as_soap_fault() {
use crate::error::OnvifError;
use crate::soap::SoapError;
let t = MockTransport::new();
t.inject_fault("GetProfiles", "ter:NotAuthorized", "nope");
let c = client_with(t);
let err = c.get_profiles("http://mock/media").await.unwrap_err();
assert!(matches!(err, OnvifError::Soap(SoapError::Fault { .. })));
}
#[tokio::test]
async fn with_auth_rejects_missing_credentials() {
use crate::error::OnvifError;
use crate::soap::SoapError;
let c = client_with(MockTransport::new().with_auth());
let err = c.get_device_info().await.unwrap_err();
assert!(matches!(err, OnvifError::Soap(SoapError::Fault { .. })));
}
#[tokio::test]
async fn instances_have_independent_state() {
let a = client_with(MockTransport::new());
let b = MockTransport::new();
let bc = client_with(b.clone());
a.set_hostname("host-a").await.unwrap();
bc.set_hostname("host-b").await.unwrap();
assert_eq!(b.device().read().hostname, "host-b");
assert_ne!(b.device().read().hostname, "host-a");
}
}