1#![cfg(native)]
19
20use std::collections::HashMap;
21
22use bytes::Bytes;
23use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header::HeaderName};
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone)]
31pub struct MockHttpRequest {
32 pub method: Method,
34 pub uri: Uri,
36 pub headers: HeaderMap,
38 pub body: Bytes,
40 pub cookies: HashMap<String, String>,
42 pub query_params: HashMap<String, String>,
44}
45
46impl Default for MockHttpRequest {
47 fn default() -> Self {
48 Self {
49 method: Method::GET,
50 uri: "/".parse().unwrap(),
51 headers: HeaderMap::new(),
52 body: Bytes::new(),
53 cookies: HashMap::new(),
54 query_params: HashMap::new(),
55 }
56 }
57}
58
59impl MockHttpRequest {
60 pub fn new(method: Method, uri: &str) -> Self {
62 let parsed_uri: Uri = uri.parse().unwrap_or_else(|_| "/".parse().unwrap());
63
64 let query_params = parsed_uri
66 .query()
67 .map(|q| {
68 url::form_urlencoded::parse(q.as_bytes())
69 .map(|(k, v)| (k.to_string(), v.to_string()))
70 .collect()
71 })
72 .unwrap_or_default();
73
74 Self {
75 method,
76 uri: parsed_uri,
77 query_params,
78 ..Default::default()
79 }
80 }
81
82 pub fn get(uri: &str) -> Self {
84 Self::new(Method::GET, uri)
85 }
86
87 pub fn post(uri: &str) -> Self {
89 Self::new(Method::POST, uri)
90 }
91
92 pub fn put(uri: &str) -> Self {
94 Self::new(Method::PUT, uri)
95 }
96
97 pub fn patch(uri: &str) -> Self {
99 Self::new(Method::PATCH, uri)
100 }
101
102 pub fn delete(uri: &str) -> Self {
104 Self::new(Method::DELETE, uri)
105 }
106
107 pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
118 let bytes = serde_json::to_vec(body).unwrap_or_else(|err| {
119 panic!("MockHttpRequest::with_json: failed to serialize body to JSON: {err}")
120 });
121 self.body = Bytes::from(bytes);
122 self.headers.insert(
123 http::header::CONTENT_TYPE,
124 HeaderValue::from_static("application/json"),
125 );
126 self
127 }
128
129 pub fn with_form<T: Serialize>(mut self, body: &T) -> Self {
140 let encoded = serde_urlencoded::to_string(body).unwrap_or_else(|err| {
141 panic!("MockHttpRequest::with_form: failed to serialize body as form data: {err}")
142 });
143 self.body = Bytes::from(encoded);
144 self.headers.insert(
145 http::header::CONTENT_TYPE,
146 HeaderValue::from_static("application/x-www-form-urlencoded"),
147 );
148 self
149 }
150
151 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
153 self.body = body.into();
154 self
155 }
156
157 pub fn with_text(mut self, body: impl Into<String>) -> Self {
159 self.body = Bytes::from(body.into());
160 self.headers.insert(
161 http::header::CONTENT_TYPE,
162 HeaderValue::from_static("text/plain"),
163 );
164 self
165 }
166
167 pub fn with_header(mut self, name: &str, value: &str) -> Self {
169 if let (Ok(header_name), Ok(header_value)) = (
170 HeaderName::from_bytes(name.as_bytes()),
171 HeaderValue::from_str(value),
172 ) {
173 self.headers.insert(header_name, header_value);
174 }
175 self
176 }
177
178 pub fn with_headers<'a>(
180 mut self,
181 headers: impl IntoIterator<Item = (&'a str, &'a str)>,
182 ) -> Self {
183 for (name, value) in headers {
184 if let (Ok(header_name), Ok(header_value)) = (
185 HeaderName::from_bytes(name.as_bytes()),
186 HeaderValue::from_str(value),
187 ) {
188 self.headers.insert(header_name, header_value);
189 }
190 }
191 self
192 }
193
194 pub fn with_cookie(mut self, name: &str, value: &str) -> Self {
196 self.cookies.insert(name.to_string(), value.to_string());
197 self.update_cookie_header();
198 self
199 }
200
201 pub fn with_cookies<'a>(
203 mut self,
204 cookies: impl IntoIterator<Item = (&'a str, &'a str)>,
205 ) -> Self {
206 for (name, value) in cookies {
207 self.cookies.insert(name.to_string(), value.to_string());
208 }
209 self.update_cookie_header();
210 self
211 }
212
213 pub fn with_query(mut self, name: &str, value: &str) -> Self {
215 self.query_params
216 .insert(name.to_string(), value.to_string());
217 self.update_uri_query();
218 self
219 }
220
221 pub fn with_query_params<'a>(
223 mut self,
224 params: impl IntoIterator<Item = (&'a str, &'a str)>,
225 ) -> Self {
226 for (name, value) in params {
227 self.query_params
228 .insert(name.to_string(), value.to_string());
229 }
230 self.update_uri_query();
231 self
232 }
233
234 pub fn with_bearer_token(self, token: &str) -> Self {
236 self.with_header("Authorization", &format!("Bearer {}", token))
237 }
238
239 pub fn with_basic_auth(self, username: &str, password: &str) -> Self {
241 let credentials =
242 base64_simd::STANDARD.encode_to_string(format!("{}:{}", username, password));
243 self.with_header("Authorization", &format!("Basic {}", credentials))
244 }
245
246 pub fn with_content_type(self, content_type: &str) -> Self {
248 self.with_header("Content-Type", content_type)
249 }
250
251 pub fn with_accept(self, accept: &str) -> Self {
253 self.with_header("Accept", accept)
254 }
255
256 pub fn path(&self) -> &str {
258 self.uri.path()
259 }
260
261 pub fn uri_string(&self) -> String {
263 self.uri.to_string()
264 }
265
266 pub fn get_header(&self, name: &str) -> Option<&str> {
268 self.headers.get(name).and_then(|v| v.to_str().ok())
269 }
270
271 pub fn get_cookie(&self, name: &str) -> Option<&str> {
273 self.cookies.get(name).map(|s| s.as_str())
274 }
275
276 pub fn get_query(&self, name: &str) -> Option<&str> {
278 self.query_params.get(name).map(|s| s.as_str())
279 }
280
281 pub fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
283 serde_json::from_slice(&self.body)
284 }
285
286 pub fn form<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_urlencoded::de::Error> {
288 serde_urlencoded::from_bytes(&self.body)
289 }
290
291 pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
293 String::from_utf8(self.body.to_vec())
294 }
295
296 fn update_cookie_header(&mut self) {
297 if self.cookies.is_empty() {
298 self.headers.remove(http::header::COOKIE);
299 } else {
300 let cookie_str: String = self
301 .cookies
302 .iter()
303 .map(|(k, v)| format!("{}={}", k, v))
304 .collect::<Vec<_>>()
305 .join("; ");
306
307 if let Ok(value) = HeaderValue::from_str(&cookie_str) {
308 self.headers.insert(http::header::COOKIE, value);
309 }
310 }
311 }
312
313 fn update_uri_query(&mut self) {
314 let path = self.uri.path().to_string();
315 if self.query_params.is_empty() {
316 if let Ok(uri) = path.parse() {
317 self.uri = uri;
318 }
319 } else {
320 let query: String = self
321 .query_params
322 .iter()
323 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
324 .collect::<Vec<_>>()
325 .join("&");
326
327 if let Ok(uri) = format!("{}?{}", path, query).parse() {
328 self.uri = uri;
329 }
330 }
331 }
332}
333
334#[derive(Debug, Clone)]
339pub struct MockHttpResponse {
340 pub status: StatusCode,
342 pub headers: HeaderMap,
344 pub body: Bytes,
346}
347
348impl Default for MockHttpResponse {
349 fn default() -> Self {
350 Self {
351 status: StatusCode::OK,
352 headers: HeaderMap::new(),
353 body: Bytes::new(),
354 }
355 }
356}
357
358impl MockHttpResponse {
359 pub fn new(status: StatusCode) -> Self {
361 Self {
362 status,
363 ..Default::default()
364 }
365 }
366
367 pub fn ok() -> Self {
369 Self::new(StatusCode::OK)
370 }
371
372 pub fn created() -> Self {
374 Self::new(StatusCode::CREATED)
375 }
376
377 pub fn no_content() -> Self {
379 Self::new(StatusCode::NO_CONTENT)
380 }
381
382 pub fn bad_request() -> Self {
384 Self::new(StatusCode::BAD_REQUEST)
385 }
386
387 pub fn unauthorized() -> Self {
389 Self::new(StatusCode::UNAUTHORIZED)
390 }
391
392 pub fn forbidden() -> Self {
394 Self::new(StatusCode::FORBIDDEN)
395 }
396
397 pub fn not_found() -> Self {
399 Self::new(StatusCode::NOT_FOUND)
400 }
401
402 pub fn internal_error() -> Self {
404 Self::new(StatusCode::INTERNAL_SERVER_ERROR)
405 }
406
407 pub fn json<T: Serialize>(body: &T) -> Self {
409 let mut response = Self::ok();
410 if let Ok(bytes) = serde_json::to_vec(body) {
411 response.body = Bytes::from(bytes);
412 response.headers.insert(
413 http::header::CONTENT_TYPE,
414 HeaderValue::from_static("application/json"),
415 );
416 }
417 response
418 }
419
420 pub fn text(body: impl Into<String>) -> Self {
422 let mut response = Self::ok();
423 response.body = Bytes::from(body.into());
424 response.headers.insert(
425 http::header::CONTENT_TYPE,
426 HeaderValue::from_static("text/plain"),
427 );
428 response
429 }
430
431 pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
433 if let Ok(bytes) = serde_json::to_vec(body) {
434 self.body = Bytes::from(bytes);
435 self.headers.insert(
436 http::header::CONTENT_TYPE,
437 HeaderValue::from_static("application/json"),
438 );
439 }
440 self
441 }
442
443 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
445 self.body = body.into();
446 self
447 }
448
449 pub fn with_status(mut self, status: StatusCode) -> Self {
451 self.status = status;
452 self
453 }
454
455 pub fn with_header(mut self, name: &str, value: &str) -> Self {
457 if let (Ok(header_name), Ok(header_value)) = (
458 HeaderName::from_bytes(name.as_bytes()),
459 HeaderValue::from_str(value),
460 ) {
461 self.headers.insert(header_name, header_value);
462 }
463 self
464 }
465
466 pub fn with_cookie(mut self, name: &str, value: &str, options: Option<CookieOptions>) -> Self {
468 let opts = options.unwrap_or_default();
469 let mut cookie = format!("{}={}", name, value);
470
471 if let Some(max_age) = opts.max_age {
472 cookie.push_str(&format!("; Max-Age={}", max_age));
473 }
474 if let Some(ref path) = opts.path {
475 cookie.push_str(&format!("; Path={}", path));
476 }
477 if let Some(ref domain) = opts.domain {
478 cookie.push_str(&format!("; Domain={}", domain));
479 }
480 if opts.secure {
481 cookie.push_str("; Secure");
482 }
483 if opts.http_only {
484 cookie.push_str("; HttpOnly");
485 }
486 if let Some(ref same_site) = opts.same_site {
487 cookie.push_str(&format!("; SameSite={}", same_site));
488 }
489
490 if let Ok(value) = HeaderValue::from_str(&cookie) {
491 self.headers.append(http::header::SET_COOKIE, value);
492 }
493 self
494 }
495
496 pub fn is_success(&self) -> bool {
498 self.status.is_success()
499 }
500
501 pub fn is_client_error(&self) -> bool {
503 self.status.is_client_error()
504 }
505
506 pub fn is_server_error(&self) -> bool {
508 self.status.is_server_error()
509 }
510
511 pub fn get_header(&self, name: &str) -> Option<&str> {
513 self.headers.get(name).and_then(|v| v.to_str().ok())
514 }
515
516 pub fn json_body<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
518 serde_json::from_slice(&self.body)
519 }
520
521 pub fn text_body(&self) -> Result<String, std::string::FromUtf8Error> {
523 String::from_utf8(self.body.to_vec())
524 }
525}
526
527#[derive(Debug, Clone, Default)]
529pub struct CookieOptions {
530 pub max_age: Option<i64>,
532 pub path: Option<String>,
534 pub domain: Option<String>,
536 pub secure: bool,
538 pub http_only: bool,
540 pub same_site: Option<String>,
542}
543
544impl CookieOptions {
545 pub fn new() -> Self {
547 Self::default()
548 }
549
550 pub fn max_age(mut self, seconds: i64) -> Self {
552 self.max_age = Some(seconds);
553 self
554 }
555
556 pub fn path(mut self, path: impl Into<String>) -> Self {
558 self.path = Some(path.into());
559 self
560 }
561
562 pub fn domain(mut self, domain: impl Into<String>) -> Self {
564 self.domain = Some(domain.into());
565 self
566 }
567
568 pub fn secure(mut self) -> Self {
570 self.secure = true;
571 self
572 }
573
574 pub fn http_only(mut self) -> Self {
576 self.http_only = true;
577 self
578 }
579
580 pub fn same_site_strict(mut self) -> Self {
582 self.same_site = Some("Strict".to_string());
583 self
584 }
585
586 pub fn same_site_lax(mut self) -> Self {
588 self.same_site = Some("Lax".to_string());
589 self
590 }
591
592 pub fn same_site_none(mut self) -> Self {
594 self.same_site = Some("None".to_string());
595 self.secure = true; self
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_mock_request_get() {
606 let request = MockHttpRequest::get("/api/users");
607 assert_eq!(request.method, Method::GET);
608 assert_eq!(request.path(), "/api/users");
609 }
610
611 #[test]
612 fn test_mock_request_post_json() {
613 #[derive(Serialize)]
614 struct Input {
615 name: String,
616 }
617
618 let request = MockHttpRequest::post("/api/users").with_json(&Input {
619 name: "Alice".to_string(),
620 });
621
622 assert_eq!(request.method, Method::POST);
623 assert_eq!(request.get_header("content-type"), Some("application/json"));
624 assert!(request.text().unwrap().contains("Alice"));
625 }
626
627 #[test]
628 fn test_mock_request_with_headers() {
629 let request = MockHttpRequest::get("/api")
630 .with_header("X-Custom", "value")
631 .with_bearer_token("token123");
632
633 assert_eq!(request.get_header("x-custom"), Some("value"));
634 assert_eq!(request.get_header("authorization"), Some("Bearer token123"));
635 }
636
637 #[test]
638 fn test_mock_request_with_cookies() {
639 let request = MockHttpRequest::get("/api")
640 .with_cookie("session", "abc")
641 .with_cookie("user", "123");
642
643 assert_eq!(request.get_cookie("session"), Some("abc"));
644 assert_eq!(request.get_cookie("user"), Some("123"));
645 }
646
647 #[test]
648 fn test_mock_request_with_query() {
649 let request = MockHttpRequest::get("/api/search")
650 .with_query("q", "test")
651 .with_query("page", "1");
652
653 assert_eq!(request.get_query("q"), Some("test"));
654 assert_eq!(request.get_query("page"), Some("1"));
655 }
656
657 #[test]
658 fn test_mock_response_json() {
659 #[derive(Serialize, Deserialize, PartialEq, Debug)]
660 struct Output {
661 id: i32,
662 }
663
664 let response = MockHttpResponse::json(&Output { id: 1 });
665
666 assert!(response.is_success());
667 assert_eq!(
668 response.get_header("content-type"),
669 Some("application/json")
670 );
671
672 let body: Output = response.json_body().unwrap();
673 assert_eq!(body.id, 1);
674 }
675
676 #[test]
677 fn test_mock_response_with_cookie() {
678 let response = MockHttpResponse::ok().with_cookie(
679 "session",
680 "xyz",
681 Some(
682 CookieOptions::new()
683 .max_age(3600)
684 .path("/")
685 .secure()
686 .http_only(),
687 ),
688 );
689
690 let cookie = response.get_header("set-cookie").unwrap();
691 assert!(cookie.contains("session=xyz"));
692 assert!(cookie.contains("Max-Age=3600"));
693 assert!(cookie.contains("Path=/"));
694 assert!(cookie.contains("Secure"));
695 assert!(cookie.contains("HttpOnly"));
696 }
697}