x_http/
response.rs

1use crate::error::{Error, Result};
2use reqwest::blocking::Response as ReqwestResponse;
3use reqwest::header::HeaderMap;
4use reqwest::StatusCode;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7use std::time::Duration;
8
9#[derive(Debug)]
10pub struct Response {
11    status: StatusCode,
12    headers: HeaderMap,
13    body: Vec<u8>,
14    duration: Duration,
15}
16
17impl Response {
18    pub(crate) fn from_reqwest(response: ReqwestResponse, duration: Duration) -> Result<Self> {
19        let status = response.status();
20        let headers = response.headers().clone();
21        let body = response.bytes()?.to_vec();
22
23        Ok(Self {
24            status,
25            headers,
26            body,
27            duration,
28        })
29    }
30
31    pub fn status(&self) -> u16 {
32        self.status.as_u16()
33    }
34
35    pub fn status_code(&self) -> StatusCode {
36        self.status
37    }
38
39    pub fn is_success(&self) -> bool {
40        self.status.is_success()
41    }
42
43    pub fn is_error(&self) -> bool {
44        self.status.is_client_error() || self.status.is_server_error()
45    }
46
47    pub fn headers(&self) -> &HeaderMap {
48        &self.headers
49    }
50
51    pub fn header(&self, key: &str) -> Option<&str> {
52        self.headers.get(key)?.to_str().ok()
53    }
54
55    pub fn duration(&self) -> Duration {
56        self.duration
57    }
58
59    pub fn body_bytes(&self) -> &[u8] {
60        &self.body
61    }
62
63    pub fn text(&self) -> Result<String> {
64        String::from_utf8(self.body.clone())
65            .map_err(|e| Error::Assertion(format!("Response body is not valid UTF-8: {}", e)))
66    }
67
68    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
69        let text = self.text()?;
70        serde_json::from_str(&text).map_err(|e| e.into())
71    }
72
73    pub fn json_value(&self) -> Result<Value> {
74        self.json()
75    }
76
77    // Assertion methods - chainable
78    pub fn expect_status(self, expected: u16) -> Result<Self> {
79        let actual = self.status();
80        if actual != expected {
81            return Err(Error::StatusMismatch { expected, actual });
82        }
83        Ok(self)
84    }
85
86    pub fn expect_success(self) -> Result<Self> {
87        if !self.is_success() {
88            return Err(Error::Assertion(format!(
89                "Expected success status, got {}",
90                self.status()
91            )));
92        }
93        Ok(self)
94    }
95
96    pub fn expect_error(self) -> Result<Self> {
97        if !self.is_error() {
98            return Err(Error::Assertion(format!(
99                "Expected error status, got {}",
100                self.status()
101            )));
102        }
103        Ok(self)
104    }
105
106    pub fn expect_json(self) -> Result<Self> {
107        let content_type = self.header("content-type").unwrap_or("unknown");
108
109        if !content_type.contains("application/json") {
110            return Err(Error::NotJson(content_type.to_string()));
111        }
112
113        self.json_value()?;
114        Ok(self)
115    }
116
117    pub fn expect_text(self) -> Result<Self> {
118        self.text()?;
119        Ok(self)
120    }
121
122    pub fn expect_body_contains(self, text: &str) -> Result<Self> {
123        let body = self.text()?;
124        if !body.contains(text) {
125            return Err(Error::Assertion(format!(
126                "Expected body to contain '{}', but it didn't",
127                text
128            )));
129        }
130        Ok(self)
131    }
132
133    pub fn expect_header(self, key: &str, expected: &str) -> Result<Self> {
134        let actual = self
135            .header(key)
136            .ok_or_else(|| Error::Assertion(format!("Header '{}' not found", key)))?;
137
138        if actual != expected {
139            return Err(Error::HeaderMismatch {
140                key: key.to_string(),
141                expected: expected.to_string(),
142                actual: actual.to_string(),
143            });
144        }
145        Ok(self)
146    }
147
148    pub fn expect_content_type(self, content_type: &str) -> Result<Self> {
149        self.expect_header("content-type", content_type)
150    }
151
152    pub fn assert_field(self, path: &str, expected: impl Into<Value>) -> Result<Self> {
153        let json = self.json_value()?;
154        let expected_value = expected.into();
155
156        let actual_value = extract_json_path(&json, path).ok_or_else(|| Error::PathNotFound {
157            path: path.to_string(),
158        })?;
159
160        if actual_value != &expected_value {
161            return Err(Error::FieldMismatch {
162                field: path.to_string(),
163                expected: expected_value.to_string(),
164                actual: actual_value.to_string(),
165            });
166        }
167
168        Ok(self)
169    }
170
171    pub fn assert_field_exists(self, path: &str) -> Result<Self> {
172        let json = self.json_value()?;
173
174        extract_json_path(&json, path).ok_or_else(|| Error::PathNotFound {
175            path: path.to_string(),
176        })?;
177
178        Ok(self)
179    }
180
181    pub fn assert_array_length(self, path: &str, expected_length: usize) -> Result<Self> {
182        let json = self.json_value()?;
183
184        let array = extract_json_path(&json, path)
185            .and_then(|v| v.as_array())
186            .ok_or_else(|| Error::Assertion(format!("Path '{}' is not an array", path)))?;
187
188        if array.len() != expected_length {
189            return Err(Error::Assertion(format!(
190                "Array at '{}' expected length {}, got {}",
191                path,
192                expected_length,
193                array.len()
194            )));
195        }
196
197        Ok(self)
198    }
199}
200
201fn extract_json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
202    let parts: Vec<&str> = path.split('.').collect();
203    let mut current = value;
204
205    for part in parts {
206        if let Some(index_start) = part.find('[') {
207            let field = &part[..index_start];
208            let index_str = &part[index_start + 1..part.len() - 1];
209            let index: usize = index_str.parse().ok()?;
210
211            current = current.get(field)?.get(index)?;
212        } else {
213            current = current.get(part)?;
214        }
215    }
216
217    Some(current)
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use serde_json::json;
224
225    #[test]
226    fn test_json_path_extraction() {
227        let json = json!({
228            "user": {
229                "name": "John",
230                "age": 30
231            },
232            "items": [
233                {"id": 1, "name": "First"},
234                {"id": 2, "name": "Second"}
235            ]
236        });
237
238        assert_eq!(extract_json_path(&json, "user.name"), Some(&json!("John")));
239        assert_eq!(extract_json_path(&json, "user.age"), Some(&json!(30)));
240        assert_eq!(
241            extract_json_path(&json, "items[0].name"),
242            Some(&json!("First"))
243        );
244        assert_eq!(extract_json_path(&json, "items[1].id"), Some(&json!(2)));
245        assert_eq!(extract_json_path(&json, "nonexistent"), None);
246    }
247}