1use std::fmt;
31
32use turnframe_core::case::CaseKey;
33use turnframe_core::interaction::InteractionStatus;
34
35use crate::corpus::{
36 ActExpectation, Expectations, InteractionStatusExpectation, OutcomeExpectation,
37 RevisionExpectation, StateExpectation, TargetExpectation,
38};
39use crate::observation::Observation;
40
41#[derive(
47 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
48)]
49#[serde(rename_all = "snake_case")]
50#[non_exhaustive]
51pub enum ExpectationName {
52 Outcome,
54 CaseState,
56 WorkflowState,
58 CaseCount,
60 Acts,
62 TargetResolution,
64 Commands,
66 ForbiddenCommand,
68 Events,
70 ForbiddenEvent,
72 TruncatedLedger,
76 CaseRevision,
78 InteractionStatus,
80 ResponseBlocks,
82 TurnPhase,
84}
85
86impl ExpectationName {
87 #[must_use]
89 pub const fn as_str(self) -> &'static str {
90 match self {
91 Self::Outcome => "outcome",
92 Self::Acts => "acts",
93 Self::TargetResolution => "target_resolution",
94 Self::Commands => "commands",
95 Self::ForbiddenCommand => "forbidden_command",
96 Self::Events => "events",
97 Self::ForbiddenEvent => "forbidden_event",
98 Self::TruncatedLedger => "truncated_ledger",
99 Self::CaseRevision => "case_revision",
100 Self::CaseState => "case_state",
101 Self::WorkflowState => "workflow_state",
102 Self::CaseCount => "case_count",
103 Self::InteractionStatus => "interaction_status",
104 Self::ResponseBlocks => "response_blocks",
105 Self::TurnPhase => "turn_phase",
106 }
107 }
108}
109
110impl fmt::Display for ExpectationName {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct AssertionFailure {
120 pub expectation: ExpectationName,
122 pub expected: String,
124 pub actual: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub subject: Option<String>,
129}
130
131impl AssertionFailure {
132 #[must_use]
134 pub fn new(
135 expectation: ExpectationName,
136 expected: impl Into<String>,
137 actual: impl Into<String>,
138 ) -> Self {
139 Self {
140 expectation,
141 expected: expected.into(),
142 actual: actual.into(),
143 subject: None,
144 }
145 }
146
147 #[must_use]
149 pub fn about(mut self, subject: impl Into<String>) -> Self {
150 self.subject = Some(subject.into());
151 self
152 }
153}
154
155impl fmt::Display for AssertionFailure {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{}", self.expectation)?;
158 if let Some(subject) = &self.subject {
159 write!(f, " [{subject}]")?;
160 }
161 write!(f, ": expected {}, got {}", self.expected, self.actual)
162 }
163}
164
165#[must_use]
171pub fn check(expect: &Expectations, observed: &Observation) -> Vec<AssertionFailure> {
172 let mut failures = Vec::new();
173 check_outcome(&expect.outcome, observed, &mut failures);
174 check_acts(expect.acts.as_deref(), observed, &mut failures);
175 check_resolutions(&expect.target_resolution, observed, &mut failures);
176 check_state(&expect.case_state, observed, &mut failures);
177 check_workflow_state(&expect.workflow_state, observed, &mut failures);
178 check_case_count(&expect.case_count, observed, &mut failures);
179 check_sequence(
180 ExpectationName::Commands,
181 expect.commands.as_deref(),
182 &observed.commands,
183 &mut failures,
184 );
185 check_sequence(
186 ExpectationName::Events,
187 expect.events.as_deref(),
188 &observed.events,
189 &mut failures,
190 );
191 check_forbidden(
192 ExpectationName::ForbiddenCommand,
193 &expect.forbid.commands,
194 &observed.commands,
195 &mut failures,
196 );
197 check_forbidden(
198 ExpectationName::ForbiddenEvent,
199 &expect.forbid.events,
200 &observed.events,
201 &mut failures,
202 );
203 check_ledger(expect, observed, &mut failures);
204 check_revisions(&expect.case_revision, observed, &mut failures);
205 check_interactions(&expect.interaction_status, observed, &mut failures);
206 check_blocks(expect, observed, &mut failures);
207 check_phase(expect, observed, &mut failures);
208 failures
209}
210
211fn check_outcome(
212 expected: &OutcomeExpectation,
213 observed: &Observation,
214 failures: &mut Vec<AssertionFailure>,
215) {
216 let actual = observed.error_code.as_deref();
217 match (expected, actual) {
218 (OutcomeExpectation::Succeeds, None) | (OutcomeExpectation::Fails, Some(_)) => {}
219 (OutcomeExpectation::Succeeds, Some(code)) => failures.push(AssertionFailure::new(
220 ExpectationName::Outcome,
221 "the turn to complete",
222 format!("it failed with `{code}`"),
223 )),
224 (OutcomeExpectation::Fails, None) => failures.push(AssertionFailure::new(
225 ExpectationName::Outcome,
226 "the turn to fail",
227 "it completed",
228 )),
229 (OutcomeExpectation::FailsWith(code), Some(found)) if code.as_str() == found => {}
230 (OutcomeExpectation::FailsWith(code), found) => failures.push(AssertionFailure::new(
231 ExpectationName::Outcome,
232 format!("the turn to fail with `{code}`"),
233 found.map_or_else(|| "it completed".to_owned(), |code| format!("`{code}`")),
234 )),
235 }
236}
237
238fn check_acts(
239 expected: Option<&[ActExpectation]>,
240 observed: &Observation,
241 failures: &mut Vec<AssertionFailure>,
242) {
243 let Some(expected) = expected else {
244 return;
245 };
246 let rendered_actual = render(observed.acts.iter().map(|act| match &act.operation {
247 Some(operation) => format!("{}({operation})", act.kind),
248 None => act.kind.clone(),
249 }));
250 let rendered_expected = render(expected.iter().map(render_expected_act));
251
252 if expected.len() != observed.acts.len() {
253 failures.push(AssertionFailure::new(
254 ExpectationName::Acts,
255 rendered_expected,
256 rendered_actual,
257 ));
258 return;
259 }
260 for (want, got) in expected.iter().zip(&observed.acts) {
261 if !want.admits(&got.kind, got.operation.as_ref()) {
262 failures.push(AssertionFailure::new(
263 ExpectationName::Acts,
264 rendered_expected,
265 rendered_actual,
266 ));
267 return;
268 }
269 }
270}
271
272fn render_expected_act(act: &ActExpectation) -> String {
278 let mut shapes = Vec::new();
279 collect_shapes(act, &mut shapes);
280 shapes.join(" | ")
281}
282
283fn collect_shapes(act: &ActExpectation, into: &mut Vec<String>) {
284 into.push(match &act.operation {
285 Some(operation) => format!("{}({operation})", act.kind),
286 None => act.kind.to_string(),
287 });
288 for alternative in &act.or {
289 collect_shapes(alternative, into);
290 }
291}
292
293fn check_resolutions(
294 expected: &[TargetExpectation],
295 observed: &Observation,
296 failures: &mut Vec<AssertionFailure>,
297) {
298 for want in expected {
299 let Some(got) = observed
300 .target_resolutions
301 .iter()
302 .find(|entry| entry.act_index == want.act_index)
303 else {
304 failures.push(
305 AssertionFailure::new(
306 ExpectationName::TargetResolution,
307 want.resolution.to_string(),
308 "no resolution was recorded for that act".to_owned(),
309 )
310 .about(format!("act {}", want.act_index)),
311 );
312 continue;
313 };
314 let case_differs = want
315 .case_id
316 .as_ref()
317 .is_some_and(|case| Some(case) != got.case_id.as_ref());
318 if want.resolution.as_str() != got.resolution || case_differs {
319 failures.push(
320 AssertionFailure::new(
321 ExpectationName::TargetResolution,
322 describe_resolution(want.resolution.as_str(), want.case_id.as_ref()),
323 describe_resolution(&got.resolution, got.case_id.as_ref()),
324 )
325 .about(format!("act {}", want.act_index)),
326 );
327 }
328 }
329}
330
331fn describe_resolution(kind: &str, case: Option<&turnframe_core::ids::CaseId>) -> String {
332 match case {
333 Some(case) => format!("{kind} on {case}"),
334 None => kind.to_owned(),
335 }
336}
337
338fn check_sequence(
339 name: ExpectationName,
340 expected: Option<&[String]>,
341 actual: &[String],
342 failures: &mut Vec<AssertionFailure>,
343) {
344 let Some(expected) = expected else {
345 return;
346 };
347 if expected != actual {
348 failures.push(AssertionFailure::new(
349 name,
350 render(expected.iter().cloned()),
351 render(actual.iter().cloned()),
352 ));
353 }
354}
355
356fn check_forbidden(
358 name: ExpectationName,
359 forbidden: &[String],
360 actual: &[String],
361 failures: &mut Vec<AssertionFailure>,
362) {
363 for banned in forbidden {
364 if actual.contains(banned) {
365 failures.push(AssertionFailure::new(
366 name,
367 format!("`{banned}` never to appear"),
368 format!("it appeared in {}", render(actual.iter().cloned())),
369 ));
370 }
371 }
372}
373
374fn check_ledger(
381 expect: &Expectations,
382 observed: &Observation,
383 failures: &mut Vec<AssertionFailure>,
384) {
385 if !observed.events_truncated {
386 return;
387 }
388 if expect.events.is_none() && expect.forbid.events.is_empty() {
389 return;
390 }
391 failures.push(AssertionFailure::new(
392 ExpectationName::TruncatedLedger,
393 "the whole event ledger of the turn",
394 format!(
395 "the first {} events only, because max_observed_events cut the read short",
396 observed.events.len()
397 ),
398 ));
399}
400
401fn check_revisions(
402 expected: &[RevisionExpectation],
403 observed: &Observation,
404 failures: &mut Vec<AssertionFailure>,
405) {
406 for want in expected {
407 let case = CaseKey::new(want.workflow.clone(), want.case_id.clone());
408 let subject = format!("{}/{}", case.workflow, case.case_id);
409 match observed.revision_of(&case) {
410 Some(revision) if revision == want.revision => {}
411 Some(revision) => failures.push(
412 AssertionFailure::new(
413 ExpectationName::CaseRevision,
414 format!("revision {}", want.revision),
415 format!("revision {revision}"),
416 )
417 .about(subject),
418 ),
419 None => failures.push(
420 AssertionFailure::new(
421 ExpectationName::CaseRevision,
422 format!("revision {}", want.revision),
423 "the case was not observed".to_owned(),
424 )
425 .about(subject),
426 ),
427 }
428 }
429}
430
431fn check_interactions(
432 expected: &[InteractionStatusExpectation],
433 observed: &Observation,
434 failures: &mut Vec<AssertionFailure>,
435) {
436 for want in expected {
437 let case = CaseKey::new(want.workflow.clone(), want.case_id.clone());
438 let actual: Vec<InteractionStatus> = observed
439 .interactions_of(&case)
440 .into_iter()
441 .map(|card| card.status)
442 .collect();
443 if actual != want.statuses {
444 failures.push(
445 AssertionFailure::new(
446 ExpectationName::InteractionStatus,
447 render_debug(&want.statuses),
448 render_debug(&actual),
449 )
450 .about(format!("{}/{}", case.workflow, case.case_id)),
451 );
452 }
453 }
454}
455
456fn check_blocks(
457 expect: &Expectations,
458 observed: &Observation,
459 failures: &mut Vec<AssertionFailure>,
460) {
461 let Some(expected) = expect.blocks.as_deref() else {
462 return;
463 };
464 if expected != observed.blocks.as_slice() {
465 failures.push(AssertionFailure::new(
466 ExpectationName::ResponseBlocks,
467 render(expected.iter().map(ToString::to_string)),
468 render(observed.blocks.iter().map(ToString::to_string)),
469 ));
470 }
471}
472
473fn check_phase(
474 expect: &Expectations,
475 observed: &Observation,
476 failures: &mut Vec<AssertionFailure>,
477) {
478 let Some(expected) = expect.turn_phase else {
479 return;
480 };
481 if observed.phase != Some(expected) {
482 failures.push(AssertionFailure::new(
483 ExpectationName::TurnPhase,
484 format!("{expected:?}"),
485 observed.phase.map_or_else(
486 || "no phase marker".to_owned(),
487 |phase| format!("{phase:?}"),
488 ),
489 ));
490 }
491}
492
493fn render<I: IntoIterator<Item = String>>(items: I) -> String {
494 let joined: Vec<String> = items.into_iter().collect();
495 format!("[{}]", joined.join(", "))
496}
497
498fn render_debug<T: fmt::Debug>(items: &[T]) -> String {
499 let joined: Vec<String> = items.iter().map(|item| format!("{item:?}")).collect();
500 format!("[{}]", joined.join(", "))
501}
502
503fn same_value(
509 found: Option<&serde_json::Value>,
510 wanted: &serde_json::Value,
511 ignore_case: bool,
512) -> bool {
513 match (found, ignore_case) {
514 (Some(serde_json::Value::String(found)), true) => wanted
518 .as_str()
519 .is_some_and(|wanted| found.to_lowercase() == wanted.to_lowercase()),
520 (found, _) => found == Some(wanted),
521 }
522}
523
524fn check_state(
531 expected: &[StateExpectation],
532 observed: &Observation,
533 failures: &mut Vec<AssertionFailure>,
534) {
535 for expectation in expected {
536 let mut matching = observed
544 .states
545 .iter()
546 .filter(|(case, _)| case.case_id == expectation.case_id);
547 let found = matching.next();
548 if matching.next().is_some() {
549 failures.push(
550 AssertionFailure::new(
551 ExpectationName::CaseState,
552 &expectation.path,
553 "two seeded workflows hold a case with this id, so the expectation does not say which record it is about",
554 )
555 .about(expectation.case_id.as_str()),
556 );
557 continue;
558 }
559 let Some((_, state)) = found else {
560 failures.push(
561 AssertionFailure::new(
562 ExpectationName::CaseState,
563 &expectation.path,
564 "the case was not read back",
565 )
566 .about(expectation.case_id.as_str()),
567 );
568 continue;
569 };
570 let after = state
571 .after
572 .as_ref()
573 .and_then(|v| v.pointer(&expectation.path));
574 if let Some(wanted) = &expectation.equals {
575 let found =
576 after.map_or_else(|| String::from("nothing at that path"), ToString::to_string);
577 if !same_value(after, wanted, expectation.ignore_case) {
578 failures.push(
579 AssertionFailure::new(ExpectationName::CaseState, wanted.to_string(), &found)
580 .about(format!(
581 "{}{}",
582 expectation.case_id.as_str(),
583 expectation.path
584 )),
585 );
586 }
587 } else if !expectation.one_of.is_empty() {
588 if !expectation
589 .one_of
590 .iter()
591 .any(|wanted| same_value(after, wanted, expectation.ignore_case))
592 {
593 let found =
594 after.map_or_else(|| String::from("nothing at that path"), ToString::to_string);
595 let wanted: Vec<String> =
596 expectation.one_of.iter().map(ToString::to_string).collect();
597 failures.push(
598 AssertionFailure::new(
599 ExpectationName::CaseState,
600 format!("one of {}", wanted.join(", ")),
601 &found,
602 )
603 .about(format!(
604 "{}{}",
605 expectation.case_id.as_str(),
606 expectation.path
607 )),
608 );
609 }
610 } else if expectation.unchanged {
611 let before = state.before.pointer(&expectation.path);
612 if after != before {
613 let was = before.map_or_else(|| String::from("nothing"), ToString::to_string);
614 let now = after.map_or_else(|| String::from("nothing"), ToString::to_string);
615 failures.push(
616 AssertionFailure::new(
617 ExpectationName::CaseState,
618 format!("{was} (unchanged)"),
619 &now,
620 )
621 .about(format!(
622 "{}{}",
623 expectation.case_id.as_str(),
624 expectation.path
625 )),
626 );
627 }
628 } else if expectation.absent {
629 if !matches!(after, None | Some(serde_json::Value::Null)) {
634 let found = after.map_or_else(String::new, ToString::to_string);
635 failures.push(
636 AssertionFailure::new(ExpectationName::CaseState, "nothing there", &found)
637 .about(format!(
638 "{}{}",
639 expectation.case_id.as_str(),
640 expectation.path
641 )),
642 );
643 }
644 }
645 }
646}
647
648fn check_workflow_state(
649 expected: &[crate::corpus::WorkflowStateExpectation],
650 observed: &Observation,
651 failures: &mut Vec<AssertionFailure>,
652) {
653 for expectation in expected {
654 let found: Vec<String> = observed
655 .states
656 .iter()
657 .filter(|(case, _)| case.workflow == expectation.workflow)
658 .map(|(_, state)| {
659 state
660 .after
661 .as_ref()
662 .and_then(|after| after.pointer(&expectation.path))
663 .map_or_else(|| "nothing".to_owned(), ToString::to_string)
664 })
665 .collect();
666 let holds = observed.states.iter().any(|(case, state)| {
667 case.workflow == expectation.workflow
668 && same_value(
669 state
670 .after
671 .as_ref()
672 .and_then(|after| after.pointer(&expectation.path)),
673 &expectation.equals,
674 expectation.ignore_case,
675 )
676 });
677 if !holds {
678 failures.push(
679 AssertionFailure::new(
680 ExpectationName::WorkflowState,
681 expectation.equals.to_string(),
682 format!("[{}]", found.join(", ")),
683 )
684 .about(format!("{}{}", expectation.workflow, expectation.path)),
685 );
686 }
687 }
688}
689
690fn check_case_count(
691 expected: &[crate::corpus::CaseCountExpectation],
692 observed: &Observation,
693 failures: &mut Vec<AssertionFailure>,
694) {
695 for expectation in expected {
696 let count = observed
697 .states
698 .iter()
699 .filter(|(case, state)| case.workflow == expectation.workflow && state.after.is_some())
700 .count();
701 if count != expectation.count {
702 failures.push(
703 AssertionFailure::new(
704 ExpectationName::CaseCount,
705 expectation.count.to_string(),
706 count.to_string(),
707 )
708 .about(expectation.workflow.as_str()),
709 );
710 }
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use turnframe_core::ids::TurnId;
717
718 use super::*;
719 use crate::corpus::{ActKind, BlockKind, ForbiddenEffects, StateExpectation};
720 use crate::observation::ObservedAct;
721
722 fn observation() -> Observation {
723 Observation {
724 turn_id: TurnId::nil(),
725 error_code: None,
726 acts: vec![ObservedAct {
727 kind: "apply_operation".to_owned(),
728 operation: Some("trip.set_name".to_owned()),
729 outcome: Some("ready_to_execute".to_owned()),
730 }],
731 target_resolutions: Vec::new(),
732 understanding: None,
733 commands: vec!["trip.set_name".to_owned()],
734 events: vec!["trip.name_set".to_owned()],
735 events_truncated: false,
736 revisions: Vec::new(),
737 states: Vec::new(),
738 interactions: Vec::new(),
739 blocks: vec![BlockKind::Receipt],
740 phase: None,
741 provider_failures: 0,
742 cards_created: 0,
743 answer: String::new(),
744 discarded_answers: Vec::new(),
745 }
746 }
747
748 fn with_state(before: serde_json::Value, after: Option<serde_json::Value>) -> Observation {
749 let mut observed = observation();
750 observed.states = vec![(
751 turnframe_core::case::CaseKey::new("trip", "trip-1"),
752 crate::observation::ObservedState { before, after },
753 )];
754 observed
755 }
756
757 fn expect_state(expectation: StateExpectation) -> Expectations {
758 Expectations {
759 case_state: vec![expectation],
760 ..Expectations::default()
761 }
762 }
763
764 fn at(path: &str) -> StateExpectation {
765 StateExpectation {
766 case_id: turnframe_core::ids::CaseId::new("trip-1"),
767 path: path.to_owned(),
768 equals: None,
769 one_of: Vec::new(),
770 unchanged: false,
771 absent: false,
772 ignore_case: false,
773 }
774 }
775
776 #[test]
779 fn a_value_may_be_one_of_several_right_readings() {
780 let expectation = StateExpectation {
781 one_of: vec![
782 serde_json::json!("offsite di Lisbona"),
783 serde_json::json!("l'offsite di Lisbona"),
784 ],
785 ignore_case: true,
786 ..at("/name")
787 };
788 assert!(expectation.validate().is_ok());
789 let read = |name: &str| {
790 with_state(
791 serde_json::json!({"name": null}),
792 Some(serde_json::json!({ "name": name })),
793 )
794 };
795 assert!(
796 check(
797 &expect_state(expectation.clone()),
798 &read("L'offsite di Lisbona")
799 )
800 .is_empty()
801 );
802 assert_eq!(
803 check(
804 &expect_state(expectation),
805 &read("il viaggio è per Lisbona")
806 )
807 .len(),
808 1
809 );
810 }
811
812 #[test]
822 fn a_free_text_value_may_be_compared_without_its_case_and_nothing_else_is() {
823 let capitalised = with_state(
824 serde_json::json!({"city": null}),
825 Some(serde_json::json!({"city": "Lisbon"})),
826 );
827 assert!(
828 check(
829 &expect_state(StateExpectation {
830 equals: Some(serde_json::json!("lisbon")),
831 ignore_case: true,
832 ..at("/city")
833 }),
834 &capitalised
835 )
836 .is_empty(),
837 "«lisbon» and «Lisbon» are the same city"
838 );
839
840 assert_eq!(
843 check(
844 &expect_state(StateExpectation {
845 equals: Some(serde_json::json!("lisbon")),
846 ..at("/city")
847 }),
848 &capitalised
849 )
850 .len(),
851 1
852 );
853
854 let elsewhere = with_state(
856 serde_json::json!({"city": null}),
857 Some(serde_json::json!({"city": "Porto"})),
858 );
859 assert_eq!(
860 check(
861 &expect_state(StateExpectation {
862 equals: Some(serde_json::json!("lisbon")),
863 ignore_case: true,
864 ..at("/city")
865 }),
866 &elsewhere
867 )
868 .len(),
869 1,
870 "a different city is a failure whatever the case"
871 );
872 }
873
874 #[test]
876 fn ignoring_the_case_of_an_absence_is_refused_rather_than_ignored() {
877 assert!(
878 StateExpectation {
879 absent: true,
880 ignore_case: true,
881 ..at("/email")
882 }
883 .validate()
884 .is_err()
885 );
886 assert!(
887 StateExpectation {
888 equals: Some(serde_json::json!("lisbon")),
889 ignore_case: true,
890 ..at("/city")
891 }
892 .validate()
893 .is_ok()
894 );
895 }
896
897 #[test]
910 fn a_field_the_user_declined_is_asserted_empty_and_fails_when_something_was_written() {
911 let declined = with_state(
912 serde_json::json!({"email": "old@aurora.example"}),
913 Some(serde_json::json!({"email": null})),
914 );
915 assert!(
916 check(
917 &expect_state(StateExpectation {
918 absent: true,
919 ..at("/email")
920 }),
921 &declined
922 )
923 .is_empty(),
924 "a field left with no value is what a decline looks like"
925 );
926
927 let dropped = with_state(
930 serde_json::json!({"email": "old@aurora.example"}),
931 Some(serde_json::json!({})),
932 );
933 assert!(
934 check(
935 &expect_state(StateExpectation {
936 absent: true,
937 ..at("/email")
938 }),
939 &dropped
940 )
941 .is_empty()
942 );
943
944 let written = with_state(
946 serde_json::json!({"email": null}),
947 Some(serde_json::json!({"email": "made-up@aurora.example"})),
948 );
949 let failures = check(
950 &expect_state(StateExpectation {
951 absent: true,
952 ..at("/email")
953 }),
954 &written,
955 );
956 assert_eq!(failures.len(), 1, "{failures:?}");
957 assert!(
958 failures[0].actual.contains("made-up@aurora.example"),
959 "the failure names what was written: {:?}",
960 failures[0]
961 );
962 }
963
964 #[test]
965 fn a_value_that_ended_up_right_passes_and_a_wrong_one_names_both_sides() {
966 let observed = with_state(
967 serde_json::json!({"gate": "B12"}),
968 Some(serde_json::json!({"gate": "B14"})),
969 );
970 let wanted = StateExpectation {
971 equals: Some(serde_json::json!("B14")),
972 ..at("/gate")
973 };
974 assert!(check(&expect_state(wanted), &observed).is_empty());
975
976 let stale = StateExpectation {
979 equals: Some(serde_json::json!("B12")),
980 ..at("/gate")
981 };
982 let failures = check(&expect_state(stale), &observed);
983 assert_eq!(failures.len(), 1);
984 assert!(failures[0].actual.contains("B14"), "{failures:?}");
985 }
986
987 #[test]
988 fn a_field_that_must_not_move_fails_when_it_moved() {
989 let untouched = with_state(
990 serde_json::json!({"email": "a@b.it"}),
991 Some(serde_json::json!({"email": "a@b.it"})),
992 );
993 let expectation = StateExpectation {
994 unchanged: true,
995 ..at("/email")
996 };
997 assert!(check(&expect_state(expectation.clone()), &untouched).is_empty());
998
999 let moved = with_state(
1000 serde_json::json!({"email": "a@b.it"}),
1001 Some(serde_json::json!({"email": "c@d.it"})),
1002 );
1003 let failures = check(&expect_state(expectation), &moved);
1004 assert_eq!(failures.len(), 1, "{failures:?}");
1005 assert!(failures[0].expected.contains("unchanged"), "{failures:?}");
1006 }
1007
1008 #[test]
1009 fn a_case_that_was_never_read_back_fails_instead_of_passing_quietly() {
1010 let expectation = StateExpectation {
1011 unchanged: true,
1012 ..at("/email")
1013 };
1014 let failures = check(&expect_state(expectation), &observation());
1017 assert_eq!(failures.len(), 1, "{failures:?}");
1018 }
1019
1020 #[test]
1021 fn an_item_that_asserts_nothing_cannot_fail_on_a_completed_turn() {
1022 assert!(check(&Expectations::default(), &observation()).is_empty());
1023 }
1024
1025 fn with_cases(cases: &[(&str, &str, serde_json::Value)]) -> Observation {
1026 let mut observed = observation();
1027 observed.states = cases
1028 .iter()
1029 .map(|(workflow, case_id, after)| {
1030 (
1031 turnframe_core::case::CaseKey::new(*workflow, *case_id),
1032 crate::observation::ObservedState {
1033 before: serde_json::Value::Null,
1034 after: Some(after.clone()),
1035 },
1036 )
1037 })
1038 .collect();
1039 observed
1040 }
1041
1042 #[test]
1043 fn some_case_of_a_workflow_holding_the_value_is_enough() {
1044 let observed = with_cases(&[
1045 ("trip", "trip-1", serde_json::json!({"traveler": null})),
1046 (
1047 "trip",
1048 "tf_2",
1049 serde_json::json!({"traveler": {"display_name": "Nadia Rinaldi"}}),
1050 ),
1051 ]);
1052 let expect = |equals: &str| Expectations {
1053 workflow_state: vec![crate::corpus::WorkflowStateExpectation {
1054 workflow: "trip".into(),
1055 path: "/traveler/display_name".to_owned(),
1056 equals: serde_json::json!(equals),
1057 ignore_case: true,
1058 }],
1059 ..Expectations::default()
1060 };
1061 assert!(check(&expect("nadia rinaldi"), &observed).is_empty());
1062 let failures = check(&expect("Omar"), &observed);
1063 assert_eq!(failures.len(), 1, "{failures:?}");
1064 assert_eq!(failures[0].expectation, ExpectationName::WorkflowState);
1065 }
1066
1067 #[test]
1068 fn the_cases_of_a_workflow_are_counted() {
1069 let observed = with_cases(&[
1070 ("trip", "trip-1", serde_json::json!({})),
1071 ("trip", "tf_2", serde_json::json!({})),
1072 ("traveler", "tf_3", serde_json::json!({})),
1073 ]);
1074 let expect = |count: usize| Expectations {
1075 case_count: vec![crate::corpus::CaseCountExpectation {
1076 workflow: "trip".into(),
1077 count,
1078 }],
1079 ..Expectations::default()
1080 };
1081 assert!(check(&expect(2), &observed).is_empty());
1082 let failures = check(&expect(1), &observed);
1083 assert_eq!(failures.len(), 1, "{failures:?}");
1084 assert_eq!(failures[0].expectation, ExpectationName::CaseCount);
1085 }
1086
1087 #[test]
1088 fn a_wrong_command_names_both_sides() {
1089 let expect = Expectations {
1090 commands: Some(vec!["trip.set_travel_date".to_owned()]),
1091 ..Expectations::default()
1092 };
1093 let failures = check(&expect, &observation());
1094 let message = failures[0].to_string();
1095 assert!(message.contains("trip.set_travel_date"), "{message}");
1096 assert!(message.contains("trip.set_name"), "{message}");
1097 }
1098
1099 #[test]
1100 fn a_forbidden_command_that_appeared_fails() {
1101 let expect = Expectations {
1102 forbid: ForbiddenEffects {
1103 commands: vec!["trip.set_name".to_owned()],
1104 events: Vec::new(),
1105 },
1106 ..Expectations::default()
1107 };
1108 let failures = check(&expect, &observation());
1109 assert_eq!(failures.len(), 1);
1110 assert_eq!(failures[0].expectation, ExpectationName::ForbiddenCommand);
1111 assert!(failures[0].to_string().contains("never to appear"));
1112 }
1113
1114 #[test]
1115 fn an_operation_mismatch_on_a_matching_kind_fails() {
1116 let expect = Expectations {
1117 acts: Some(vec![ActExpectation {
1118 kind: ActKind::ApplyOperation,
1119 operation: Some("trip.rebook".to_owned()),
1120 or: Vec::new(),
1121 }]),
1122 ..Expectations::default()
1123 };
1124 let failures = check(&expect, &observation());
1125 assert_eq!(failures.len(), 1);
1126 assert!(failures[0].to_string().contains("trip.rebook"));
1127 }
1128
1129 #[test]
1134 fn a_position_that_admits_two_shapes_accepts_either_of_them() {
1135 let expect = Expectations {
1136 acts: Some(vec![ActExpectation {
1137 kind: ActKind::StartWorkflow,
1138 operation: None,
1139 or: vec![ActExpectation {
1140 kind: ActKind::ApplyOperation,
1141 operation: Some("trip.set_name".to_owned()),
1142 or: Vec::new(),
1143 }],
1144 }]),
1145 ..Expectations::default()
1146 };
1147 assert!(
1148 check(&expect, &observation())
1149 .iter()
1150 .all(|failure| failure.expectation != ExpectationName::Acts),
1151 "the observed act is the second shape, and the second shape is admitted"
1152 );
1153 }
1154
1155 #[test]
1158 fn a_shape_no_alternative_names_still_fails() {
1159 let expect = Expectations {
1160 acts: Some(vec![ActExpectation {
1161 kind: ActKind::StartWorkflow,
1162 operation: None,
1163 or: vec![ActExpectation {
1164 kind: ActKind::ApplyOperation,
1165 operation: Some("trip.rebook".to_owned()),
1166 or: Vec::new(),
1167 }],
1168 }]),
1169 ..Expectations::default()
1170 };
1171 let failures = check(&expect, &observation());
1172 assert_eq!(failures.len(), 1);
1173 assert_eq!(failures[0].expectation, ExpectationName::Acts);
1174 assert!(
1175 failures[0]
1176 .to_string()
1177 .contains("start_workflow | apply_operation(trip.rebook)"),
1178 "and the report names every shape that would have done: {}",
1179 failures[0]
1180 );
1181 }
1182
1183 #[test]
1184 fn a_crashed_turn_fails_even_when_nothing_else_is_asserted() {
1185 let mut observed = observation();
1186 observed.error_code = Some("store.unavailable".to_owned());
1187 let failures = check(&Expectations::default(), &observed);
1188 assert_eq!(failures[0].expectation, ExpectationName::Outcome);
1189 }
1190
1191 #[test]
1192 fn an_expected_failure_is_not_a_failure() {
1193 let mut observed = observation();
1194 observed.error_code = Some("store.unavailable".to_owned());
1195 let expect = Expectations {
1196 outcome: OutcomeExpectation::FailsWith("store.unavailable".to_owned()),
1197 ..Expectations::default()
1198 };
1199 assert!(check(&expect, &observed).is_empty());
1200 }
1201}