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