http_client_vcr/
matcher.rs

1use crate::serializable::SerializableRequest;
2use http_client::Request;
3use std::fmt::Debug;
4
5pub trait RequestMatcher: Debug + Send + Sync {
6    fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool;
7
8    fn matches_serializable(
9        &self,
10        request: &SerializableRequest,
11        recorded_request: &SerializableRequest,
12    ) -> bool {
13        // Default implementation compares serialized forms
14        request.method == recorded_request.method && request.url == recorded_request.url
15    }
16}
17
18#[derive(Debug)]
19pub struct DefaultMatcher {
20    match_method: bool,
21    match_url: bool,
22    match_headers: Vec<String>,
23    match_body: bool,
24}
25
26impl DefaultMatcher {
27    pub fn new() -> Self {
28        Self {
29            match_method: true,
30            match_url: true,
31            // By default, match common headers including cookies - this is the correct behavior
32            match_headers: vec![
33                "authorization".to_string(),
34                "cookie".to_string(),
35                "content-type".to_string(),
36                "user-agent".to_string(),
37            ],
38            match_body: false,
39        }
40    }
41
42    /// Create a matcher that ignores cookies - useful for tests where cookies change
43    pub fn without_cookies() -> Self {
44        Self {
45            match_method: true,
46            match_url: true,
47            match_headers: vec![
48                "authorization".to_string(),
49                "content-type".to_string(),
50                "user-agent".to_string(),
51            ],
52            match_body: false,
53        }
54    }
55
56    pub fn with_method(mut self, match_method: bool) -> Self {
57        self.match_method = match_method;
58        self
59    }
60
61    pub fn with_url(mut self, match_url: bool) -> Self {
62        self.match_url = match_url;
63        self
64    }
65
66    pub fn with_headers(mut self, headers: Vec<String>) -> Self {
67        self.match_headers = headers;
68        self
69    }
70
71    pub fn with_body(mut self, match_body: bool) -> Self {
72        self.match_body = match_body;
73        self
74    }
75}
76
77impl RequestMatcher for DefaultMatcher {
78    fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool {
79        log::debug!(
80            "Matching request: {} {} against recorded: {} {}",
81            request.method(),
82            request.url(),
83            recorded_request.method,
84            recorded_request.url
85        );
86
87        if self.match_method && request.method().to_string() != recorded_request.method {
88            log::debug!(
89                "Method mismatch: {} != {}",
90                request.method(),
91                recorded_request.method
92            );
93            return false;
94        }
95
96        if self.match_url && request.url().to_string() != recorded_request.url {
97            log::debug!(
98                "URL mismatch: {} != {}",
99                request.url(),
100                recorded_request.url
101            );
102            return false;
103        }
104
105        if !self.match_headers.is_empty() {
106            log::debug!("Checking {} headers for matching", self.match_headers.len());
107            for header_name in &self.match_headers {
108                let request_header = request.header(header_name.as_str());
109                let recorded_header = recorded_request.headers.get(header_name);
110
111                log::debug!(
112                    "Comparing header '{}': request={:?}, recorded={:?}",
113                    header_name,
114                    request_header.map(|v| v.iter().map(|h| h.as_str()).collect::<Vec<_>>()),
115                    recorded_header
116                );
117
118                match (request_header, recorded_header) {
119                    (Some(req_val), Some(rec_val)) => {
120                        let req_values: Vec<String> =
121                            req_val.iter().map(|v| v.as_str().to_string()).collect();
122                        if &req_values != rec_val {
123                            log::debug!(
124                                "Header '{header_name}' values mismatch: request={req_values:?} != recorded={rec_val:?}"
125                            );
126                            return false;
127                        } else {
128                            log::debug!("Header '{header_name}' matched: {req_values:?}");
129                        }
130                    }
131                    (None, None) => {
132                        log::debug!("Header '{header_name}' both absent (matched)");
133                    }
134                    _ => {
135                        log::debug!("Header '{}' presence mismatch: request present={}, recorded present={}",
136                                   header_name, request_header.is_some(), recorded_header.is_some());
137                        return false;
138                    }
139                }
140            }
141        }
142
143        log::debug!("Request matched successfully");
144        true
145    }
146
147    fn matches_serializable(
148        &self,
149        request: &SerializableRequest,
150        recorded_request: &SerializableRequest,
151    ) -> bool {
152        if self.match_method && request.method != recorded_request.method {
153            return false;
154        }
155
156        if self.match_url && request.url != recorded_request.url {
157            return false;
158        }
159
160        if !self.match_headers.is_empty() {
161            for header_name in &self.match_headers {
162                let request_header = request.headers.get(header_name);
163                let recorded_header = recorded_request.headers.get(header_name);
164
165                match (request_header, recorded_header) {
166                    (Some(req_val), Some(rec_val)) => {
167                        if req_val != rec_val {
168                            return false;
169                        }
170                    }
171                    (None, None) => {}
172                    _ => return false,
173                }
174            }
175        }
176
177        true
178    }
179}
180
181impl Default for DefaultMatcher {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187#[derive(Debug)]
188pub struct ExactMatcher;
189
190impl RequestMatcher for ExactMatcher {
191    fn matches(&self, request: &Request, recorded_request: &SerializableRequest) -> bool {
192        if request.method().to_string() != recorded_request.method {
193            return false;
194        }
195
196        if request.url().to_string() != recorded_request.url {
197            return false;
198        }
199
200        let mut request_headers = std::collections::HashMap::new();
201        for (name, values) in request.iter() {
202            let header_values: Vec<String> =
203                values.iter().map(|v| v.as_str().to_string()).collect();
204            request_headers.insert(name.as_str().to_string(), header_values);
205        }
206
207        if request_headers != recorded_request.headers {
208            return false;
209        }
210
211        true
212    }
213
214    fn matches_serializable(
215        &self,
216        request: &SerializableRequest,
217        recorded_request: &SerializableRequest,
218    ) -> bool {
219        request.method == recorded_request.method
220            && request.url == recorded_request.url
221            && request.headers == recorded_request.headers
222    }
223}