Skip to main content

turnframe_test/strategies/
command.rs

1//! Strategies for command origins and policies (spec §14).
2
3use proptest::prelude::*;
4use turnframe_core::command::{
5    AtomicityScope, ClaimMode, CommandOrigin, CommandPolicy, ConfirmationPolicy, ResolutionChannel,
6    RiskClass,
7};
8use turnframe_core::interaction::{ActionClass, InteractionKind};
9
10use crate::strategies::ids;
11
12/// An arbitrary answer channel.
13pub fn resolution_channel() -> impl Strategy<Value = ResolutionChannel> {
14    prop_oneof![
15        Just(ResolutionChannel::Click),
16        Just(ResolutionChannel::ModelInterpreted),
17    ]
18}
19
20/// An arbitrary action class.
21pub fn action_class() -> impl Strategy<Value = ActionClass> {
22    prop_oneof![
23        Just(ActionClass::ConfirmsCommands),
24        Just(ActionClass::AppliesOperation),
25        Just(ActionClass::NoCommands),
26    ]
27}
28
29/// An arbitrary origin, trusted or not.
30///
31/// There is deliberately no "the model proposed it" variant to generate: a
32/// model proposal is not an origin (spec §14.2).
33pub fn command_origin() -> impl Strategy<Value = CommandOrigin> {
34    prop_oneof![
35        ids::digest()
36            .prop_map(|evidence_digest| CommandOrigin::DirectSafeUserAct { evidence_digest }),
37        (
38            ids::interaction_id(),
39            ids::digest(),
40            crate::strategies::interaction::interaction_kind(),
41            action_class(),
42            resolution_channel(),
43        )
44            .prop_map(
45                |(interaction_id, payload_hash, interaction_kind, action_class, channel)| {
46                    CommandOrigin::ConfirmedInteraction {
47                        interaction_id,
48                        payload_hash,
49                        interaction_kind,
50                        action_class,
51                        channel,
52                    }
53                },
54            ),
55        ids::label().prop_map(|policy_key| CommandOrigin::InternalPolicy { policy_key }),
56        (ids::label(), any::<bool>()).prop_map(|(callback_id, signature_verified)| {
57            CommandOrigin::ExternalCallback {
58                callback_id,
59                signature_verified,
60            }
61        }),
62    ]
63}
64
65/// An origin that may authorize a consequential command (I12): a confirmed
66/// interaction, an internal policy, or a callback whose signature was verified.
67pub fn trusted_origin() -> impl Strategy<Value = CommandOrigin> {
68    prop_oneof![
69        (
70            ids::interaction_id(),
71            ids::digest(),
72            prop_oneof![
73                Just(InteractionKind::ConfirmCommand),
74                Just(InteractionKind::ReviewChanges),
75            ],
76            prop_oneof![
77                Just(ActionClass::ConfirmsCommands),
78                Just(ActionClass::AppliesOperation),
79            ],
80            Just(ResolutionChannel::Click),
81        )
82            .prop_map(
83                |(interaction_id, payload_hash, interaction_kind, action_class, channel)| {
84                    CommandOrigin::ConfirmedInteraction {
85                        interaction_id,
86                        payload_hash,
87                        interaction_kind,
88                        action_class,
89                        channel,
90                    }
91                },
92            ),
93        ids::label().prop_map(|policy_key| CommandOrigin::InternalPolicy { policy_key }),
94        ids::label().prop_map(|callback_id| CommandOrigin::ExternalCallback {
95            callback_id,
96            signature_verified: true,
97        }),
98    ]
99}
100
101/// An arbitrary risk class.
102pub fn risk_class() -> impl Strategy<Value = RiskClass> {
103    prop_oneof![
104        Just(RiskClass::ReadOnly),
105        Just(RiskClass::ReversibleLowRisk),
106        Just(RiskClass::SensitiveDataChange),
107        Just(RiskClass::Destructive),
108        Just(RiskClass::Irreversible),
109        Just(RiskClass::ExternalRegulated),
110    ]
111}
112
113/// An arbitrary confirmation policy.
114pub fn confirmation_policy() -> impl Strategy<Value = ConfirmationPolicy> {
115    prop_oneof![
116        Just(ConfirmationPolicy::None),
117        Just(ConfirmationPolicy::ReviewCard),
118        Just(ConfirmationPolicy::ExplicitClick),
119        Just(ConfirmationPolicy::Reauthentication),
120        Just(ConfirmationPolicy::QualifiedSignature),
121        Just(ConfirmationPolicy::HumanProfessionalReview),
122    ]
123}
124
125/// An arbitrary atomicity scope.
126pub fn atomicity_scope() -> impl Strategy<Value = AtomicityScope> {
127    prop_oneof![
128        Just(AtomicityScope::PerCommand),
129        Just(AtomicityScope::PerCase),
130        ids::label().prop_map(|group| AtomicityScope::ExplicitGroup { group }),
131        ids::label().prop_map(|saga| AtomicityScope::ExternalSaga { saga }),
132    ]
133}
134
135/// An arbitrary claim mode.
136pub fn claim_mode() -> impl Strategy<Value = ClaimMode> {
137    prop_oneof![
138        Just(ClaimMode::ServerReceiptOnly),
139        Just(ClaimMode::EventReferencedParaphrase),
140        Just(ClaimMode::FreeExplanation),
141    ]
142}
143
144/// An arbitrary command policy. Combinations are free on purpose: the point of
145/// a property test is to try the ones a domain author would not think of.
146pub fn command_policy() -> impl Strategy<Value = CommandPolicy> {
147    (
148        risk_class(),
149        confirmation_policy(),
150        atomicity_scope(),
151        claim_mode(),
152    )
153        .prop_map(
154            |(risk, confirmation, atomicity, claim_mode)| CommandPolicy {
155                risk,
156                confirmation,
157                atomicity,
158                claim_mode,
159            },
160        )
161}
162
163/// A policy that demands a trusted origin, i.e. one where
164/// [`CommandPolicy::requires_trusted_origin`] holds.
165pub fn consequential_policy() -> impl Strategy<Value = CommandPolicy> {
166    command_policy().prop_filter("policy must require a trusted origin", |policy| {
167        policy.requires_trusted_origin()
168    })
169}