use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
pub fn shadow_mode_enabled() -> bool {
std::env::var("MOCKFORGE_SHADOW_MODE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnknownPathRequest {
pub timestamp: DateTime<Utc>,
pub method: String,
pub path: String,
pub client_ip: String,
pub query: String,
#[serde(default = "default_unknown_status")]
pub status: u16,
}
fn default_unknown_status() -> u16 {
404
}
const DEFAULT_BUFFER_SIZE: usize = 256;
static UNKNOWN_PATHS: Lazy<Mutex<VecDeque<UnknownPathRequest>>> =
Lazy::new(|| Mutex::new(VecDeque::with_capacity(DEFAULT_BUFFER_SIZE)));
pub fn record(req: UnknownPathRequest) {
let mut buf = UNKNOWN_PATHS.lock();
if buf.len() == DEFAULT_BUFFER_SIZE {
buf.pop_front();
}
buf.push_back(req);
}
pub fn snapshot() -> Vec<UnknownPathRequest> {
let buf = UNKNOWN_PATHS.lock();
buf.iter().rev().cloned().collect()
}
pub fn len() -> usize {
UNKNOWN_PATHS.lock().len()
}
pub fn clear() {
UNKNOWN_PATHS.lock().clear();
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn req(path: &str) -> UnknownPathRequest {
UnknownPathRequest {
timestamp: Utc::now(),
method: "GET".into(),
path: path.into(),
client_ip: "127.0.0.1".into(),
query: String::new(),
status: 404,
}
}
#[test]
fn record_and_snapshot_lifo() {
let _guard = TEST_LOCK.lock();
clear();
record(req("/first"));
record(req("/second"));
let snap = snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].path, "/second");
assert_eq!(snap[1].path, "/first");
}
#[test]
fn drops_oldest_at_capacity() {
let _guard = TEST_LOCK.lock();
clear();
for i in 0..(DEFAULT_BUFFER_SIZE + 5) {
record(req(&format!("/p/{i}")));
}
assert_eq!(len(), DEFAULT_BUFFER_SIZE);
let snap = snapshot();
assert_eq!(snap[0].path, format!("/p/{}", DEFAULT_BUFFER_SIZE + 4));
assert_eq!(snap[DEFAULT_BUFFER_SIZE - 1].path, "/p/5");
}
#[test]
fn shadow_mode_reads_env() {
std::env::set_var("MOCKFORGE_SHADOW_MODE", "true");
assert!(shadow_mode_enabled());
std::env::set_var("MOCKFORGE_SHADOW_MODE", "1");
assert!(shadow_mode_enabled());
std::env::set_var("MOCKFORGE_SHADOW_MODE", "0");
assert!(!shadow_mode_enabled());
std::env::remove_var("MOCKFORGE_SHADOW_MODE");
assert!(!shadow_mode_enabled());
}
#[test]
fn status_defaults_to_404_on_legacy_payload() {
let json = r#"{"timestamp":"2026-05-26T00:00:00Z","method":"GET","path":"/x","client_ip":"unknown","query":""}"#;
let parsed: UnknownPathRequest = serde_json::from_str(json).unwrap();
assert_eq!(parsed.status, 404);
}
}