use std::fmt; use std::str::FromStr; use axum::http::HeaderMap; use serde::{Deserialize, Serialize}; pub fn validate_bearer(headers: &HeaderMap, expected: &str) -> bool { headers .get("authorization") .and_then(|v| v.to_str().ok()) .map(|v| v.strip_prefix("Bearer ").unwrap_or("") == expected) .unwrap_or(false) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum DoorStatus { Open, Closed, Offline, } impl DoorStatus { pub const ALL: [Self; 3] = [Self::Open, Self::Closed, Self::Offline]; pub const fn as_str(self) -> &'static str { match self { Self::Open => "open", Self::Closed => "closed", Self::Offline => "offline", } } pub const fn from_is_open(is_open: bool) -> Self { if is_open { Self::Open } else { Self::Closed } } } impl fmt::Display for DoorStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParseDoorStatusError; impl fmt::Display for ParseDoorStatusError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("invalid door status") } } impl std::error::Error for ParseDoorStatusError {} impl FromStr for DoorStatus { type Err = ParseDoorStatusError; fn from_str(s: &str) -> Result { match s { "open" => Ok(Self::Open), "closed" => Ok(Self::Closed), "offline" => Ok(Self::Offline), _ => Err(ParseDoorStatusError), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebhookPayload { pub status: DoorStatus, pub timestamp: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CacheStatusResponse { pub status: DoorStatus, pub since: Option, pub last_checked: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PiStatusResponse { pub status: DoorStatus, pub timestamp: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum SignalLevel { Low, High, } impl SignalLevel { pub const fn as_str(self) -> &'static str { match self { Self::Low => "low", Self::High => "high", } } } impl fmt::Display for SignalLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } #[cfg(test)] mod tests { use super::*; #[test] fn validate_bearer_accepts_correct_token() { let mut headers = HeaderMap::new(); headers.insert("authorization", "Bearer secret123".parse().unwrap()); assert!(validate_bearer(&headers, "secret123")); } #[test] fn validate_bearer_rejects_wrong_token() { let mut headers = HeaderMap::new(); headers.insert("authorization", "Bearer wrong".parse().unwrap()); assert!(!validate_bearer(&headers, "secret123")); } #[test] fn validate_bearer_rejects_missing_header() { let headers = HeaderMap::new(); assert!(!validate_bearer(&headers, "secret123")); } #[test] fn validate_bearer_rejects_non_bearer_scheme() { let mut headers = HeaderMap::new(); headers.insert("authorization", "Basic secret123".parse().unwrap()); assert!(!validate_bearer(&headers, "secret123")); } #[test] fn door_status_round_trips() { for status in DoorStatus::ALL { assert_eq!(status.as_str().parse::().unwrap(), status); assert_eq!( serde_json::to_string(&status).unwrap(), format!("\"{status}\"") ); } } #[test] fn door_status_rejects_unknown_values() { assert!("unknown".parse::().is_err()); assert!(serde_json::from_str::("\"unknown\"").is_err()); } #[test] fn webhook_payload_round_trips() { let payload = WebhookPayload { status: DoorStatus::Open, timestamp: 1234567890, }; let json = serde_json::to_string(&payload).unwrap(); let deserialized: WebhookPayload = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.status, DoorStatus::Open); assert_eq!(deserialized.timestamp, 1234567890); } #[test] fn cache_status_response_serializes_with_enum_status() { let response = CacheStatusResponse { status: DoorStatus::Closed, since: Some(123), last_checked: Some(456), }; let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["status"], "closed"); assert_eq!(json["since"], 123); assert_eq!(json["last_checked"], 456); } }