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