confium_patterns/revocation/
revocation_submission.rs1use crate::revocation::revocation_blob::RevocationBlob;
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Submission {
10 pub id: String,
12 pub blob: RevocationBlob,
14 pub submitted_at: DateTime<Utc>,
16 pub state: SubmissionState,
18 pub first_confirmation_at: Option<DateTime<Utc>>,
20 pub delay_until: Option<DateTime<Utc>>,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum SubmissionState {
28 AwaitingEmailVerification,
30 FirstConfirmedDelay,
32 SecondConfirmed,
34 Decrypted,
36 Published,
38 Cancelled,
40 Expired,
42}
43
44impl Submission {
45 pub fn new(id: impl Into<String>, blob: RevocationBlob) -> Self {
47 Self {
48 id: id.into(),
49 blob,
50 submitted_at: Utc::now(),
51 state: SubmissionState::AwaitingEmailVerification,
52 first_confirmation_at: None,
53 delay_until: None,
54 }
55 }
56
57 pub fn confirm_first(&mut self) -> Result<(), String> {
59 if self.state != SubmissionState::AwaitingEmailVerification {
60 return Err(format!("invalid state: {:?}", self.state));
61 }
62 let now = Utc::now();
63 self.first_confirmation_at = Some(now);
64 self.delay_until = Some(now + Duration::hours(24));
65 self.state = SubmissionState::FirstConfirmedDelay;
66 Ok(())
67 }
68
69 pub fn confirm_second(&mut self) -> Result<(), String> {
71 if self.state != SubmissionState::FirstConfirmedDelay {
72 return Err(format!("invalid state: {:?}", self.state));
73 }
74 if let Some(until) = self.delay_until {
75 if Utc::now() < until {
76 return Err("24h delay not yet elapsed".into());
77 }
78 }
79 self.state = SubmissionState::SecondConfirmed;
80 Ok(())
81 }
82
83 pub fn mark_decrypted(&mut self) -> Result<(), String> {
85 if self.state != SubmissionState::SecondConfirmed {
86 return Err(format!("invalid state: {:?}", self.state));
87 }
88 self.state = SubmissionState::Decrypted;
89 Ok(())
90 }
91
92 pub fn mark_published(&mut self) -> Result<(), String> {
94 if self.state != SubmissionState::Decrypted {
95 return Err(format!("invalid state: {:?}", self.state));
96 }
97 self.state = SubmissionState::Published;
98 Ok(())
99 }
100
101 pub fn cancel(&mut self) -> Result<(), String> {
103 if matches!(
104 self.state,
105 SubmissionState::Published | SubmissionState::Cancelled
106 ) {
107 return Err("cannot cancel terminal submission".into());
108 }
109 self.state = SubmissionState::Cancelled;
110 Ok(())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::revocation::revocation_blob::RevocationBlob;
118
119 fn sample_blob() -> RevocationBlob {
120 RevocationBlob {
121 user_email: "alice@example.com".into(),
122 key_fingerprint: "ABCDEF0123456789".into(),
123 encapsulated_key: vec![0u8; 32],
124 ciphertext: vec![0u8; 100],
125 nonce: vec![0u8; 12],
126 }
127 }
128
129 #[test]
130 fn full_submission_lifecycle() {
131 let mut sub = Submission::new("sub-1", sample_blob());
132 assert_eq!(sub.state, SubmissionState::AwaitingEmailVerification);
133
134 sub.confirm_first().unwrap();
135 assert_eq!(sub.state, SubmissionState::FirstConfirmedDelay);
136
137 sub.delay_until = Some(Utc::now() - Duration::hours(1));
139 sub.confirm_second().unwrap();
140 assert_eq!(sub.state, SubmissionState::SecondConfirmed);
141
142 sub.mark_decrypted().unwrap();
143 assert_eq!(sub.state, SubmissionState::Decrypted);
144
145 sub.mark_published().unwrap();
146 assert_eq!(sub.state, SubmissionState::Published);
147 }
148
149 #[test]
150 fn confirm_second_before_delay_fails() {
151 let mut sub = Submission::new("sub-2", sample_blob());
152 sub.confirm_first().unwrap();
153 sub.delay_until = Some(Utc::now() + Duration::hours(24));
155 let result = sub.confirm_second();
156 assert!(result.is_err());
157 }
158
159 #[test]
160 fn cancel_works_from_any_nonterminal_state() {
161 let mut sub = Submission::new("sub-3", sample_blob());
162 sub.cancel().unwrap();
163 assert_eq!(sub.state, SubmissionState::Cancelled);
164 }
165}