mockforge_foundation/
unknown_paths.rs1use chrono::{DateTime, Utc};
15use once_cell::sync::Lazy;
16use parking_lot::Mutex;
17use serde::{Deserialize, Serialize};
18use std::collections::VecDeque;
19
20pub fn shadow_mode_enabled() -> bool {
31 std::env::var("MOCKFORGE_SHADOW_MODE")
32 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
33 .unwrap_or(false)
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct UnknownPathRequest {
40 pub timestamp: DateTime<Utc>,
42 pub method: String,
44 pub path: String,
46 pub client_ip: String,
48 pub query: String,
50 #[serde(default = "default_unknown_status")]
55 pub status: u16,
56}
57
58fn default_unknown_status() -> u16 {
59 404
60}
61
62const DEFAULT_BUFFER_SIZE: usize = 256;
63
64static UNKNOWN_PATHS: Lazy<Mutex<VecDeque<UnknownPathRequest>>> =
65 Lazy::new(|| Mutex::new(VecDeque::with_capacity(DEFAULT_BUFFER_SIZE)));
66
67pub fn record(req: UnknownPathRequest) {
69 let mut buf = UNKNOWN_PATHS.lock();
70 if buf.len() == DEFAULT_BUFFER_SIZE {
71 buf.pop_front();
72 }
73 buf.push_back(req);
74}
75
76pub fn snapshot() -> Vec<UnknownPathRequest> {
78 let buf = UNKNOWN_PATHS.lock();
79 buf.iter().rev().cloned().collect()
80}
81
82pub fn len() -> usize {
84 UNKNOWN_PATHS.lock().len()
85}
86
87pub fn clear() {
89 UNKNOWN_PATHS.lock().clear();
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 static TEST_LOCK: Mutex<()> = Mutex::new(());
100
101 fn req(path: &str) -> UnknownPathRequest {
102 UnknownPathRequest {
103 timestamp: Utc::now(),
104 method: "GET".into(),
105 path: path.into(),
106 client_ip: "127.0.0.1".into(),
107 query: String::new(),
108 status: 404,
109 }
110 }
111
112 #[test]
113 fn record_and_snapshot_lifo() {
114 let _guard = TEST_LOCK.lock();
115 clear();
116 record(req("/first"));
117 record(req("/second"));
118 let snap = snapshot();
119 assert_eq!(snap.len(), 2);
120 assert_eq!(snap[0].path, "/second");
121 assert_eq!(snap[1].path, "/first");
122 }
123
124 #[test]
125 fn drops_oldest_at_capacity() {
126 let _guard = TEST_LOCK.lock();
127 clear();
128 for i in 0..(DEFAULT_BUFFER_SIZE + 5) {
129 record(req(&format!("/p/{i}")));
130 }
131 assert_eq!(len(), DEFAULT_BUFFER_SIZE);
132 let snap = snapshot();
133 assert_eq!(snap[0].path, format!("/p/{}", DEFAULT_BUFFER_SIZE + 4));
134 assert_eq!(snap[DEFAULT_BUFFER_SIZE - 1].path, "/p/5");
135 }
136
137 #[test]
138 fn shadow_mode_reads_env() {
139 std::env::set_var("MOCKFORGE_SHADOW_MODE", "true");
140 assert!(shadow_mode_enabled());
141 std::env::set_var("MOCKFORGE_SHADOW_MODE", "1");
142 assert!(shadow_mode_enabled());
143 std::env::set_var("MOCKFORGE_SHADOW_MODE", "0");
144 assert!(!shadow_mode_enabled());
145 std::env::remove_var("MOCKFORGE_SHADOW_MODE");
146 assert!(!shadow_mode_enabled());
147 }
148
149 #[test]
150 fn status_defaults_to_404_on_legacy_payload() {
151 let json = r#"{"timestamp":"2026-05-26T00:00:00Z","method":"GET","path":"/x","client_ip":"unknown","query":""}"#;
154 let parsed: UnknownPathRequest = serde_json::from_str(json).unwrap();
155 assert_eq!(parsed.status, 404);
156 }
157}