koprogo_api/domain/copropriete/
lien_notaire.rs1use chrono::{DateTime, Duration, Utc};
28use serde::{Deserialize, Serialize};
29use sha2::{Digest, Sha256};
30use uuid::Uuid;
31
32pub const DUREE_JOURS: i64 = 7;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum LienNotaireError {
39 EtatDateIdNul,
41 EmisParNul,
43 DejaRevoque,
45}
46
47impl std::fmt::Display for LienNotaireError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::EtatDateIdNul => write!(f, "etat_date_id must not be nil"),
51 Self::EmisParNul => write!(f, "emis_par must not be nil"),
52 Self::DejaRevoque => write!(f, "a revoked notary link cannot be renewed"),
53 }
54 }
55}
56
57impl std::error::Error for LienNotaireError {}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct LienNotaire {
62 pub id: Uuid,
63 pub etat_date_id: Uuid,
66 pub token_hash: String,
68 pub emis_par: Uuid,
70 pub cree_le: DateTime<Utc>,
71 pub expire_le: DateTime<Utc>,
72 pub revoque_le: Option<DateTime<Utc>>,
74 pub revoque_par: Option<Uuid>,
75 pub renouvele_le: Option<DateTime<Utc>>,
77 pub mis_a_jour_le: DateTime<Utc>,
78}
79
80impl LienNotaire {
81 pub fn emettre(etat_date_id: Uuid, emis_par: Uuid) -> Result<(Self, String), LienNotaireError> {
84 if etat_date_id.is_nil() {
85 return Err(LienNotaireError::EtatDateIdNul);
86 }
87 if emis_par.is_nil() {
88 return Err(LienNotaireError::EmisParNul);
89 }
90
91 let clair = Self::generer_jeton();
92 let token_hash = Self::hacher(&clair);
93 let maintenant = Utc::now();
94
95 let entite = Self {
96 id: Uuid::new_v4(),
97 etat_date_id,
98 token_hash,
99 emis_par,
100 cree_le: maintenant,
101 expire_le: maintenant + Duration::days(DUREE_JOURS),
102 revoque_le: None,
103 revoque_par: None,
104 renouvele_le: None,
105 mis_a_jour_le: maintenant,
106 };
107
108 Ok((entite, clair))
109 }
110
111 pub fn hacher(clair: &str) -> String {
114 let mut hasher = Sha256::new();
115 hasher.update(clair.as_bytes());
116 format!("{:x}", hasher.finalize())
117 }
118
119 fn generer_jeton() -> String {
121 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
122 let mut octets = [0u8; 32];
123 for o in octets.iter_mut() {
124 *o = rand::random::<u8>();
125 }
126 URL_SAFE_NO_PAD.encode(octets)
127 }
128
129 pub fn est_expire(&self, moment: DateTime<Utc>) -> bool {
130 moment > self.expire_le
131 }
132
133 pub fn est_revoque(&self) -> bool {
134 self.revoque_le.is_some()
135 }
136
137 pub fn est_valide(&self, moment: DateTime<Utc>) -> bool {
140 !self.est_expire(moment) && !self.est_revoque()
141 }
142
143 pub fn renouveler(&mut self, moment: DateTime<Utc>) -> Result<(), LienNotaireError> {
150 if self.est_revoque() {
151 return Err(LienNotaireError::DejaRevoque);
152 }
153 self.expire_le = moment + Duration::days(DUREE_JOURS);
154 self.renouvele_le = Some(moment);
155 self.mis_a_jour_le = moment;
156 Ok(())
157 }
158
159 pub fn revoquer(&mut self, moment: DateTime<Utc>, revoque_par: Uuid) {
163 if self.revoque_le.is_none() {
164 self.revoque_le = Some(moment);
165 self.revoque_par = Some(revoque_par);
166 }
167 self.mis_a_jour_le = moment;
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn paire() -> (Uuid, Uuid) {
180 (Uuid::new_v4(), Uuid::new_v4())
181 }
182
183 #[test]
188 fn happy_emettre_retourne_un_lien_valide_sept_jours() {
189 let (etat_date_id, emis_par) = paire();
190 let (lien, clair) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
191
192 assert_eq!(lien.etat_date_id, etat_date_id);
193 assert_eq!(lien.emis_par, emis_par);
194 assert!(lien.est_valide(Utc::now()));
195 assert_eq!(
196 (lien.expire_le - lien.cree_le).num_days(),
197 DUREE_JOURS,
198 "ADR 0051 fixe la durée à sept jours"
199 );
200 assert!(
201 clair.len() >= 40,
202 "jeton clair trop court : {}",
203 clair.len()
204 );
205 }
206
207 #[test]
208 fn happy_hacher_est_deterministe_et_produit_64_caracteres_hex() {
209 let h1 = LienNotaire::hacher("un-jeton");
210 let h2 = LienNotaire::hacher("un-jeton");
211 assert_eq!(h1, h2);
212 assert_eq!(h1.len(), 64);
213 assert!(h1.chars().all(|c| c.is_ascii_hexdigit()));
214 }
215
216 #[test]
217 fn happy_lecture_dans_la_fenetre_est_valide() {
218 let (etat_date_id, emis_par) = paire();
220 let (lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
221 let jour_3 = lien.cree_le + Duration::days(3);
222 assert!(lien.est_valide(jour_3));
223 }
224
225 #[test]
226 fn happy_renouveler_prolonge_de_sept_jours_depuis_le_moment_donne() {
227 let (etat_date_id, emis_par) = paire();
228 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
229 let renouvellement = lien.cree_le + Duration::days(6);
230
231 lien.renouveler(renouvellement).unwrap();
232
233 assert_eq!(lien.renouvele_le, Some(renouvellement));
234 assert_eq!(lien.expire_le, renouvellement + Duration::days(DUREE_JOURS));
235 }
236
237 #[test]
242 fn edge_lecture_a_j7_pile_est_encore_valide() {
243 let (etat_date_id, emis_par) = paire();
244 let (lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
245 assert!(
246 lien.est_valide(lien.expire_le),
247 "l'instant exact de l'échéance n'est pas encore un dépassement"
248 );
249 }
250
251 #[test]
252 fn edge_lecture_a_j8_apres_lecheance_est_invalide() {
253 let (etat_date_id, emis_par) = paire();
254 let (lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
255 let j8 = lien.expire_le + Duration::seconds(1);
256 assert!(!lien.est_valide(j8));
257 assert!(lien.est_expire(j8));
258 }
259
260 #[test]
261 fn edge_renouvellement_en_chaine_repousse_lecheance_a_chaque_fois() {
262 let (etat_date_id, emis_par) = paire();
263 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
264
265 let premiere_echeance = lien.expire_le;
266 let renouvellement_1 = premiere_echeance - Duration::days(1);
267 lien.renouveler(renouvellement_1).unwrap();
268 let deuxieme_echeance = lien.expire_le;
269 assert!(deuxieme_echeance > premiere_echeance);
270
271 let renouvellement_2 = deuxieme_echeance - Duration::days(1);
272 lien.renouveler(renouvellement_2).unwrap();
273 assert!(lien.expire_le > deuxieme_echeance);
274 assert_eq!(lien.renouvele_le, Some(renouvellement_2));
275 }
276
277 #[test]
278 fn edge_renouveler_un_lien_deja_expire_le_repousse_depuis_maintenant() {
279 let (etat_date_id, emis_par) = paire();
282 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
283 let bien_apres_lecheance = lien.expire_le + Duration::days(10);
284
285 lien.renouveler(bien_apres_lecheance).unwrap();
286
287 assert!(lien.est_valide(bien_apres_lecheance));
288 }
289
290 #[test]
295 fn security_deux_emissions_produisent_des_jetons_et_hachages_distincts() {
296 let (etat_date_id, emis_par) = paire();
297 let (lien_a, clair_a) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
298 let (lien_b, clair_b) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
299
300 assert_ne!(clair_a, clair_b);
301 assert_ne!(lien_a.token_hash, lien_b.token_hash);
302 assert_ne!(lien_a.id, lien_b.id);
303 }
304
305 #[test]
306 fn security_le_jeton_clair_ne_vaut_jamais_le_hachage_stocke() {
307 let (etat_date_id, emis_par) = paire();
308 let (lien, clair) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
309 assert_ne!(clair, lien.token_hash);
310 assert_eq!(LienNotaire::hacher(&clair), lien.token_hash);
311 }
312
313 #[test]
314 fn security_lien_revoque_est_invalide_meme_avant_lecheance() {
315 let (etat_date_id, emis_par) = paire();
316 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
317 let revocateur = Uuid::new_v4();
318
319 lien.revoquer(Utc::now(), revocateur);
320
321 assert!(lien.est_revoque());
322 assert!(!lien.est_valide(Utc::now()));
323 assert_eq!(lien.revoque_par, Some(revocateur));
324 }
325
326 #[test]
327 fn security_double_revocation_est_idempotente_et_garde_le_premier_auteur() {
328 let (etat_date_id, emis_par) = paire();
329 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
330 let premier = Uuid::new_v4();
331 let second = Uuid::new_v4();
332
333 lien.revoquer(Utc::now(), premier);
334 let premiere_date = lien.revoque_le;
335 lien.revoquer(Utc::now() + Duration::seconds(5), second);
336
337 assert_eq!(lien.revoque_le, premiere_date);
338 assert_eq!(lien.revoque_par, Some(premier));
339 }
340
341 #[test]
342 fn security_renouveler_un_lien_revoque_est_refuse() {
343 let (etat_date_id, emis_par) = paire();
344 let (mut lien, _) = LienNotaire::emettre(etat_date_id, emis_par).unwrap();
345 lien.revoquer(Utc::now(), Uuid::new_v4());
346
347 let err = lien.renouveler(Utc::now()).unwrap_err();
348 assert_eq!(err, LienNotaireError::DejaRevoque);
349 }
350
351 #[test]
356 fn negative_etat_date_id_nul_est_rejete() {
357 let err = LienNotaire::emettre(Uuid::nil(), Uuid::new_v4()).unwrap_err();
358 assert_eq!(err, LienNotaireError::EtatDateIdNul);
359 }
360
361 #[test]
362 fn negative_emis_par_nul_est_rejete() {
363 let err = LienNotaire::emettre(Uuid::new_v4(), Uuid::nil()).unwrap_err();
364 assert_eq!(err, LienNotaireError::EmisParNul);
365 }
366
367 #[test]
368 fn negative_display_des_erreurs_ne_panique_pas_et_reste_lisible() {
369 for err in [
370 LienNotaireError::EtatDateIdNul,
371 LienNotaireError::EmisParNul,
372 LienNotaireError::DejaRevoque,
373 ] {
374 assert!(!err.to_string().is_empty());
375 }
376 }
377}