use std::collections::HashMap;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use bairelay::camera::CameraHandle;
use bairelay::config::test_helpers::minimal_camera_config;
use bairelay::mqtt_dispatch::dispatch_control;
use bairelay_mqtt::control::parse_control_message;
const PREFIX: &str = "bairelay";
fn dummy_mqtt_client() -> bairelay_mqtt::SharedMqttClient {
let cfg = bairelay_mqtt::MqttConfig {
broker_addr: "127.0.0.1".to_string(),
port: 0,
credentials: None,
ca: None,
client_auth: None,
};
let (client, _event_loop) = bairelay_mqtt::connect(&cfg, "test-dispatch", PREFIX).unwrap();
client
}
#[tokio::test]
async fn dispatch_unknown_camera_does_not_panic() {
let cameras: HashMap<String, Arc<CameraHandle>> = HashMap::new();
let mqtt = dummy_mqtt_client();
let cmd = parse_control_message(PREFIX, "bairelay/nonexistent/control/floodlight", b"on")
.expect("valid control message");
dispatch_control(cmd, &cameras, &mqtt, PREFIX).await;
}
#[tokio::test]
async fn dispatch_disconnected_camera_does_not_panic() {
let cancel = CancellationToken::new();
let config = minimal_camera_config("garden");
let cam = Arc::new(CameraHandle::new(config, cancel.clone(), None));
let mut cameras: HashMap<String, Arc<CameraHandle>> = HashMap::new();
cameras.insert("garden".to_string(), cam);
let mqtt = dummy_mqtt_client();
let cmd = parse_control_message(PREFIX, "bairelay/garden/control/floodlight", b"on")
.expect("valid control message");
cancel.cancel();
dispatch_control(cmd, &cameras, &mqtt, PREFIX).await;
}
#[tokio::test]
async fn dispatch_to_disconnected_camera_wakes_then_bails_on_cancel() {
let cancel = CancellationToken::new();
let config = minimal_camera_config("porch");
let cam = Arc::new(CameraHandle::new(config, cancel.clone(), None));
assert!(cam.wake_lock().is_idle(), "wake lock should start idle");
let mut cameras: HashMap<String, Arc<CameraHandle>> = HashMap::new();
cameras.insert("porch".to_string(), Arc::clone(&cam));
let mqtt = dummy_mqtt_client();
let cmd = parse_control_message(PREFIX, "bairelay/porch/control/reboot", b"")
.expect("valid control message");
cancel.cancel();
dispatch_control(cmd, &cameras, &mqtt, PREFIX).await;
assert!(
cam.wake_lock().is_idle(),
"wake lock must be released back to idle once dispatch returns"
);
}
#[tokio::test]
async fn dispatch_to_unknown_does_not_leak_wake_lock() {
let cameras: HashMap<String, Arc<CameraHandle>> = HashMap::new();
let mqtt = dummy_mqtt_client();
let cmd = parse_control_message(PREFIX, "bairelay/ghost/control/siren", b"on")
.expect("valid control message");
dispatch_control(cmd, &cameras, &mqtt, PREFIX).await;
}
#[tokio::test]
async fn dispatch_multiple_commands_to_disconnected() {
let cancel = CancellationToken::new();
let config = minimal_camera_config("multi");
let cam = Arc::new(CameraHandle::new(config, cancel.clone(), None));
let mut cameras: HashMap<String, Arc<CameraHandle>> = HashMap::new();
cameras.insert("multi".to_string(), Arc::clone(&cam));
let mqtt = dummy_mqtt_client();
let topics_and_payloads = [
("bairelay/multi/control/floodlight", b"on" as &[u8]),
("bairelay/multi/control/led", b"off"),
("bairelay/multi/control/ir", b"auto"),
("bairelay/multi/control/pir", b"on"),
("bairelay/multi/control/reboot", b""),
("bairelay/multi/control/ptz", b"up 10"),
("bairelay/multi/control/zoom", b"0.5"),
("bairelay/multi/control/siren", b"on"),
("bairelay/multi/control/wakeup", b"5"),
("bairelay/multi/query/battery", b""),
("bairelay/multi/query/pir", b""),
];
cancel.cancel();
for (topic, payload) in &topics_and_payloads {
let cmd = parse_control_message(PREFIX, topic, payload)
.unwrap_or_else(|| panic!("valid control message for {topic}"));
dispatch_control(cmd, &cameras, &mqtt, PREFIX).await;
}
assert!(
cam.wake_lock().is_idle(),
"wake lock must return to idle after dispatch unwinds"
);
}