spikard 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -508
  3. data/ext/spikard_rb/Cargo.lock +3287 -0
  4. data/ext/spikard_rb/Cargo.toml +1 -1
  5. data/ext/spikard_rb/extconf.rb +3 -3
  6. data/lib/spikard/app.rb +72 -49
  7. data/lib/spikard/background.rb +38 -7
  8. data/lib/spikard/testing.rb +42 -4
  9. data/lib/spikard/version.rb +1 -1
  10. data/sig/spikard.rbs +4 -0
  11. data/vendor/crates/spikard-bindings-shared/Cargo.toml +1 -1
  12. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
  13. data/vendor/crates/spikard-core/Cargo.toml +1 -1
  14. data/vendor/crates/spikard-core/src/http.rs +1 -0
  15. data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
  16. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
  17. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
  18. data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
  19. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
  20. data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
  21. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
  22. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
  23. data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
  24. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
  25. data/vendor/crates/spikard-http/Cargo.toml +1 -1
  26. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
  27. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
  28. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
  29. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
  30. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
  31. data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
  32. data/vendor/crates/spikard-http/src/testing.rs +171 -0
  33. data/vendor/crates/spikard-http/src/websocket.rs +79 -6
  34. data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
  35. data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
  36. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
  37. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
  38. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
  39. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
  40. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
  41. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
  42. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
  43. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
  44. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
  45. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
  46. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
  47. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
  48. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
  49. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
  50. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
  51. data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
  52. data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
  53. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
  54. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
  55. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
  56. data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
  57. data/vendor/crates/spikard-rb/Cargo.toml +1 -1
  58. data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
  59. data/vendor/crates/spikard-rb/src/handler.rs +12 -9
  60. data/vendor/crates/spikard-rb/src/lib.rs +137 -124
  61. data/vendor/crates/spikard-rb/src/request.rs +342 -0
  62. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
  63. data/vendor/crates/spikard-rb/src/server.rs +1 -8
  64. data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
  65. data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
  66. data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
  67. data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
  68. metadata +44 -1
@@ -0,0 +1,513 @@
1
+ #![allow(clippy::pedantic, clippy::nursery, clippy::all)]
2
+ //! Comprehensive integration tests for HTTP request extraction and parsing
3
+ //!
4
+ //! Tests the observable behavior of request extraction covering:
5
+ //! - Query parameter parsing (single/multiple values, URL encoding, special chars)
6
+ //! - Cookie header parsing (single/multiple cookies, encoding, empty values)
7
+ //! - Path parameter extraction (single/multiple params)
8
+ //! - Header extraction (case-insensitive, special chars)
9
+ //! - Body parsing and preservation
10
+ //!
11
+ //! Each test verifies that RequestData is correctly populated from HTTP requests
12
+ //! with various edge cases and special characters.
13
+
14
+ mod common;
15
+
16
+ use axum::http::Method;
17
+ use serde_json::json;
18
+ use std::collections::HashMap;
19
+ use std::sync::Arc;
20
+
21
+ use crate::common::test_builders::RequestBuilder;
22
+
23
+ /// Test single query parameter extraction
24
+ ///
25
+ /// Query: `?name=john`
26
+ /// Expected: raw_query_params contains {"name": ["john"]}
27
+ #[test]
28
+ fn test_query_params_single_value() {
29
+ let (_request, request_data) = RequestBuilder::new()
30
+ .path("/search")
31
+ .query_param("name", "john")
32
+ .build();
33
+
34
+ assert_eq!(
35
+ request_data.raw_query_params.get("name"),
36
+ Some(&vec!["john".to_string()])
37
+ );
38
+ assert_eq!(request_data.query_params["name"], "john");
39
+ }
40
+
41
+ /// Test multiple values for same query parameter
42
+ ///
43
+ /// Query: `?id=1&id=2&id=3`
44
+ /// Expected: raw_query_params contains {"id": ["1", "2", "3"]}
45
+ #[test]
46
+ fn test_query_params_multiple_values() {
47
+ let (_request, request_data) = RequestBuilder::new()
48
+ .path("/filter")
49
+ .query_param("id", "1")
50
+ .query_param("id", "2")
51
+ .query_param("id", "3")
52
+ .build();
53
+
54
+ let ids = request_data.raw_query_params.get("id").unwrap();
55
+ assert_eq!(ids.len(), 3);
56
+ assert_eq!(ids[0], "1");
57
+ assert_eq!(ids[1], "2");
58
+ assert_eq!(ids[2], "3");
59
+
60
+ assert!(request_data.query_params["id"].is_array());
61
+ }
62
+
63
+ /// Test URL-encoded query parameters with spaces
64
+ ///
65
+ /// Query: `?search=hello world` (RequestBuilder takes decoded values)
66
+ /// Expected: stored as-is
67
+ #[test]
68
+ fn test_query_params_url_encoded_space() {
69
+ let (_request, request_data) = RequestBuilder::new()
70
+ .path("/search")
71
+ .query_param("search", "hello world")
72
+ .build();
73
+
74
+ let search = request_data.raw_query_params.get("search").unwrap();
75
+ assert_eq!(search[0], "hello world");
76
+ }
77
+
78
+ /// Test plus sign handling in query parameters
79
+ ///
80
+ /// Query: `?name=john doe` with space
81
+ /// Expected: preserved as-is
82
+ #[test]
83
+ fn test_query_params_url_encoded_plus_as_space() {
84
+ let (_request, request_data) = RequestBuilder::new()
85
+ .path("/search")
86
+ .query_param("name", "john doe")
87
+ .build();
88
+
89
+ let name = request_data.raw_query_params.get("name").unwrap();
90
+ assert_eq!(name[0], "john doe");
91
+ }
92
+
93
+ /// Test special characters in query parameters
94
+ ///
95
+ /// Query: `?value=10+20=30` (special chars)
96
+ /// Expected: properly preserved
97
+ #[test]
98
+ fn test_query_params_special_characters() {
99
+ let (_request, request_data) = RequestBuilder::new()
100
+ .path("/calc")
101
+ .query_param("value", "10+20=30")
102
+ .build();
103
+
104
+ let value = request_data.raw_query_params.get("value").unwrap();
105
+ assert_eq!(value[0], "10+20=30");
106
+ }
107
+
108
+ /// Test email-like query parameters with @ symbol
109
+ ///
110
+ /// Query: `?email=test@example.com`
111
+ /// Expected: preserved with @ symbol
112
+ #[test]
113
+ fn test_query_params_email_encoding() {
114
+ let (_request, request_data) = RequestBuilder::new()
115
+ .path("/subscribe")
116
+ .query_param("email", "test@example.com")
117
+ .build();
118
+
119
+ let email = request_data.raw_query_params.get("email").unwrap();
120
+ assert_eq!(email[0], "test@example.com");
121
+ }
122
+
123
+ /// Test empty query parameter value
124
+ ///
125
+ /// Query: `?key=`
126
+ /// Expected: raw_query_params contains {"key": [""]}
127
+ #[test]
128
+ fn test_query_params_empty_value() {
129
+ let (_request, request_data) = RequestBuilder::new().path("/search").query_param("key", "").build();
130
+
131
+ let value = request_data.raw_query_params.get("key").unwrap();
132
+ assert_eq!(value[0], "");
133
+ }
134
+
135
+ /// Test query parameters with numeric values preserved as strings
136
+ ///
137
+ /// Query: `?page=1&limit=50`
138
+ /// Expected: values preserved as strings in raw_query_params
139
+ #[test]
140
+ fn test_query_params_numeric_values_as_strings() {
141
+ let (_request, request_data) = RequestBuilder::new()
142
+ .path("/posts")
143
+ .query_param("page", "1")
144
+ .query_param("limit", "50")
145
+ .build();
146
+
147
+ let page = request_data.raw_query_params.get("page").unwrap();
148
+ assert_eq!(page[0], "1");
149
+
150
+ let limit = request_data.raw_query_params.get("limit").unwrap();
151
+ assert_eq!(limit[0], "50");
152
+ }
153
+
154
+ /// Test mixed query parameters (different types and counts)
155
+ ///
156
+ /// Query: `?page=1&tags=rust&tags=web&active=true&search=hello world`
157
+ /// Expected: all parsed correctly with multiple values for tags
158
+ #[test]
159
+ fn test_query_params_mixed_types_and_counts() {
160
+ let (_request, request_data) = RequestBuilder::new()
161
+ .path("/api/posts")
162
+ .query_param("page", "1")
163
+ .query_param("tags", "rust")
164
+ .query_param("tags", "web")
165
+ .query_param("active", "true")
166
+ .query_param("search", "hello world")
167
+ .build();
168
+
169
+ assert_eq!(request_data.raw_query_params.get("page").unwrap()[0], "1");
170
+ assert_eq!(request_data.raw_query_params.get("active").unwrap()[0], "true");
171
+
172
+ let tags = request_data.raw_query_params.get("tags").unwrap();
173
+ assert_eq!(tags.len(), 2);
174
+ assert_eq!(tags[0], "rust");
175
+ assert_eq!(tags[1], "web");
176
+
177
+ assert_eq!(request_data.raw_query_params.get("search").unwrap()[0], "hello world");
178
+ }
179
+
180
+ /// Test single cookie extraction
181
+ ///
182
+ /// Cookie: `session=abc123`
183
+ /// Expected: cookies contains {"session": "abc123"}
184
+ #[test]
185
+ fn test_cookies_single_cookie() {
186
+ let (_request, request_data) = RequestBuilder::new().cookie("session", "abc123").build();
187
+
188
+ assert_eq!(request_data.cookies.get("session"), Some(&"abc123".to_string()));
189
+ }
190
+
191
+ /// Test multiple cookies in single header
192
+ ///
193
+ /// Cookie: `session=abc123; user_id=42; theme=dark`
194
+ /// Expected: all cookies extracted separately
195
+ #[test]
196
+ fn test_cookies_multiple_cookies() {
197
+ let (_request, request_data) = RequestBuilder::new()
198
+ .cookie("session", "abc123")
199
+ .cookie("user_id", "42")
200
+ .cookie("theme", "dark")
201
+ .build();
202
+
203
+ assert_eq!(request_data.cookies.get("session"), Some(&"abc123".to_string()));
204
+ assert_eq!(request_data.cookies.get("user_id"), Some(&"42".to_string()));
205
+ assert_eq!(request_data.cookies.get("theme"), Some(&"dark".to_string()));
206
+ assert_eq!(request_data.cookies.len(), 3);
207
+ }
208
+
209
+ /// Test cookie with empty value
210
+ ///
211
+ /// Cookie: `empty=`
212
+ /// Expected: cookies contains {"empty": ""}
213
+ #[test]
214
+ fn test_cookies_empty_value() {
215
+ let (_request, request_data) = RequestBuilder::new().cookie("empty", "").build();
216
+
217
+ assert_eq!(request_data.cookies.get("empty"), Some(&String::new()));
218
+ }
219
+
220
+ /// Test cookie with URL-encoded special characters
221
+ ///
222
+ /// Cookie value with encoded characters
223
+ /// Expected: decoded properly (depends on cookie library behavior)
224
+ #[test]
225
+ fn test_cookies_with_special_chars() {
226
+ let (_request, request_data) = RequestBuilder::new()
227
+ .cookie("data", "value_with-dash")
228
+ .cookie("token", "abc123def456")
229
+ .build();
230
+
231
+ assert_eq!(request_data.cookies.get("data"), Some(&"value_with-dash".to_string()));
232
+ assert_eq!(request_data.cookies.get("token"), Some(&"abc123def456".to_string()));
233
+ }
234
+
235
+ /// Test cookie with numeric-looking values
236
+ ///
237
+ /// Cookie: `user_id=12345; port=8080; version=2`
238
+ /// Expected: values preserved as strings
239
+ #[test]
240
+ fn test_cookies_numeric_values() {
241
+ let (_request, request_data) = RequestBuilder::new()
242
+ .cookie("user_id", "12345")
243
+ .cookie("port", "8080")
244
+ .cookie("version", "2")
245
+ .build();
246
+
247
+ assert_eq!(request_data.cookies.get("user_id"), Some(&"12345".to_string()));
248
+ assert_eq!(request_data.cookies.get("port"), Some(&"8080".to_string()));
249
+ assert_eq!(request_data.cookies.get("version"), Some(&"2".to_string()));
250
+ }
251
+
252
+ /// Test single path parameter extraction
253
+ ///
254
+ /// Route: `/users/:id` with path `/users/123`
255
+ /// Expected: path_params contains {"id": "123"}
256
+ #[test]
257
+ fn test_path_params_single() {
258
+ let mut path_params = HashMap::new();
259
+ path_params.insert("id".to_string(), "123".to_string());
260
+
261
+ let (_request, request_data) = RequestBuilder::new().path("/users/123").build();
262
+
263
+ assert_eq!(request_data.path, "/users/123");
264
+ }
265
+
266
+ /// Test multiple path parameters
267
+ ///
268
+ /// Route: `/posts/:id/comments/:comment_id`
269
+ /// Expected: both parameters extracted
270
+ #[test]
271
+ fn test_path_params_multiple() {
272
+ let mut path_params = HashMap::new();
273
+ path_params.insert("post_id".to_string(), "42".to_string());
274
+ path_params.insert("comment_id".to_string(), "789".to_string());
275
+
276
+ let (_request, request_data) = RequestBuilder::new().path("/posts/42/comments/789").build();
277
+
278
+ assert_eq!(request_data.path, "/posts/42/comments/789");
279
+ }
280
+
281
+ /// Test path parameters with special formatting
282
+ ///
283
+ /// Path: `/files/2025-12-10.log` or `/api/v1/resource`
284
+ /// Expected: parameters extracted with special chars preserved
285
+ #[test]
286
+ fn test_path_params_with_special_chars() {
287
+ let (_request, request_data) = RequestBuilder::new().path("/files/document-2025-12-10.log").build();
288
+
289
+ assert_eq!(request_data.path, "/files/document-2025-12-10.log");
290
+ }
291
+
292
+ /// Test single header extraction
293
+ ///
294
+ /// Header: `Content-Type: application/json`
295
+ /// Expected: headers contains {"content-type": "application/json"}
296
+ #[test]
297
+ fn test_headers_single() {
298
+ let (_request, request_data) = RequestBuilder::new().header("content-type", "application/json").build();
299
+
300
+ assert_eq!(
301
+ request_data.headers.get("content-type"),
302
+ Some(&"application/json".to_string())
303
+ );
304
+ }
305
+
306
+ /// Test multiple headers
307
+ ///
308
+ /// Headers: Content-Type, Authorization, X-Custom-Header
309
+ /// Expected: all extracted
310
+ #[test]
311
+ fn test_headers_multiple() {
312
+ let (_request, request_data) = RequestBuilder::new()
313
+ .header("content-type", "application/json")
314
+ .header("authorization", "Bearer token123")
315
+ .header("x-custom-header", "custom-value")
316
+ .build();
317
+
318
+ assert_eq!(
319
+ request_data.headers.get("content-type"),
320
+ Some(&"application/json".to_string())
321
+ );
322
+ assert_eq!(
323
+ request_data.headers.get("authorization"),
324
+ Some(&"Bearer token123".to_string())
325
+ );
326
+ assert_eq!(
327
+ request_data.headers.get("x-custom-header"),
328
+ Some(&"custom-value".to_string())
329
+ );
330
+ }
331
+
332
+ /// Test header names are preserved in RequestBuilder
333
+ ///
334
+ /// Headers: `content-type`, `x-request-id`
335
+ /// Expected: stored with provided casing
336
+ #[test]
337
+ fn test_headers_case_insensitive() {
338
+ let (_request, request_data) = RequestBuilder::new()
339
+ .header("content-type", "text/html")
340
+ .header("x-request-id", "req-123")
341
+ .build();
342
+
343
+ assert_eq!(request_data.headers.get("content-type"), Some(&"text/html".to_string()));
344
+ assert_eq!(request_data.headers.get("x-request-id"), Some(&"req-123".to_string()));
345
+ }
346
+
347
+ /// Test headers with hyphens are preserved
348
+ ///
349
+ /// Headers: X-Custom-Header, X-Request-ID
350
+ /// Expected: hyphens preserved in header names
351
+ #[test]
352
+ fn test_headers_with_hyphens() {
353
+ let (_request, request_data) = RequestBuilder::new()
354
+ .header("x-custom-header", "value1")
355
+ .header("x-request-id", "req-456")
356
+ .header("x-api-key", "secret123")
357
+ .build();
358
+
359
+ assert_eq!(request_data.headers.get("x-custom-header"), Some(&"value1".to_string()));
360
+ assert_eq!(request_data.headers.get("x-request-id"), Some(&"req-456".to_string()));
361
+ assert_eq!(request_data.headers.get("x-api-key"), Some(&"secret123".to_string()));
362
+ }
363
+
364
+ /// Test authorization header with Bearer token
365
+ ///
366
+ /// Header: `Authorization: Bearer eyJhbGc...`
367
+ /// Expected: full token value preserved
368
+ #[test]
369
+ fn test_headers_bearer_token() {
370
+ let token =
371
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
372
+
373
+ let (_request, request_data) = RequestBuilder::new()
374
+ .header("authorization", &format!("Bearer {}", token))
375
+ .build();
376
+
377
+ let auth_header = request_data.headers.get("authorization").unwrap();
378
+ assert!(auth_header.starts_with("Bearer "));
379
+ assert!(auth_header.contains(token));
380
+ }
381
+
382
+ /// Test JSON body is stored for deferred parsing
383
+ ///
384
+ /// Content-Type: application/json, Body: `{"name": "Alice"}`
385
+ /// Expected: raw_body contains JSON bytes
386
+ #[test]
387
+ fn test_body_json_stored() {
388
+ let body_json = json!({"name": "Alice", "age": 30});
389
+
390
+ let (_request, request_data) = RequestBuilder::new()
391
+ .method(Method::POST)
392
+ .path("/users")
393
+ .json_body(body_json.clone())
394
+ .build();
395
+
396
+ assert_eq!(request_data.body, body_json);
397
+ }
398
+
399
+ /// Test empty body handling
400
+ ///
401
+ /// Body: empty (no content)
402
+ /// Expected: body is null, raw_body is None
403
+ #[test]
404
+ fn test_body_empty() {
405
+ let (_request, request_data) = RequestBuilder::new().method(Method::GET).path("/status").build();
406
+
407
+ assert_eq!(request_data.body, json!(null));
408
+ }
409
+
410
+ /// Test large JSON body
411
+ ///
412
+ /// Body: large JSON with many nested objects
413
+ /// Expected: entire body preserved
414
+ #[test]
415
+ fn test_body_large_json() {
416
+ let large_body = json!({
417
+ "data": (0..50).map(|i| json!({
418
+ "id": i,
419
+ "name": format!("item-{}", i),
420
+ "values": vec![i * 10, i * 20, i * 30]
421
+ })).collect::<Vec<_>>()
422
+ });
423
+
424
+ let (_request, request_data) = RequestBuilder::new()
425
+ .method(Method::POST)
426
+ .path("/batch")
427
+ .json_body(large_body.clone())
428
+ .build();
429
+
430
+ assert_eq!(request_data.body, large_body);
431
+ }
432
+
433
+ /// Test complete request with path, query, headers, cookies, and body
434
+ ///
435
+ /// Combines all extraction scenarios
436
+ #[test]
437
+ fn test_complete_request_with_all_components() {
438
+ let body = json!({"action": "create", "resource": "user"});
439
+
440
+ let (_request, request_data) = RequestBuilder::new()
441
+ .method(Method::POST)
442
+ .path("/api/v1/users")
443
+ .query_param("limit", "10")
444
+ .query_param("filter", "active")
445
+ .header("authorization", "Bearer token123")
446
+ .header("content-type", "application/json")
447
+ .cookie("session", "xyz789")
448
+ .cookie("preferences", "dark_mode")
449
+ .json_body(body.clone())
450
+ .build();
451
+
452
+ assert_eq!(request_data.method, "POST");
453
+ assert_eq!(request_data.path, "/api/v1/users");
454
+
455
+ assert_eq!(request_data.raw_query_params.get("limit").unwrap()[0], "10");
456
+ assert_eq!(request_data.raw_query_params.get("filter").unwrap()[0], "active");
457
+
458
+ assert!(request_data.headers.get("authorization").is_some());
459
+ assert_eq!(
460
+ request_data.headers.get("content-type"),
461
+ Some(&"application/json".to_string())
462
+ );
463
+
464
+ assert_eq!(request_data.cookies.get("session"), Some(&"xyz789".to_string()));
465
+ assert_eq!(request_data.cookies.get("preferences"), Some(&"dark_mode".to_string()));
466
+
467
+ assert_eq!(request_data.body, body);
468
+ }
469
+
470
+ /// Test Arc wrapping for efficient cloning
471
+ ///
472
+ /// RequestData uses Arc for large fields for cheap cloning
473
+ /// Expected: Arc pointers are shared on clone
474
+ #[test]
475
+ fn test_request_data_arc_cloning() {
476
+ let (_request, request_data) = RequestBuilder::new()
477
+ .path("/api")
478
+ .query_param("filter", "test")
479
+ .query_param("sort", "name")
480
+ .header("x-custom", "value")
481
+ .cookie("session", "abc123")
482
+ .build();
483
+
484
+ let cloned = request_data.clone();
485
+
486
+ assert!(Arc::ptr_eq(&request_data.headers, &cloned.headers));
487
+ assert!(Arc::ptr_eq(&request_data.cookies, &cloned.cookies));
488
+ assert!(Arc::ptr_eq(&request_data.raw_query_params, &cloned.raw_query_params));
489
+ }
490
+
491
+ /// Test different HTTP methods preserve correctly
492
+ ///
493
+ /// Methods: GET, POST, PUT, DELETE, PATCH
494
+ /// Expected: method stored correctly in RequestData
495
+ #[test]
496
+ fn test_different_http_methods() {
497
+ for (method, expected) in &[
498
+ (Method::GET, "GET"),
499
+ (Method::POST, "POST"),
500
+ (Method::PUT, "PUT"),
501
+ (Method::DELETE, "DELETE"),
502
+ (Method::PATCH, "PATCH"),
503
+ (Method::HEAD, "HEAD"),
504
+ (Method::OPTIONS, "OPTIONS"),
505
+ ] {
506
+ let (_request, request_data) = RequestBuilder::new()
507
+ .method(method.clone())
508
+ .path("/api/resource")
509
+ .build();
510
+
511
+ assert_eq!(&request_data.method, expected);
512
+ }
513
+ }