Skip to main content

rest_e2e_mcp/parser/
suite.rs

1use std::path::Path;
2
3use regex::Regex;
4
5use crate::types::TestSuite;
6
7/// YAMLファイルからテストスイートを読み込む。
8pub fn load_suite(path: &Path) -> Result<TestSuite, String> {
9    let content = std::fs::read_to_string(path)
10        .map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
11    load_suite_str(&content)
12}
13
14/// YAML文字列からテストスイートをパースする。
15pub fn load_suite_str(content: &str) -> Result<TestSuite, String> {
16    let suite: TestSuite =
17        serde_yml::from_str(content).map_err(|e| format!("YAML parse error: {e}"))?;
18    validate_regex_patterns(&suite)?;
19    Ok(suite)
20}
21
22/// `body_matches` / `body_not_matches` に指定された正規表現パターンが
23/// 全てコンパイル可能かを検証する。不正なパターンは silent skip せず、
24/// 該当パターンとリクエスト名を含む明示エラーを返す。
25fn validate_regex_patterns(suite: &TestSuite) -> Result<(), String> {
26    for req in &suite.requests {
27        let Some(expect) = &req.expect else {
28            continue;
29        };
30        for pattern in &expect.body_matches {
31            if let Err(e) = Regex::new(pattern) {
32                return Err(format!(
33                    "Invalid regex in body_matches for request \"{}\": pattern \"{pattern}\" — {e}",
34                    req.name
35                ));
36            }
37        }
38        for pattern in &expect.body_not_matches {
39            if let Err(e) = Regex::new(pattern) {
40                return Err(format!(
41                    "Invalid regex in body_not_matches for request \"{}\": pattern \"{pattern}\" — {e}",
42                    req.name
43                ));
44            }
45        }
46    }
47    Ok(())
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn load_minimal() {
56        let yaml = r#"
57requests:
58  - name: ヘルスチェック
59    method: GET
60    url: https://example.com/health
61    expect:
62      status: 200
63"#;
64        let suite = load_suite_str(yaml).unwrap();
65        assert_eq!(suite.requests.len(), 1);
66        assert_eq!(suite.requests[0].name, "ヘルスチェック");
67        assert_eq!(suite.requests[0].method, "GET");
68    }
69
70    #[test]
71    fn load_with_variables() {
72        let yaml = r#"
73variables:
74  BASE_URL: https://example.com/api
75  ASSET_ID: abc123
76
77requests:
78  - name: テスト
79    method: GET
80    url: "{{BASE_URL}}/health"
81"#;
82        let suite = load_suite_str(yaml).unwrap();
83        assert_eq!(
84            suite.variables.get("BASE_URL").unwrap(),
85            "https://example.com/api"
86        );
87        assert_eq!(suite.requests.len(), 1);
88    }
89
90    #[test]
91    fn load_full_request() {
92        let yaml = r#"
93requests:
94  - name: エクスポート
95    method: POST
96    url: https://example.com/api/export
97    headers:
98      Content-Type: application/json
99    body: '{"assetId": "abc123"}'
100    timeout_ms: 60000
101    tags: [csv, export]
102    expect:
103      status: 200
104      headers:
105        content-type: text/csv
106        x-result-count: "15"
107      body_contains:
108        - 会社名
109        - メールアドレス
110      body_not_contains:
111        - error
112"#;
113        let suite = load_suite_str(yaml).unwrap();
114        let req = &suite.requests[0];
115        assert_eq!(req.method, "POST");
116        assert_eq!(req.timeout_ms, Some(60000));
117        assert_eq!(req.tags, vec!["csv", "export"]);
118
119        let expect = req.expect.as_ref().unwrap();
120        assert_eq!(expect.headers.get("content-type").unwrap(), "text/csv");
121        assert_eq!(expect.body_contains, vec!["会社名", "メールアドレス"]);
122        assert_eq!(expect.body_not_contains, vec!["error"]);
123    }
124
125    #[test]
126    fn load_multiple_status() {
127        let yaml = r#"
128requests:
129  - name: テスト
130    method: GET
131    url: https://example.com
132    expect:
133      status: [200, 404]
134"#;
135        let suite = load_suite_str(yaml).unwrap();
136        let expect = suite.requests[0].expect.as_ref().unwrap();
137        let status = expect.status.as_ref().unwrap();
138        assert!(status.matches(200));
139        assert!(status.matches(404));
140        assert!(!status.matches(500));
141    }
142
143    #[test]
144    fn load_skip_and_no_auth() {
145        let yaml = r#"
146requests:
147  - name: スキップ
148    method: GET
149    url: https://example.com
150    skip: true
151    no_auth: true
152"#;
153        let suite = load_suite_str(yaml).unwrap();
154        assert!(suite.requests[0].skip);
155        assert!(suite.requests[0].no_auth);
156    }
157
158    #[test]
159    fn invalid_yaml_returns_error() {
160        let yaml = "{{invalid: yaml";
161        assert!(load_suite_str(yaml).is_err());
162    }
163
164    #[test]
165    fn load_with_suite_timeout() {
166        let yaml = r#"
167timeout_ms: 60000
168
169requests:
170  - name: dummy
171    method: GET
172    url: https://example.com/dummy
173"#;
174        let suite = load_suite_str(yaml).unwrap();
175        assert_eq!(suite.timeout_ms, Some(60000));
176    }
177
178    #[test]
179    fn load_without_suite_timeout() {
180        let yaml = r#"
181requests:
182  - name: dummy
183    method: GET
184    url: https://example.com/dummy
185"#;
186        let suite = load_suite_str(yaml).unwrap();
187        assert_eq!(suite.timeout_ms, None);
188    }
189
190    #[test]
191    fn load_body_matches_and_not_matches() {
192        let yaml = r#"
193requests:
194  - name: 日付チェック
195    method: GET
196    url: https://example.com/api/status
197    expect:
198      status: 200
199      body_matches:
200        - "beforeSystemDate=\\d{4}-\\d{2}-\\d{2}"
201      body_not_matches:
202        - "^ERROR:"
203"#;
204        let suite = load_suite_str(yaml).unwrap();
205        let expect = suite.requests[0].expect.as_ref().unwrap();
206        assert_eq!(
207            expect.body_matches,
208            vec![r"beforeSystemDate=\d{4}-\d{2}-\d{2}"]
209        );
210        assert_eq!(expect.body_not_matches, vec!["^ERROR:"]);
211    }
212
213    #[test]
214    fn invalid_body_matches_regex_returns_explicit_error() {
215        let yaml = r#"
216requests:
217  - name: 壊れた正規表現
218    method: GET
219    url: https://example.com
220    expect:
221      body_matches:
222        - "(unclosed"
223"#;
224        let err = load_suite_str(yaml).unwrap_err();
225        assert!(err.contains("(unclosed"), "error was: {err}");
226        assert!(err.contains("壊れた正規表現"), "error was: {err}");
227        assert!(err.contains("body_matches"), "error was: {err}");
228    }
229
230    #[test]
231    fn invalid_body_not_matches_regex_returns_explicit_error() {
232        let yaml = r#"
233requests:
234  - name: 壊れた否定正規表現
235    method: GET
236    url: https://example.com
237    expect:
238      body_not_matches:
239        - "[unclosed"
240"#;
241        let err = load_suite_str(yaml).unwrap_err();
242        assert!(err.contains("[unclosed"), "error was: {err}");
243        assert!(err.contains("壊れた否定正規表現"), "error was: {err}");
244        assert!(err.contains("body_not_matches"), "error was: {err}");
245    }
246}