mockforge_http/middleware/
production_headers.rs

1//! Production headers middleware for deceptive deploy
2//!
3//! This module provides middleware that adds production-like headers to all responses,
4//! supporting template expansion for dynamic values like request IDs.
5
6use axum::{
7    body::Body,
8    extract::State,
9    http::{HeaderName, HeaderValue, Request},
10    middleware::Next,
11    response::Response,
12};
13use tracing::debug;
14use uuid::Uuid;
15
16use crate::HttpServerState;
17
18/// Production headers middleware
19///
20/// Adds configured headers to all responses, with support for template expansion.
21/// Templates supported:
22/// - `{{uuid}}` - Generates a new UUID for each request
23/// - `{{now}}` - Current timestamp in RFC3339 format
24/// - `{{timestamp}}` - Current Unix timestamp
25pub async fn production_headers_middleware(
26    State(state): State<HttpServerState>,
27    req: Request<Body>,
28    next: Next,
29) -> Response<Body> {
30    // Process the request
31    let mut response = next.run(req).await;
32
33    // Get headers configuration from state
34    if let Some(headers) = &state.production_headers {
35        for (key, value) in headers.iter() {
36            // Expand templates in header values
37            let expanded_value = expand_templates(value);
38
39            // Parse header name and value
40            if let (Ok(header_name), Ok(header_value)) =
41                (key.parse::<HeaderName>(), expanded_value.parse::<HeaderValue>())
42            {
43                // Only add if not already present (don't override existing headers)
44                if !response.headers().contains_key(&header_name) {
45                    response.headers_mut().insert(header_name, header_value);
46                    debug!("Added production header: {} = {}", key, expanded_value);
47                }
48            } else {
49                tracing::warn!("Failed to parse production header: {} = {}", key, expanded_value);
50            }
51        }
52    }
53
54    response
55}
56
57/// Expand template placeholders in header values
58///
59/// Supported templates:
60/// - `{{uuid}}` - Generates a new UUID v4
61/// - `{{now}}` - Current timestamp in RFC3339 format
62/// - `{{timestamp}}` - Current Unix timestamp (seconds)
63fn expand_templates(value: &str) -> String {
64    let mut result = value.to_string();
65
66    // Replace {{uuid}} with a new UUID
67    if result.contains("{{uuid}}") {
68        let uuid = Uuid::new_v4().to_string();
69        result = result.replace("{{uuid}}", &uuid);
70    }
71
72    // Replace {{now}} with current RFC3339 timestamp
73    if result.contains("{{now}}") {
74        let now = chrono::Utc::now().to_rfc3339();
75        result = result.replace("{{now}}", &now);
76    }
77
78    // Replace {{timestamp}} with Unix timestamp
79    if result.contains("{{timestamp}}") {
80        let timestamp = chrono::Utc::now().timestamp().to_string();
81        result = result.replace("{{timestamp}}", &timestamp);
82    }
83
84    result
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_expand_uuid_template() {
93        let value = "{{uuid}}";
94        let expanded = expand_templates(value);
95        // UUID should be 36 characters (with hyphens)
96        assert_eq!(expanded.len(), 36);
97        assert!(!expanded.contains("{{uuid}}"));
98    }
99
100    #[test]
101    fn test_expand_now_template() {
102        let value = "{{now}}";
103        let expanded = expand_templates(value);
104        // RFC3339 timestamp should be around 20 characters
105        assert!(expanded.len() > 15);
106        assert!(!expanded.contains("{{now}}"));
107        // Should contain 'T' separator (RFC3339 format)
108        assert!(expanded.contains('T'));
109    }
110
111    #[test]
112    fn test_expand_timestamp_template() {
113        let value = "{{timestamp}}";
114        let expanded = expand_templates(value);
115        // Unix timestamp should be numeric
116        assert!(expanded.parse::<i64>().is_ok());
117        assert!(!expanded.contains("{{timestamp}}"));
118    }
119
120    #[test]
121    fn test_expand_multiple_templates() {
122        let value = "Request-{{uuid}} at {{timestamp}}";
123        let expanded = expand_templates(value);
124        assert!(!expanded.contains("{{uuid}}"));
125        assert!(!expanded.contains("{{timestamp}}"));
126        assert!(expanded.starts_with("Request-"));
127    }
128
129    #[test]
130    fn test_no_templates() {
131        let value = "Static header value";
132        let expanded = expand_templates(value);
133        assert_eq!(expanded, value);
134    }
135}