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,464 @@
1
+ #![allow(clippy::pedantic, clippy::nursery, clippy::all)]
2
+ //! Integration tests for server handler wrappers (ValidatingHandler, DependencyInjectingHandler)
3
+ //!
4
+ //! These tests verify critical validation and dependency injection behavior across
5
+ //! the HTTP request pipeline. They test observable behaviors like status codes,
6
+ //! error messages, and request enrichment rather than internal validation logic.
7
+
8
+ mod common;
9
+
10
+ use axum::http::StatusCode;
11
+ use common::test_builders::{HandlerBuilder, RequestBuilder, assert_status};
12
+ use serde_json::json;
13
+ use spikard_core::Route;
14
+ use spikard_http::{Handler, server::handler::ValidatingHandler};
15
+ use std::sync::Arc;
16
+
17
+ mod validating_handler {
18
+ use super::*;
19
+
20
+ /// Test 1: Valid request passes validation and calls handler
21
+ #[tokio::test]
22
+ async fn test_validating_handler_allows_valid_request() {
23
+ let schema = json!({
24
+ "type": "object",
25
+ "properties": {
26
+ "name": {"type": "string"},
27
+ "email": {"type": "string"}
28
+ },
29
+ "required": ["name", "email"]
30
+ });
31
+
32
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
33
+
34
+ let route = Route {
35
+ method: spikard_core::http::Method::Post,
36
+ path: "/users".to_string(),
37
+ handler_name: "create_user".to_string(),
38
+ request_validator: Some(validator),
39
+ response_validator: None,
40
+ parameter_validator: None,
41
+ file_params: None,
42
+ is_async: true,
43
+ cors: None,
44
+ expects_json_body: true,
45
+ #[cfg(feature = "di")]
46
+ handler_dependencies: vec![],
47
+ jsonrpc_method: None,
48
+ };
49
+
50
+ let inner_handler = HandlerBuilder::new()
51
+ .status(201)
52
+ .json_body(json!({"id": 1, "created": true}))
53
+ .build();
54
+
55
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
56
+
57
+ let (request, request_data) = RequestBuilder::new()
58
+ .method(axum::http::Method::POST)
59
+ .path("/users")
60
+ .json_body(json!({"name": "Alice", "email": "alice@example.com"}))
61
+ .build();
62
+
63
+ let response = validator_handler.call(request, request_data).await.unwrap();
64
+
65
+ assert_status(&response, StatusCode::CREATED);
66
+ }
67
+
68
+ /// Test 2: Request with invalid JSON body returns 422
69
+ #[tokio::test]
70
+ async fn test_validating_handler_rejects_invalid_json_body() {
71
+ let schema = json!({
72
+ "type": "object",
73
+ "properties": {
74
+ "email": {"type": "string"}
75
+ },
76
+ "required": ["email"]
77
+ });
78
+
79
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
80
+
81
+ let route = Route {
82
+ method: spikard_core::http::Method::Post,
83
+ path: "/users".to_string(),
84
+ handler_name: "create_user".to_string(),
85
+ request_validator: Some(validator),
86
+ response_validator: None,
87
+ parameter_validator: None,
88
+ file_params: None,
89
+ is_async: true,
90
+ cors: None,
91
+ expects_json_body: true,
92
+ #[cfg(feature = "di")]
93
+ handler_dependencies: vec![],
94
+ jsonrpc_method: None,
95
+ };
96
+
97
+ let inner_handler = HandlerBuilder::new().status(200).build();
98
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
99
+
100
+ let (request, request_data) = RequestBuilder::new()
101
+ .method(axum::http::Method::POST)
102
+ .path("/users")
103
+ .json_body(json!({"name": "Alice"}))
104
+ .build();
105
+
106
+ let result = validator_handler.call(request, request_data).await;
107
+
108
+ assert!(result.is_err());
109
+ let (status, body) = result.unwrap_err();
110
+ assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
111
+
112
+ let error: serde_json::Value = serde_json::from_str(&body).unwrap();
113
+ assert!(error["errors"].is_array());
114
+ }
115
+
116
+ /// Test 3: Missing required field returns 422 with field-specific error
117
+ #[tokio::test]
118
+ async fn test_validating_handler_rejects_missing_required_field() {
119
+ let schema = json!({
120
+ "type": "object",
121
+ "properties": {
122
+ "name": {"type": "string"},
123
+ "age": {"type": "integer"}
124
+ },
125
+ "required": ["name"]
126
+ });
127
+
128
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
129
+
130
+ let route = Route {
131
+ method: spikard_core::http::Method::Post,
132
+ path: "/api/test".to_string(),
133
+ handler_name: "test_handler".to_string(),
134
+ request_validator: Some(validator),
135
+ response_validator: None,
136
+ parameter_validator: None,
137
+ file_params: None,
138
+ is_async: true,
139
+ cors: None,
140
+ expects_json_body: true,
141
+ #[cfg(feature = "di")]
142
+ handler_dependencies: vec![],
143
+ jsonrpc_method: None,
144
+ };
145
+
146
+ let inner_handler = HandlerBuilder::new().build();
147
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
148
+
149
+ let (request, request_data) = RequestBuilder::new()
150
+ .method(axum::http::Method::POST)
151
+ .json_body(json!({"age": 25}))
152
+ .build();
153
+
154
+ let result = validator_handler.call(request, request_data).await;
155
+
156
+ assert!(result.is_err());
157
+ let (status, body) = result.unwrap_err();
158
+ assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
159
+
160
+ let error: serde_json::Value = serde_json::from_str(&body).unwrap();
161
+ assert!(error["errors"][0]["loc"][1].as_str().map_or(false, |s| s == "name"));
162
+ }
163
+
164
+ /// Test 4: Wrong type in field returns 422 with type error
165
+ #[tokio::test]
166
+ async fn test_validating_handler_rejects_wrong_type() {
167
+ let schema = json!({
168
+ "type": "object",
169
+ "properties": {
170
+ "count": {"type": "integer"}
171
+ },
172
+ "required": ["count"]
173
+ });
174
+
175
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
176
+
177
+ let route = Route {
178
+ method: spikard_core::http::Method::Post,
179
+ path: "/api/test".to_string(),
180
+ handler_name: "test_handler".to_string(),
181
+ request_validator: Some(validator),
182
+ response_validator: None,
183
+ parameter_validator: None,
184
+ file_params: None,
185
+ is_async: true,
186
+ cors: None,
187
+ expects_json_body: true,
188
+ #[cfg(feature = "di")]
189
+ handler_dependencies: vec![],
190
+ jsonrpc_method: None,
191
+ };
192
+
193
+ let inner_handler = HandlerBuilder::new().build();
194
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
195
+
196
+ let (request, request_data) = RequestBuilder::new()
197
+ .method(axum::http::Method::POST)
198
+ .json_body(json!({"count": "not_a_number"}))
199
+ .build();
200
+
201
+ let result = validator_handler.call(request, request_data).await;
202
+
203
+ assert!(result.is_err());
204
+ let (status, body) = result.unwrap_err();
205
+ assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
206
+
207
+ let error: serde_json::Value = serde_json::from_str(&body).unwrap();
208
+ assert!(error["errors"].is_array());
209
+ }
210
+
211
+ /// Test 5: Optional fields missing still allows handler execution
212
+ #[tokio::test]
213
+ async fn test_validating_handler_allows_optional_fields() {
214
+ let schema = json!({
215
+ "type": "object",
216
+ "properties": {
217
+ "name": {"type": "string"},
218
+ "description": {"type": "string"}
219
+ },
220
+ "required": ["name"]
221
+ });
222
+
223
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
224
+
225
+ let route = Route {
226
+ method: spikard_core::http::Method::Post,
227
+ path: "/api/test".to_string(),
228
+ handler_name: "test_handler".to_string(),
229
+ request_validator: Some(validator),
230
+ response_validator: None,
231
+ parameter_validator: None,
232
+ file_params: None,
233
+ is_async: true,
234
+ cors: None,
235
+ expects_json_body: true,
236
+ #[cfg(feature = "di")]
237
+ handler_dependencies: vec![],
238
+ jsonrpc_method: None,
239
+ };
240
+
241
+ let inner_handler = HandlerBuilder::new().status(200).build();
242
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
243
+
244
+ let (request, request_data) = RequestBuilder::new()
245
+ .method(axum::http::Method::POST)
246
+ .json_body(json!({"name": "Test"}))
247
+ .build();
248
+
249
+ let result = validator_handler.call(request, request_data).await;
250
+
251
+ assert!(result.is_ok());
252
+ let response = result.unwrap();
253
+ assert_status(&response, StatusCode::OK);
254
+ }
255
+
256
+ /// Test 6: Nested object validation returns 422 with error path
257
+ #[tokio::test]
258
+ async fn test_validating_handler_validates_nested_objects() {
259
+ let schema = json!({
260
+ "type": "object",
261
+ "properties": {
262
+ "user": {
263
+ "type": "object",
264
+ "properties": {
265
+ "name": {"type": "string"}
266
+ },
267
+ "required": ["name"]
268
+ }
269
+ },
270
+ "required": ["user"]
271
+ });
272
+
273
+ let validator = Arc::new(spikard_core::SchemaValidator::new(schema).unwrap());
274
+
275
+ let route = Route {
276
+ method: spikard_core::http::Method::Post,
277
+ path: "/api/test".to_string(),
278
+ handler_name: "test_handler".to_string(),
279
+ request_validator: Some(validator),
280
+ response_validator: None,
281
+ parameter_validator: None,
282
+ file_params: None,
283
+ is_async: true,
284
+ cors: None,
285
+ expects_json_body: true,
286
+ #[cfg(feature = "di")]
287
+ handler_dependencies: vec![],
288
+ jsonrpc_method: None,
289
+ };
290
+
291
+ let inner_handler = HandlerBuilder::new().build();
292
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
293
+
294
+ let (request, request_data) = RequestBuilder::new()
295
+ .method(axum::http::Method::POST)
296
+ .json_body(json!({"user": {}}))
297
+ .build();
298
+
299
+ let result = validator_handler.call(request, request_data).await;
300
+
301
+ assert!(result.is_err());
302
+ let (status, body) = result.unwrap_err();
303
+ assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
304
+
305
+ let error: serde_json::Value = serde_json::from_str(&body).unwrap();
306
+ assert!(error["errors"].is_array());
307
+ }
308
+
309
+ /// Test 7: Panicking handler returns 500 with structured panic error
310
+ #[tokio::test]
311
+ async fn test_validating_handler_catches_handler_panic() {
312
+ let route = Route {
313
+ method: spikard_core::http::Method::Post,
314
+ path: "/api/test".to_string(),
315
+ handler_name: "test_handler".to_string(),
316
+ request_validator: None,
317
+ response_validator: None,
318
+ parameter_validator: None,
319
+ file_params: None,
320
+ is_async: true,
321
+ cors: None,
322
+ expects_json_body: false,
323
+ #[cfg(feature = "di")]
324
+ handler_dependencies: vec![],
325
+ jsonrpc_method: None,
326
+ };
327
+
328
+ let inner_handler = HandlerBuilder::new().panics().build();
329
+ let validator_handler = ValidatingHandler::new(inner_handler, &route);
330
+
331
+ let (request, request_data) = RequestBuilder::new().method(axum::http::Method::POST).build();
332
+
333
+ let result = validator_handler.call(request, request_data).await;
334
+
335
+ assert!(result.is_err());
336
+ let (status, body) = result.unwrap_err();
337
+ assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
338
+
339
+ let error: serde_json::Value = serde_json::from_str(&body).unwrap();
340
+ assert_eq!(error["code"], "panic");
341
+ }
342
+ }
343
+
344
+ #[cfg(feature = "di")]
345
+ mod dependency_injecting_handler {
346
+ use super::*;
347
+ use spikard_core::di::{DependencyContainer, ValueDependency};
348
+ use spikard_http::DependencyInjectingHandler;
349
+
350
+ /// Test 8: Dependencies are resolved and injected into handler
351
+ #[tokio::test]
352
+ async fn test_di_handler_injects_dependencies() {
353
+ let mut container = DependencyContainer::new();
354
+ container
355
+ .register(
356
+ "config".to_string(),
357
+ Arc::new(ValueDependency::new("config", "test_config_value")),
358
+ )
359
+ .unwrap();
360
+
361
+ let inner_handler = HandlerBuilder::new()
362
+ .status(200)
363
+ .json_body(json!({"status": "ok"}))
364
+ .build();
365
+
366
+ let di_handler =
367
+ DependencyInjectingHandler::new(inner_handler, Arc::new(container), vec!["config".to_string()]);
368
+
369
+ let (request, request_data) = RequestBuilder::new().method(axum::http::Method::GET).build();
370
+
371
+ let result = di_handler.call(request, request_data).await;
372
+
373
+ assert!(result.is_ok());
374
+ let response = result.unwrap();
375
+ assert_status(&response, StatusCode::OK);
376
+ }
377
+
378
+ /// Test 9: Missing dependency returns 500 with resolution error
379
+ #[tokio::test]
380
+ async fn test_di_handler_resolution_failure_returns_500() {
381
+ let container = DependencyContainer::new();
382
+
383
+ let inner_handler = HandlerBuilder::new().build();
384
+ let di_handler =
385
+ DependencyInjectingHandler::new(inner_handler, Arc::new(container), vec!["missing_db".to_string()]);
386
+
387
+ let (request, request_data) = RequestBuilder::new().method(axum::http::Method::GET).build();
388
+
389
+ let result = di_handler.call(request, request_data).await;
390
+
391
+ assert!(result.is_ok());
392
+ let response = result.unwrap();
393
+ assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
394
+ }
395
+
396
+ /// Test 10: Multiple dependencies are resolved correctly
397
+ #[tokio::test]
398
+ async fn test_di_handler_multiple_dependencies() {
399
+ let mut container = DependencyContainer::new();
400
+ container
401
+ .register("db".to_string(), Arc::new(ValueDependency::new("db", "postgres")))
402
+ .unwrap();
403
+ container
404
+ .register("cache".to_string(), Arc::new(ValueDependency::new("cache", "redis")))
405
+ .unwrap();
406
+ container
407
+ .register("logger".to_string(), Arc::new(ValueDependency::new("logger", "slog")))
408
+ .unwrap();
409
+
410
+ let inner_handler = HandlerBuilder::new()
411
+ .status(200)
412
+ .json_body(json!({"services": ["db", "cache", "logger"]}))
413
+ .build();
414
+
415
+ let di_handler = DependencyInjectingHandler::new(
416
+ inner_handler,
417
+ Arc::new(container),
418
+ vec!["db".to_string(), "cache".to_string(), "logger".to_string()],
419
+ );
420
+
421
+ let (request, request_data) = RequestBuilder::new().method(axum::http::Method::GET).build();
422
+
423
+ let result = di_handler.call(request, request_data).await;
424
+
425
+ assert!(result.is_ok());
426
+ let response = result.unwrap();
427
+ assert_status(&response, StatusCode::OK);
428
+ }
429
+
430
+ /// Test 11: Request-scoped dependencies are unique per request
431
+ #[tokio::test]
432
+ async fn test_di_handler_scoped_dependencies() {
433
+ let mut container = DependencyContainer::new();
434
+ container
435
+ .register(
436
+ "request_id".to_string(),
437
+ Arc::new(ValueDependency::new("request_id", "uuid-123")),
438
+ )
439
+ .unwrap();
440
+
441
+ let inner_handler = HandlerBuilder::new().status(200).build();
442
+ let di_handler =
443
+ DependencyInjectingHandler::new(inner_handler, Arc::new(container), vec!["request_id".to_string()]);
444
+
445
+ let (request1, request_data1) = RequestBuilder::new()
446
+ .method(axum::http::Method::GET)
447
+ .path("/request/1")
448
+ .build();
449
+
450
+ let result1 = di_handler.call(request1, request_data1).await;
451
+ assert!(result1.is_ok());
452
+
453
+ let (request2, request_data2) = RequestBuilder::new()
454
+ .method(axum::http::Method::GET)
455
+ .path("/request/2")
456
+ .build();
457
+
458
+ let result2 = di_handler.call(request2, request_data2).await;
459
+ assert!(result2.is_ok());
460
+
461
+ assert_eq!(result1.unwrap().status(), StatusCode::OK);
462
+ assert_eq!(result2.unwrap().status(), StatusCode::OK);
463
+ }
464
+ }