1use serde_json::{Value as JsonValue};
8use crate::{TestError, TestResult};
9
10pub struct TestAssertions;
12
13impl TestAssertions {
14 pub fn assert_json_eq(actual: &JsonValue, expected: &JsonValue) -> TestResult<()> {
16 if actual != expected {
17 return Err(TestError::Assertion {
18 message: format!("JSON assertion failed:\nExpected: {}\nActual: {}",
19 serde_json::to_string_pretty(expected).unwrap_or_default(),
20 serde_json::to_string_pretty(actual).unwrap_or_default()
21 ),
22 });
23 }
24 Ok(())
25 }
26
27 pub fn assert_json_contains(actual: &JsonValue, expected: &JsonValue) -> TestResult<()> {
29 if !json_contains(actual, expected) {
30 return Err(TestError::Assertion {
31 message: format!("JSON does not contain expected values:\nExpected to contain: {}\nActual: {}",
32 serde_json::to_string_pretty(expected).unwrap_or_default(),
33 serde_json::to_string_pretty(actual).unwrap_or_default()
34 ),
35 });
36 }
37 Ok(())
38 }
39
40 pub fn assert_in_range<T>(value: T, min: T, max: T) -> TestResult<()>
42 where
43 T: PartialOrd + std::fmt::Display,
44 {
45 if value < min || value > max {
46 return Err(TestError::Assertion {
47 message: format!("Value {} is not in range [{}, {}]", value, min, max),
48 });
49 }
50 Ok(())
51 }
52
53 pub fn assert_matches_pattern(text: &str, pattern: &str) -> TestResult<()> {
55 use regex::Regex;
56
57 let regex = Regex::new(pattern).map_err(|e| TestError::Assertion {
58 message: format!("Invalid regex pattern '{}': {}", pattern, e),
59 })?;
60
61 if !regex.is_match(text) {
62 return Err(TestError::Assertion {
63 message: format!("Text '{}' does not match pattern '{}'", text, pattern),
64 });
65 }
66 Ok(())
67 }
68
69 pub fn assert_contains<T, I>(collection: &[T], item: &I) -> TestResult<()>
71 where
72 T: PartialEq<I>,
73 T: std::fmt::Debug,
74 I: std::fmt::Debug,
75 {
76 if !collection.iter().any(|x| x == item) {
77 return Err(TestError::Assertion {
78 message: format!("Collection {:?} does not contain item {:?}", collection, item),
79 });
80 }
81 Ok(())
82 }
83
84 pub fn assert_length<T>(collection: &[T], expected_length: usize) -> TestResult<()>
86 where
87 T: std::fmt::Debug,
88 {
89 if collection.len() != expected_length {
90 return Err(TestError::Assertion {
91 message: format!("Expected collection length {}, got {}: {:?}",
92 expected_length, collection.len(), collection),
93 });
94 }
95 Ok(())
96 }
97
98 pub fn assert_empty<T>(collection: &[T]) -> TestResult<()>
100 where
101 T: std::fmt::Debug,
102 {
103 if !collection.is_empty() {
104 return Err(TestError::Assertion {
105 message: format!("Expected empty collection, got {:?}", collection),
106 });
107 }
108 Ok(())
109 }
110
111 pub fn assert_not_empty<T>(collection: &[T]) -> TestResult<()>
113 where
114 T: std::fmt::Debug,
115 {
116 if collection.is_empty() {
117 return Err(TestError::Assertion {
118 message: "Expected non-empty collection, got empty collection".to_string(),
119 });
120 }
121 Ok(())
122 }
123
124 pub fn assert_all<T, F>(collection: &[T], predicate: F, message: &str) -> TestResult<()>
126 where
127 T: std::fmt::Debug,
128 F: Fn(&T) -> bool,
129 {
130 let failing_items: Vec<&T> = collection.iter().filter(|item| !predicate(item)).collect();
131
132 if !failing_items.is_empty() {
133 return Err(TestError::Assertion {
134 message: format!("{}: failing items: {:?}", message, failing_items),
135 });
136 }
137 Ok(())
138 }
139
140 pub fn assert_any<T, F>(collection: &[T], predicate: F, message: &str) -> TestResult<()>
142 where
143 T: std::fmt::Debug,
144 F: Fn(&T) -> bool,
145 {
146 if !collection.iter().any(predicate) {
147 return Err(TestError::Assertion {
148 message: format!("{}: no items match condition in {:?}", message, collection),
149 });
150 }
151 Ok(())
152 }
153
154 pub fn assert_time_close(
156 actual: chrono::DateTime<chrono::Utc>,
157 expected: chrono::DateTime<chrono::Utc>,
158 tolerance_seconds: i64,
159 ) -> TestResult<()> {
160 let diff = (actual - expected).num_seconds().abs();
161 if diff > tolerance_seconds {
162 return Err(TestError::Assertion {
163 message: format!("Time difference too large: {} seconds (tolerance: {} seconds)",
164 diff, tolerance_seconds),
165 });
166 }
167 Ok(())
168 }
169}
170
171fn json_contains(actual: &JsonValue, expected: &JsonValue) -> bool {
173 match (actual, expected) {
174 (JsonValue::Object(actual_map), JsonValue::Object(expected_map)) => {
175 for (key, expected_value) in expected_map {
176 if let Some(actual_value) = actual_map.get(key) {
177 if !json_contains(actual_value, expected_value) {
178 return false;
179 }
180 } else {
181 return false;
182 }
183 }
184 true
185 },
186 (JsonValue::Array(actual_arr), JsonValue::Array(expected_arr)) => {
187 expected_arr.iter().all(|expected_item| {
189 actual_arr.iter().any(|actual_item| json_contains(actual_item, expected_item))
190 })
191 },
192 _ => actual == expected,
193 }
194}
195
196#[macro_export]
198macro_rules! assert_json_eq {
199 ($actual:expr, $expected:expr) => {
200 $crate::assertions::TestAssertions::assert_json_eq($actual, $expected)?
201 };
202}
203
204#[macro_export]
205macro_rules! assert_json_contains {
206 ($actual:expr, $expected:expr) => {
207 $crate::assertions::TestAssertions::assert_json_contains($actual, $expected)?
208 };
209}
210
211#[macro_export]
212macro_rules! assert_in_range {
213 ($value:expr, $min:expr, $max:expr) => {
214 $crate::assertions::TestAssertions::assert_in_range($value, $min, $max)?
215 };
216}
217
218#[macro_export]
219macro_rules! assert_matches {
220 ($text:expr, $pattern:expr) => {
221 $crate::assertions::TestAssertions::assert_matches_pattern($text, $pattern)?
222 };
223}
224
225#[macro_export]
226macro_rules! assert_contains {
227 ($collection:expr, $item:expr) => {
228 $crate::assertions::TestAssertions::assert_contains($collection, $item)?
229 };
230}
231
232#[macro_export]
233macro_rules! assert_length {
234 ($collection:expr, $length:expr) => {
235 $crate::assertions::TestAssertions::assert_length($collection, $length)?
236 };
237}
238
239#[macro_export]
240macro_rules! assert_empty {
241 ($collection:expr) => {
242 $crate::assertions::TestAssertions::assert_empty($collection)?
243 };
244}
245
246#[macro_export]
247macro_rules! assert_not_empty {
248 ($collection:expr) => {
249 $crate::assertions::TestAssertions::assert_not_empty($collection)?
250 };
251}
252
253#[macro_export]
254macro_rules! assert_all {
255 ($collection:expr, $predicate:expr, $message:expr) => {
256 $crate::assertions::TestAssertions::assert_all($collection, $predicate, $message)?
257 };
258}
259
260#[macro_export]
261macro_rules! assert_any {
262 ($collection:expr, $predicate:expr, $message:expr) => {
263 $crate::assertions::TestAssertions::assert_any($collection, $predicate, $message)?
264 };
265}
266
267#[macro_export]
268macro_rules! assert_time_close {
269 ($actual:expr, $expected:expr, $tolerance:expr) => {
270 $crate::assertions::TestAssertions::assert_time_close($actual, $expected, $tolerance)?
271 };
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use serde_json::json;
278 use chrono::{Utc, Duration};
279
280 #[test]
281 fn test_json_equality() -> TestResult<()> {
282 let json1 = json!({"name": "John", "age": 30});
283 let json2 = json!({"name": "John", "age": 30});
284 let json3 = json!({"name": "Jane", "age": 25});
285
286 TestAssertions::assert_json_eq(&json1, &json2)?;
287
288 let result = TestAssertions::assert_json_eq(&json1, &json3);
289 assert!(result.is_err());
290
291 Ok(())
292 }
293
294 #[test]
295 fn test_json_contains() -> TestResult<()> {
296 let actual = json!({
297 "user": {
298 "name": "John",
299 "age": 30,
300 "active": true
301 },
302 "posts": [
303 {"title": "Post 1"},
304 {"title": "Post 2"}
305 ]
306 });
307
308 let expected = json!({
309 "user": {
310 "name": "John"
311 }
312 });
313
314 TestAssertions::assert_json_contains(&actual, &expected)?;
315
316 let expected_fail = json!({
317 "user": {
318 "name": "Jane"
319 }
320 });
321
322 let result = TestAssertions::assert_json_contains(&actual, &expected_fail);
323 assert!(result.is_err());
324
325 Ok(())
326 }
327
328 #[test]
329 fn test_range_assertion() -> TestResult<()> {
330 TestAssertions::assert_in_range(5, 1, 10)?;
331
332 let result = TestAssertions::assert_in_range(15, 1, 10);
333 assert!(result.is_err());
334
335 Ok(())
336 }
337
338 #[test]
339 fn test_pattern_matching() -> TestResult<()> {
340 TestAssertions::assert_matches_pattern("test@example.com", r"^[^@]+@[^@]+\.[^@]+$")?;
341
342 let result = TestAssertions::assert_matches_pattern("invalid-email", r"^[^@]+@[^@]+\.[^@]+$");
343 assert!(result.is_err());
344
345 Ok(())
346 }
347
348 #[test]
349 fn test_collection_assertions() -> TestResult<()> {
350 let collection = vec![1, 2, 3, 4, 5];
351
352 TestAssertions::assert_contains(&collection, &3)?;
353 TestAssertions::assert_length(&collection, 5)?;
354 TestAssertions::assert_not_empty(&collection)?;
355
356 TestAssertions::assert_all(&collection, |x| *x > 0, "All items should be positive")?;
357 TestAssertions::assert_any(&collection, |x| *x > 4, "Some items should be greater than 4")?;
358
359 let empty_collection: Vec<i32> = vec![];
360 TestAssertions::assert_empty(&empty_collection)?;
361
362 Ok(())
363 }
364
365 #[test]
366 fn test_time_assertion() -> TestResult<()> {
367 let now = Utc::now();
368 let close_time = now + Duration::seconds(2);
369 let far_time = now + Duration::seconds(60);
370
371 TestAssertions::assert_time_close(close_time, now, 10)?;
372
373 let result = TestAssertions::assert_time_close(far_time, now, 10);
374 assert!(result.is_err());
375
376 Ok(())
377 }
378
379 #[test]
380 fn test_macro_usage() -> TestResult<()> {
381 let json1 = json!({"test": "value"});
382 let json2 = json!({"test": "value"});
383
384 assert_json_eq!(&json1, &json2);
385 assert_in_range!(5, 1, 10);
386
387 let collection = vec![1, 2, 3];
388 assert_contains!(&collection, &2);
389 assert_length!(&collection, 3);
390
391 Ok(())
392 }
393}