spikard 0.6.2 → 0.7.2
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 +2 -2
- 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,761 @@
|
|
|
1
|
+
//! Comprehensive error mapping and Problem Details generation tests
|
|
2
|
+
//!
|
|
3
|
+
//! These tests verify error message formatting, error code mapping, and
|
|
4
|
+
//! RFC 9457 Problem Details structure generation.
|
|
5
|
+
|
|
6
|
+
use http::StatusCode;
|
|
7
|
+
use serde_json::json;
|
|
8
|
+
use spikard_core::problem::ProblemDetails;
|
|
9
|
+
use spikard_core::validation::SchemaValidator;
|
|
10
|
+
|
|
11
|
+
#[test]
|
|
12
|
+
fn test_single_string_too_short_error() {
|
|
13
|
+
let schema = json!({
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"username": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 3
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"required": ["username"]
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
25
|
+
let data = json!({
|
|
26
|
+
"username": "ab"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let result = validator.validate(&data);
|
|
30
|
+
assert!(result.is_err(), "Validation should fail for string too short");
|
|
31
|
+
|
|
32
|
+
let err = result.unwrap_err();
|
|
33
|
+
assert_eq!(err.errors.len(), 1);
|
|
34
|
+
assert_eq!(err.errors[0].error_type, "string_too_short");
|
|
35
|
+
assert_eq!(err.errors[0].loc, vec!["body", "username"]);
|
|
36
|
+
assert!(err.errors[0].msg.contains("at least 3"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn test_single_string_too_long_error() {
|
|
41
|
+
let schema = json!({
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"password": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"maxLength": 20
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": ["password"]
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
53
|
+
let data = json!({
|
|
54
|
+
"password": "this_is_a_very_long_password_string"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let result = validator.validate(&data);
|
|
58
|
+
assert!(result.is_err(), "Validation should fail for string too long");
|
|
59
|
+
|
|
60
|
+
let err = result.unwrap_err();
|
|
61
|
+
assert_eq!(err.errors[0].error_type, "string_too_long");
|
|
62
|
+
assert!(err.errors[0].msg.contains("at most 20"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn test_single_enum_error() {
|
|
67
|
+
let schema = json!({
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"status": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": ["active", "inactive", "pending"]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": ["status"]
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
79
|
+
let data = json!({
|
|
80
|
+
"status": "unknown"
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let result = validator.validate(&data);
|
|
84
|
+
assert!(result.is_err(), "Validation should fail for invalid enum");
|
|
85
|
+
|
|
86
|
+
let err = result.unwrap_err();
|
|
87
|
+
assert_eq!(err.errors[0].error_type, "enum");
|
|
88
|
+
assert!(err.errors[0].msg.contains("active"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn test_single_type_error_string_instead_of_integer() {
|
|
93
|
+
let schema = json!({
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"age": {
|
|
97
|
+
"type": "integer"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"required": ["age"]
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
104
|
+
let data = json!({
|
|
105
|
+
"age": "not-a-number"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let result = validator.validate(&data);
|
|
109
|
+
assert!(result.is_err(), "Validation should fail for type mismatch");
|
|
110
|
+
|
|
111
|
+
let err = result.unwrap_err();
|
|
112
|
+
assert_eq!(err.errors[0].error_type, "int_parsing");
|
|
113
|
+
assert!(err.errors[0].msg.to_lowercase().contains("integer"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_single_missing_required_field_error() {
|
|
118
|
+
let schema = json!({
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"email": {
|
|
122
|
+
"type": "string"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"required": ["email"]
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
129
|
+
let data = json!({});
|
|
130
|
+
|
|
131
|
+
let result = validator.validate(&data);
|
|
132
|
+
assert!(result.is_err(), "Validation should fail for missing required field");
|
|
133
|
+
|
|
134
|
+
let err = result.unwrap_err();
|
|
135
|
+
assert_eq!(err.errors[0].error_type, "missing");
|
|
136
|
+
assert!(err.errors[0].loc.contains(&"email".to_string()));
|
|
137
|
+
assert_eq!(err.errors[0].msg, "Field required");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn test_single_email_format_error() {
|
|
142
|
+
let schema = json!({
|
|
143
|
+
"type": "object",
|
|
144
|
+
"properties": {
|
|
145
|
+
"contact_email": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"format": "email"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"required": ["contact_email"]
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
154
|
+
let data = json!({
|
|
155
|
+
"contact_email": "not-an-email"
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let result = validator.validate(&data);
|
|
159
|
+
assert!(result.is_err(), "Validation should fail for invalid email");
|
|
160
|
+
|
|
161
|
+
let err = result.unwrap_err();
|
|
162
|
+
assert_eq!(err.errors[0].error_type, "string_pattern_mismatch");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn test_single_uuid_format_error() {
|
|
167
|
+
let schema = json!({
|
|
168
|
+
"type": "object",
|
|
169
|
+
"properties": {
|
|
170
|
+
"request_id": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"format": "uuid"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"required": ["request_id"]
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
179
|
+
let data = json!({
|
|
180
|
+
"request_id": "not-a-uuid"
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
let result = validator.validate(&data);
|
|
184
|
+
assert!(result.is_err(), "Validation should fail for invalid UUID");
|
|
185
|
+
|
|
186
|
+
let err = result.unwrap_err();
|
|
187
|
+
assert_eq!(err.errors[0].error_type, "uuid_parsing");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn test_single_date_format_error() {
|
|
192
|
+
let schema = json!({
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {
|
|
195
|
+
"birth_date": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"format": "date"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": ["birth_date"]
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
204
|
+
let data = json!({
|
|
205
|
+
"birth_date": "13/25/99"
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let result = validator.validate(&data);
|
|
209
|
+
assert!(result.is_err(), "Validation should fail for invalid date");
|
|
210
|
+
|
|
211
|
+
let err = result.unwrap_err();
|
|
212
|
+
assert_eq!(err.errors[0].error_type, "date_parsing");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn test_single_minimum_constraint_error() {
|
|
217
|
+
let schema = json!({
|
|
218
|
+
"type": "object",
|
|
219
|
+
"properties": {
|
|
220
|
+
"quantity": {
|
|
221
|
+
"type": "integer",
|
|
222
|
+
"minimum": 1
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": ["quantity"]
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
229
|
+
let data = json!({
|
|
230
|
+
"quantity": 0
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
let result = validator.validate(&data);
|
|
234
|
+
assert!(result.is_err(), "Validation should fail for value below minimum");
|
|
235
|
+
|
|
236
|
+
let err = result.unwrap_err();
|
|
237
|
+
assert_eq!(err.errors[0].error_type, "greater_than_equal");
|
|
238
|
+
assert!(err.errors[0].msg.contains("greater than or equal to"));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn test_single_maximum_constraint_error() {
|
|
243
|
+
let schema = json!({
|
|
244
|
+
"type": "object",
|
|
245
|
+
"properties": {
|
|
246
|
+
"rating": {
|
|
247
|
+
"type": "integer",
|
|
248
|
+
"maximum": 5
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
"required": ["rating"]
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
255
|
+
let data = json!({
|
|
256
|
+
"rating": 10
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
let result = validator.validate(&data);
|
|
260
|
+
assert!(result.is_err(), "Validation should fail for value above maximum");
|
|
261
|
+
|
|
262
|
+
let err = result.unwrap_err();
|
|
263
|
+
assert_eq!(err.errors[0].error_type, "less_than_equal");
|
|
264
|
+
assert!(err.errors[0].msg.contains("less than or equal to"));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn test_multiple_validation_errors_different_fields() {
|
|
269
|
+
let schema = json!({
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"username": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"minLength": 3
|
|
275
|
+
},
|
|
276
|
+
"email": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"format": "email"
|
|
279
|
+
},
|
|
280
|
+
"age": {
|
|
281
|
+
"type": "integer",
|
|
282
|
+
"minimum": 0
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
"required": ["username", "email", "age"]
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
289
|
+
let data = json!({
|
|
290
|
+
"username": "ab",
|
|
291
|
+
"email": "not-email",
|
|
292
|
+
"age": -5
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
let result = validator.validate(&data);
|
|
296
|
+
assert!(result.is_err(), "Validation should fail with multiple errors");
|
|
297
|
+
|
|
298
|
+
let err = result.unwrap_err();
|
|
299
|
+
assert_eq!(err.errors.len(), 3);
|
|
300
|
+
|
|
301
|
+
let error_types: Vec<&str> = err.errors.iter().map(|e| e.error_type.as_str()).collect();
|
|
302
|
+
assert!(error_types.contains(&"string_too_short"));
|
|
303
|
+
assert!(error_types.contains(&"string_pattern_mismatch"));
|
|
304
|
+
assert!(error_types.contains(&"greater_than_equal"));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn test_multiple_missing_required_fields() {
|
|
309
|
+
let schema = json!({
|
|
310
|
+
"type": "object",
|
|
311
|
+
"properties": {
|
|
312
|
+
"first_name": {
|
|
313
|
+
"type": "string"
|
|
314
|
+
},
|
|
315
|
+
"last_name": {
|
|
316
|
+
"type": "string"
|
|
317
|
+
},
|
|
318
|
+
"email": {
|
|
319
|
+
"type": "string"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"required": ["first_name", "last_name", "email"]
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
326
|
+
let data = json!({});
|
|
327
|
+
|
|
328
|
+
let result = validator.validate(&data);
|
|
329
|
+
assert!(result.is_err(), "Validation should fail with multiple missing fields");
|
|
330
|
+
|
|
331
|
+
let err = result.unwrap_err();
|
|
332
|
+
assert_eq!(err.errors.len(), 3);
|
|
333
|
+
assert!(err.errors.iter().all(|e| e.error_type == "missing"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
fn test_nested_object_error_path() {
|
|
338
|
+
let schema = json!({
|
|
339
|
+
"type": "object",
|
|
340
|
+
"properties": {
|
|
341
|
+
"user": {
|
|
342
|
+
"type": "object",
|
|
343
|
+
"properties": {
|
|
344
|
+
"profile": {
|
|
345
|
+
"type": "object",
|
|
346
|
+
"properties": {
|
|
347
|
+
"email": {
|
|
348
|
+
"type": "string",
|
|
349
|
+
"format": "email"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"required": ["email"]
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
"required": ["profile"]
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
"required": ["user"]
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
362
|
+
let data = json!({
|
|
363
|
+
"user": {
|
|
364
|
+
"profile": {
|
|
365
|
+
"email": "invalid-email"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
let result = validator.validate(&data);
|
|
371
|
+
assert!(result.is_err(), "Validation should fail for nested error");
|
|
372
|
+
|
|
373
|
+
let err = result.unwrap_err();
|
|
374
|
+
assert_eq!(err.errors.len(), 1);
|
|
375
|
+
assert_eq!(err.errors[0].error_type, "string_pattern_mismatch");
|
|
376
|
+
assert_eq!(err.errors[0].loc, vec!["body", "user", "profile", "email"]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn test_nested_object_missing_required_field() {
|
|
381
|
+
let schema = json!({
|
|
382
|
+
"type": "object",
|
|
383
|
+
"properties": {
|
|
384
|
+
"user": {
|
|
385
|
+
"type": "object",
|
|
386
|
+
"properties": {
|
|
387
|
+
"contact": {
|
|
388
|
+
"type": "object",
|
|
389
|
+
"properties": {
|
|
390
|
+
"phone": {
|
|
391
|
+
"type": "string"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"required": ["phone"]
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
"required": ["contact"]
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
"required": ["user"]
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
404
|
+
let data = json!({
|
|
405
|
+
"user": {
|
|
406
|
+
"contact": {}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
let result = validator.validate(&data);
|
|
411
|
+
assert!(result.is_err(), "Validation should fail for missing nested field");
|
|
412
|
+
|
|
413
|
+
let err = result.unwrap_err();
|
|
414
|
+
assert_eq!(err.errors.len(), 1);
|
|
415
|
+
assert_eq!(err.errors[0].error_type, "missing");
|
|
416
|
+
assert_eq!(err.errors[0].loc, vec!["body", "user", "contact", "phone"]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn test_error_has_input_value() {
|
|
421
|
+
let schema = json!({
|
|
422
|
+
"type": "object",
|
|
423
|
+
"properties": {
|
|
424
|
+
"count": {
|
|
425
|
+
"type": "integer",
|
|
426
|
+
"minimum": 0
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
"required": ["count"]
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
433
|
+
let data = json!({
|
|
434
|
+
"count": -10
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
let result = validator.validate(&data);
|
|
438
|
+
assert!(result.is_err());
|
|
439
|
+
|
|
440
|
+
let err = result.unwrap_err();
|
|
441
|
+
assert_eq!(err.errors[0].input, -10);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
#[test]
|
|
445
|
+
fn test_error_context_includes_constraints() {
|
|
446
|
+
let schema = json!({
|
|
447
|
+
"type": "object",
|
|
448
|
+
"properties": {
|
|
449
|
+
"code": {
|
|
450
|
+
"type": "string",
|
|
451
|
+
"minLength": 5,
|
|
452
|
+
"maxLength": 10
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"required": ["code"]
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
459
|
+
let data = json!({
|
|
460
|
+
"code": "ab"
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
let result = validator.validate(&data);
|
|
464
|
+
assert!(result.is_err());
|
|
465
|
+
|
|
466
|
+
let err = result.unwrap_err();
|
|
467
|
+
assert!(err.errors[0].ctx.is_some());
|
|
468
|
+
let ctx = err.errors[0].ctx.as_ref().unwrap();
|
|
469
|
+
assert_eq!(ctx.get("min_length").and_then(|v| v.as_u64()), Some(5));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#[test]
|
|
473
|
+
fn test_error_context_for_enum() {
|
|
474
|
+
let schema = json!({
|
|
475
|
+
"type": "object",
|
|
476
|
+
"properties": {
|
|
477
|
+
"level": {
|
|
478
|
+
"type": "string",
|
|
479
|
+
"enum": ["low", "medium", "high"]
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
"required": ["level"]
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
486
|
+
let data = json!({
|
|
487
|
+
"level": "critical"
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
let result = validator.validate(&data);
|
|
491
|
+
assert!(result.is_err());
|
|
492
|
+
|
|
493
|
+
let err = result.unwrap_err();
|
|
494
|
+
assert!(err.errors[0].ctx.is_some());
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#[test]
|
|
498
|
+
fn test_problem_details_structure() {
|
|
499
|
+
let problem = ProblemDetails::new(
|
|
500
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
501
|
+
"Validation Error",
|
|
502
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
503
|
+
)
|
|
504
|
+
.with_detail("1 validation error in request");
|
|
505
|
+
|
|
506
|
+
assert_eq!(problem.type_uri, ProblemDetails::TYPE_VALIDATION_ERROR);
|
|
507
|
+
assert_eq!(problem.title, "Validation Error");
|
|
508
|
+
assert_eq!(problem.status, 422);
|
|
509
|
+
assert_eq!(problem.detail, Some("1 validation error in request".to_string()));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#[test]
|
|
513
|
+
fn test_problem_details_with_instance() {
|
|
514
|
+
let problem = ProblemDetails::new(
|
|
515
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
516
|
+
"Validation Error",
|
|
517
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
518
|
+
)
|
|
519
|
+
.with_instance("/api/users");
|
|
520
|
+
|
|
521
|
+
assert_eq!(problem.instance, Some("/api/users".to_string()));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn test_problem_details_with_extensions() {
|
|
526
|
+
let mut problem = ProblemDetails::new(
|
|
527
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
528
|
+
"Validation Error",
|
|
529
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
problem = problem.with_extensions(json!({
|
|
533
|
+
"errors": [{
|
|
534
|
+
"type": "missing",
|
|
535
|
+
"loc": ["body", "email"],
|
|
536
|
+
"msg": "Field required"
|
|
537
|
+
}]
|
|
538
|
+
}));
|
|
539
|
+
|
|
540
|
+
assert!(problem.extensions.contains_key("errors"));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#[test]
|
|
544
|
+
fn test_problem_details_not_found() {
|
|
545
|
+
let problem = ProblemDetails::new(ProblemDetails::TYPE_NOT_FOUND, "Not Found", StatusCode::NOT_FOUND)
|
|
546
|
+
.with_detail("Resource not found");
|
|
547
|
+
|
|
548
|
+
assert_eq!(problem.status, 404);
|
|
549
|
+
assert_eq!(problem.type_uri, ProblemDetails::TYPE_NOT_FOUND);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#[test]
|
|
553
|
+
fn test_problem_details_internal_server_error() {
|
|
554
|
+
let problem = ProblemDetails::new(
|
|
555
|
+
ProblemDetails::TYPE_INTERNAL_SERVER_ERROR,
|
|
556
|
+
"Internal Server Error",
|
|
557
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
558
|
+
)
|
|
559
|
+
.with_detail("An unexpected error occurred");
|
|
560
|
+
|
|
561
|
+
assert_eq!(problem.status, 500);
|
|
562
|
+
assert_eq!(problem.type_uri, ProblemDetails::TYPE_INTERNAL_SERVER_ERROR);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#[test]
|
|
566
|
+
fn test_problem_details_serialization() {
|
|
567
|
+
let problem = ProblemDetails::new(
|
|
568
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
569
|
+
"Validation Error",
|
|
570
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
571
|
+
)
|
|
572
|
+
.with_detail("Validation failed");
|
|
573
|
+
|
|
574
|
+
let serialized = serde_json::to_value(&problem).expect("Failed to serialize");
|
|
575
|
+
assert_eq!(serialized["type"], ProblemDetails::TYPE_VALIDATION_ERROR);
|
|
576
|
+
assert_eq!(serialized["title"], "Validation Error");
|
|
577
|
+
assert_eq!(serialized["status"], 422);
|
|
578
|
+
assert_eq!(serialized["detail"], "Validation failed");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#[test]
|
|
582
|
+
fn test_error_messages_do_not_leak_schema_paths() {
|
|
583
|
+
let schema = json!({
|
|
584
|
+
"type": "object",
|
|
585
|
+
"properties": {
|
|
586
|
+
"password": {
|
|
587
|
+
"type": "string",
|
|
588
|
+
"minLength": 8
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
"required": ["password"]
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
595
|
+
let data = json!({
|
|
596
|
+
"password": "short"
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
let result = validator.validate(&data);
|
|
600
|
+
assert!(result.is_err());
|
|
601
|
+
|
|
602
|
+
let err = result.unwrap_err();
|
|
603
|
+
let msg = &err.errors[0].msg;
|
|
604
|
+
assert!(!msg.contains("properties/password"));
|
|
605
|
+
assert!(!msg.contains("minLength"));
|
|
606
|
+
assert!(msg.contains("at least 8"));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
fn test_error_messages_are_user_friendly() {
|
|
611
|
+
let schema = json!({
|
|
612
|
+
"type": "object",
|
|
613
|
+
"properties": {
|
|
614
|
+
"age": {
|
|
615
|
+
"type": "integer",
|
|
616
|
+
"minimum": 18
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
"required": ["age"]
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
623
|
+
let data = json!({
|
|
624
|
+
"age": 15
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
let result = validator.validate(&data);
|
|
628
|
+
assert!(result.is_err());
|
|
629
|
+
|
|
630
|
+
let err = result.unwrap_err();
|
|
631
|
+
let msg = &err.errors[0].msg;
|
|
632
|
+
assert!(msg.contains("18") || msg.contains("minimum"));
|
|
633
|
+
assert!(!msg.contains("exclusiveMinimum"));
|
|
634
|
+
assert!(!msg.contains("$"));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
#[test]
|
|
638
|
+
fn test_array_too_few_items_error() {
|
|
639
|
+
let schema = json!({
|
|
640
|
+
"type": "object",
|
|
641
|
+
"properties": {
|
|
642
|
+
"tags": {
|
|
643
|
+
"type": "array",
|
|
644
|
+
"minItems": 1
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
"required": ["tags"]
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
651
|
+
let data = json!({
|
|
652
|
+
"tags": []
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
let result = validator.validate(&data);
|
|
656
|
+
assert!(result.is_err(), "Validation should fail for array with too few items");
|
|
657
|
+
|
|
658
|
+
let err = result.unwrap_err();
|
|
659
|
+
assert_eq!(err.errors[0].error_type, "too_short");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
fn test_array_item_type_validation() {
|
|
664
|
+
let schema = json!({
|
|
665
|
+
"type": "object",
|
|
666
|
+
"properties": {
|
|
667
|
+
"ids": {
|
|
668
|
+
"type": "array",
|
|
669
|
+
"items": {
|
|
670
|
+
"type": "integer"
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
"required": ["ids"]
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
678
|
+
let data = json!({
|
|
679
|
+
"ids": [1, 2, "three", 4]
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
let result = validator.validate(&data);
|
|
683
|
+
assert!(result.is_err(), "Validation should fail for invalid array item");
|
|
684
|
+
|
|
685
|
+
let err = result.unwrap_err();
|
|
686
|
+
assert_eq!(err.errors.len(), 1);
|
|
687
|
+
assert_eq!(err.errors[0].error_type, "type_error");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[test]
|
|
691
|
+
fn test_datetime_format_error() {
|
|
692
|
+
let schema = json!({
|
|
693
|
+
"type": "object",
|
|
694
|
+
"properties": {
|
|
695
|
+
"timestamp": {
|
|
696
|
+
"type": "string",
|
|
697
|
+
"format": "date-time"
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
"required": ["timestamp"]
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
704
|
+
let data = json!({
|
|
705
|
+
"timestamp": "not-a-datetime"
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
let result = validator.validate(&data);
|
|
709
|
+
assert!(result.is_err(), "Validation should fail for invalid datetime");
|
|
710
|
+
|
|
711
|
+
let err = result.unwrap_err();
|
|
712
|
+
assert_eq!(err.errors[0].error_type, "datetime_parsing");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
#[test]
|
|
716
|
+
fn test_datetime_format_success() {
|
|
717
|
+
let schema = json!({
|
|
718
|
+
"type": "object",
|
|
719
|
+
"properties": {
|
|
720
|
+
"timestamp": {
|
|
721
|
+
"type": "string",
|
|
722
|
+
"format": "date-time"
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
"required": ["timestamp"]
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
729
|
+
let data = json!({
|
|
730
|
+
"timestamp": "2024-12-25T10:30:00Z"
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
let result = validator.validate(&data);
|
|
734
|
+
assert!(result.is_ok(), "Validation should succeed for valid ISO 8601 datetime");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#[test]
|
|
738
|
+
fn test_additional_properties_error() {
|
|
739
|
+
let schema = json!({
|
|
740
|
+
"type": "object",
|
|
741
|
+
"properties": {
|
|
742
|
+
"name": {
|
|
743
|
+
"type": "string"
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
"required": ["name"],
|
|
747
|
+
"additionalProperties": false
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
let validator = SchemaValidator::new(schema).expect("Failed to create validator");
|
|
751
|
+
let data = json!({
|
|
752
|
+
"name": "Alice",
|
|
753
|
+
"extra_field": "not allowed"
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
let result = validator.validate(&data);
|
|
757
|
+
assert!(result.is_err(), "Validation should fail for additional properties");
|
|
758
|
+
|
|
759
|
+
let err = result.unwrap_err();
|
|
760
|
+
assert_eq!(err.errors[0].error_type, "validation_error");
|
|
761
|
+
}
|