rest_e2e_mcp/parser/
suite.rs1use std::path::Path;
2
3use regex::Regex;
4
5use crate::types::TestSuite;
6
7pub 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
14pub 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
22fn 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}