noisebell/remote/noisebell-common/src/lib.rs

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);
}
}