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