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,633 @@
|
|
|
1
|
+
//! Test builder utilities for fluent test construction
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides builder APIs for constructing mock handlers and test requests,
|
|
4
|
+
//! eliminating boilerplate and improving test readability. All builders follow a
|
|
5
|
+
//! fluent API pattern enabling method chaining.
|
|
6
|
+
//!
|
|
7
|
+
//! # Examples
|
|
8
|
+
//!
|
|
9
|
+
//! ```ignore
|
|
10
|
+
//! // Build a mock handler
|
|
11
|
+
//! let handler = HandlerBuilder::new()
|
|
12
|
+
//! .status(200)
|
|
13
|
+
//! .json_body(json!({"message": "ok"}))
|
|
14
|
+
//! .delay(Duration::from_millis(50))
|
|
15
|
+
//! .build();
|
|
16
|
+
//!
|
|
17
|
+
//! // Build a test request
|
|
18
|
+
//! let (request, request_data) = RequestBuilder::new()
|
|
19
|
+
//! .method(Method::POST)
|
|
20
|
+
//! .path("/api/users")
|
|
21
|
+
//! .json_body(json!({"name": "test"}))
|
|
22
|
+
//! .build();
|
|
23
|
+
//! ```
|
|
24
|
+
|
|
25
|
+
use axum::body::Body;
|
|
26
|
+
use axum::http::{Method, Request, Response, StatusCode};
|
|
27
|
+
use serde_json::{Value, json};
|
|
28
|
+
use spikard_http::{Handler, HandlerResult, RequestData};
|
|
29
|
+
use std::collections::HashMap;
|
|
30
|
+
use std::future::Future;
|
|
31
|
+
use std::pin::Pin;
|
|
32
|
+
use std::sync::Arc;
|
|
33
|
+
use std::time::Duration;
|
|
34
|
+
use tokio::time::sleep;
|
|
35
|
+
|
|
36
|
+
/// Fluent builder for creating mock handlers with customizable behavior
|
|
37
|
+
///
|
|
38
|
+
/// Provides a fluent API for configuring handler responses without needing to
|
|
39
|
+
/// implement the Handler trait manually. Useful for testing middleware, routing,
|
|
40
|
+
/// and error handling without language bindings.
|
|
41
|
+
///
|
|
42
|
+
/// # Example
|
|
43
|
+
///
|
|
44
|
+
/// ```ignore
|
|
45
|
+
/// let handler = HandlerBuilder::new()
|
|
46
|
+
/// .status(StatusCode::CREATED)
|
|
47
|
+
/// .json_body(json!({"id": 1, "created": true}))
|
|
48
|
+
/// .build();
|
|
49
|
+
///
|
|
50
|
+
/// let response = handler.call(request, request_data).await?;
|
|
51
|
+
/// assert_eq!(response.status(), StatusCode::CREATED);
|
|
52
|
+
/// ```
|
|
53
|
+
pub struct HandlerBuilder {
|
|
54
|
+
status: StatusCode,
|
|
55
|
+
body: Value,
|
|
56
|
+
delay: Option<Duration>,
|
|
57
|
+
should_panic: bool,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl HandlerBuilder {
|
|
61
|
+
/// Create a new handler builder with default 200 OK status
|
|
62
|
+
pub fn new() -> Self {
|
|
63
|
+
Self {
|
|
64
|
+
status: StatusCode::OK,
|
|
65
|
+
body: json!({}),
|
|
66
|
+
delay: None,
|
|
67
|
+
should_panic: false,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Set the HTTP status code for the response
|
|
72
|
+
///
|
|
73
|
+
/// Default: 200 OK
|
|
74
|
+
pub fn status(mut self, code: u16) -> Self {
|
|
75
|
+
self.status = StatusCode::from_u16(code).unwrap_or(StatusCode::OK);
|
|
76
|
+
self
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Set the JSON body to return in the response
|
|
80
|
+
///
|
|
81
|
+
/// Default: empty object `{}`
|
|
82
|
+
pub fn json_body(mut self, body: Value) -> Self {
|
|
83
|
+
self.body = body;
|
|
84
|
+
self
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Add a delay to the handler response for testing timeouts
|
|
88
|
+
///
|
|
89
|
+
/// Useful for simulating slow handlers and testing timeout middleware.
|
|
90
|
+
pub fn delay(mut self, duration: Duration) -> Self {
|
|
91
|
+
self.delay = Some(duration);
|
|
92
|
+
self
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Configure the handler to panic when called
|
|
96
|
+
///
|
|
97
|
+
/// Useful for testing panic recovery and error handling middleware.
|
|
98
|
+
pub fn panics(mut self) -> Self {
|
|
99
|
+
self.should_panic = true;
|
|
100
|
+
self
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Build the configured handler into an Arc<dyn Handler>
|
|
104
|
+
///
|
|
105
|
+
/// Returns a handler ready for use in tests.
|
|
106
|
+
pub fn build(self) -> Arc<dyn Handler> {
|
|
107
|
+
Arc::new(ConfiguredHandler {
|
|
108
|
+
status: self.status,
|
|
109
|
+
body: self.body,
|
|
110
|
+
delay: self.delay,
|
|
111
|
+
should_panic: self.should_panic,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl Default for HandlerBuilder {
|
|
117
|
+
fn default() -> Self {
|
|
118
|
+
Self::new()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Internal handler implementation constructed by HandlerBuilder
|
|
123
|
+
struct ConfiguredHandler {
|
|
124
|
+
status: StatusCode,
|
|
125
|
+
body: Value,
|
|
126
|
+
delay: Option<Duration>,
|
|
127
|
+
should_panic: bool,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
impl Handler for ConfiguredHandler {
|
|
131
|
+
fn call(
|
|
132
|
+
&self,
|
|
133
|
+
_request: Request<Body>,
|
|
134
|
+
_request_data: RequestData,
|
|
135
|
+
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
|
|
136
|
+
let status = self.status;
|
|
137
|
+
let body = self.body.clone();
|
|
138
|
+
let delay = self.delay;
|
|
139
|
+
let should_panic = self.should_panic;
|
|
140
|
+
|
|
141
|
+
Box::pin(async move {
|
|
142
|
+
if should_panic {
|
|
143
|
+
panic!("Handler configured to panic");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if let Some(duration) = delay {
|
|
147
|
+
sleep(duration).await;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let response = Response::builder()
|
|
151
|
+
.status(status)
|
|
152
|
+
.header("content-type", "application/json")
|
|
153
|
+
.body(Body::from(body.to_string()))
|
|
154
|
+
.unwrap();
|
|
155
|
+
|
|
156
|
+
Ok(response)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Fluent builder for constructing test HTTP requests
|
|
162
|
+
///
|
|
163
|
+
/// Provides a fluent API for building both hyper Request objects and RequestData
|
|
164
|
+
/// structures needed for handler testing. Handles typical test scenarios without
|
|
165
|
+
/// requiring manual construction of all components.
|
|
166
|
+
///
|
|
167
|
+
/// # Example
|
|
168
|
+
///
|
|
169
|
+
/// ```ignore
|
|
170
|
+
/// let (request, request_data) = RequestBuilder::new()
|
|
171
|
+
/// .method(Method::POST)
|
|
172
|
+
/// .path("/api/users")
|
|
173
|
+
/// .headers(vec![("authorization".to_string(), "Bearer token".to_string())])
|
|
174
|
+
/// .json_body(json!({"name": "Alice", "email": "alice@example.com"}))
|
|
175
|
+
/// .build();
|
|
176
|
+
///
|
|
177
|
+
/// assert_eq!(request_data.method, "POST");
|
|
178
|
+
/// assert_eq!(request_data.path, "/api/users");
|
|
179
|
+
/// ```
|
|
180
|
+
pub struct RequestBuilder {
|
|
181
|
+
method: Method,
|
|
182
|
+
path: String,
|
|
183
|
+
headers: HashMap<String, String>,
|
|
184
|
+
cookies: HashMap<String, String>,
|
|
185
|
+
body: Value,
|
|
186
|
+
query_params: HashMap<String, Vec<String>>,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
impl RequestBuilder {
|
|
190
|
+
/// Create a new request builder with default GET method
|
|
191
|
+
pub fn new() -> Self {
|
|
192
|
+
Self {
|
|
193
|
+
method: Method::GET,
|
|
194
|
+
path: "/".to_string(),
|
|
195
|
+
headers: HashMap::new(),
|
|
196
|
+
cookies: HashMap::new(),
|
|
197
|
+
body: json!(null),
|
|
198
|
+
query_params: HashMap::new(),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Set the HTTP method
|
|
203
|
+
///
|
|
204
|
+
/// Default: GET
|
|
205
|
+
pub fn method(mut self, method: Method) -> Self {
|
|
206
|
+
self.method = method;
|
|
207
|
+
self
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Set the request path
|
|
211
|
+
///
|
|
212
|
+
/// Default: "/"
|
|
213
|
+
pub fn path(mut self, path: &str) -> Self {
|
|
214
|
+
self.path = path.to_string();
|
|
215
|
+
self
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Add or replace headers from a HashMap
|
|
219
|
+
///
|
|
220
|
+
/// Values are stored as-is; no normalization is performed.
|
|
221
|
+
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
|
|
222
|
+
self.headers = headers;
|
|
223
|
+
self
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Add a single header
|
|
227
|
+
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
|
228
|
+
self.headers.insert(name.into(), value.into());
|
|
229
|
+
self
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Add or replace cookies from a HashMap
|
|
233
|
+
pub fn cookies(mut self, cookies: HashMap<String, String>) -> Self {
|
|
234
|
+
self.cookies = cookies;
|
|
235
|
+
self
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Add a single cookie
|
|
239
|
+
pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
|
240
|
+
self.cookies.insert(name.into(), value.into());
|
|
241
|
+
self
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Set the JSON request body
|
|
245
|
+
///
|
|
246
|
+
/// Default: null
|
|
247
|
+
pub fn json_body(mut self, body: Value) -> Self {
|
|
248
|
+
self.body = body;
|
|
249
|
+
self
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Set query parameters as a HashMap of name to values
|
|
253
|
+
///
|
|
254
|
+
/// Values are stored as Vec<String> to support multi-valued parameters.
|
|
255
|
+
pub fn query_params(mut self, params: HashMap<String, Vec<String>>) -> Self {
|
|
256
|
+
self.query_params = params;
|
|
257
|
+
self
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Add a single query parameter
|
|
261
|
+
pub fn query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
|
262
|
+
self.query_params
|
|
263
|
+
.entry(name.into())
|
|
264
|
+
.or_insert_with(Vec::new)
|
|
265
|
+
.push(value.into());
|
|
266
|
+
self
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Build the request into (Request<Body>, RequestData) tuple
|
|
270
|
+
///
|
|
271
|
+
/// The Request can be passed directly to handler.call(). RequestData contains
|
|
272
|
+
/// all extracted request information (params, body, headers, etc.).
|
|
273
|
+
pub fn build(self) -> (Request<Body>, RequestData) {
|
|
274
|
+
let body = if self.body.is_null() {
|
|
275
|
+
Body::empty()
|
|
276
|
+
} else {
|
|
277
|
+
Body::from(self.body.to_string())
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
let mut request_builder = Request::builder().method(self.method.clone()).uri(&self.path);
|
|
281
|
+
|
|
282
|
+
for (name, value) in &self.headers {
|
|
283
|
+
request_builder = request_builder.header(name, value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let request = request_builder.body(body).unwrap();
|
|
287
|
+
|
|
288
|
+
let request_data = RequestData {
|
|
289
|
+
path_params: Arc::new(HashMap::new()),
|
|
290
|
+
query_params: build_query_json(&self.query_params),
|
|
291
|
+
validated_params: None,
|
|
292
|
+
raw_query_params: Arc::new(self.query_params),
|
|
293
|
+
body: self.body,
|
|
294
|
+
raw_body: None,
|
|
295
|
+
headers: Arc::new(self.headers),
|
|
296
|
+
cookies: Arc::new(self.cookies),
|
|
297
|
+
method: self.method.to_string(),
|
|
298
|
+
path: self.path,
|
|
299
|
+
#[cfg(feature = "di")]
|
|
300
|
+
dependencies: None,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
(request, request_data)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
impl Default for RequestBuilder {
|
|
308
|
+
fn default() -> Self {
|
|
309
|
+
Self::new()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Convert raw query parameters into JSON format
|
|
314
|
+
fn build_query_json(raw_params: &HashMap<String, Vec<String>>) -> Value {
|
|
315
|
+
let mut map = serde_json::Map::new();
|
|
316
|
+
|
|
317
|
+
for (key, values) in raw_params {
|
|
318
|
+
if values.is_empty() {
|
|
319
|
+
map.insert(key.clone(), json!(null));
|
|
320
|
+
} else if values.len() == 1 {
|
|
321
|
+
map.insert(key.clone(), json!(values[0].clone()));
|
|
322
|
+
} else {
|
|
323
|
+
map.insert(key.clone(), json!(values.clone()));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Value::Object(map)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Load a JSON fixture from the testing_data directory
|
|
331
|
+
///
|
|
332
|
+
/// # Arguments
|
|
333
|
+
///
|
|
334
|
+
/// * `relative_path` - Path relative to project root, e.g., "testing_data/headers/01_user_agent_default.json"
|
|
335
|
+
///
|
|
336
|
+
/// # Example
|
|
337
|
+
///
|
|
338
|
+
/// ```ignore
|
|
339
|
+
/// let fixture = load_fixture("testing_data/headers/01_user_agent_default.json")?;
|
|
340
|
+
/// assert!(fixture.is_object());
|
|
341
|
+
/// ```
|
|
342
|
+
///
|
|
343
|
+
/// # Errors
|
|
344
|
+
///
|
|
345
|
+
/// Returns error if file doesn't exist or is not valid JSON.
|
|
346
|
+
pub fn load_fixture(relative_path: &str) -> Result<Value, Box<dyn std::error::Error>> {
|
|
347
|
+
use std::path::PathBuf;
|
|
348
|
+
|
|
349
|
+
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
350
|
+
while root.pop() {
|
|
351
|
+
if root.join("Cargo.toml").exists() {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let path = root.join(relative_path);
|
|
357
|
+
let content = std::fs::read_to_string(&path)?;
|
|
358
|
+
let value = serde_json::from_str(&content)?;
|
|
359
|
+
Ok(value)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// Assert that a response has the expected status code
|
|
363
|
+
///
|
|
364
|
+
/// # Panics
|
|
365
|
+
///
|
|
366
|
+
/// Panics if the response status doesn't match the expected value.
|
|
367
|
+
///
|
|
368
|
+
/// # Example
|
|
369
|
+
///
|
|
370
|
+
/// ```ignore
|
|
371
|
+
/// let response = handler.call(request, request_data).await?;
|
|
372
|
+
/// assert_status(&response, StatusCode::CREATED);
|
|
373
|
+
/// ```
|
|
374
|
+
pub fn assert_status(response: &Response<Body>, expected: StatusCode) {
|
|
375
|
+
assert_eq!(
|
|
376
|
+
response.status(),
|
|
377
|
+
expected,
|
|
378
|
+
"Expected status {} but got {}",
|
|
379
|
+
expected,
|
|
380
|
+
response.status()
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Parse a response body as JSON
|
|
385
|
+
///
|
|
386
|
+
/// # Errors
|
|
387
|
+
///
|
|
388
|
+
/// Returns error if the body cannot be read or is not valid JSON.
|
|
389
|
+
///
|
|
390
|
+
/// # Example
|
|
391
|
+
///
|
|
392
|
+
/// ```ignore
|
|
393
|
+
/// let mut response = handler.call(request, request_data).await?;
|
|
394
|
+
/// let json = parse_json_body(&mut response).await?;
|
|
395
|
+
/// assert_eq!(json["id"], 123);
|
|
396
|
+
/// ```
|
|
397
|
+
pub async fn parse_json_body(response: &mut Response<Body>) -> Result<Value, Box<dyn std::error::Error>> {
|
|
398
|
+
use axum::body::to_bytes;
|
|
399
|
+
use std::mem;
|
|
400
|
+
|
|
401
|
+
let body = mem::take(response.body_mut());
|
|
402
|
+
let bytes = to_bytes(body, usize::MAX).await?;
|
|
403
|
+
let value = serde_json::from_slice(&bytes)?;
|
|
404
|
+
Ok(value)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#[cfg(test)]
|
|
408
|
+
mod tests {
|
|
409
|
+
use super::*;
|
|
410
|
+
|
|
411
|
+
#[tokio::test]
|
|
412
|
+
async fn test_handler_builder_default() {
|
|
413
|
+
let handler = HandlerBuilder::new().build();
|
|
414
|
+
let request = Request::builder().body(Body::empty()).unwrap();
|
|
415
|
+
let request_data = RequestData {
|
|
416
|
+
path_params: Arc::new(HashMap::new()),
|
|
417
|
+
query_params: json!({}),
|
|
418
|
+
validated_params: None,
|
|
419
|
+
raw_query_params: Arc::new(HashMap::new()),
|
|
420
|
+
body: json!(null),
|
|
421
|
+
raw_body: None,
|
|
422
|
+
headers: Arc::new(HashMap::new()),
|
|
423
|
+
cookies: Arc::new(HashMap::new()),
|
|
424
|
+
method: "GET".to_string(),
|
|
425
|
+
path: "/".to_string(),
|
|
426
|
+
#[cfg(feature = "di")]
|
|
427
|
+
dependencies: None,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
let result = handler.call(request, request_data).await;
|
|
431
|
+
assert!(result.is_ok());
|
|
432
|
+
|
|
433
|
+
let response = result.unwrap();
|
|
434
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[tokio::test]
|
|
438
|
+
async fn test_handler_builder_custom_status() {
|
|
439
|
+
let handler = HandlerBuilder::new().status(201).build();
|
|
440
|
+
let request = Request::builder().body(Body::empty()).unwrap();
|
|
441
|
+
let request_data = RequestData {
|
|
442
|
+
path_params: Arc::new(HashMap::new()),
|
|
443
|
+
query_params: json!({}),
|
|
444
|
+
validated_params: None,
|
|
445
|
+
raw_query_params: Arc::new(HashMap::new()),
|
|
446
|
+
body: json!(null),
|
|
447
|
+
raw_body: None,
|
|
448
|
+
headers: Arc::new(HashMap::new()),
|
|
449
|
+
cookies: Arc::new(HashMap::new()),
|
|
450
|
+
method: "POST".to_string(),
|
|
451
|
+
path: "/".to_string(),
|
|
452
|
+
#[cfg(feature = "di")]
|
|
453
|
+
dependencies: None,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
let result = handler.call(request, request_data).await;
|
|
457
|
+
assert!(result.is_ok());
|
|
458
|
+
|
|
459
|
+
let response = result.unwrap();
|
|
460
|
+
assert_eq!(response.status(), StatusCode::CREATED);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[tokio::test]
|
|
464
|
+
async fn test_handler_builder_with_body() {
|
|
465
|
+
let body = json!({"message": "success", "code": 42});
|
|
466
|
+
let handler = HandlerBuilder::new().json_body(body.clone()).build();
|
|
467
|
+
|
|
468
|
+
let request = Request::builder().body(Body::empty()).unwrap();
|
|
469
|
+
let request_data = RequestData {
|
|
470
|
+
path_params: Arc::new(HashMap::new()),
|
|
471
|
+
query_params: json!({}),
|
|
472
|
+
validated_params: None,
|
|
473
|
+
raw_query_params: Arc::new(HashMap::new()),
|
|
474
|
+
body: json!(null),
|
|
475
|
+
raw_body: None,
|
|
476
|
+
headers: Arc::new(HashMap::new()),
|
|
477
|
+
cookies: Arc::new(HashMap::new()),
|
|
478
|
+
method: "GET".to_string(),
|
|
479
|
+
path: "/".to_string(),
|
|
480
|
+
#[cfg(feature = "di")]
|
|
481
|
+
dependencies: None,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
let result = handler.call(request, request_data).await;
|
|
485
|
+
assert!(result.is_ok());
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[tokio::test]
|
|
489
|
+
async fn test_handler_builder_with_delay() {
|
|
490
|
+
let start = std::time::Instant::now();
|
|
491
|
+
let handler = HandlerBuilder::new().delay(Duration::from_millis(10)).build();
|
|
492
|
+
|
|
493
|
+
let request = Request::builder().body(Body::empty()).unwrap();
|
|
494
|
+
let request_data = RequestData {
|
|
495
|
+
path_params: Arc::new(HashMap::new()),
|
|
496
|
+
query_params: json!({}),
|
|
497
|
+
validated_params: None,
|
|
498
|
+
raw_query_params: Arc::new(HashMap::new()),
|
|
499
|
+
body: json!(null),
|
|
500
|
+
raw_body: None,
|
|
501
|
+
headers: Arc::new(HashMap::new()),
|
|
502
|
+
cookies: Arc::new(HashMap::new()),
|
|
503
|
+
method: "GET".to_string(),
|
|
504
|
+
path: "/".to_string(),
|
|
505
|
+
#[cfg(feature = "di")]
|
|
506
|
+
dependencies: None,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
let _result = handler.call(request, request_data).await;
|
|
510
|
+
let elapsed = start.elapsed();
|
|
511
|
+
|
|
512
|
+
assert!(elapsed >= Duration::from_millis(10));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[test]
|
|
516
|
+
fn test_request_builder_default() {
|
|
517
|
+
let (request, request_data) = RequestBuilder::new().build();
|
|
518
|
+
|
|
519
|
+
assert_eq!(request.method(), &Method::GET);
|
|
520
|
+
assert_eq!(request_data.path, "/");
|
|
521
|
+
assert_eq!(request_data.method, "GET");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn test_request_builder_post_with_body() {
|
|
526
|
+
let body = json!({"name": "Alice", "age": 30});
|
|
527
|
+
let (request, request_data) = RequestBuilder::new()
|
|
528
|
+
.method(Method::POST)
|
|
529
|
+
.path("/users")
|
|
530
|
+
.json_body(body.clone())
|
|
531
|
+
.build();
|
|
532
|
+
|
|
533
|
+
assert_eq!(request.method(), &Method::POST);
|
|
534
|
+
assert_eq!(request_data.path, "/users");
|
|
535
|
+
assert_eq!(request_data.body, body);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#[test]
|
|
539
|
+
fn test_request_builder_with_headers() {
|
|
540
|
+
let mut headers = HashMap::new();
|
|
541
|
+
headers.insert("authorization".to_string(), "Bearer token".to_string());
|
|
542
|
+
headers.insert("x-custom".to_string(), "value".to_string());
|
|
543
|
+
|
|
544
|
+
let (_request, request_data) = RequestBuilder::new().headers(headers.clone()).build();
|
|
545
|
+
|
|
546
|
+
assert_eq!(
|
|
547
|
+
request_data.headers.get("authorization"),
|
|
548
|
+
Some(&"Bearer token".to_string())
|
|
549
|
+
);
|
|
550
|
+
assert_eq!(request_data.headers.get("x-custom"), Some(&"value".to_string()));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
fn test_request_builder_with_single_header() {
|
|
555
|
+
let (_request, request_data) = RequestBuilder::new()
|
|
556
|
+
.header("x-api-key", "secret123")
|
|
557
|
+
.header("accept", "application/json")
|
|
558
|
+
.build();
|
|
559
|
+
|
|
560
|
+
assert_eq!(request_data.headers.get("x-api-key"), Some(&"secret123".to_string()));
|
|
561
|
+
assert_eq!(
|
|
562
|
+
request_data.headers.get("accept"),
|
|
563
|
+
Some(&"application/json".to_string())
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#[test]
|
|
568
|
+
fn test_request_builder_with_cookies() {
|
|
569
|
+
let mut cookies = HashMap::new();
|
|
570
|
+
cookies.insert("session".to_string(), "abc123".to_string());
|
|
571
|
+
cookies.insert("preferences".to_string(), "dark_mode".to_string());
|
|
572
|
+
|
|
573
|
+
let (_request, request_data) = RequestBuilder::new().cookies(cookies).build();
|
|
574
|
+
|
|
575
|
+
assert_eq!(request_data.cookies.get("session"), Some(&"abc123".to_string()));
|
|
576
|
+
assert_eq!(request_data.cookies.get("preferences"), Some(&"dark_mode".to_string()));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[test]
|
|
580
|
+
fn test_request_builder_with_query_params() {
|
|
581
|
+
let mut params = HashMap::new();
|
|
582
|
+
params.insert("page".to_string(), vec!["1".to_string()]);
|
|
583
|
+
params.insert("sort".to_string(), vec!["name".to_string()]);
|
|
584
|
+
params.insert("filter".to_string(), vec!["active".to_string(), "verified".to_string()]);
|
|
585
|
+
|
|
586
|
+
let (_request, request_data) = RequestBuilder::new().query_params(params).build();
|
|
587
|
+
|
|
588
|
+
assert_eq!(request_data.query_params["page"], "1");
|
|
589
|
+
assert_eq!(request_data.query_params["sort"], "name");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn test_request_builder_single_query_param() {
|
|
594
|
+
let (_request, request_data) = RequestBuilder::new()
|
|
595
|
+
.query_param("limit", "10")
|
|
596
|
+
.query_param("offset", "5")
|
|
597
|
+
.build();
|
|
598
|
+
|
|
599
|
+
assert_eq!(request_data.query_params["limit"], "10");
|
|
600
|
+
assert_eq!(request_data.query_params["offset"], "5");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#[test]
|
|
604
|
+
fn test_request_builder_fluent_api() {
|
|
605
|
+
let body = json!({"name": "Bob"});
|
|
606
|
+
let (_request, request_data) = RequestBuilder::new()
|
|
607
|
+
.method(Method::PUT)
|
|
608
|
+
.path("/users/42")
|
|
609
|
+
.header("authorization", "Bearer abc123")
|
|
610
|
+
.cookie("session", "xyz789")
|
|
611
|
+
.json_body(body.clone())
|
|
612
|
+
.query_param("notify", "true")
|
|
613
|
+
.build();
|
|
614
|
+
|
|
615
|
+
assert_eq!(request_data.method, "PUT");
|
|
616
|
+
assert_eq!(request_data.path, "/users/42");
|
|
617
|
+
assert_eq!(request_data.body, body);
|
|
618
|
+
assert_eq!(
|
|
619
|
+
request_data.headers.get("authorization"),
|
|
620
|
+
Some(&"Bearer abc123".to_string())
|
|
621
|
+
);
|
|
622
|
+
assert_eq!(request_data.cookies.get("session"), Some(&"xyz789".to_string()));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn test_query_params_conversion() {
|
|
627
|
+
let mut params = HashMap::new();
|
|
628
|
+
params.insert("single".to_string(), vec!["value".to_string()]);
|
|
629
|
+
|
|
630
|
+
let query_json = build_query_json(¶ms);
|
|
631
|
+
assert_eq!(query_json["single"], "value");
|
|
632
|
+
}
|
|
633
|
+
}
|