Skip to main content

rest_e2e_mcp/assert/
checker.rs

1use regex::Regex;
2
3use crate::types::{ExpectDef, Failure, HttpResponse};
4
5/// レスポンスを期待値と照合し、失敗リストを返す。
6pub 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    // ステータスコード検証
14    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    // ヘッダー検証(キーはcase-insensitive、値は部分一致)
25    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    // body_contains 検証
52    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    // body_not_contains 検証
63    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    // body_matches 検証(正規表現)
74    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    // body_not_matches 検証(正規表現)
96    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}