expect_json/expect/ops/
expect_iso_date_time.rs1use crate::JsonType;
2use crate::expect::ops::utils::DurationFormatter;
3use crate::expect_core::Context;
4use crate::expect_core::ExpectOp;
5use crate::expect_core::ExpectOpError;
6use crate::expect_core::ExpectOpResult;
7use crate::expect_core::expect_op;
8use chrono::DateTime;
9use chrono::Duration as ChronoDuration;
10use chrono::FixedOffset;
11use chrono::Offset;
12use chrono::Utc;
13use std::time::Duration as StdDuration;
14
15#[expect_op(internal, name = "iso_date_time")]
47#[derive(Debug, Clone, Default, PartialEq)]
48pub struct ExpectIsoDateTime {
49 is_utc_only: bool,
50 maybe_past_duration: Option<StdDuration>,
51 maybe_future_duration: Option<StdDuration>,
52}
53
54impl ExpectIsoDateTime {
55 pub(crate) fn new() -> Self {
56 Self {
57 is_utc_only: false,
58 maybe_past_duration: None,
59 maybe_future_duration: None,
60 }
61 }
62
63 pub fn utc(self) -> Self {
98 Self {
99 is_utc_only: true,
100 ..self
101 }
102 }
103
104 pub fn within_past(self, duration: StdDuration) -> Self {
142 Self {
143 maybe_past_duration: Some(duration),
144 ..self
145 }
146 }
147
148 pub fn within_future(self, duration: StdDuration) -> Self {
185 Self {
186 maybe_future_duration: Some(duration),
187 ..self
188 }
189 }
190}
191
192impl ExpectOp for ExpectIsoDateTime {
193 fn on_string(&self, context: &mut Context, received: &str) -> ExpectOpResult<()> {
194 let date_time = DateTime::<FixedOffset>::parse_from_rfc3339(received).map_err(|error| {
195 let error_message = format!("failed to parse string '{received}' as iso date time");
196 ExpectOpError::custom_error(self, context, error_message, error)
197 })?;
198
199 if self.is_utc_only {
200 let is_date_time_utc = date_time.offset().fix().utc_minus_local() != 0;
201 if is_date_time_utc {
202 let error_message = format!(
203 "ISO datetime '{received}' is using a non-UTC timezone, expected UTC only"
204 );
205 return Err(ExpectOpError::custom(self, context, error_message));
206 }
207 }
208
209 match (self.maybe_past_duration, self.maybe_future_duration) {
210 (None, None) => {}
211 (Some(past_duration), None) => {
212 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
213 if is_date_time_outside_past {
214 let duration =
215 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
216 let error_message = format!(
217 "ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now"
218 );
219 return Err(ExpectOpError::custom(self, context, error_message));
220 }
221
222 let is_date_time_ahead_of_now = date_time > Utc::now();
223 if is_date_time_ahead_of_now {
224 let duration =
225 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
226 let error_message = format!(
227 "ISO datetime '{received}' is in the future of now, expected between '{duration}' ago and now"
228 );
229 return Err(ExpectOpError::custom(self, context, error_message));
230 }
231 }
232 (None, Some(future_duration)) => {
233 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
234 if is_date_time_outside_future {
235 let duration =
236 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
237 let error_message = format!(
238 "ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future"
239 );
240 return Err(ExpectOpError::custom(self, context, error_message));
241 }
242
243 let is_date_time_behind_of_now = date_time < Utc::now();
244 if is_date_time_behind_of_now {
245 let duration =
246 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
247 let error_message = format!(
248 "ISO datetime '{received}' is in the past of now, expected between now and '{duration}' in the future"
249 );
250 return Err(ExpectOpError::custom(self, context, error_message));
251 }
252 }
253 (Some(past_duration), Some(future_duration)) => {
254 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
255 if is_date_time_outside_past {
256 let duration =
257 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
258 let error_message = format!(
259 "ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now"
260 );
261 return Err(ExpectOpError::custom(self, context, error_message));
262 }
263
264 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
265 if is_date_time_outside_future {
266 let duration =
267 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
268 let error_message = format!(
269 "ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future"
270 );
271 return Err(ExpectOpError::custom(self, context, error_message));
272 }
273 }
274 }
275
276 Ok(())
277 }
278
279 fn debug_supported_types(&self) -> &'static [JsonType] {
280 &[JsonType::String]
281 }
282}
283
284#[cfg(test)]
285mod test_iso_date_time {
286 use crate::expect;
287 use crate::expect_json_eq;
288 use pretty_assertions::assert_eq;
289 use serde_json::json;
290
291 #[test]
292 fn it_should_parse_iso_datetime_with_utc_timezone() {
293 let left = json!("2024-01-15T13:45:30Z");
294 let right = json!(expect::iso_date_time());
295
296 let output = expect_json_eq(&left, &right);
297 assert!(output.is_ok(), "assertion error: {output:#?}");
298 }
299
300 #[test]
301 fn it_should_parse_iso_datetime_with_non_utc_timezone_by_default() {
302 let left = json!("2024-01-15T13:45:30+01:00");
303 let right = json!(expect::iso_date_time());
304
305 let output = expect_json_eq(&left, &right);
306 assert!(output.is_ok(), "assertion error: {output:#?}");
307 }
308
309 #[test]
310 fn it_should_fail_to_parse_iso_datetime_without_timezone() {
311 let left = json!("2024-01-15T13:45:30");
312 let right = json!(expect::iso_date_time());
313
314 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
315 assert_eq!(
316 output,
317 r#"Json expect::iso_date_time() error at root:
318 failed to parse string '2024-01-15T13:45:30' as iso date time,
319 premature end of input"#
320 );
321 }
322}
323
324#[cfg(test)]
325mod test_utc {
326 use crate::expect;
327 use crate::expect_json_eq;
328 use pretty_assertions::assert_eq;
329 use serde_json::json;
330
331 #[test]
332 fn it_should_parse_iso_datetime_with_utc_timezone_when_set() {
333 let left = json!("2024-01-15T13:45:30Z");
334 let right = json!(expect::iso_date_time().utc());
335
336 let output = expect_json_eq(&left, &right);
337 assert!(output.is_ok(), "assertion error: {output:#?}");
338 }
339
340 #[test]
341 fn it_should_not_parse_iso_datetime_with_non_utc_timezone_when_set() {
342 let left = json!("2024-01-15T13:45:30+01:00");
343 let right = json!(expect::iso_date_time().utc());
344
345 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
346 assert_eq!(
347 output,
348 "Json expect::iso_date_time() error at root:
349 ISO datetime '2024-01-15T13:45:30+01:00' is using a non-UTC timezone, expected UTC only"
350 );
351 }
352}
353
354#[cfg(test)]
355mod test_within_past {
356 use super::*;
357 use crate::expect;
358 use crate::expect_json_eq;
359 use pretty_assertions::assert_eq;
360 use serde_json::json;
361
362 #[test]
363 fn it_should_parse_iso_datetime_within_past_set() {
364 let now = Utc::now();
365 let now_str = (now - ChronoDuration::seconds(30)).to_rfc3339();
366 let left = json!(now_str);
367 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
368
369 let output = expect_json_eq(&left, &right);
370 assert!(output.is_ok(), "assertion error: {output:#?}");
371 }
372
373 #[test]
374 fn it_should_not_parse_iso_datetime_within_past_too_far() {
375 let now = Utc::now();
376 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
377 let left = json!(now_str);
378 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
379
380 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
381 assert_eq!(
382 output,
383 format!(
384 r#"Json expect::iso_date_time() error at root:
385 ISO datetime '{now_str}' is too far from the past, expected between '1 minute' ago and now"#
386 )
387 );
388 }
389
390 #[test]
391 fn it_should_not_parse_iso_datetime_ahead_of_now() {
392 let now = Utc::now();
393 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
394 let left = json!(now_str);
395 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
396
397 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
398 assert_eq!(
399 output,
400 format!(
401 r#"Json expect::iso_date_time() error at root:
402 ISO datetime '{now_str}' is in the future of now, expected between '1 minute' ago and now"#
403 )
404 );
405 }
406}
407
408#[cfg(test)]
409mod test_within_future {
410 use super::*;
411 use crate::expect;
412 use crate::expect_json_eq;
413 use pretty_assertions::assert_eq;
414 use serde_json::json;
415
416 #[test]
417 fn it_should_parse_iso_datetime_within_future_set() {
418 let now = Utc::now();
419 let now_str = (now + ChronoDuration::seconds(30)).to_rfc3339();
420 let left = json!(now_str);
421 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
422
423 let output = expect_json_eq(&left, &right);
424 assert!(output.is_ok(), "assertion error: {output:#?}");
425 }
426
427 #[test]
428 fn it_should_not_parse_iso_datetime_within_past_too_far() {
429 let now = Utc::now();
430 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
431 let left = json!(now_str);
432 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
433
434 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
435 assert_eq!(
436 output,
437 format!(
438 r#"Json expect::iso_date_time() error at root:
439 ISO datetime '{now_str}' is too far in the future, expected between now and '1 minute' in the future"#
440 )
441 );
442 }
443
444 #[test]
445 fn it_should_not_parse_iso_datetime_before_now() {
446 let now = Utc::now();
447 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
448 let left = json!(now_str);
449 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
450
451 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
452 assert_eq!(
453 output,
454 format!(
455 r#"Json expect::iso_date_time() error at root:
456 ISO datetime '{now_str}' is in the past of now, expected between now and '1 minute' in the future"#
457 )
458 );
459 }
460
461 #[test]
462 fn it_should_pass_if_date_within_past_and_future() {
463 let now = Utc::now();
464 let now_str = now.to_rfc3339();
465 let left = json!(now_str);
466 let right = json!(
467 expect::iso_date_time()
468 .within_past(StdDuration::from_secs(60))
469 .within_future(StdDuration::from_secs(60))
470 );
471
472 let output = expect_json_eq(&left, &right);
473 assert!(output.is_ok(), "assertion error: {output:#?}");
474 }
475
476 #[test]
477 fn it_should_fail_if_date_behind_past_and_future() {
478 let now = Utc::now();
479 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
480 let left = json!(now_str);
481 let right = json!(
482 expect::iso_date_time()
483 .within_past(StdDuration::from_secs(60))
484 .within_future(StdDuration::from_secs(60))
485 );
486
487 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
488 assert_eq!(
489 output,
490 format!(
491 r#"Json expect::iso_date_time() error at root:
492 ISO datetime '{now_str}' is too far from the past, expected between '1 minute' ago and now"#
493 )
494 );
495 }
496
497 #[test]
498 fn it_should_fail_if_date_ahead_of_past_and_future() {
499 let now = Utc::now();
500 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
501 let left = json!(now_str);
502 let right = json!(
503 expect::iso_date_time()
504 .within_past(StdDuration::from_secs(60))
505 .within_future(StdDuration::from_secs(60))
506 );
507
508 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
509 assert_eq!(
510 output,
511 format!(
512 r#"Json expect::iso_date_time() error at root:
513 ISO datetime '{now_str}' is too far in the future, expected between now and '1 minute' in the future"#
514 )
515 );
516 }
517}