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,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(&params);
631
+ assert_eq!(query_json["single"], "value");
632
+ }
633
+ }