1use std::collections::{BTreeSet, HashMap};
2use std::io::BufRead;
3use std::sync::Arc;
4use std::time::Duration;
5use uuid::Uuid;
6
7use crate::admin::prompt::confirm;
8use crate::audit::{Actor, AuditEvent, AuditRecord, ClientContext};
9use crate::config::Config;
10use crate::signer::{SignerBackend, SignerError};
11use crate::sqlite::account::Account;
12use crate::sqlite::audit::{AuditEntry, AuditQuery};
13use crate::sqlite::authz::{Authorization, Challenge};
14use crate::sqlite::db::Database;
15use crate::sqlite::nonce::{Nonce, now_secs};
16use crate::sqlite::order::{Order, UNPARSABLE_NOT_AFTER};
17
18#[derive(Debug, PartialEq, Eq)]
24pub enum DeleteOutcome {
25 NotFound,
26 Cancelled,
27 Deleted,
28}
29
30#[derive(Debug, PartialEq, Eq)]
36pub struct Deleted {
37 pub cascaded: u64,
40}
41
42#[derive(Debug)]
44pub struct OrderDetail {
45 pub order: Order,
46 pub authorizations: Vec<(Authorization, Vec<Challenge>)>,
47}
48
49#[derive(Debug)]
51pub enum RevokeOutcome {
52 NotFound,
53 NotIssued,
54 AlreadyRevoked,
55 Revoked(Box<Order>),
56}
57
58fn signer_detail(error: &SignerError) -> String {
64 match error {
65 SignerError::Internal(detail) => detail.clone(),
66 SignerError::BadCsr => "unexpected badCsr from revoke".to_string(),
67 }
68}
69
70#[derive(Debug, thiserror::Error)]
72pub enum RevokeError {
73 #[error("database error: {0}")]
74 Database(sqlx::Error),
75 #[error("signer error: {}", signer_detail(.0))]
76 Signer(SignerError),
77 #[error("internal error: {0}")]
78 Internal(String),
79 #[error("unsupported revocation reason code {0}")]
80 BadReason(u32),
81}
82
83impl From<sqlx::Error> for RevokeError {
84 fn from(error: sqlx::Error) -> Self {
85 Self::Database(error)
86 }
87}
88
89impl From<SignerError> for RevokeError {
90 fn from(error: SignerError) -> Self {
91 Self::Signer(error)
92 }
93}
94
95pub async fn delete_account(
106 id: &str,
107 database: Arc<Database>,
108) -> Result<Option<Deleted>, sqlx::Error> {
109 let Some(cascaded) = account_cascade(id, database.clone()).await? else {
110 return Ok(None);
111 };
112 Account::delete(id, &database).await?;
113 Ok(Some(Deleted { cascaded }))
114}
115
116pub async fn confirm_delete_account(
118 id: &str,
119 assume_yes: bool,
120 reader: &mut impl BufRead,
121 database: Arc<Database>,
122) -> Result<DeleteOutcome, sqlx::Error> {
123 let Some(account) = Account::find_any_by_id(id, &database).await? else {
124 return Ok(DeleteOutcome::NotFound);
125 };
126 let order_count = Order::count_by_account(account.id, &database).await?;
127 let prompt = format!(
128 "Delete account {id} (status: {}, {order_count} order(s) will cascade)?",
129 account.status
130 );
131 if !confirm(&prompt, assume_yes, reader) {
132 return Ok(DeleteOutcome::Cancelled);
133 }
134 Account::delete(id, &database).await?;
135 Ok(DeleteOutcome::Deleted)
136}
137
138pub async fn delete_order(
141 id: &str,
142 database: Arc<Database>,
143) -> Result<Option<Deleted>, sqlx::Error> {
144 let Some(cascaded) = order_cascade(id, database.clone()).await? else {
145 return Ok(None);
146 };
147 Order::delete(id, &database).await?;
148 Ok(Some(Deleted { cascaded }))
149}
150
151pub async fn confirm_delete_order(
153 id: &str,
154 assume_yes: bool,
155 reader: &mut impl BufRead,
156 database: Arc<Database>,
157) -> Result<DeleteOutcome, sqlx::Error> {
158 let Some(order) = Order::find_by_id(id, &database).await? else {
159 return Ok(DeleteOutcome::NotFound);
160 };
161 let authz_count = Authorization::count_by_order(order.id, &database).await?;
162 let prompt = format!(
163 "Delete order {id} (status: {}, {authz_count} authorization(s) will cascade)?",
164 order.status
165 );
166 if !confirm(&prompt, assume_yes, reader) {
167 return Ok(DeleteOutcome::Cancelled);
168 }
169 Order::delete(id, &database).await?;
170 Ok(DeleteOutcome::Deleted)
171}
172
173pub async fn cleanup_nonces(ttl: Duration, database: Arc<Database>) -> Result<u64, sqlx::Error> {
175 Nonce::cleanup(&database, ttl).await
176}
177
178pub async fn confirm_cleanup_nonces(
180 ttl: Duration,
181 assume_yes: bool,
182 reader: &mut impl BufRead,
183 database: Arc<Database>,
184) -> Result<Option<u64>, sqlx::Error> {
185 let prompt = format!("Delete all nonces older than {}s?", ttl.as_secs());
186 if !confirm(&prompt, assume_yes, reader) {
187 return Ok(None);
188 }
189 Ok(Some(cleanup_nonces(ttl, database).await?))
190}
191
192async fn account_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
195 let Some(account) = Account::find_any_by_id(id, &database).await? else {
196 return Ok(None);
197 };
198 Ok(Some(
199 Order::count_by_account(account.id, &database).await? as u64,
200 ))
201}
202
203async fn order_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
205 let Some(order) = Order::find_by_id(id, &database).await? else {
206 return Ok(None);
207 };
208 Ok(Some(
209 Authorization::count_by_order(order.id, &database).await? as u64,
210 ))
211}
212
213pub async fn update_account_contact(
215 id: &str,
216 contact: Vec<String>,
217 database: Arc<Database>,
218) -> Result<Option<Account>, sqlx::Error> {
219 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
220 return Ok(None);
221 };
222 account.update_contact(contact, &database).await?;
223 Ok(Some(account))
224}
225
226pub async fn deactivate_account(
228 id: &str,
229 database: Arc<Database>,
230) -> Result<Option<Account>, sqlx::Error> {
231 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
232 return Ok(None);
233 };
234 account.deactivate(&database).await?;
235 Ok(Some(account))
236}
237
238pub async fn revoke_order(
253 id: &str,
254 reason: Option<u32>,
255 actor: Actor,
256 client: ClientContext,
257 database: Arc<Database>,
258 signer: Arc<dyn SignerBackend>,
259) -> Result<RevokeOutcome, RevokeError> {
260 let Some(mut order) = Order::find_by_id(id, &database).await? else {
261 return Ok(RevokeOutcome::NotFound);
262 };
263 let Some(chain) = order.certificate.clone() else {
264 return Ok(RevokeOutcome::NotIssued);
265 };
266 if order.revoked_at.is_some() {
267 return Ok(RevokeOutcome::AlreadyRevoked);
268 }
269 if let Some(r) = reason
270 && !crate::cert::is_valid_revocation_reason(r)
271 {
272 return Err(RevokeError::BadReason(r));
273 }
274
275 let cert_der = crate::cert::leaf_der_from_chain(&chain).map_err(|error| {
276 RevokeError::Internal(format!("stored certificate chain is unparsable: {error}"))
277 })?;
278 let mut record = AuditRecord::new(
279 AuditEvent::CertificateRevoked,
280 &order.profile,
281 actor.clone(),
282 )
283 .with_order(&order)
284 .with_client(client.clone());
285 if let Some(serial) = order.cert_serial.clone() {
286 record = record.with_serial(serial);
287 }
288 if let Some(reason) = reason {
291 record = record.with_reason(reason.to_string());
292 }
293
294 if let Err(error) = signer.revoke(&cert_der, reason).await {
298 crate::audit::write(
299 AuditRecord::new(AuditEvent::CertificateRevokeFailed, &order.profile, actor)
300 .with_order(&order)
301 .with_client(client)
302 .with_reason("serverInternal")
303 .with_detail(error.to_string()),
304 &database,
305 )
306 .await;
307 return Err(error.into());
308 }
309 order.revoke(reason.map(i64::from), &database).await?;
310 crate::audit::write(record, &database).await;
311 Ok(RevokeOutcome::Revoked(Box::new(order)))
312}
313
314#[must_use]
320pub fn audit_cutoff(days: u64) -> i64 {
321 let seconds = i64::try_from(days.saturating_mul(24 * 60 * 60)).unwrap_or(i64::MAX);
322 crate::sqlite::nonce::now_secs().saturating_sub(seconds)
323}
324
325pub async fn list_audit(
327 query: &AuditQuery,
328 database: Arc<Database>,
329) -> Result<(Vec<AuditEntry>, i64), sqlx::Error> {
330 AuditEntry::search(query, &database).await
331}
332
333pub async fn find_audit(
335 id: i64,
336 database: Arc<Database>,
337) -> Result<Option<AuditEntry>, sqlx::Error> {
338 AuditEntry::find_by_id(id, &database).await
339}
340
341pub async fn cleanup_audit(days: u64, database: Arc<Database>) -> Result<u64, sqlx::Error> {
343 AuditEntry::cleanup(audit_cutoff(days), &database).await
344}
345
346pub async fn confirm_cleanup_audit(
352 days: u64,
353 assume_yes: bool,
354 reader: &mut impl BufRead,
355 database: Arc<Database>,
356) -> Result<Option<u64>, sqlx::Error> {
357 let cutoff = audit_cutoff(days);
358 let doomed = AuditEntry::count_older_than(cutoff, &database).await?;
359 let prompt =
360 format!("Delete {doomed} audit row(s) older than {days} day(s)? This cannot be undone.");
361 if !confirm(&prompt, assume_yes, reader) {
362 return Ok(None);
363 }
364 Ok(Some(AuditEntry::cleanup(cutoff, &database).await?))
365}
366
367pub async fn load_order_detail(
369 id: &str,
370 database: Arc<Database>,
371) -> Result<Option<OrderDetail>, sqlx::Error> {
372 let Some(order) = Order::find_by_id(id, &database).await? else {
373 return Ok(None);
374 };
375 let authzs = Authorization::find_by_order(order.id, &database).await?;
376 let mut authorizations = Vec::with_capacity(authzs.len());
377 for authz in authzs {
378 let challenges = Challenge::find_by_authz(authz.id, &database).await?;
379 authorizations.push((authz, challenges));
380 }
381 Ok(Some(OrderDetail {
382 order,
383 authorizations,
384 }))
385}
386
387const DEFAULT_LEAD_DAYS: u64 = 30;
403
404#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
413pub struct SupersededBy {
414 pub order_id: String,
415 pub cert_serial: String,
416 pub not_after: i64,
417 pub via: String,
419}
420
421#[derive(Debug)]
424pub struct ExpiringEntry {
425 pub order: Order,
426 pub days_remaining: i64,
427 pub superseded_by: Option<SupersededBy>,
428}
429
430pub struct ExpiringQuery {
432 pub profile: Option<String>,
434 pub before: i64,
437 pub include_superseded: bool,
441 pub limit: i64,
442 pub offset: i64,
443}
444
445#[must_use]
451pub fn expiring_horizon(days: u64) -> i64 {
452 let seconds = i64::try_from(days.saturating_mul(24 * 60 * 60)).unwrap_or(i64::MAX);
453 now_secs().saturating_add(seconds)
454}
455
456#[must_use]
463pub fn days_remaining(not_after: i64, now: i64) -> i64 {
464 not_after.saturating_sub(now).max(0) / (24 * 60 * 60)
465}
466
467#[must_use]
470pub fn default_lead_days(config: &Config) -> u64 {
471 match config.notify.expiry.lead_days {
472 0 => DEFAULT_LEAD_DAYS,
473 days => days,
474 }
475}
476
477pub async fn superseded_by(
490 order: &Order,
491 candidates: &[Order],
492 database: &Database,
493) -> Result<Option<SupersededBy>, sqlx::Error> {
494 if let Some(chain) = order.certificate.as_deref()
504 && let Some(cert_id) = ari_cert_id(chain)
505 && let Some(successor) = Order::find_by_replaces(&order.profile, &cert_id, database).await?
506 && successor.certificate.is_some()
507 && successor.revoked_at.is_none()
508 {
509 return Ok(Some(SupersededBy {
510 order_id: successor.id.to_string(),
511 cert_serial: successor.cert_serial.unwrap_or_default(),
512 not_after: successor.cert_not_after.unwrap_or_default(),
513 via: "replaces".to_string(),
514 }));
515 }
516
517 let names: BTreeSet<&str> = order
523 .identifiers
524 .iter()
525 .map(|identifier| identifier.value.as_str())
526 .collect();
527 let expires = order.cert_not_after.unwrap_or_default();
528 for candidate in candidates {
529 if candidate.id == order.id
530 || candidate.certificate.is_none()
531 || candidate.revoked_at.is_some()
532 || candidate.cert_not_after.unwrap_or(UNPARSABLE_NOT_AFTER) <= expires
533 {
534 continue;
535 }
536 let covered: BTreeSet<&str> = candidate
537 .identifiers
538 .iter()
539 .map(|identifier| identifier.value.as_str())
540 .collect();
541 if names.is_subset(&covered) {
542 return Ok(Some(SupersededBy {
543 order_id: candidate.id.to_string(),
544 cert_serial: candidate.cert_serial.clone().unwrap_or_default(),
545 not_after: candidate.cert_not_after.unwrap_or_default(),
546 via: "identifiers".to_string(),
547 }));
548 }
549 }
550
551 Ok(None)
552}
553
554pub async fn annotate_expiring(
566 orders: Vec<Order>,
567 database: &Database,
568) -> Result<Vec<ExpiringEntry>, sqlx::Error> {
569 let now = now_secs();
570 let mut by_account: HashMap<Uuid, Vec<Order>> = HashMap::new();
571 let mut entries = Vec::with_capacity(orders.len());
572
573 for order in orders {
574 if let std::collections::hash_map::Entry::Vacant(slot) = by_account.entry(order.account_id)
575 {
576 slot.insert(Order::find_by_account(order.account_id, database).await?);
577 }
578 let candidates = by_account
581 .get(&order.account_id)
582 .map_or(&[][..], Vec::as_slice);
583 let superseded = superseded_by(&order, candidates, database).await?;
584 entries.push(ExpiringEntry {
585 days_remaining: days_remaining(order.cert_not_after.unwrap_or_default(), now),
586 superseded_by: superseded,
587 order,
588 });
589 }
590
591 Ok(entries)
592}
593
594pub async fn list_expiring(
606 query: &ExpiringQuery,
607 database: Arc<Database>,
608) -> Result<(Vec<ExpiringEntry>, i64, i64), sqlx::Error> {
609 let (orders, total) = Order::find_expiring(
610 query.profile.as_deref(),
611 query.before,
612 query.limit,
613 query.offset,
614 &database,
615 )
616 .await?;
617 let entries = annotate_expiring(orders, &database).await?;
618
619 if query.include_superseded {
620 return Ok((entries, total, 0));
621 }
622 let annotated = i64::try_from(entries.len()).unwrap_or(i64::MAX);
624 let kept: Vec<ExpiringEntry> = entries
625 .into_iter()
626 .filter(|entry| entry.superseded_by.is_none())
627 .collect();
628 let hidden = annotated.saturating_sub(i64::try_from(kept.len()).unwrap_or(i64::MAX));
629 Ok((kept, total, hidden))
630}
631
632fn ari_cert_id(chain: &str) -> Option<String> {
634 crate::cert::leaf_der_from_chain(chain)
635 .ok()
636 .and_then(|der| crate::cert::ari_cert_id(&der).ok())
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642 use crate::sqlite::order::Identifier;
643 use crate::testutil::{account_id, issued_order};
644
645 const DAY: i64 = 24 * 60 * 60;
646
647 async fn db() -> Arc<Database> {
648 Arc::new(Database::connect_in_memory().await.unwrap())
649 }
650
651 async fn issued(
653 db: &Database,
654 account: uuid::Uuid,
655 names: &[&str],
656 not_after_days: i64,
657 ) -> Order {
658 issued_order(db, "default", account, names, not_after_days).await
659 }
660
661 async fn annotation(order: &Order, db: &Database) -> Option<SupersededBy> {
664 let candidates = Order::find_by_account(order.account_id, db).await.unwrap();
665 superseded_by(order, &candidates, db).await.unwrap()
666 }
667
668 fn cli_actor() -> Actor {
672 Actor::cli()
673 }
674
675 async fn audit_rows(db: &Arc<Database>) -> Vec<AuditEntry> {
676 AuditEntry::search(
677 &AuditQuery {
678 limit: 50,
679 ..AuditQuery::default()
680 },
681 db,
682 )
683 .await
684 .unwrap()
685 .0
686 }
687
688 #[test]
691 fn the_audit_cutoff_is_days_before_now_and_saturates_rather_than_overflowing() {
692 let now = crate::sqlite::nonce::now_secs();
693 assert!((audit_cutoff(0) - now).abs() <= 1);
694 let week = audit_cutoff(7);
695 assert!((now - week - 7 * 24 * 60 * 60).abs() <= 1, "{week}");
696 assert!(audit_cutoff(u64::MAX) <= now);
698 }
699
700 #[tokio::test]
703 async fn cleaning_the_audit_trail_is_confirm_gated_and_bounded_by_age() {
704 let db = Arc::new(Database::connect_in_memory().await.unwrap());
705 AuditEntry::insert(
706 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
707 &db,
708 )
709 .await
710 .unwrap();
711
712 let mut declined: &[u8] = b"n\n";
713 assert_eq!(
714 confirm_cleanup_audit(0, false, &mut declined, db.clone())
715 .await
716 .unwrap(),
717 None
718 );
719 assert_eq!(audit_rows(&db).await.len(), 1);
720
721 let mut reader: &[u8] = &[];
723 assert_eq!(
724 confirm_cleanup_audit(7, true, &mut reader, db.clone())
725 .await
726 .unwrap(),
727 Some(0)
728 );
729 assert_eq!(audit_rows(&db).await.len(), 1);
730
731 assert_eq!(cleanup_audit(0, db.clone()).await.unwrap(), 0);
732
733 assert_eq!(
735 AuditEntry::cleanup(audit_cutoff(0) + 3600, &db)
736 .await
737 .unwrap(),
738 1
739 );
740 assert!(audit_rows(&db).await.is_empty());
741 }
742
743 #[tokio::test]
747 async fn listing_and_finding_audit_rows_pages_and_resolves() {
748 let db = Arc::new(Database::connect_in_memory().await.unwrap());
749 let mut ids = Vec::new();
750 for _ in 0..3 {
751 ids.push(
752 AuditEntry::insert(
753 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
754 &db,
755 )
756 .await
757 .unwrap(),
758 );
759 }
760
761 let (page, total) = list_audit(
762 &AuditQuery {
763 limit: 2,
764 ..AuditQuery::default()
765 },
766 db.clone(),
767 )
768 .await
769 .unwrap();
770 assert_eq!(total, 3);
771 assert_eq!(page.len(), 2);
772
773 assert!(find_audit(ids[0], db.clone()).await.unwrap().is_some());
774 assert!(find_audit(9_999, db).await.unwrap().is_none());
775 }
776
777 #[tokio::test]
781 async fn revoking_writes_one_audit_row_naming_the_caller() {
782 let db = Arc::new(Database::connect_in_memory().await.unwrap());
783 let signer = in_memory_ca();
784 let order = finalized_order(db.clone(), &signer).await;
785
786 let outcome = revoke_order(
787 order.id.to_string().as_str(),
788 Some(1),
789 Actor::admin("root"),
790 ClientContext {
791 ip: Some("203.0.113.7".to_string()),
792 ptr: Some("desk.example.com".to_string()),
793 ..ClientContext::default()
794 },
795 db.clone(),
796 signer.clone(),
797 )
798 .await
799 .unwrap();
800 assert!(matches!(outcome, RevokeOutcome::Revoked(_)));
801
802 let rows = audit_rows(&db).await;
803 assert_eq!(rows.len(), 1, "{rows:?}");
804 let row = &rows[0];
805 assert_eq!(row.event, "certificate_revoked");
806 assert_eq!(row.outcome, "success");
807 assert_eq!(row.actor_kind, "admin");
808 assert_eq!(row.actor_id.as_deref(), Some("root"));
809 assert_eq!(row.account_id, Some(order.account_id.to_string()));
810 assert_eq!(row.order_id, Some(order.id.to_string()));
811 assert_eq!(row.cert_serial, order.cert_serial);
812 assert_eq!(row.client_ip.as_deref(), Some("203.0.113.7"));
813 assert_eq!(row.client_ptr.as_deref(), Some("desk.example.com"));
814 assert_eq!(row.reason.as_deref(), Some("1"));
815
816 let outcome = revoke_order(
819 order.id.to_string().as_str(),
820 None,
821 Actor::admin("root"),
822 ClientContext::default(),
823 db.clone(),
824 signer,
825 )
826 .await
827 .unwrap();
828 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
829 assert_eq!(audit_rows(&db).await.len(), 1);
830 }
831
832 #[tokio::test]
835 async fn a_revocation_with_no_reason_leaves_the_column_absent() {
836 let db = Arc::new(Database::connect_in_memory().await.unwrap());
837 let signer = in_memory_ca();
838 let order = finalized_order(db.clone(), &signer).await;
839
840 revoke_order(
841 order.id.to_string().as_str(),
842 None,
843 cli_actor(),
844 ClientContext::default(),
845 db.clone(),
846 signer,
847 )
848 .await
849 .unwrap();
850
851 let rows = audit_rows(&db).await;
852 assert_eq!(rows[0].reason, None);
853 assert_eq!(rows[0].actor_kind, "cli");
854 assert_eq!(rows[0].client_ip, None);
856 assert_eq!(rows[0].client_ptr, None);
857 }
858
859 #[tokio::test]
860 async fn delete_account_not_found() {
861 let db = Arc::new(Database::connect_in_memory().await.unwrap());
862 let mut reader: &[u8] = &[];
863 let outcome = confirm_delete_account("nope", true, &mut reader, db)
864 .await
865 .unwrap();
866 assert_eq!(outcome, DeleteOutcome::NotFound);
867 }
868
869 #[tokio::test]
870 async fn delete_account_cancelled_leaves_row() {
871 let db = Arc::new(Database::connect_in_memory().await.unwrap());
872 let acct = account_id(&db).await;
873
874 let mut reader = b"n\n".as_slice();
875 let outcome =
876 confirm_delete_account(acct.to_string().as_str(), false, &mut reader, db.clone())
877 .await
878 .unwrap();
879 assert_eq!(outcome, DeleteOutcome::Cancelled);
880 assert!(
881 Account::find_by_id("default", acct.to_string().as_str(), &db)
882 .await
883 .unwrap()
884 .is_some()
885 );
886 }
887
888 #[tokio::test]
889 async fn delete_account_confirmed_deletes_and_cascades() {
890 let db = Arc::new(Database::connect_in_memory().await.unwrap());
891 let acct = account_id(&db).await;
892 let order = Order::create(
893 "default",
894 acct,
895 vec![Identifier::dns("example.com")],
896 crate::sqlite::nonce::now_secs() + 3600,
897 None,
898 None,
899 &db,
900 )
901 .await
902 .unwrap();
903
904 let mut reader: &[u8] = &[];
905 let outcome =
906 confirm_delete_account(acct.to_string().as_str(), true, &mut reader, db.clone())
907 .await
908 .unwrap();
909 assert_eq!(outcome, DeleteOutcome::Deleted);
910 assert!(
911 Account::find_by_id("default", acct.to_string().as_str(), &db)
912 .await
913 .unwrap()
914 .is_none()
915 );
916 assert!(
917 Order::find_by_id(order.id.to_string().as_str(), &db)
918 .await
919 .unwrap()
920 .is_none()
921 );
922 }
923
924 #[tokio::test]
925 async fn delete_order_not_found() {
926 let db = Arc::new(Database::connect_in_memory().await.unwrap());
927 let mut reader: &[u8] = &[];
928 let outcome = confirm_delete_order("nope", true, &mut reader, db)
929 .await
930 .unwrap();
931 assert_eq!(outcome, DeleteOutcome::NotFound);
932 }
933
934 #[tokio::test]
935 async fn delete_order_cancelled_leaves_row() {
936 let db = Arc::new(Database::connect_in_memory().await.unwrap());
937 let acct = account_id(&db).await;
938 let order = Order::create(
939 "default",
940 acct,
941 vec![Identifier::dns("example.com")],
942 crate::sqlite::nonce::now_secs() + 3600,
943 None,
944 None,
945 &db,
946 )
947 .await
948 .unwrap();
949
950 let mut reader = b"no\n".as_slice();
951 let outcome = confirm_delete_order(
952 order.id.to_string().as_str(),
953 false,
954 &mut reader,
955 db.clone(),
956 )
957 .await
958 .unwrap();
959 assert_eq!(outcome, DeleteOutcome::Cancelled);
960 assert!(
961 Order::find_by_id(order.id.to_string().as_str(), &db)
962 .await
963 .unwrap()
964 .is_some()
965 );
966 }
967
968 #[tokio::test]
969 async fn delete_order_confirmed_deletes_and_cascades() {
970 let db = Arc::new(Database::connect_in_memory().await.unwrap());
971 let acct = account_id(&db).await;
972 let order = Order::create(
973 "default",
974 acct,
975 vec![Identifier::dns("example.com")],
976 crate::sqlite::nonce::now_secs() + 3600,
977 None,
978 None,
979 &db,
980 )
981 .await
982 .unwrap();
983 let authz = Authorization::create(
984 order.id,
985 Identifier::dns("example.com"),
986 crate::sqlite::nonce::now_secs() + 3600,
987 &db,
988 )
989 .await
990 .unwrap();
991
992 let mut reader: &[u8] = &[];
993 let outcome =
994 confirm_delete_order(order.id.to_string().as_str(), true, &mut reader, db.clone())
995 .await
996 .unwrap();
997 assert_eq!(outcome, DeleteOutcome::Deleted);
998 assert!(
999 Order::find_by_id(order.id.to_string().as_str(), &db)
1000 .await
1001 .unwrap()
1002 .is_none()
1003 );
1004 assert!(
1005 Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1006 .await
1007 .unwrap()
1008 .is_none()
1009 );
1010 }
1011
1012 #[tokio::test]
1016 async fn bare_delete_account_reports_none_for_an_unknown_id() {
1017 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1018 assert_eq!(delete_account("nope", db).await.unwrap(), None);
1019 }
1020
1021 #[tokio::test]
1022 async fn bare_delete_account_deletes_and_counts_the_cascade() {
1023 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1024 let acct = account_id(&db).await;
1025 for _ in 0..2 {
1026 Order::create(
1027 "default",
1028 acct,
1029 vec![Identifier::dns("example.com")],
1030 crate::sqlite::nonce::now_secs() + 3600,
1031 None,
1032 None,
1033 &db,
1034 )
1035 .await
1036 .unwrap();
1037 }
1038
1039 assert_eq!(
1040 delete_account(acct.to_string().as_str(), db.clone())
1041 .await
1042 .unwrap(),
1043 Some(Deleted { cascaded: 2 })
1044 );
1045 assert!(
1046 Account::find_by_id("default", acct.to_string().as_str(), &db)
1047 .await
1048 .unwrap()
1049 .is_none()
1050 );
1051 }
1052
1053 #[tokio::test]
1054 async fn bare_delete_order_reports_none_for_an_unknown_id() {
1055 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1056 assert_eq!(delete_order("nope", db).await.unwrap(), None);
1057 }
1058
1059 #[tokio::test]
1060 async fn bare_delete_order_deletes_and_counts_the_cascade() {
1061 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1062 let acct = account_id(&db).await;
1063 let order = Order::create(
1064 "default",
1065 acct,
1066 vec![Identifier::dns("example.com")],
1067 crate::sqlite::nonce::now_secs() + 3600,
1068 None,
1069 None,
1070 &db,
1071 )
1072 .await
1073 .unwrap();
1074 Authorization::create(
1075 order.id,
1076 Identifier::dns("example.com"),
1077 crate::sqlite::nonce::now_secs() + 3600,
1078 &db,
1079 )
1080 .await
1081 .unwrap();
1082
1083 assert_eq!(
1084 delete_order(order.id.to_string().as_str(), db.clone())
1085 .await
1086 .unwrap(),
1087 Some(Deleted { cascaded: 1 })
1088 );
1089 assert!(
1090 Order::find_by_id(order.id.to_string().as_str(), &db)
1091 .await
1092 .unwrap()
1093 .is_none()
1094 );
1095 }
1096
1097 #[tokio::test]
1098 async fn bare_cleanup_nonces_removes_stale_rows_without_asking() {
1099 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1100 let stale = Nonce {
1101 value: "stale".to_string(),
1102 created_at: crate::sqlite::nonce::now_secs() - 10_000,
1103 };
1104 stale.save(&db).await.unwrap();
1105 Nonce::new().save(&db).await.unwrap();
1106
1107 assert_eq!(
1108 cleanup_nonces(Duration::from_secs(300), db.clone())
1109 .await
1110 .unwrap(),
1111 1
1112 );
1113 assert!(
1114 !Nonce::verify("stale", &db, Duration::from_secs(300))
1115 .await
1116 .unwrap()
1117 );
1118 }
1119
1120 fn in_memory_ca() -> Arc<dyn SignerBackend> {
1121 Arc::new(
1122 crate::signer::local_ca::LocalCa::generate_in_memory("ecdsa-p256", 90)
1123 .expect("in-memory CA"),
1124 )
1125 }
1126
1127 async fn finalized_order(db: Arc<Database>, signer: &Arc<dyn SignerBackend>) -> Order {
1128 let acct = account_id(&db).await;
1129 let mut order = Order::create(
1130 "default",
1131 acct,
1132 vec![Identifier::dns("example.com")],
1133 crate::sqlite::nonce::now_secs() + 3600,
1134 None,
1135 None,
1136 &db,
1137 )
1138 .await
1139 .unwrap();
1140
1141 let key_pair = rcgen::KeyPair::generate().unwrap();
1142 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
1143 let csr = params.serialize_request(&key_pair).unwrap();
1144 let chain = match signer
1145 .issue(
1146 order.id.to_string().as_str(),
1147 csr.der(),
1148 &order.identifiers,
1149 crate::signer::RequestedValidity::default(),
1150 )
1151 .await
1152 .unwrap()
1153 {
1154 crate::signer::IssueOutcome::Issued(chain) => chain,
1155 crate::signer::IssueOutcome::Processing => {
1156 panic!("the in-memory local CA issues synchronously")
1157 }
1158 };
1159 let leaf = crate::cert::leaf_der_from_chain(&chain).unwrap();
1160 let (serial, pubkey) = crate::cert::cert_serial_and_spki(&leaf).unwrap();
1161 let not_after = crate::cert::cert_validity(&leaf).ok().map(|(_, na)| na);
1162 order
1163 .finalize(chain, serial, pubkey, not_after, &db)
1164 .await
1165 .unwrap();
1166 order
1167 }
1168
1169 #[tokio::test]
1170 async fn revoke_order_not_found() {
1171 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1172 let outcome = revoke_order(
1173 "nope",
1174 None,
1175 cli_actor(),
1176 ClientContext::default(),
1177 db,
1178 in_memory_ca(),
1179 )
1180 .await
1181 .unwrap();
1182 assert!(matches!(outcome, RevokeOutcome::NotFound));
1183 }
1184
1185 #[tokio::test]
1186 async fn revoke_order_without_a_certificate_is_refused() {
1187 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1188 let acct = account_id(&db).await;
1189 let order = Order::create(
1190 "default",
1191 acct,
1192 vec![Identifier::dns("example.com")],
1193 crate::sqlite::nonce::now_secs() + 3600,
1194 None,
1195 None,
1196 &db,
1197 )
1198 .await
1199 .unwrap();
1200
1201 let outcome = revoke_order(
1202 order.id.to_string().as_str(),
1203 None,
1204 cli_actor(),
1205 ClientContext::default(),
1206 db,
1207 in_memory_ca(),
1208 )
1209 .await
1210 .unwrap();
1211 assert!(matches!(outcome, RevokeOutcome::NotIssued));
1212 }
1213
1214 #[tokio::test]
1215 async fn revoke_order_persists() {
1216 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1217 let signer = in_memory_ca();
1218 let order = finalized_order(db.clone(), &signer).await;
1219
1220 let outcome = revoke_order(
1221 order.id.to_string().as_str(),
1222 Some(1),
1223 cli_actor(),
1224 ClientContext::default(),
1225 db.clone(),
1226 signer.clone(),
1227 )
1228 .await
1229 .unwrap();
1230 let RevokeOutcome::Revoked(revoked) = outcome else {
1231 panic!("expected Revoked, got {outcome:?}");
1232 };
1233 assert!(revoked.revoked_at.is_some());
1234 assert_eq!(revoked.revocation_reason, Some(1));
1235
1236 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1237 .await
1238 .unwrap()
1239 .unwrap();
1240 assert!(reloaded.revoked_at.is_some());
1241
1242 use x509_parser::prelude::FromDer;
1243 let der = signer.crl_der().await.unwrap();
1244 let (_, crl) =
1245 x509_parser::revocation_list::CertificateRevocationList::from_der(&der).unwrap();
1246 assert_eq!(crl.iter_revoked_certificates().count(), 1);
1247 }
1248
1249 #[tokio::test]
1250 async fn revoke_order_already_revoked() {
1251 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1252 let signer = in_memory_ca();
1253 let order = finalized_order(db.clone(), &signer).await;
1254
1255 revoke_order(
1256 order.id.to_string().as_str(),
1257 None,
1258 cli_actor(),
1259 ClientContext::default(),
1260 db.clone(),
1261 signer.clone(),
1262 )
1263 .await
1264 .unwrap();
1265 let outcome = revoke_order(
1266 order.id.to_string().as_str(),
1267 None,
1268 cli_actor(),
1269 ClientContext::default(),
1270 db,
1271 signer,
1272 )
1273 .await
1274 .unwrap();
1275 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
1276 }
1277
1278 #[tokio::test]
1279 async fn revoke_order_bad_reason_is_refused() {
1280 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1281 let signer = in_memory_ca();
1282 let order = finalized_order(db.clone(), &signer).await;
1283
1284 let error = revoke_order(
1285 order.id.to_string().as_str(),
1286 Some(999),
1287 cli_actor(),
1288 ClientContext::default(),
1289 db,
1290 signer,
1291 )
1292 .await
1293 .unwrap_err();
1294 assert!(matches!(error, RevokeError::BadReason(999)));
1295 }
1296
1297 #[tokio::test]
1298 async fn cleanup_nonces_cancelled_leaves_nonces() {
1299 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1300 Nonce {
1301 value: "stale".to_string(),
1302 created_at: crate::sqlite::nonce::now_secs() - 600,
1303 }
1304 .save(&db)
1305 .await
1306 .unwrap();
1307
1308 let mut reader = b"n\n".as_slice();
1309 let outcome =
1310 confirm_cleanup_nonces(Duration::from_secs(300), false, &mut reader, db.clone())
1311 .await
1312 .unwrap();
1313 assert_eq!(outcome, None);
1314
1315 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1316 .fetch_one(&db.pool)
1317 .await
1318 .unwrap();
1319 assert_eq!(count, 1);
1320 }
1321
1322 #[tokio::test]
1323 async fn cleanup_nonces_confirmed_removes_stale_and_reports_count() {
1324 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1325 Nonce {
1326 value: "stale".to_string(),
1327 created_at: crate::sqlite::nonce::now_secs() - 600,
1328 }
1329 .save(&db)
1330 .await
1331 .unwrap();
1332
1333 let mut reader: &[u8] = &[];
1334 let outcome =
1335 confirm_cleanup_nonces(Duration::from_secs(300), true, &mut reader, db.clone())
1336 .await
1337 .unwrap();
1338 assert_eq!(outcome, Some(1));
1339
1340 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1341 .fetch_one(&db.pool)
1342 .await
1343 .unwrap();
1344 assert_eq!(count, 0);
1345 }
1346
1347 #[tokio::test]
1348 async fn update_account_contact_not_found() {
1349 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1350 assert!(
1351 update_account_contact("nope", vec![], db)
1352 .await
1353 .unwrap()
1354 .is_none()
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn update_account_contact_persists() {
1360 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1361 let acct = account_id(&db).await;
1362
1363 let contact = vec!["mailto:a@example.com".to_string()];
1364 let updated =
1365 update_account_contact(acct.to_string().as_str(), contact.clone(), db.clone())
1366 .await
1367 .unwrap()
1368 .unwrap();
1369 assert_eq!(updated.contact, contact);
1370
1371 let reloaded = Account::find_by_id("default", acct.to_string().as_str(), &db)
1372 .await
1373 .unwrap()
1374 .unwrap();
1375 assert_eq!(reloaded.contact, contact);
1376 }
1377
1378 #[tokio::test]
1379 async fn deactivate_account_not_found() {
1380 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1381 assert!(deactivate_account("nope", db).await.unwrap().is_none());
1382 }
1383
1384 #[tokio::test]
1385 async fn deactivate_account_persists() {
1386 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1387 let acct = account_id(&db).await;
1388
1389 let updated = deactivate_account(acct.to_string().as_str(), db.clone())
1390 .await
1391 .unwrap()
1392 .unwrap();
1393 assert_eq!(updated.status, "deactivated");
1394
1395 let reloaded = Account::find_by_id("default", acct.to_string().as_str(), &db)
1396 .await
1397 .unwrap()
1398 .unwrap();
1399 assert_eq!(reloaded.status, "deactivated");
1400 }
1401
1402 #[tokio::test]
1403 async fn load_order_detail_not_found() {
1404 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1405 assert!(load_order_detail("nope", db).await.unwrap().is_none());
1406 }
1407
1408 #[tokio::test]
1409 async fn load_order_detail_nests_authorizations_and_challenges() {
1410 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1411 let acct = account_id(&db).await;
1412 let order = Order::create(
1413 "default",
1414 acct,
1415 vec![Identifier::dns("example.com")],
1416 crate::sqlite::nonce::now_secs() + 3600,
1417 None,
1418 None,
1419 &db,
1420 )
1421 .await
1422 .unwrap();
1423 let authz = Authorization::create(
1424 order.id,
1425 Identifier::dns("example.com"),
1426 crate::sqlite::nonce::now_secs() + 3600,
1427 &db,
1428 )
1429 .await
1430 .unwrap();
1431 Challenge::create(authz.id, "http-01", &db).await.unwrap();
1432
1433 let detail = load_order_detail(order.id.to_string().as_str(), db)
1434 .await
1435 .unwrap()
1436 .unwrap();
1437 assert_eq!(detail.order.id, order.id);
1438 assert_eq!(detail.authorizations.len(), 1);
1439 assert_eq!(detail.authorizations[0].0.id, authz.id);
1440 assert_eq!(detail.authorizations[0].1.len(), 1);
1441 assert_eq!(detail.authorizations[0].1[0].typ, "http-01");
1442 }
1443
1444 #[test]
1445 fn revoke_error_display_formatting() {
1446 let db_err: RevokeError = sqlx::Error::RowNotFound.into();
1447 assert!(format!("{db_err}").contains("database error"));
1448
1449 let signer_internal: RevokeError = SignerError::Internal("test".to_string()).into();
1450 assert!(format!("{signer_internal}").contains("signer error: test"));
1451
1452 let signer_bad_csr: RevokeError = SignerError::BadCsr.into();
1453 assert!(format!("{signer_bad_csr}").contains("unexpected badCsr"));
1454
1455 let internal = RevokeError::Internal("detail".to_string());
1456 assert!(format!("{internal}").contains("internal error: detail"));
1457
1458 let bad_reason = RevokeError::BadReason(7);
1459 assert!(format!("{bad_reason}").contains("unsupported revocation reason code 7"));
1460 }
1461
1462 #[tokio::test]
1464 async fn a_replaces_claim_marks_the_predecessor_superseded() {
1465 let db = db().await;
1466 let acct = account_id(&db).await;
1467 let old = issued(&db, acct, &["a.example.com"], 3).await;
1468
1469 let cert_id = ari_cert_id(old.certificate.as_deref().unwrap()).unwrap();
1470 let successor = issued(&db, acct, &["a.example.com"], 90).await;
1471 sqlx::query("UPDATE orders SET replaces = ? WHERE id = ?;")
1472 .bind(&cert_id)
1473 .bind(successor.id)
1474 .execute(&db.pool)
1475 .await
1476 .unwrap();
1477
1478 let reloaded = Order::find_by_id(old.id.to_string().as_str(), &db)
1479 .await
1480 .unwrap()
1481 .unwrap();
1482 let superseded = annotation(&reloaded, &db).await.unwrap();
1483 assert_eq!(superseded.order_id, successor.id.to_string());
1484 assert_eq!(superseded.via, "replaces");
1485 }
1486
1487 #[tokio::test]
1492 async fn a_pending_replaces_claim_supersedes_nothing() {
1493 let db = db().await;
1494 let acct = account_id(&db).await;
1495 let old = issued(&db, acct, &["a.example.com"], 3).await;
1496 let cert_id = ari_cert_id(old.certificate.as_deref().unwrap()).unwrap();
1497
1498 let pending = Order::create(
1500 "default",
1501 acct,
1502 vec![Identifier::dns("a.example.com")],
1503 now_secs() + 3600,
1504 None,
1505 None,
1506 &db,
1507 )
1508 .await
1509 .unwrap();
1510 sqlx::query("UPDATE orders SET replaces = ? WHERE id = ?;")
1511 .bind(&cert_id)
1512 .bind(pending.id)
1513 .execute(&db.pool)
1514 .await
1515 .unwrap();
1516
1517 let reloaded = Order::find_by_id(old.id.to_string().as_str(), &db)
1518 .await
1519 .unwrap()
1520 .unwrap();
1521 assert!(annotation(&reloaded, &db).await.is_none());
1522 }
1523
1524 #[tokio::test]
1526 async fn a_later_certificate_over_the_same_names_supersedes() {
1527 let db = db().await;
1528 let acct = account_id(&db).await;
1529 let old = issued(&db, acct, &["a.example.com"], 3).await;
1530 let new = issued(&db, acct, &["a.example.com", "b.example.com"], 90).await;
1531
1532 let superseded = annotation(&old, &db).await.unwrap();
1533 assert_eq!(superseded.order_id, new.id.to_string());
1534 assert_eq!(
1535 superseded.via, "identifiers",
1536 "a superset covers these names, so it is a renewal"
1537 );
1538 }
1539
1540 #[tokio::test]
1544 async fn a_partial_a_revoked_and_another_accounts_certificate_supersede_nothing() {
1545 let db = db().await;
1546 let acct = account_id(&db).await;
1547 let old = issued(&db, acct, &["a.example.com", "b.example.com"], 3).await;
1548
1549 issued(&db, acct, &["a.example.com"], 90).await;
1551 assert!(
1552 annotation(&old, &db).await.is_none(),
1553 "a subset is not a renewal"
1554 );
1555
1556 let mut revoked = issued(&db, acct, &["a.example.com", "b.example.com"], 90).await;
1558 revoked.revoke(Some(1), &db).await.unwrap();
1559 assert!(
1560 annotation(&old, &db).await.is_none(),
1561 "a revoked certificate covers nothing"
1562 );
1563
1564 let (other, _created) = crate::sqlite::account::Account::find_or_create(
1566 "default",
1567 b"other-key",
1568 Vec::new(),
1569 &crate::audit::ClientContext::default(),
1570 &db,
1571 )
1572 .await
1573 .unwrap();
1574 issued(&db, other.id, &["a.example.com", "b.example.com"], 90).await;
1575 assert!(
1576 annotation(&old, &db).await.is_none(),
1577 "another subscriber's certificate is not this one's renewal"
1578 );
1579 }
1580
1581 #[test]
1585 fn the_horizon_and_the_day_count_agree_on_a_whole_day() {
1586 let now = now_secs();
1587 assert!((expiring_horizon(7) - now - 7 * DAY).abs() <= 1);
1588 assert_eq!(expiring_horizon(u64::MAX), i64::MAX);
1591
1592 assert_eq!(days_remaining(1_000 + 3 * DAY + DAY / 2, 1_000), 3);
1596 assert_eq!(days_remaining(1_000 + DAY - 1, 1_000), 0);
1597 assert_eq!(days_remaining(1_000, 1_000 + 5 * DAY), 0);
1600 assert_eq!(days_remaining(i64::MIN, i64::MAX), 0);
1601 }
1602
1603 #[test]
1606 fn the_default_window_falls_back_only_when_the_digest_is_off() {
1607 let mut config = Config::default();
1608 assert_eq!(config.notify.expiry.lead_days, 0, "off by default");
1609 assert_eq!(default_lead_days(&config), DEFAULT_LEAD_DAYS);
1610
1611 config.notify.expiry.lead_days = 3;
1612 assert_eq!(
1613 default_lead_days(&config),
1614 3,
1615 "a deployment that chose a lead time gets it"
1616 );
1617 }
1618
1619 #[tokio::test]
1628 async fn a_listing_reads_each_accounts_orders_once_and_still_annotates_each_row() {
1629 let db = db().await;
1630 let acct = account_id(&db).await;
1631
1632 let a = issued(&db, acct, &["a.example.com"], 3).await;
1633 let b = issued(&db, acct, &["b.example.com"], 5).await;
1634 let renewal = issued(&db, acct, &["a.example.com"], 90).await;
1637
1638 let (orders, _total) = Order::find_expiring(None, expiring_horizon(30), 50, 0, &db)
1639 .await
1640 .unwrap();
1641 let entries = annotate_expiring(orders, &db).await.unwrap();
1642
1643 let annotated = |id: &str| -> Option<SupersededBy> {
1644 entries
1645 .iter()
1646 .find(|entry| entry.order.id.to_string() == id)
1647 .and_then(|entry| entry.superseded_by.clone())
1648 };
1649 assert_eq!(
1650 annotated(a.id.to_string().as_str()).unwrap().order_id,
1651 renewal.id.to_string()
1652 );
1653 assert!(
1654 annotated(b.id.to_string().as_str()).is_none(),
1655 "a shared candidate list must not leak one row's renewal onto another"
1656 );
1657 Order::set_cert_not_after(a.id, now_secs() + 3 * DAY + DAY / 2, &db)
1661 .await
1662 .unwrap();
1663 let (orders, _total) = Order::find_expiring(None, expiring_horizon(30), 50, 0, &db)
1664 .await
1665 .unwrap();
1666 let entries = annotate_expiring(orders, &db).await.unwrap();
1667 let a_entry = entries.iter().find(|e| e.order.id == a.id).unwrap();
1668 assert_eq!(a_entry.days_remaining, 3, "floored, not rounded");
1669 }
1670
1671 #[tokio::test]
1675 async fn hiding_superseded_rows_reports_the_count_rather_than_shrinking_the_total() {
1676 let db = db().await;
1677 let acct = account_id(&db).await;
1678 let a = issued(&db, acct, &["a.example.com"], 3).await;
1679 issued(&db, acct, &["b.example.com"], 5).await;
1680 issued(&db, acct, &["a.example.com"], 90).await;
1681
1682 let query = |include: bool| ExpiringQuery {
1683 profile: None,
1684 before: expiring_horizon(30),
1685 include_superseded: include,
1686 limit: 50,
1687 offset: 0,
1688 };
1689
1690 let (shown, total, hidden) = list_expiring(&query(true), db.clone()).await.unwrap();
1691 assert_eq!(shown.len(), 2, "both expiring rows, annotated");
1692 assert_eq!(total, 2);
1693 assert_eq!(hidden, 0);
1694
1695 let (kept, total, hidden) = list_expiring(&query(false), db.clone()).await.unwrap();
1696 assert_eq!(kept.len(), 1);
1697 assert!(kept.iter().all(|entry| entry.superseded_by.is_none()));
1698 assert_ne!(kept[0].order.id, a.id, "the replaced row is the one hidden");
1699 assert_eq!(hidden, 1);
1700 assert_eq!(
1701 total, 2,
1702 "the total counts the window, not the answer -- documented on list_expiring"
1703 );
1704 }
1705
1706 #[tokio::test]
1709 async fn the_listing_scopes_by_profile_and_answers_soonest_first() {
1710 let db = db().await;
1711 let acct = account_id(&db).await;
1712 let here = issued_order(&db, "default", acct, &["a.example.com"], 5).await;
1713 let sooner = issued_order(&db, "default", acct, &["b.example.com"], 2).await;
1714 issued_order(&db, "other", acct, &["c.example.com"], 1).await;
1715
1716 let scoped = ExpiringQuery {
1717 profile: Some("default".to_string()),
1718 before: expiring_horizon(30),
1719 include_superseded: true,
1720 limit: 50,
1721 offset: 0,
1722 };
1723 let (entries, total, _) = list_expiring(&scoped, db.clone()).await.unwrap();
1724 let ids: Vec<String> = entries
1725 .iter()
1726 .map(|entry| entry.order.id.to_string())
1727 .collect();
1728 assert_eq!(ids, vec![sooner.id.to_string(), here.id.to_string()]);
1729 assert_eq!(total, 2);
1730
1731 let unscoped = ExpiringQuery {
1732 profile: None,
1733 ..scoped
1734 };
1735 let (entries, total, _) = list_expiring(&unscoped, db).await.unwrap();
1736 assert_eq!(entries.len(), 3);
1737 assert_eq!(total, 3);
1738 }
1739}