mockforge_core/
time_travel_handler.rs

1//! # Time Travel Response Handler
2//!
3//! This module provides HTTP request handling logic for scheduled responses
4//! that are triggered based on the virtual clock time.
5
6use crate::time_travel::{ResponseScheduler, ScheduledResponse, VirtualClock};
7use axum::{
8    body::Body,
9    http::{HeaderValue, Response, StatusCode},
10    response::IntoResponse,
11};
12use std::sync::Arc;
13use tracing::info;
14
15/// Handler that checks for and returns scheduled responses
16pub struct TimeTravelHandler {
17    /// Response scheduler
18    scheduler: Arc<ResponseScheduler>,
19    /// Virtual clock
20    clock: Arc<VirtualClock>,
21}
22
23impl TimeTravelHandler {
24    /// Create a new time travel handler
25    pub fn new(scheduler: Arc<ResponseScheduler>, clock: Arc<VirtualClock>) -> Self {
26        Self { scheduler, clock }
27    }
28
29    /// Check if there are any due responses and return the first one
30    pub fn check_for_scheduled_response(&self) -> Option<ScheduledResponseWrapper> {
31        if !self.clock.is_enabled() {
32            return None;
33        }
34
35        let due_responses = self.scheduler.get_due_responses();
36        if due_responses.is_empty() {
37            return None;
38        }
39
40        // Return the first due response
41        due_responses.into_iter().next().map(ScheduledResponseWrapper::new)
42    }
43
44    /// Get all due responses
45    pub fn get_all_due_responses(&self) -> Vec<ScheduledResponseWrapper> {
46        if !self.clock.is_enabled() {
47            return Vec::new();
48        }
49
50        self.scheduler
51            .get_due_responses()
52            .into_iter()
53            .map(ScheduledResponseWrapper::new)
54            .collect()
55    }
56
57    /// Check if time travel is enabled
58    pub fn is_enabled(&self) -> bool {
59        self.clock.is_enabled()
60    }
61}
62
63/// Wrapper around ScheduledResponse for converting to HTTP responses
64#[derive(Debug, Clone)]
65pub struct ScheduledResponseWrapper {
66    inner: ScheduledResponse,
67}
68
69impl ScheduledResponseWrapper {
70    /// Create a new wrapper
71    pub fn new(response: ScheduledResponse) -> Self {
72        Self { inner: response }
73    }
74
75    /// Get the inner scheduled response
76    pub fn inner(&self) -> &ScheduledResponse {
77        &self.inner
78    }
79
80    /// Convert to an Axum response
81    pub fn into_response(self) -> Response<Body> {
82        let mut response = Response::builder().status(self.inner.status);
83
84        // Add headers
85        if let Some(headers) = response.headers_mut() {
86            for (key, value) in &self.inner.headers {
87                if let Ok(header_name) = key.parse::<axum::http::HeaderName>() {
88                    if let Ok(header_value) = HeaderValue::from_str(value) {
89                        headers.insert(header_name, header_value);
90                    }
91                }
92            }
93
94            // Add custom header to indicate this is a scheduled response
95            headers.insert("X-MockForge-Scheduled-Response", HeaderValue::from_static("true"));
96
97            if let Some(name) = &self.inner.name {
98                if let Ok(value) = HeaderValue::from_str(name) {
99                    headers.insert("X-MockForge-Schedule-Name", value);
100                }
101            }
102        }
103
104        // Set body
105        let body_str = serde_json::to_string(&self.inner.body).unwrap_or_else(|_| "{}".to_string());
106        response.body(Body::from(body_str)).unwrap_or_else(|_| {
107            Response::builder()
108                .status(StatusCode::INTERNAL_SERVER_ERROR)
109                .body(Body::from("Failed to build response"))
110                .unwrap()
111        })
112    }
113}
114
115impl IntoResponse for ScheduledResponseWrapper {
116    fn into_response(self) -> axum::response::Response {
117        let mut response = Response::builder().status(self.inner.status);
118
119        // Add headers
120        let headers = response.headers_mut();
121        if let Some(headers) = headers {
122            for (key, value) in &self.inner.headers {
123                if let Ok(header_name) = key.parse::<axum::http::HeaderName>() {
124                    if let Ok(header_value) = HeaderValue::from_str(value) {
125                        headers.insert(header_name, header_value);
126                    }
127                }
128            }
129
130            // Add custom header
131            headers.insert("X-MockForge-Scheduled-Response", HeaderValue::from_static("true"));
132        }
133
134        // Set body
135        let body_str = serde_json::to_string(&self.inner.body).unwrap_or_else(|_| "{}".to_string());
136        response.body(Body::from(body_str)).unwrap_or_else(|_| {
137            Response::builder()
138                .status(StatusCode::INTERNAL_SERVER_ERROR)
139                .body(Body::from("Failed to build response"))
140                .unwrap()
141        })
142    }
143}
144
145/// Middleware layer for checking scheduled responses
146pub async fn time_travel_middleware(
147    handler: Arc<TimeTravelHandler>,
148    request: axum::http::Request<Body>,
149    next: axum::middleware::Next,
150) -> impl IntoResponse {
151    // Check if there's a scheduled response
152    if let Some(scheduled) = handler.check_for_scheduled_response() {
153        info!("Returning scheduled response: {}", scheduled.inner().id);
154        return scheduled.into_response();
155    }
156
157    // Otherwise, pass through to the next handler
158    next.run(request).await
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::time_travel::{ScheduledResponse, VirtualClock};
165    use chrono::{Duration, Utc};
166    use std::collections::HashMap;
167
168    #[test]
169    fn test_time_travel_handler_creation() {
170        let clock = Arc::new(VirtualClock::new());
171        let scheduler = Arc::new(ResponseScheduler::new(clock.clone()));
172        let handler = TimeTravelHandler::new(scheduler, clock);
173
174        assert!(!handler.is_enabled());
175    }
176
177    #[test]
178    fn test_scheduled_response_wrapper() {
179        let response = ScheduledResponse {
180            id: "test-1".to_string(),
181            trigger_time: Utc::now(),
182            body: serde_json::json!({"message": "Hello"}),
183            status: 200,
184            headers: HashMap::new(),
185            name: Some("test".to_string()),
186            repeat: None,
187        };
188
189        let wrapper = ScheduledResponseWrapper::new(response.clone());
190        assert_eq!(wrapper.inner().id, "test-1");
191    }
192
193    #[test]
194    fn test_check_for_scheduled_response() {
195        let clock = Arc::new(VirtualClock::new());
196        let test_time = Utc::now();
197        clock.enable_and_set(test_time);
198
199        let scheduler = Arc::new(ResponseScheduler::new(clock.clone()));
200
201        let response = ScheduledResponse {
202            id: "test-1".to_string(),
203            trigger_time: test_time + Duration::seconds(10),
204            body: serde_json::json!({"message": "Hello"}),
205            status: 200,
206            headers: HashMap::new(),
207            name: None,
208            repeat: None,
209        };
210
211        scheduler.schedule(response).unwrap();
212
213        let handler = TimeTravelHandler::new(scheduler, clock.clone());
214
215        // Should not be due yet
216        assert!(handler.check_for_scheduled_response().is_none());
217
218        // Advance time
219        clock.advance(Duration::seconds(15));
220
221        // Should be due now
222        assert!(handler.check_for_scheduled_response().is_some());
223    }
224}