1use super::{CallCtx, TestClientRenderer, has_meaningful_body, is_skipped};
17use crate::fixture::Fixture;
18
19pub const DEFAULT_RESPONSE_VAR: &str = "response";
21
22pub fn render_http_test<R: TestClientRenderer + ?Sized>(out: &mut String, renderer: &R, fixture: &Fixture) -> bool {
28 let Some(http) = fixture.http.as_ref() else {
29 return false;
30 };
31
32 let fn_name = renderer.sanitize_test_name(&fixture.id);
33
34 let skip_reason = if is_skipped(fixture, renderer.language_name()) {
35 Some(
36 fixture
37 .skip
38 .as_ref()
39 .and_then(|s| s.reason.as_deref())
40 .unwrap_or("skipped"),
41 )
42 } else {
43 None
44 };
45
46 renderer.render_test_open(out, &fn_name, &fixture.description, skip_reason);
47
48 if skip_reason.is_some() {
49 renderer.render_test_close(out);
53 return true;
54 }
55
56 let response_var = DEFAULT_RESPONSE_VAR;
57 let ctx = CallCtx::from_request(&http.request, response_var);
58 renderer.render_call(out, &ctx);
59
60 renderer.render_assert_status(out, response_var, http.expected_response.status_code);
61
62 let mut header_names: Vec<&String> = http.expected_response.headers.keys().collect();
65 header_names.sort();
66 for name in header_names {
67 let value = &http.expected_response.headers[name];
68 if name.eq_ignore_ascii_case("content-encoding") {
69 continue;
72 }
73 renderer.render_assert_header(out, response_var, name, value);
74 }
75
76 if has_meaningful_body(&http.expected_response) {
77 if let Some(body) = http.expected_response.body.as_ref() {
78 renderer.render_assert_json_body(out, response_var, body);
79 }
80 }
81
82 if let Some(partial) = http.expected_response.body_partial.as_ref() {
83 renderer.render_assert_partial_body(out, response_var, partial);
84 }
85
86 if let Some(errors) = http.expected_response.validation_errors.as_ref() {
87 if !errors.is_empty() {
88 renderer.render_assert_validation_errors(out, response_var, errors);
89 }
90 }
91
92 renderer.render_test_close(out);
93 true
94}
95
96#[cfg(test)]
97mod tests {
98 use super::super::{CallCtx, TestClientRenderer};
99 use super::render_http_test;
100 use crate::fixture::{Fixture, HttpExpectedResponse, HttpFixture, HttpRequest, ValidationErrorExpectation};
101 use std::collections::HashMap;
102
103 struct TagRenderer;
107
108 impl TestClientRenderer for TagRenderer {
109 fn language_name(&self) -> &'static str {
110 "mock"
111 }
112 fn render_test_open(&self, out: &mut String, fn_name: &str, _: &str, skip: Option<&str>) {
113 let skip_marker = skip.map(|r| format!("|skip={r}")).unwrap_or_default();
114 out.push_str(&format!("OPEN({fn_name}{skip_marker})\n"));
115 }
116 fn render_test_close(&self, out: &mut String) {
117 out.push_str("CLOSE\n");
118 }
119 fn render_call(&self, out: &mut String, ctx: &CallCtx<'_>) {
120 out.push_str(&format!("CALL({} {} -> {})\n", ctx.method, ctx.path, ctx.response_var));
121 }
122 fn render_assert_status(&self, out: &mut String, _: &str, status: u16) {
123 out.push_str(&format!("STATUS={status}\n"));
124 }
125 fn render_assert_header(&self, out: &mut String, _: &str, name: &str, value: &str) {
126 out.push_str(&format!("HEADER({name}={value})\n"));
127 }
128 fn render_assert_json_body(&self, out: &mut String, _: &str, expected: &serde_json::Value) {
129 out.push_str(&format!("JSON_BODY({expected})\n"));
130 }
131 fn render_assert_partial_body(&self, out: &mut String, _: &str, expected: &serde_json::Value) {
132 out.push_str(&format!("PARTIAL_BODY({expected})\n"));
133 }
134 fn render_assert_validation_errors(&self, out: &mut String, _: &str, errors: &[ValidationErrorExpectation]) {
135 out.push_str(&format!("VALIDATION({})\n", errors.len()));
136 }
137 }
138
139 fn http_fixture(id: &str, expected: HttpExpectedResponse) -> Fixture {
140 Fixture {
141 id: id.into(),
142 description: "test".into(),
143 category: Some("smoke".into()),
144 tags: vec![],
145 skip: None,
146 env: None,
147 call: None,
148 input: serde_json::Value::Null,
149 mock_response: None,
150 visitor: None,
151 assertions: vec![],
152 source: String::new(),
153 http: Some(HttpFixture {
154 handler: crate::fixture::HttpHandler {
155 route: format!("/fixtures/{id}"),
156 method: "GET".into(),
157 body_schema: None,
158 parameters: HashMap::new(),
159 middleware: None,
160 },
161 request: HttpRequest {
162 method: "GET".into(),
163 path: format!("/fixtures/{id}"),
164 headers: HashMap::new(),
165 query_params: HashMap::new(),
166 cookies: HashMap::new(),
167 body: None,
168 content_type: None,
169 },
170 expected_response: expected,
171 }),
172 }
173 }
174
175 fn empty_expected(status: u16) -> HttpExpectedResponse {
176 HttpExpectedResponse {
177 status_code: status,
178 body: None,
179 body_partial: None,
180 headers: HashMap::new(),
181 validation_errors: None,
182 }
183 }
184
185 #[test]
186 fn driver_emits_open_call_status_close_in_order() {
187 let fixture = http_fixture("simple", empty_expected(200));
188 let mut out = String::new();
189 let emitted = render_http_test(&mut out, &TagRenderer, &fixture);
190 assert!(emitted);
191 assert_eq!(
192 out,
193 "OPEN(simple)\nCALL(GET /fixtures/simple -> response)\nSTATUS=200\nCLOSE\n"
194 );
195 }
196
197 #[test]
198 fn driver_skips_when_no_http_block() {
199 let mut fixture = http_fixture("noop", empty_expected(200));
200 fixture.http = None;
201 let mut out = String::new();
202 let emitted = render_http_test(&mut out, &TagRenderer, &fixture);
203 assert!(!emitted);
204 assert!(out.is_empty());
205 }
206
207 #[test]
208 fn driver_emits_skip_marker_and_short_circuits_assertions() {
209 let mut fixture = http_fixture("skipme", empty_expected(200));
210 fixture.skip = Some(crate::fixture::SkipDirective {
211 languages: vec!["mock".into()],
212 reason: Some("not yet".into()),
213 });
214 let mut out = String::new();
215 render_http_test(&mut out, &TagRenderer, &fixture);
216 assert!(out.contains("OPEN(skipme|skip=not yet)"));
217 assert!(out.contains("CLOSE"));
218 assert!(!out.contains("CALL"));
219 assert!(!out.contains("STATUS"));
220 }
221
222 #[test]
223 fn driver_strips_content_encoding_header_assertion() {
224 let mut expected = empty_expected(200);
225 expected.headers.insert("Content-Encoding".into(), "gzip".into());
226 expected.headers.insert("X-Foo".into(), "bar".into());
227 let fixture = http_fixture("hdr", expected);
228 let mut out = String::new();
229 render_http_test(&mut out, &TagRenderer, &fixture);
230 assert!(!out.contains("HEADER(Content-Encoding"));
231 assert!(out.contains("HEADER(X-Foo=bar)"));
232 }
233
234 #[test]
235 fn driver_emits_headers_in_sorted_order() {
236 let mut expected = empty_expected(200);
237 expected.headers.insert("Z-Header".into(), "z".into());
238 expected.headers.insert("A-Header".into(), "a".into());
239 expected.headers.insert("M-Header".into(), "m".into());
240 let fixture = http_fixture("hdr", expected);
241 let mut out = String::new();
242 render_http_test(&mut out, &TagRenderer, &fixture);
243 let a_pos = out.find("HEADER(A-Header").unwrap();
244 let m_pos = out.find("HEADER(M-Header").unwrap();
245 let z_pos = out.find("HEADER(Z-Header").unwrap();
246 assert!(a_pos < m_pos);
247 assert!(m_pos < z_pos);
248 }
249
250 #[test]
251 fn driver_skips_body_assert_for_null_and_empty_string_sentinels() {
252 let mut expected = empty_expected(200);
253 expected.body = Some(serde_json::Value::Null);
254 let fixture = http_fixture("nullbody", expected);
255 let mut out = String::new();
256 render_http_test(&mut out, &TagRenderer, &fixture);
257 assert!(!out.contains("JSON_BODY"));
258
259 let mut expected = empty_expected(200);
260 expected.body = Some(serde_json::Value::String(String::new()));
261 let fixture = http_fixture("emptybody", expected);
262 let mut out = String::new();
263 render_http_test(&mut out, &TagRenderer, &fixture);
264 assert!(!out.contains("JSON_BODY"));
265 }
266
267 #[test]
268 fn driver_emits_body_partial_assertion_independently_of_body() {
269 let mut expected = empty_expected(200);
270 expected.body_partial = Some(serde_json::json!({"k": "v"}));
271 let fixture = http_fixture("partial", expected);
272 let mut out = String::new();
273 render_http_test(&mut out, &TagRenderer, &fixture);
274 assert!(out.contains("PARTIAL_BODY"));
275 }
276
277 #[test]
278 fn driver_emits_validation_errors_assertion_when_present_and_nonempty() {
279 let mut expected = empty_expected(422);
280 expected.validation_errors = Some(vec![ValidationErrorExpectation {
281 loc: vec!["name".into()],
282 msg: "field required".into(),
283 error_type: "missing".into(),
284 }]);
285 let fixture = http_fixture("ve", expected);
286 let mut out = String::new();
287 render_http_test(&mut out, &TagRenderer, &fixture);
288 assert!(out.contains("VALIDATION(1)"));
289
290 let mut expected = empty_expected(422);
292 expected.validation_errors = Some(vec![]);
293 let fixture = http_fixture("ve_empty", expected);
294 let mut out = String::new();
295 render_http_test(&mut out, &TagRenderer, &fixture);
296 assert!(!out.contains("VALIDATION"));
297 }
298}