1use regex::Regex;
2
3use crate::types::{ExpectDef, Failure, HttpResponse};
4
5pub fn check(expect: Option<&ExpectDef>, response: &HttpResponse) -> Vec<Failure> {
7 let Some(expect) = expect else {
8 return Vec::new();
9 };
10
11 let mut failures = Vec::new();
12
13 if let Some(status) = &expect.status
15 && !status.matches(response.status)
16 {
17 failures.push(Failure {
18 check: "status".to_string(),
19 expected: status.to_string(),
20 actual: response.status.to_string(),
21 });
22 }
23
24 for (key, expected_val) in &expect.headers {
26 let actual_val = response
27 .headers
28 .iter()
29 .find(|(k, _)| k.eq_ignore_ascii_case(key))
30 .map(|(_, v)| v.as_str());
31
32 match actual_val {
33 None => {
34 failures.push(Failure {
35 check: format!("header[{key}]"),
36 expected: expected_val.clone(),
37 actual: "(missing)".to_string(),
38 });
39 }
40 Some(actual) if !actual.contains(expected_val.as_str()) => {
41 failures.push(Failure {
42 check: format!("header[{key}]"),
43 expected: expected_val.clone(),
44 actual: actual.to_string(),
45 });
46 }
47 _ => {}
48 }
49 }
50
51 for needle in &expect.body_contains {
53 if !response.body.contains(needle.as_str()) {
54 failures.push(Failure {
55 check: "body_contains".to_string(),
56 expected: needle.clone(),
57 actual: format!("(not found in {} bytes)", response.body.len()),
58 });
59 }
60 }
61
62 for needle in &expect.body_not_contains {
64 if response.body.contains(needle.as_str()) {
65 failures.push(Failure {
66 check: "body_not_contains".to_string(),
67 expected: format!("not contain: {needle}"),
68 actual: "(found)".to_string(),
69 });
70 }
71 }
72
73 for pattern in &expect.body_matches {
75 match Regex::new(pattern) {
76 Ok(re) => {
77 if !re.is_match(&response.body) {
78 failures.push(Failure {
79 check: "body_matches".to_string(),
80 expected: pattern.clone(),
81 actual: format!("(no match in {} bytes)", response.body.len()),
82 });
83 }
84 }
85 Err(e) => {
86 failures.push(Failure {
87 check: "body_matches (invalid pattern)".to_string(),
88 expected: pattern.clone(),
89 actual: format!("(regex compile error: {e})"),
90 });
91 }
92 }
93 }
94
95 for pattern in &expect.body_not_matches {
97 match Regex::new(pattern) {
98 Ok(re) => {
99 if re.is_match(&response.body) {
100 failures.push(Failure {
101 check: "body_not_matches".to_string(),
102 expected: format!("not match: {pattern}"),
103 actual: "(matched)".to_string(),
104 });
105 }
106 }
107 Err(e) => {
108 failures.push(Failure {
109 check: "body_not_matches (invalid pattern)".to_string(),
110 expected: pattern.clone(),
111 actual: format!("(regex compile error: {e})"),
112 });
113 }
114 }
115 }
116
117 failures
118}
119
120#[cfg(test)]
121mod tests {
122 use std::collections::HashMap;
123
124 use crate::types::StatusExpect;
125
126 use super::*;
127
128 fn make_response(status: u16, headers: &[(&str, &str)], body: &str) -> HttpResponse {
129 HttpResponse {
130 status,
131 http_version: "HTTP/1.1".to_string(),
132 headers: headers
133 .iter()
134 .map(|(k, v)| (k.to_string(), v.to_string()))
135 .collect(),
136 actual_request_headers: HashMap::new(),
137 body: body.to_string(),
138 charset: None,
139 elapsed_ms: 100,
140 size_bytes: body.len(),
141 }
142 }
143
144 #[test]
145 fn no_expect_always_pass() {
146 let resp = make_response(500, &[], "");
147 assert!(check(None, &resp).is_empty());
148 }
149
150 #[test]
151 fn status_pass() {
152 let expect = ExpectDef {
153 status: Some(StatusExpect::Single(200)),
154 headers: HashMap::new(),
155 body_contains: Vec::new(),
156 body_not_contains: Vec::new(),
157 body_matches: Vec::new(),
158 body_not_matches: Vec::new(),
159 };
160 let resp = make_response(200, &[], "");
161 assert!(check(Some(&expect), &resp).is_empty());
162 }
163
164 #[test]
165 fn status_fail() {
166 let expect = ExpectDef {
167 status: Some(StatusExpect::Single(200)),
168 headers: HashMap::new(),
169 body_contains: Vec::new(),
170 body_not_contains: Vec::new(),
171 body_matches: Vec::new(),
172 body_not_matches: Vec::new(),
173 };
174 let resp = make_response(500, &[], "");
175 let failures = check(Some(&expect), &resp);
176 assert_eq!(failures.len(), 1);
177 assert_eq!(failures[0].check, "status");
178 }
179
180 #[test]
181 fn status_multiple_pass() {
182 let expect = ExpectDef {
183 status: Some(StatusExpect::Multiple(vec![200, 404])),
184 headers: HashMap::new(),
185 body_contains: Vec::new(),
186 body_not_contains: Vec::new(),
187 body_matches: Vec::new(),
188 body_not_matches: Vec::new(),
189 };
190 let resp = make_response(404, &[], "");
191 assert!(check(Some(&expect), &resp).is_empty());
192 }
193
194 #[test]
195 fn header_pass() {
196 let expect = ExpectDef {
197 status: None,
198 headers: HashMap::from([("content-type".to_string(), "text/csv".to_string())]),
199 body_contains: Vec::new(),
200 body_not_contains: Vec::new(),
201 body_matches: Vec::new(),
202 body_not_matches: Vec::new(),
203 };
204 let resp = make_response(200, &[("Content-Type", "text/csv; charset=utf-8")], "");
205 assert!(check(Some(&expect), &resp).is_empty());
206 }
207
208 #[test]
209 fn header_fail() {
210 let expect = ExpectDef {
211 status: None,
212 headers: HashMap::from([("content-type".to_string(), "text/csv".to_string())]),
213 body_contains: Vec::new(),
214 body_not_contains: Vec::new(),
215 body_matches: Vec::new(),
216 body_not_matches: Vec::new(),
217 };
218 let resp = make_response(200, &[("Content-Type", "application/json")], "");
219 let failures = check(Some(&expect), &resp);
220 assert_eq!(failures.len(), 1);
221 }
222
223 #[test]
224 fn header_missing() {
225 let expect = ExpectDef {
226 status: None,
227 headers: HashMap::from([("x-custom".to_string(), "foo".to_string())]),
228 body_contains: Vec::new(),
229 body_not_contains: Vec::new(),
230 body_matches: Vec::new(),
231 body_not_matches: Vec::new(),
232 };
233 let resp = make_response(200, &[], "");
234 let failures = check(Some(&expect), &resp);
235 assert_eq!(failures.len(), 1);
236 assert!(failures[0].actual.contains("missing"));
237 }
238
239 #[test]
240 fn body_contains_pass() {
241 let expect = ExpectDef {
242 status: None,
243 headers: HashMap::new(),
244 body_contains: vec!["会社名".to_string()],
245 body_not_contains: Vec::new(),
246 body_matches: Vec::new(),
247 body_not_matches: Vec::new(),
248 };
249 let resp = make_response(200, &[], "会社名,メール");
250 assert!(check(Some(&expect), &resp).is_empty());
251 }
252
253 #[test]
254 fn body_contains_fail() {
255 let expect = ExpectDef {
256 status: None,
257 headers: HashMap::new(),
258 body_contains: vec!["会社名".to_string()],
259 body_not_contains: Vec::new(),
260 body_matches: Vec::new(),
261 body_not_matches: Vec::new(),
262 };
263 let resp = make_response(200, &[], "no match");
264 assert_eq!(check(Some(&expect), &resp).len(), 1);
265 }
266
267 #[test]
268 fn body_not_contains_pass() {
269 let expect = ExpectDef {
270 status: None,
271 headers: HashMap::new(),
272 body_contains: Vec::new(),
273 body_not_contains: vec!["error".to_string()],
274 body_matches: Vec::new(),
275 body_not_matches: Vec::new(),
276 };
277 let resp = make_response(200, &[], "ok");
278 assert!(check(Some(&expect), &resp).is_empty());
279 }
280
281 #[test]
282 fn body_not_contains_fail() {
283 let expect = ExpectDef {
284 status: None,
285 headers: HashMap::new(),
286 body_contains: Vec::new(),
287 body_not_contains: vec!["error".to_string()],
288 body_matches: Vec::new(),
289 body_not_matches: Vec::new(),
290 };
291 let resp = make_response(200, &[], "error occurred");
292 assert_eq!(check(Some(&expect), &resp).len(), 1);
293 }
294
295 #[test]
296 fn body_matches_pass() {
297 let expect = ExpectDef {
298 status: None,
299 headers: HashMap::new(),
300 body_contains: Vec::new(),
301 body_not_contains: Vec::new(),
302 body_matches: vec![r"beforeSystemDate=\d{4}-\d{2}-\d{2}".to_string()],
303 body_not_matches: Vec::new(),
304 };
305 let resp = make_response(200, &[], "beforeSystemDate=2026-07-09");
306 assert!(check(Some(&expect), &resp).is_empty());
307 }
308
309 #[test]
310 fn body_matches_fail() {
311 let expect = ExpectDef {
312 status: None,
313 headers: HashMap::new(),
314 body_contains: Vec::new(),
315 body_not_contains: Vec::new(),
316 body_matches: vec![r"beforeSystemDate=\d{4}-\d{2}-\d{2}".to_string()],
317 body_not_matches: Vec::new(),
318 };
319 let resp = make_response(200, &[], "no date here");
320 let failures = check(Some(&expect), &resp);
321 assert_eq!(failures.len(), 1);
322 assert_eq!(failures[0].check, "body_matches");
323 }
324
325 #[test]
326 fn body_matches_invalid_pattern() {
327 let expect = ExpectDef {
328 status: None,
329 headers: HashMap::new(),
330 body_contains: Vec::new(),
331 body_not_contains: Vec::new(),
332 body_matches: vec!["(unclosed".to_string()],
333 body_not_matches: Vec::new(),
334 };
335 let resp = make_response(200, &[], "anything");
336 let failures = check(Some(&expect), &resp);
337 assert_eq!(failures.len(), 1);
338 assert_eq!(failures[0].check, "body_matches (invalid pattern)");
339 }
340
341 #[test]
342 fn body_not_matches_pass() {
343 let expect = ExpectDef {
344 status: None,
345 headers: HashMap::new(),
346 body_contains: Vec::new(),
347 body_not_contains: Vec::new(),
348 body_matches: Vec::new(),
349 body_not_matches: vec![r"^ERROR:".to_string()],
350 };
351 let resp = make_response(200, &[], "ok: all good");
352 assert!(check(Some(&expect), &resp).is_empty());
353 }
354
355 #[test]
356 fn body_not_matches_fail() {
357 let expect = ExpectDef {
358 status: None,
359 headers: HashMap::new(),
360 body_contains: Vec::new(),
361 body_not_contains: Vec::new(),
362 body_matches: Vec::new(),
363 body_not_matches: vec![r"^ERROR:".to_string()],
364 };
365 let resp = make_response(200, &[], "ERROR: something broke");
366 let failures = check(Some(&expect), &resp);
367 assert_eq!(failures.len(), 1);
368 assert_eq!(failures[0].check, "body_not_matches");
369 }
370
371 #[test]
372 fn body_not_matches_invalid_pattern() {
373 let expect = ExpectDef {
374 status: None,
375 headers: HashMap::new(),
376 body_contains: Vec::new(),
377 body_not_contains: Vec::new(),
378 body_matches: Vec::new(),
379 body_not_matches: vec!["[unclosed".to_string()],
380 };
381 let resp = make_response(200, &[], "anything");
382 let failures = check(Some(&expect), &resp);
383 assert_eq!(failures.len(), 1);
384 assert_eq!(failures[0].check, "body_not_matches (invalid pattern)");
385 }
386}