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 call: None,
147 input: serde_json::Value::Null,
148 mock_response: None,
149 visitor: None,
150 assertions: vec![],
151 source: String::new(),
152 http: Some(HttpFixture {
153 handler: crate::fixture::HttpHandler {
154 route: format!("/fixtures/{id}"),
155 method: "GET".into(),
156 body_schema: None,
157 parameters: HashMap::new(),
158 middleware: None,
159 },
160 request: HttpRequest {
161 method: "GET".into(),
162 path: format!("/fixtures/{id}"),
163 headers: HashMap::new(),
164 query_params: HashMap::new(),
165 cookies: HashMap::new(),
166 body: None,
167 content_type: None,
168 },
169 expected_response: expected,
170 }),
171 }
172 }
173
174 fn empty_expected(status: u16) -> HttpExpectedResponse {
175 HttpExpectedResponse {
176 status_code: status,
177 body: None,
178 body_partial: None,
179 headers: HashMap::new(),
180 validation_errors: None,
181 }
182 }
183
184 #[test]
185 fn driver_emits_open_call_status_close_in_order() {
186 let fixture = http_fixture("simple", empty_expected(200));
187 let mut out = String::new();
188 let emitted = render_http_test(&mut out, &TagRenderer, &fixture);
189 assert!(emitted);
190 assert_eq!(
191 out,
192 "OPEN(simple)\nCALL(GET /fixtures/simple -> response)\nSTATUS=200\nCLOSE\n"
193 );
194 }
195
196 #[test]
197 fn driver_skips_when_no_http_block() {
198 let mut fixture = http_fixture("noop", empty_expected(200));
199 fixture.http = None;
200 let mut out = String::new();
201 let emitted = render_http_test(&mut out, &TagRenderer, &fixture);
202 assert!(!emitted);
203 assert!(out.is_empty());
204 }
205
206 #[test]
207 fn driver_emits_skip_marker_and_short_circuits_assertions() {
208 let mut fixture = http_fixture("skipme", empty_expected(200));
209 fixture.skip = Some(crate::fixture::SkipDirective {
210 languages: vec!["mock".into()],
211 reason: Some("not yet".into()),
212 });
213 let mut out = String::new();
214 render_http_test(&mut out, &TagRenderer, &fixture);
215 assert!(out.contains("OPEN(skipme|skip=not yet)"));
216 assert!(out.contains("CLOSE"));
217 assert!(!out.contains("CALL"));
218 assert!(!out.contains("STATUS"));
219 }
220
221 #[test]
222 fn driver_strips_content_encoding_header_assertion() {
223 let mut expected = empty_expected(200);
224 expected.headers.insert("Content-Encoding".into(), "gzip".into());
225 expected.headers.insert("X-Foo".into(), "bar".into());
226 let fixture = http_fixture("hdr", expected);
227 let mut out = String::new();
228 render_http_test(&mut out, &TagRenderer, &fixture);
229 assert!(!out.contains("HEADER(Content-Encoding"));
230 assert!(out.contains("HEADER(X-Foo=bar)"));
231 }
232
233 #[test]
234 fn driver_emits_headers_in_sorted_order() {
235 let mut expected = empty_expected(200);
236 expected.headers.insert("Z-Header".into(), "z".into());
237 expected.headers.insert("A-Header".into(), "a".into());
238 expected.headers.insert("M-Header".into(), "m".into());
239 let fixture = http_fixture("hdr", expected);
240 let mut out = String::new();
241 render_http_test(&mut out, &TagRenderer, &fixture);
242 let a_pos = out.find("HEADER(A-Header").unwrap();
243 let m_pos = out.find("HEADER(M-Header").unwrap();
244 let z_pos = out.find("HEADER(Z-Header").unwrap();
245 assert!(a_pos < m_pos);
246 assert!(m_pos < z_pos);
247 }
248
249 #[test]
250 fn driver_skips_body_assert_for_null_and_empty_string_sentinels() {
251 let mut expected = empty_expected(200);
252 expected.body = Some(serde_json::Value::Null);
253 let fixture = http_fixture("nullbody", expected);
254 let mut out = String::new();
255 render_http_test(&mut out, &TagRenderer, &fixture);
256 assert!(!out.contains("JSON_BODY"));
257
258 let mut expected = empty_expected(200);
259 expected.body = Some(serde_json::Value::String(String::new()));
260 let fixture = http_fixture("emptybody", expected);
261 let mut out = String::new();
262 render_http_test(&mut out, &TagRenderer, &fixture);
263 assert!(!out.contains("JSON_BODY"));
264 }
265
266 #[test]
267 fn driver_emits_body_partial_assertion_independently_of_body() {
268 let mut expected = empty_expected(200);
269 expected.body_partial = Some(serde_json::json!({"k": "v"}));
270 let fixture = http_fixture("partial", expected);
271 let mut out = String::new();
272 render_http_test(&mut out, &TagRenderer, &fixture);
273 assert!(out.contains("PARTIAL_BODY"));
274 }
275
276 #[test]
277 fn driver_emits_validation_errors_assertion_when_present_and_nonempty() {
278 let mut expected = empty_expected(422);
279 expected.validation_errors = Some(vec![ValidationErrorExpectation {
280 loc: vec!["name".into()],
281 msg: "field required".into(),
282 error_type: "missing".into(),
283 }]);
284 let fixture = http_fixture("ve", expected);
285 let mut out = String::new();
286 render_http_test(&mut out, &TagRenderer, &fixture);
287 assert!(out.contains("VALIDATION(1)"));
288
289 let mut expected = empty_expected(422);
291 expected.validation_errors = Some(vec![]);
292 let fixture = http_fixture("ve_empty", expected);
293 let mut out = String::new();
294 render_http_test(&mut out, &TagRenderer, &fixture);
295 assert!(!out.contains("VALIDATION"));
296 }
297}