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.
- checksums.yaml +4 -4
- data/README.md +90 -508
- data/ext/spikard_rb/Cargo.lock +3287 -0
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/ext/spikard_rb/extconf.rb +3 -3
- data/lib/spikard/app.rb +72 -49
- data/lib/spikard/background.rb +38 -7
- data/lib/spikard/testing.rb +42 -4
- data/lib/spikard/version.rb +1 -1
- data/sig/spikard.rbs +4 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +1 -1
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-core/src/http.rs +1 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
- data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
- data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
- data/vendor/crates/spikard-http/Cargo.toml +1 -1
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
- data/vendor/crates/spikard-http/src/testing.rs +171 -0
- data/vendor/crates/spikard-http/src/websocket.rs +79 -6
- data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
- data/vendor/crates/spikard-rb/Cargo.toml +1 -1
- data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
- data/vendor/crates/spikard-rb/src/handler.rs +12 -9
- data/vendor/crates/spikard-rb/src/lib.rs +137 -124
- data/vendor/crates/spikard-rb/src/request.rs +342 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
- data/vendor/crates/spikard-rb/src/server.rs +1 -8
- data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
- data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
- data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
- data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
- 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
|
+
}
|