1use serde_json::Value;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct Response {
11 pub content: Option<Value>,
13 pub status_code: u16,
15 pub headers: HashMap<String, String>,
17}
18
19impl Response {
20 pub fn new(content: Option<Value>) -> Self {
22 Self {
23 content,
24 status_code: 200,
25 headers: HashMap::new(),
26 }
27 }
28
29 pub fn with_status(content: Option<Value>, status_code: u16) -> Self {
31 Self {
32 content,
33 status_code,
34 headers: HashMap::new(),
35 }
36 }
37
38 pub fn set_header(&mut self, key: String, value: String) {
40 self.headers.insert(key, value);
41 }
42
43 #[allow(clippy::too_many_arguments)]
45 pub fn set_cookie(
46 &mut self,
47 key: String,
48 value: String,
49 max_age: Option<i64>,
50 domain: Option<String>,
51 path: Option<String>,
52 secure: bool,
53 http_only: bool,
54 same_site: Option<String>,
55 ) {
56 let mut cookie_value = format!("{}={}", key, value);
57
58 if let Some(age) = max_age {
59 cookie_value.push_str(&format!("; Max-Age={}", age));
60 }
61 if let Some(d) = domain {
62 cookie_value.push_str(&format!("; Domain={}", d));
63 }
64 if let Some(p) = path {
65 cookie_value.push_str(&format!("; Path={}", p));
66 }
67 if secure {
68 cookie_value.push_str("; Secure");
69 }
70 if http_only {
71 cookie_value.push_str("; HttpOnly");
72 }
73 if let Some(ss) = same_site {
74 cookie_value.push_str(&format!("; SameSite={}", ss));
75 }
76
77 self.headers.insert("set-cookie".to_string(), cookie_value);
78 }
79}
80
81impl Default for Response {
82 fn default() -> Self {
83 Self::new(None)
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use serde_json::json;
91
92 #[test]
93 fn response_new_creates_default_status() {
94 let response = Response::new(None);
95 assert_eq!(response.status_code, 200);
96 assert!(response.headers.is_empty());
97 assert!(response.content.is_none());
98 }
99
100 #[test]
101 fn response_new_with_content() {
102 let content = json!({"key": "value"});
103 let response = Response::new(Some(content.clone()));
104 assert_eq!(response.status_code, 200);
105 assert_eq!(response.content, Some(content));
106 }
107
108 #[test]
109 fn response_with_status() {
110 let response = Response::with_status(None, 404);
111 assert_eq!(response.status_code, 404);
112 assert!(response.headers.is_empty());
113 }
114
115 #[test]
116 fn response_with_status_and_content() {
117 let content = json!({"error": "not found"});
118 let response = Response::with_status(Some(content.clone()), 404);
119 assert_eq!(response.status_code, 404);
120 assert_eq!(response.content, Some(content));
121 }
122
123 #[test]
124 fn response_set_header() {
125 let mut response = Response::new(None);
126 response.set_header("X-Custom".to_string(), "custom-value".to_string());
127 assert_eq!(response.headers.get("X-Custom"), Some(&"custom-value".to_string()));
128 }
129
130 #[test]
131 fn response_set_multiple_headers() {
132 let mut response = Response::new(None);
133 response.set_header("Content-Type".to_string(), "application/json".to_string());
134 response.set_header("X-Custom".to_string(), "custom-value".to_string());
135 assert_eq!(response.headers.len(), 2);
136 assert_eq!(
137 response.headers.get("Content-Type"),
138 Some(&"application/json".to_string())
139 );
140 assert_eq!(response.headers.get("X-Custom"), Some(&"custom-value".to_string()));
141 }
142
143 #[test]
144 fn response_set_header_overwrites() {
145 let mut response = Response::new(None);
146 response.set_header("X-Custom".to_string(), "value1".to_string());
147 response.set_header("X-Custom".to_string(), "value2".to_string());
148 assert_eq!(response.headers.get("X-Custom"), Some(&"value2".to_string()));
149 }
150
151 #[test]
152 fn response_set_cookie_minimal() {
153 let mut response = Response::new(None);
154 response.set_cookie(
155 "session_id".to_string(),
156 "abc123".to_string(),
157 None,
158 None,
159 None,
160 false,
161 false,
162 None,
163 );
164 let cookie = response.headers.get("set-cookie").unwrap();
165 assert_eq!(cookie, "session_id=abc123");
166 }
167
168 #[test]
169 fn response_set_cookie_with_max_age() {
170 let mut response = Response::new(None);
171 response.set_cookie(
172 "session".to_string(),
173 "token".to_string(),
174 Some(3600),
175 None,
176 None,
177 false,
178 false,
179 None,
180 );
181 let cookie = response.headers.get("set-cookie").unwrap();
182 assert!(cookie.contains("session=token"));
183 assert!(cookie.contains("Max-Age=3600"));
184 }
185
186 #[test]
187 fn response_set_cookie_with_domain() {
188 let mut response = Response::new(None);
189 response.set_cookie(
190 "session".to_string(),
191 "token".to_string(),
192 None,
193 Some("example.com".to_string()),
194 None,
195 false,
196 false,
197 None,
198 );
199 let cookie = response.headers.get("set-cookie").unwrap();
200 assert!(cookie.contains("Domain=example.com"));
201 }
202
203 #[test]
204 fn response_set_cookie_with_path() {
205 let mut response = Response::new(None);
206 response.set_cookie(
207 "session".to_string(),
208 "token".to_string(),
209 None,
210 None,
211 Some("/app".to_string()),
212 false,
213 false,
214 None,
215 );
216 let cookie = response.headers.get("set-cookie").unwrap();
217 assert!(cookie.contains("Path=/app"));
218 }
219
220 #[test]
221 fn response_set_cookie_secure() {
222 let mut response = Response::new(None);
223 response.set_cookie(
224 "session".to_string(),
225 "token".to_string(),
226 None,
227 None,
228 None,
229 true,
230 false,
231 None,
232 );
233 let cookie = response.headers.get("set-cookie").unwrap();
234 assert!(cookie.contains("Secure"));
235 }
236
237 #[test]
238 fn response_set_cookie_http_only() {
239 let mut response = Response::new(None);
240 response.set_cookie(
241 "session".to_string(),
242 "token".to_string(),
243 None,
244 None,
245 None,
246 false,
247 true,
248 None,
249 );
250 let cookie = response.headers.get("set-cookie").unwrap();
251 assert!(cookie.contains("HttpOnly"));
252 }
253
254 #[test]
255 fn response_set_cookie_same_site() {
256 let mut response = Response::new(None);
257 response.set_cookie(
258 "session".to_string(),
259 "token".to_string(),
260 None,
261 None,
262 None,
263 false,
264 false,
265 Some("Strict".to_string()),
266 );
267 let cookie = response.headers.get("set-cookie").unwrap();
268 assert!(cookie.contains("SameSite=Strict"));
269 }
270
271 #[test]
272 fn response_set_cookie_all_attributes() {
273 let mut response = Response::new(None);
274 response.set_cookie(
275 "session".to_string(),
276 "token123".to_string(),
277 Some(3600),
278 Some("example.com".to_string()),
279 Some("/app".to_string()),
280 true,
281 true,
282 Some("Lax".to_string()),
283 );
284 let cookie = response.headers.get("set-cookie").unwrap();
285 assert!(cookie.contains("session=token123"));
286 assert!(cookie.contains("Max-Age=3600"));
287 assert!(cookie.contains("Domain=example.com"));
288 assert!(cookie.contains("Path=/app"));
289 assert!(cookie.contains("Secure"));
290 assert!(cookie.contains("HttpOnly"));
291 assert!(cookie.contains("SameSite=Lax"));
292 }
293
294 #[test]
295 fn response_set_cookie_overwrites_previous() {
296 let mut response = Response::new(None);
297 response.set_cookie(
298 "session".to_string(),
299 "old_token".to_string(),
300 None,
301 None,
302 None,
303 false,
304 false,
305 None,
306 );
307 response.set_cookie(
308 "session".to_string(),
309 "new_token".to_string(),
310 None,
311 None,
312 None,
313 false,
314 false,
315 None,
316 );
317 let cookie = response.headers.get("set-cookie").unwrap();
318 assert!(cookie.contains("new_token"));
319 assert!(!cookie.contains("old_token"));
320 }
321
322 #[test]
323 fn response_default() {
324 let response = Response::default();
325 assert_eq!(response.status_code, 200);
326 assert!(response.headers.is_empty());
327 assert!(response.content.is_none());
328 }
329
330 #[test]
331 fn response_cookie_with_special_chars_in_value() {
332 let mut response = Response::new(None);
333 response.set_cookie(
334 "name".to_string(),
335 "value%3D123".to_string(),
336 None,
337 None,
338 None,
339 false,
340 false,
341 None,
342 );
343 let cookie = response.headers.get("set-cookie").unwrap();
344 assert_eq!(cookie, "name=value%3D123");
345 }
346
347 #[test]
348 fn response_same_site_variants() {
349 for same_site in &["Strict", "Lax", "None"] {
350 let mut response = Response::new(None);
351 response.set_cookie(
352 "test".to_string(),
353 "value".to_string(),
354 None,
355 None,
356 None,
357 false,
358 false,
359 Some(same_site.to_string()),
360 );
361 let cookie = response.headers.get("set-cookie").unwrap();
362 assert!(cookie.contains(&format!("SameSite={}", same_site)));
363 }
364 }
365
366 #[test]
367 fn response_zero_max_age() {
368 let mut response = Response::new(None);
369 response.set_cookie(
370 "session".to_string(),
371 "token".to_string(),
372 Some(0),
373 None,
374 None,
375 false,
376 false,
377 None,
378 );
379 let cookie = response.headers.get("set-cookie").unwrap();
380 assert!(cookie.contains("Max-Age=0"));
381 }
382
383 #[test]
384 fn response_negative_max_age() {
385 let mut response = Response::new(None);
386 response.set_cookie(
387 "session".to_string(),
388 "token".to_string(),
389 Some(-1),
390 None,
391 None,
392 false,
393 false,
394 None,
395 );
396 let cookie = response.headers.get("set-cookie").unwrap();
397 assert!(cookie.contains("Max-Age=-1"));
398 }
399
400 #[test]
401 fn response_various_status_codes() {
402 let status_codes = vec![
403 (200, "OK"),
404 (201, "Created"),
405 (204, "No Content"),
406 (301, "Moved Permanently"),
407 (302, "Found"),
408 (304, "Not Modified"),
409 (400, "Bad Request"),
410 (401, "Unauthorized"),
411 (403, "Forbidden"),
412 (404, "Not Found"),
413 (500, "Internal Server Error"),
414 (502, "Bad Gateway"),
415 (503, "Service Unavailable"),
416 ];
417
418 for (code, _name) in status_codes {
419 let response = Response::with_status(None, code);
420 assert_eq!(response.status_code, code);
421 assert!(response.headers.is_empty());
422 }
423 }
424
425 #[test]
426 fn response_with_large_json_body() {
427 let mut items = vec![];
428 for i in 0..1000 {
429 items.push(json!({"id": i, "name": format!("item_{}", i)}));
430 }
431 let large_array = serde_json::Value::Array(items);
432 let response = Response::new(Some(large_array.clone()));
433 assert_eq!(response.status_code, 200);
434 assert_eq!(response.content, Some(large_array));
435 }
436
437 #[test]
438 fn response_with_deeply_nested_json() {
439 let nested = json!({
440 "level1": {
441 "level2": {
442 "level3": {
443 "level4": {
444 "level5": {
445 "data": "deeply nested value"
446 }
447 }
448 }
449 }
450 }
451 });
452 let response = Response::new(Some(nested.clone()));
453 assert_eq!(response.content, Some(nested));
454 }
455
456 #[test]
457 fn response_with_empty_json_object() {
458 let empty_obj = json!({});
459 let response = Response::new(Some(empty_obj.clone()));
460 assert_eq!(response.content, Some(empty_obj));
461 assert_ne!(response.content, None);
462 }
463
464 #[test]
465 fn response_with_empty_json_array() {
466 let empty_array = json!([]);
467 let response = Response::new(Some(empty_array.clone()));
468 assert_eq!(response.content, Some(empty_array));
469 assert_ne!(response.content, None);
470 }
471
472 #[test]
473 fn response_with_null_vs_none() {
474 let null_value = json!(null);
475 let response_with_null = Response::new(Some(null_value.clone()));
476 let response_with_none = Response::new(None);
477
478 assert_eq!(response_with_null.content, Some(null_value));
479 assert_eq!(response_with_none.content, None);
480 assert_ne!(response_with_null.content, response_with_none.content);
481 }
482
483 #[test]
484 fn response_with_json_primitives() {
485 let test_cases = vec![
486 json!(true),
487 json!(false),
488 json!(0),
489 json!(-1),
490 json!(42),
491 json!(3.14),
492 json!("string"),
493 json!(""),
494 ];
495
496 for test_value in test_cases {
497 let response = Response::new(Some(test_value.clone()));
498 assert_eq!(response.content, Some(test_value));
499 }
500 }
501
502 #[test]
503 fn response_header_case_sensitivity() {
504 let mut response = Response::new(None);
505 response.set_header("Content-Type".to_string(), "application/json".to_string());
506 response.set_header("content-type".to_string(), "text/plain".to_string());
507
508 assert_eq!(response.headers.len(), 2);
509 assert_eq!(
510 response.headers.get("Content-Type"),
511 Some(&"application/json".to_string())
512 );
513 assert_eq!(response.headers.get("content-type"), Some(&"text/plain".to_string()));
514 }
515
516 #[test]
517 fn response_header_with_empty_value() {
518 let mut response = Response::new(None);
519 response.set_header("X-Empty".to_string(), "".to_string());
520 assert_eq!(response.headers.get("X-Empty"), Some(&"".to_string()));
521 }
522
523 #[test]
524 fn response_header_with_special_chars() {
525 let mut response = Response::new(None);
526 response.set_header("X-Special".to_string(), "value; charset=utf-8".to_string());
527 assert_eq!(
528 response.headers.get("X-Special"),
529 Some(&"value; charset=utf-8".to_string())
530 );
531 }
532
533 #[test]
534 fn response_multiple_different_cookies() {
535 let mut response = Response::new(None);
536 response.set_cookie(
537 "session".to_string(),
538 "abc123".to_string(),
539 None,
540 None,
541 None,
542 false,
543 false,
544 None,
545 );
546 let cookie_count = response.headers.iter().filter(|(k, _)| *k == "set-cookie").count();
547 assert_eq!(cookie_count, 1);
548 }
549
550 #[test]
551 fn response_cookie_empty_value() {
552 let mut response = Response::new(None);
553 response.set_cookie(
554 "empty".to_string(),
555 "".to_string(),
556 None,
557 None,
558 None,
559 false,
560 false,
561 None,
562 );
563 let cookie = response.headers.get("set-cookie").unwrap();
564 assert_eq!(cookie, "empty=");
565 }
566
567 #[test]
568 fn response_cookie_with_equals_in_value() {
569 let mut response = Response::new(None);
570 response.set_cookie(
571 "data".to_string(),
572 "key=value&other=123".to_string(),
573 None,
574 None,
575 None,
576 false,
577 false,
578 None,
579 );
580 let cookie = response.headers.get("set-cookie").unwrap();
581 assert!(cookie.contains("key=value&other=123"));
582 }
583
584 #[test]
585 fn response_cookie_attribute_order() {
586 let mut response = Response::new(None);
587 response.set_cookie(
588 "test".to_string(),
589 "value".to_string(),
590 Some(3600),
591 Some("example.com".to_string()),
592 Some("/".to_string()),
593 true,
594 true,
595 Some("Strict".to_string()),
596 );
597 let cookie = response.headers.get("set-cookie").unwrap();
598
599 let parts: Vec<&str> = cookie.split("; ").collect();
600 assert_eq!(parts.len(), 7);
601 assert!(parts[0].starts_with("test="));
602 assert!(parts[1].starts_with("Max-Age="));
603 assert!(parts[2].starts_with("Domain="));
604 assert!(parts[3].starts_with("Path="));
605 assert_eq!(parts[4], "Secure");
606 assert_eq!(parts[5], "HttpOnly");
607 assert!(parts[6].starts_with("SameSite="));
608 }
609
610 #[test]
611 fn response_cookie_with_very_long_value() {
612 let mut response = Response::new(None);
613 let long_value = "x".repeat(4096);
614 response.set_cookie(
615 "long".to_string(),
616 long_value.clone(),
617 None,
618 None,
619 None,
620 false,
621 false,
622 None,
623 );
624 let cookie = response.headers.get("set-cookie").unwrap();
625 assert!(cookie.contains(&format!("long={}", long_value)));
626 }
627
628 #[test]
629 fn response_cookie_max_age_large_value() {
630 let mut response = Response::new(None);
631 let max_age_value = 86400 * 365;
632 response.set_cookie(
633 "session".to_string(),
634 "token".to_string(),
635 Some(max_age_value),
636 None,
637 None,
638 false,
639 false,
640 None,
641 );
642 let cookie = response.headers.get("set-cookie").unwrap();
643 assert!(cookie.contains(&format!("Max-Age={}", max_age_value)));
644 }
645
646 #[test]
647 fn response_status_code_informational() {
648 let response = Response::with_status(None, 100);
649 assert_eq!(response.status_code, 100);
650 }
651
652 #[test]
653 fn response_status_code_redirect_with_location() {
654 let mut response = Response::with_status(None, 301);
655 response.set_header("Location".to_string(), "https://example.com/new".to_string());
656 assert_eq!(response.status_code, 301);
657 assert_eq!(
658 response.headers.get("Location"),
659 Some(&"https://example.com/new".to_string())
660 );
661 }
662
663 #[test]
664 fn response_with_error_status_and_content() {
665 let error_content = json!({
666 "error": "Unauthorized",
667 "code": 401,
668 "message": "Invalid credentials"
669 });
670 let response = Response::with_status(Some(error_content.clone()), 401);
671 assert_eq!(response.status_code, 401);
672 assert_eq!(response.content, Some(error_content));
673 }
674
675 #[test]
676 fn response_clone_preserves_state() {
677 let mut original = Response::with_status(Some(json!({"key": "value"})), 202);
678 original.set_header("X-Custom".to_string(), "header-value".to_string());
679 original.set_cookie(
680 "session".to_string(),
681 "token".to_string(),
682 Some(3600),
683 None,
684 None,
685 true,
686 false,
687 None,
688 );
689
690 let cloned = original.clone();
691
692 assert_eq!(cloned.status_code, 202);
693 assert_eq!(cloned.content, original.content);
694 assert_eq!(cloned.headers, original.headers);
695 }
696
697 #[test]
698 fn response_with_numeric_status_boundaries() {
699 let boundary_codes = vec![1, 99, 100, 199, 200, 299, 300, 399, 400, 499, 500, 599, 600, 999, 65535];
700 for code in boundary_codes {
701 let response = Response::with_status(None, code);
702 assert_eq!(response.status_code, code);
703 }
704 }
705
706 #[test]
707 fn response_header_unicode_value() {
708 let mut response = Response::new(None);
709 response.set_header("X-Unicode".to_string(), "こんにちは".to_string());
710 assert_eq!(response.headers.get("X-Unicode"), Some(&"こんにちは".to_string()));
711 }
712
713 #[test]
714 fn response_debug_trait() {
715 let response = Response::with_status(Some(json!({"test": "data"})), 200);
716 let debug_str = format!("{:?}", response);
717 assert!(debug_str.contains("Response"));
718 assert!(debug_str.contains("200"));
719 }
720}