187 lines
4.9 KiB
Rust
187 lines
4.9 KiB
Rust
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<Self, Self::Err> {
|
|
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<u64>,
|
|
pub last_checked: Option<u64>,
|
|
}
|
|
|
|
#[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::<DoorStatus>().unwrap(), status);
|
|
assert_eq!(
|
|
serde_json::to_string(&status).unwrap(),
|
|
format!("\"{status}\"")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn door_status_rejects_unknown_values() {
|
|
assert!("unknown".parse::<DoorStatus>().is_err());
|
|
assert!(serde_json::from_str::<DoorStatus>("\"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);
|
|
}
|
|
}
|