1use std::fmt;
17use std::time::Duration;
18
19#[derive(Debug)]
21pub enum HttpTestError {
22 RequestFailed(String),
24 AssertionFailed {
26 expected: String,
28 actual: String,
30 context: String,
32 },
33 JsonPathError(String),
35 ConnectionError(String),
37}
38
39impl fmt::Display for HttpTestError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 HttpTestError::RequestFailed(msg) => write!(f, "request failed: {msg}"),
43 HttpTestError::AssertionFailed {
44 expected,
45 actual,
46 context,
47 } => write!(
48 f,
49 "assertion failed ({context}): expected {expected}, got {actual}"
50 ),
51 HttpTestError::JsonPathError(msg) => write!(f, "JSON path error: {msg}"),
52 HttpTestError::ConnectionError(msg) => write!(f, "connection error: {msg}"),
53 }
54 }
55}
56
57impl std::error::Error for HttpTestError {}
58
59pub struct TestRequest {
61 method: String,
62 url: String,
63 headers: Vec<(String, String)>,
64 body: Option<String>,
65 query: Vec<(String, String)>,
66 timeout: Option<Duration>,
67}
68
69pub fn get(url: &str) -> TestRequest {
71 TestRequest::new("GET", url)
72}
73
74pub fn post(url: &str) -> TestRequest {
76 TestRequest::new("POST", url)
77}
78
79pub fn put(url: &str) -> TestRequest {
81 TestRequest::new("PUT", url)
82}
83
84pub fn delete(url: &str) -> TestRequest {
86 TestRequest::new("DELETE", url)
87}
88
89pub fn patch(url: &str) -> TestRequest {
91 TestRequest::new("PATCH", url)
92}
93
94impl TestRequest {
95 fn new(method: &str, url: &str) -> Self {
96 Self {
97 method: method.to_string(),
98 url: url.to_string(),
99 headers: Vec::new(),
100 body: None,
101 query: Vec::new(),
102 timeout: None,
103 }
104 }
105
106 pub fn header(mut self, key: &str, value: &str) -> Self {
108 self.headers.push((key.to_string(), value.to_string()));
109 self
110 }
111
112 pub fn bearer_token(self, token: &str) -> Self {
114 self.header("Authorization", &format!("Bearer {token}"))
115 }
116
117 pub fn basic_auth(self, user: &str, pass: &str) -> Self {
119 use std::io::Write;
120 let mut buf = Vec::new();
121 write!(buf, "{user}:{pass}").unwrap();
122 let encoded = base64_encode(&buf);
123 self.header("Authorization", &format!("Basic {encoded}"))
124 }
125
126 pub fn query(mut self, key: &str, value: &str) -> Self {
128 self.query.push((key.to_string(), value.to_string()));
129 self
130 }
131
132 pub fn json_body(mut self, value: &serde_json::Value) -> Self {
134 self.body = Some(value.to_string());
135 self.headers
136 .push(("Content-Type".to_string(), "application/json".to_string()));
137 self
138 }
139
140 pub fn body(mut self, body: impl Into<String>) -> Self {
142 self.body = Some(body.into());
143 self
144 }
145
146 pub fn timeout(mut self, duration: Duration) -> Self {
148 self.timeout = Some(duration);
149 self
150 }
151
152 pub fn send(&self) -> Result<TestResponse, HttpTestError> {
154 let mut url = self.url.clone();
155
156 if !self.query.is_empty() {
157 let sep = if url.contains('?') { '&' } else { '?' };
158 let params: Vec<String> = self
159 .query
160 .iter()
161 .map(|(k, v)| format!("{k}={v}"))
162 .collect();
163 url = format!("{url}{sep}{}", params.join("&"));
164 }
165
166 let client_builder = reqwest::blocking::ClientBuilder::new();
167 let client_builder = if let Some(t) = self.timeout {
168 client_builder.timeout(t)
169 } else {
170 client_builder
171 };
172
173 let client = client_builder
174 .build()
175 .map_err(|e| HttpTestError::ConnectionError(e.to_string()))?;
176
177 let mut request = match self.method.as_str() {
178 "GET" => client.get(&url),
179 "POST" => client.post(&url),
180 "PUT" => client.put(&url),
181 "DELETE" => client.delete(&url),
182 "PATCH" => client.patch(&url),
183 other => {
184 return Err(HttpTestError::RequestFailed(format!(
185 "unsupported method: {other}"
186 )))
187 }
188 };
189
190 for (key, value) in &self.headers {
191 request = request.header(key, value);
192 }
193
194 if let Some(body) = &self.body {
195 request = request.body(body.clone());
196 }
197
198 let response = request
199 .send()
200 .map_err(|e| HttpTestError::ConnectionError(e.to_string()))?;
201
202 let status = response.status().as_u16();
203 let headers: Vec<(String, String)> = response
204 .headers()
205 .iter()
206 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
207 .collect();
208 let body = response
209 .text()
210 .map_err(|e| HttpTestError::RequestFailed(e.to_string()))?;
211
212 Ok(TestResponse {
213 status,
214 headers,
215 body,
216 })
217 }
218}
219
220pub struct TestResponse {
222 pub status: u16,
224 pub headers: Vec<(String, String)>,
226 pub body: String,
228}
229
230impl TestResponse {
231 pub fn assert_status(&self, expected: u16) -> &Self {
233 assert_eq!(
234 self.status, expected,
235 "expected status {expected}, got {}",
236 self.status
237 );
238 self
239 }
240
241 pub fn assert_ok(&self) -> &Self {
243 assert!(
244 (200..300).contains(&self.status),
245 "expected 2xx status, got {}",
246 self.status
247 );
248 self
249 }
250
251 pub fn assert_redirect(&self) -> &Self {
253 assert!(
254 (300..400).contains(&self.status),
255 "expected 3xx status, got {}",
256 self.status
257 );
258 self
259 }
260
261 pub fn assert_client_error(&self) -> &Self {
263 assert!(
264 (400..500).contains(&self.status),
265 "expected 4xx status, got {}",
266 self.status
267 );
268 self
269 }
270
271 pub fn assert_server_error(&self) -> &Self {
273 assert!(
274 (500..600).contains(&self.status),
275 "expected 5xx status, got {}",
276 self.status
277 );
278 self
279 }
280
281 pub fn assert_header(&self, key: &str, value: &str) -> &Self {
283 let lower_key = key.to_lowercase();
284 let found = self
285 .headers
286 .iter()
287 .find(|(k, _)| k.to_lowercase() == lower_key);
288 match found {
289 Some((_, v)) => assert_eq!(
290 v, value,
291 "header '{key}': expected '{value}', got '{v}'"
292 ),
293 None => panic!("header '{key}' not found in response"),
294 }
295 self
296 }
297
298 pub fn assert_header_exists(&self, key: &str) -> &Self {
300 let lower_key = key.to_lowercase();
301 assert!(
302 self.headers.iter().any(|(k, _)| k.to_lowercase() == lower_key),
303 "expected header '{key}' to exist"
304 );
305 self
306 }
307
308 pub fn assert_body_contains(&self, substring: &str) -> &Self {
310 assert!(
311 self.body.contains(substring),
312 "expected body to contain '{substring}', body was: {}",
313 truncate_for_display(&self.body, 200)
314 );
315 self
316 }
317
318 pub fn assert_body_equals(&self, expected: &str) -> &Self {
320 assert_eq!(
321 self.body, expected,
322 "expected body to equal '{expected}', got: {}",
323 truncate_for_display(&self.body, 200)
324 );
325 self
326 }
327
328 pub fn assert_json_path(&self, path: &str, expected: &serde_json::Value) -> &Self {
335 let json: serde_json::Value = serde_json::from_str(&self.body).unwrap_or_else(|e| {
336 panic!("failed to parse response body as JSON: {e}");
337 });
338
339 let actual = resolve_json_path(&json, path);
340
341 match actual {
342 Some(val) => assert_eq!(
343 val, expected,
344 "JSON path '{path}': expected {expected}, got {val}"
345 ),
346 None => panic!("JSON path '{path}' not found in response body"),
347 }
348 self
349 }
350
351 pub fn json(&self) -> Result<serde_json::Value, HttpTestError> {
353 serde_json::from_str(&self.body)
354 .map_err(|e| HttpTestError::JsonPathError(format!("failed to parse JSON: {e}")))
355 }
356}
357
358fn resolve_json_path<'a>(
365 value: &'a serde_json::Value,
366 path: &str,
367) -> Option<&'a serde_json::Value> {
368 let segments = parse_path_segments(path);
369 let mut current = value;
370
371 for segment in &segments {
372 match segment {
373 PathSegment::Key(key) => {
374 current = current.get(key.as_str())?;
375 }
376 PathSegment::Index(idx) => {
377 current = current.get(*idx)?;
378 }
379 }
380 }
381
382 Some(current)
383}
384
385#[derive(Debug)]
386enum PathSegment {
387 Key(String),
388 Index(usize),
389}
390
391fn parse_path_segments(path: &str) -> Vec<PathSegment> {
393 let mut segments = Vec::new();
394
395 for part in path.split('.') {
396 if part.is_empty() {
397 continue;
398 }
399 if let Some(bracket_pos) = part.find('[') {
400 let key = &part[..bracket_pos];
401 if !key.is_empty() {
402 segments.push(PathSegment::Key(key.to_string()));
403 }
404 let rest = &part[bracket_pos..];
406 let mut remaining = rest;
407 while let Some(start) = remaining.find('[') {
408 if let Some(end) = remaining.find(']') {
409 if let Ok(idx) = remaining[start + 1..end].parse::<usize>() {
410 segments.push(PathSegment::Index(idx));
411 }
412 remaining = &remaining[end + 1..];
413 } else {
414 break;
415 }
416 }
417 } else {
418 segments.push(PathSegment::Key(part.to_string()));
419 }
420 }
421
422 segments
423}
424
425fn base64_encode(input: &[u8]) -> String {
427 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
428 let mut result = String::new();
429 let mut i = 0;
430 while i < input.len() {
431 let b0 = input[i] as u32;
432 let b1 = if i + 1 < input.len() { input[i + 1] as u32 } else { 0 };
433 let b2 = if i + 2 < input.len() { input[i + 2] as u32 } else { 0 };
434 let triple = (b0 << 16) | (b1 << 8) | b2;
435 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
436 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
437 if i + 1 < input.len() {
438 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
439 } else {
440 result.push('=');
441 }
442 if i + 2 < input.len() {
443 result.push(CHARS[(triple & 0x3F) as usize] as char);
444 } else {
445 result.push('=');
446 }
447 i += 3;
448 }
449 result
450}
451
452fn truncate_for_display(s: &str, max_len: usize) -> &str {
453 if s.len() <= max_len {
454 s
455 } else {
456 &s[..max_len]
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use serde_json::json;
464 use std::io::{BufRead, BufReader, Read, Write};
465 use std::net::TcpListener;
466 use std::thread::{self, JoinHandle};
467
468 fn start_test_server(
471 status: u16,
472 body: &str,
473 headers: Vec<(&str, &str)>,
474 ) -> (String, JoinHandle<()>) {
475 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
476 let port = listener.local_addr().unwrap().port();
477 let url = format!("http://127.0.0.1:{port}");
478 let body = body.to_string();
479 let headers: Vec<(String, String)> = headers
480 .into_iter()
481 .map(|(k, v)| (k.to_string(), v.to_string()))
482 .collect();
483
484 let handle = thread::spawn(move || {
485 let (mut stream, _) = listener.accept().unwrap();
486 let mut reader = BufReader::new(stream.try_clone().unwrap());
487
488 let mut request_lines = Vec::new();
490 loop {
491 let mut line = String::new();
492 reader.read_line(&mut line).unwrap();
493 if line.trim().is_empty() {
494 break;
495 }
496 request_lines.push(line);
497 }
498
499 let content_length: usize = request_lines
501 .iter()
502 .find(|l| l.to_lowercase().starts_with("content-length:"))
503 .and_then(|l| l.split(':').nth(1))
504 .and_then(|v| v.trim().parse().ok())
505 .unwrap_or(0);
506
507 if content_length > 0 {
508 let mut body_buf = vec![0u8; content_length];
509 reader.read_exact(&mut body_buf).ok();
510 }
511
512 let status_text = match status {
513 200 => "OK",
514 201 => "Created",
515 301 => "Moved Permanently",
516 302 => "Found",
517 400 => "Bad Request",
518 401 => "Unauthorized",
519 404 => "Not Found",
520 500 => "Internal Server Error",
521 _ => "Unknown",
522 };
523
524 let mut response = format!("HTTP/1.1 {status} {status_text}\r\n");
525 response.push_str(&format!("Content-Length: {}\r\n", body.len()));
526 for (k, v) in &headers {
527 response.push_str(&format!("{k}: {v}\r\n"));
528 }
529 response.push_str("\r\n");
530 response.push_str(&body);
531
532 stream.write_all(response.as_bytes()).unwrap();
533 stream.flush().unwrap();
534 });
535
536 (url, handle)
537 }
538
539 #[test]
540 fn test_get_assert_status() {
541 let (url, handle) = start_test_server(200, "hello", vec![]);
542 let response = get(&url).send().unwrap();
543 response.assert_status(200);
544 handle.join().unwrap();
545 }
546
547 #[test]
548 fn test_post_with_json_body() {
549 let (url, handle) = start_test_server(201, r#"{"id":1}"#, vec![("Content-Type", "application/json")]);
550 let response = post(&url)
551 .json_body(&json!({"name": "Alice"}))
552 .send()
553 .unwrap();
554 response.assert_status(201);
555 handle.join().unwrap();
556 }
557
558 #[test]
559 fn test_assert_ok() {
560 let (url, handle) = start_test_server(200, "ok", vec![]);
561 let response = get(&url).send().unwrap();
562 response.assert_ok();
563 handle.join().unwrap();
564 }
565
566 #[test]
567 fn test_assert_client_error() {
568 let (url, handle) = start_test_server(404, "not found", vec![]);
569 let response = get(&url).send().unwrap();
570 response.assert_client_error();
571 handle.join().unwrap();
572 }
573
574 #[test]
575 fn test_assert_header() {
576 let (url, handle) = start_test_server(200, "ok", vec![("X-Custom", "test-value")]);
577 let response = get(&url).send().unwrap();
578 response
579 .assert_status(200)
580 .assert_header("X-Custom", "test-value");
581 handle.join().unwrap();
582 }
583
584 #[test]
585 fn test_assert_header_exists() {
586 let (url, handle) = start_test_server(200, "ok", vec![("X-Request-Id", "abc123")]);
587 let response = get(&url).send().unwrap();
588 response.assert_header_exists("X-Request-Id");
589 handle.join().unwrap();
590 }
591
592 #[test]
593 fn test_assert_body_contains() {
594 let (url, handle) = start_test_server(200, "hello world", vec![]);
595 let response = get(&url).send().unwrap();
596 response.assert_body_contains("world");
597 handle.join().unwrap();
598 }
599
600 #[test]
601 fn test_assert_body_equals() {
602 let (url, handle) = start_test_server(200, "exact match", vec![]);
603 let response = get(&url).send().unwrap();
604 response.assert_body_equals("exact match");
605 handle.join().unwrap();
606 }
607
608 #[test]
609 fn test_assert_json_path_nested() {
610 let body = r#"{"data":{"users":[{"name":"Alice"},{"name":"Bob"}]}}"#;
611 let (url, handle) = start_test_server(200, body, vec![("Content-Type", "application/json")]);
612 let response = get(&url).send().unwrap();
613 response
614 .assert_json_path("data.users[0].name", &json!("Alice"))
615 .assert_json_path("data.users[1].name", &json!("Bob"));
616 handle.join().unwrap();
617 }
618
619 #[test]
620 fn test_assert_json_path_simple() {
621 let body = r#"{"count":42,"active":true}"#;
622 let (url, handle) = start_test_server(200, body, vec![]);
623 let response = get(&url).send().unwrap();
624 response
625 .assert_json_path("count", &json!(42))
626 .assert_json_path("active", &json!(true));
627 handle.join().unwrap();
628 }
629
630 #[test]
631 fn test_bearer_token() {
632 let (url, handle) = start_test_server(200, "ok", vec![]);
635 let response = get(&url).bearer_token("my-secret-token").send().unwrap();
636 response.assert_ok();
637 handle.join().unwrap();
638 }
639
640 #[test]
641 fn test_basic_auth() {
642 let (url, handle) = start_test_server(200, "ok", vec![]);
643 let response = get(&url).basic_auth("admin", "password").send().unwrap();
644 response.assert_ok();
645 handle.join().unwrap();
646 }
647
648 #[test]
649 fn test_query_params() {
650 let (url, handle) = start_test_server(200, "ok", vec![]);
651 let response = get(&url)
652 .query("page", "1")
653 .query("limit", "10")
654 .send()
655 .unwrap();
656 response.assert_ok();
657 handle.join().unwrap();
658 }
659
660 #[test]
661 fn test_put_request() {
662 let (url, handle) = start_test_server(200, "updated", vec![]);
663 let response = put(&url).body("data").send().unwrap();
664 response.assert_ok();
665 handle.join().unwrap();
666 }
667
668 #[test]
669 fn test_delete_request() {
670 let (url, handle) = start_test_server(200, "", vec![]);
671 let response = delete(&url).send().unwrap();
672 response.assert_ok();
673 handle.join().unwrap();
674 }
675
676 #[test]
677 fn test_patch_request() {
678 let (url, handle) = start_test_server(200, "patched", vec![]);
679 let response = patch(&url).body("partial").send().unwrap();
680 response.assert_ok();
681 handle.join().unwrap();
682 }
683
684 #[test]
685 fn test_json_parsing() {
686 let body = r#"{"key":"value"}"#;
687 let (url, handle) = start_test_server(200, body, vec![]);
688 let response = get(&url).send().unwrap();
689 let json = response.json().unwrap();
690 assert_eq!(json["key"], "value");
691 handle.join().unwrap();
692 }
693
694 #[test]
695 fn test_assert_redirect() {
696 let response = TestResponse {
697 status: 302,
698 headers: vec![("location".to_string(), "https://example.com".to_string())],
699 body: String::new(),
700 };
701 response.assert_redirect();
702 }
703
704 #[test]
705 fn test_assert_server_error() {
706 let (url, handle) = start_test_server(500, "error", vec![]);
707 let response = get(&url).send().unwrap();
708 response.assert_server_error();
709 handle.join().unwrap();
710 }
711
712 #[test]
713 fn test_chained_assertions() {
714 let body = r#"{"status":"ok","count":5}"#;
715 let (url, handle) = start_test_server(200, body, vec![("X-Api", "v1")]);
716 let response = get(&url).send().unwrap();
717 response
718 .assert_ok()
719 .assert_status(200)
720 .assert_header("X-Api", "v1")
721 .assert_body_contains("ok")
722 .assert_json_path("count", &json!(5));
723 handle.join().unwrap();
724 }
725
726 #[test]
727 #[should_panic(expected = "expected status 201")]
728 fn test_assert_status_fails() {
729 let response = TestResponse {
730 status: 200,
731 headers: vec![],
732 body: String::new(),
733 };
734 response.assert_status(201);
735 }
736
737 #[test]
738 #[should_panic(expected = "expected 2xx status")]
739 fn test_assert_ok_fails() {
740 let response = TestResponse {
741 status: 404,
742 headers: vec![],
743 body: String::new(),
744 };
745 response.assert_ok();
746 }
747
748 #[test]
749 fn test_base64_encode() {
750 assert_eq!(base64_encode(b"admin:password"), "YWRtaW46cGFzc3dvcmQ=");
751 assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
752 assert_eq!(base64_encode(b""), "");
753 }
754
755 #[test]
756 fn test_resolve_json_path() {
757 let data = json!({
758 "a": {
759 "b": [1, 2, 3],
760 "c": "hello"
761 }
762 });
763 assert_eq!(resolve_json_path(&data, "a.c"), Some(&json!("hello")));
764 assert_eq!(resolve_json_path(&data, "a.b[1]"), Some(&json!(2)));
765 assert_eq!(resolve_json_path(&data, "a.missing"), None);
766 }
767
768 #[test]
769 fn test_error_display() {
770 let err = HttpTestError::RequestFailed("timeout".to_string());
771 assert_eq!(format!("{err}"), "request failed: timeout");
772
773 let err = HttpTestError::ConnectionError("refused".to_string());
774 assert_eq!(format!("{err}"), "connection error: refused");
775
776 let err = HttpTestError::JsonPathError("invalid".to_string());
777 assert_eq!(format!("{err}"), "JSON path error: invalid");
778
779 let err = HttpTestError::AssertionFailed {
780 expected: "200".to_string(),
781 actual: "404".to_string(),
782 context: "status".to_string(),
783 };
784 assert_eq!(
785 format!("{err}"),
786 "assertion failed (status): expected 200, got 404"
787 );
788 }
789}