1use std::io::BufRead;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::admin::prompt::confirm;
6use crate::audit::{Actor, AuditEvent, AuditRecord, ClientContext};
7use crate::signer::{SignerBackend, SignerError};
8use crate::sqlite::account::Account;
9use crate::sqlite::audit::{AuditEntry, AuditQuery};
10use crate::sqlite::authz::{Authorization, Challenge};
11use crate::sqlite::db::Database;
12use crate::sqlite::nonce::Nonce;
13use crate::sqlite::order::Order;
14
15#[derive(Debug, PartialEq, Eq)]
21pub enum DeleteOutcome {
22 NotFound,
23 Cancelled,
24 Deleted,
25}
26
27#[derive(Debug, PartialEq, Eq)]
33pub struct Deleted {
34 pub cascaded: u64,
37}
38
39#[derive(Debug)]
41pub struct OrderDetail {
42 pub order: Order,
43 pub authorizations: Vec<(Authorization, Vec<Challenge>)>,
44}
45
46#[derive(Debug)]
48pub enum RevokeOutcome {
49 NotFound,
50 NotIssued,
51 AlreadyRevoked,
52 Revoked(Box<Order>),
53}
54
55fn signer_detail(error: &SignerError) -> String {
61 match error {
62 SignerError::Internal(detail) => detail.clone(),
63 SignerError::BadCsr => "unexpected badCsr from revoke".to_string(),
64 }
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum RevokeError {
70 #[error("database error: {0}")]
71 Database(sqlx::Error),
72 #[error("signer error: {}", signer_detail(.0))]
73 Signer(SignerError),
74 #[error("internal error: {0}")]
75 Internal(String),
76 #[error("unsupported revocation reason code {0}")]
77 BadReason(u32),
78}
79
80impl From<sqlx::Error> for RevokeError {
81 fn from(error: sqlx::Error) -> Self {
82 Self::Database(error)
83 }
84}
85
86impl From<SignerError> for RevokeError {
87 fn from(error: SignerError) -> Self {
88 Self::Signer(error)
89 }
90}
91
92pub async fn delete_account(
103 id: &str,
104 database: Arc<Database>,
105) -> Result<Option<Deleted>, sqlx::Error> {
106 let Some(cascaded) = account_cascade(id, database.clone()).await? else {
107 return Ok(None);
108 };
109 Account::delete(id, &database).await?;
110 Ok(Some(Deleted { cascaded }))
111}
112
113pub async fn confirm_delete_account(
115 id: &str,
116 assume_yes: bool,
117 reader: &mut impl BufRead,
118 database: Arc<Database>,
119) -> Result<DeleteOutcome, sqlx::Error> {
120 let Some(account) = Account::find_any_by_id(id, &database).await? else {
121 return Ok(DeleteOutcome::NotFound);
122 };
123 let order_count = Order::count_by_account(id, &database).await?;
124 let prompt = format!(
125 "Delete account {id} (status: {}, {order_count} order(s) will cascade)?",
126 account.status
127 );
128 if !confirm(&prompt, assume_yes, reader) {
129 return Ok(DeleteOutcome::Cancelled);
130 }
131 Account::delete(id, &database).await?;
132 Ok(DeleteOutcome::Deleted)
133}
134
135pub async fn delete_order(
138 id: &str,
139 database: Arc<Database>,
140) -> Result<Option<Deleted>, sqlx::Error> {
141 let Some(cascaded) = order_cascade(id, database.clone()).await? else {
142 return Ok(None);
143 };
144 Order::delete(id, &database).await?;
145 Ok(Some(Deleted { cascaded }))
146}
147
148pub async fn confirm_delete_order(
150 id: &str,
151 assume_yes: bool,
152 reader: &mut impl BufRead,
153 database: Arc<Database>,
154) -> Result<DeleteOutcome, sqlx::Error> {
155 let Some(order) = Order::find_by_id(id, &database).await? else {
156 return Ok(DeleteOutcome::NotFound);
157 };
158 let authz_count = Authorization::count_by_order(id, &database).await?;
159 let prompt = format!(
160 "Delete order {id} (status: {}, {authz_count} authorization(s) will cascade)?",
161 order.status
162 );
163 if !confirm(&prompt, assume_yes, reader) {
164 return Ok(DeleteOutcome::Cancelled);
165 }
166 Order::delete(id, &database).await?;
167 Ok(DeleteOutcome::Deleted)
168}
169
170pub async fn cleanup_nonces(ttl: Duration, database: Arc<Database>) -> Result<u64, sqlx::Error> {
172 Nonce::cleanup(&database, ttl).await
173}
174
175pub async fn confirm_cleanup_nonces(
177 ttl: Duration,
178 assume_yes: bool,
179 reader: &mut impl BufRead,
180 database: Arc<Database>,
181) -> Result<Option<u64>, sqlx::Error> {
182 let prompt = format!("Delete all nonces older than {}s?", ttl.as_secs());
183 if !confirm(&prompt, assume_yes, reader) {
184 return Ok(None);
185 }
186 Ok(Some(cleanup_nonces(ttl, database).await?))
187}
188
189async fn account_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
192 if Account::find_any_by_id(id, &database).await?.is_none() {
193 return Ok(None);
194 }
195 Ok(Some(Order::count_by_account(id, &database).await? as u64))
196}
197
198async fn order_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
200 if Order::find_by_id(id, &database).await?.is_none() {
201 return Ok(None);
202 }
203 Ok(Some(
204 Authorization::count_by_order(id, &database).await? as u64,
205 ))
206}
207
208pub async fn update_account_contact(
210 id: &str,
211 contact: Vec<String>,
212 database: Arc<Database>,
213) -> Result<Option<Account>, sqlx::Error> {
214 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
215 return Ok(None);
216 };
217 account.update_contact(contact, &database).await?;
218 Ok(Some(account))
219}
220
221pub async fn deactivate_account(
223 id: &str,
224 database: Arc<Database>,
225) -> Result<Option<Account>, sqlx::Error> {
226 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
227 return Ok(None);
228 };
229 account.deactivate(&database).await?;
230 Ok(Some(account))
231}
232
233pub async fn revoke_order(
248 id: &str,
249 reason: Option<u32>,
250 actor: Actor,
251 client: ClientContext,
252 database: Arc<Database>,
253 signer: Arc<dyn SignerBackend>,
254) -> Result<RevokeOutcome, RevokeError> {
255 let Some(mut order) = Order::find_by_id(id, &database).await? else {
256 return Ok(RevokeOutcome::NotFound);
257 };
258 let Some(chain) = order.certificate.clone() else {
259 return Ok(RevokeOutcome::NotIssued);
260 };
261 if order.revoked_at.is_some() {
262 return Ok(RevokeOutcome::AlreadyRevoked);
263 }
264 if let Some(r) = reason
265 && !crate::cert::is_valid_revocation_reason(r)
266 {
267 return Err(RevokeError::BadReason(r));
268 }
269
270 let cert_der = crate::cert::leaf_der_from_chain(&chain).map_err(|error| {
271 RevokeError::Internal(format!("stored certificate chain is unparsable: {error}"))
272 })?;
273 let mut record = AuditRecord::new(
274 AuditEvent::CertificateRevoked,
275 &order.profile,
276 actor.clone(),
277 )
278 .with_order(&order)
279 .with_client(client.clone());
280 if let Some(serial) = order.cert_serial.clone() {
281 record = record.with_serial(serial);
282 }
283 if let Some(reason) = reason {
286 record = record.with_reason(reason.to_string());
287 }
288
289 if let Err(error) = signer.revoke(&cert_der, reason).await {
293 crate::audit::write(
294 AuditRecord::new(AuditEvent::CertificateRevokeFailed, &order.profile, actor)
295 .with_order(&order)
296 .with_client(client)
297 .with_reason("serverInternal")
298 .with_detail(error.to_string()),
299 &database,
300 )
301 .await;
302 return Err(error.into());
303 }
304 order.revoke(reason.map(i64::from), &database).await?;
305 crate::audit::write(record, &database).await;
306 Ok(RevokeOutcome::Revoked(Box::new(order)))
307}
308
309#[must_use]
315pub fn audit_cutoff(days: u64) -> i64 {
316 let seconds = i64::try_from(days.saturating_mul(24 * 60 * 60)).unwrap_or(i64::MAX);
317 crate::sqlite::nonce::now_secs().saturating_sub(seconds)
318}
319
320pub async fn list_audit(
322 query: &AuditQuery,
323 database: Arc<Database>,
324) -> Result<(Vec<AuditEntry>, i64), sqlx::Error> {
325 AuditEntry::search(query, &database).await
326}
327
328pub async fn find_audit(
330 id: i64,
331 database: Arc<Database>,
332) -> Result<Option<AuditEntry>, sqlx::Error> {
333 AuditEntry::find_by_id(id, &database).await
334}
335
336pub async fn cleanup_audit(days: u64, database: Arc<Database>) -> Result<u64, sqlx::Error> {
338 AuditEntry::cleanup(audit_cutoff(days), &database).await
339}
340
341pub async fn confirm_cleanup_audit(
347 days: u64,
348 assume_yes: bool,
349 reader: &mut impl BufRead,
350 database: Arc<Database>,
351) -> Result<Option<u64>, sqlx::Error> {
352 let cutoff = audit_cutoff(days);
353 let doomed = AuditEntry::count_older_than(cutoff, &database).await?;
354 let prompt =
355 format!("Delete {doomed} audit row(s) older than {days} day(s)? This cannot be undone.");
356 if !confirm(&prompt, assume_yes, reader) {
357 return Ok(None);
358 }
359 Ok(Some(AuditEntry::cleanup(cutoff, &database).await?))
360}
361
362pub async fn load_order_detail(
364 id: &str,
365 database: Arc<Database>,
366) -> Result<Option<OrderDetail>, sqlx::Error> {
367 let Some(order) = Order::find_by_id(id, &database).await? else {
368 return Ok(None);
369 };
370 let authzs = Authorization::find_by_order(&order.id, &database).await?;
371 let mut authorizations = Vec::with_capacity(authzs.len());
372 for authz in authzs {
373 let challenges = Challenge::find_by_authz(&authz.id, &database).await?;
374 authorizations.push((authz, challenges));
375 }
376 Ok(Some(OrderDetail {
377 order,
378 authorizations,
379 }))
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::sqlite::order::Identifier;
386 use crate::testutil::account_id;
387
388 fn cli_actor() -> Actor {
392 Actor::cli()
393 }
394
395 async fn audit_rows(db: &Arc<Database>) -> Vec<AuditEntry> {
396 AuditEntry::search(
397 &AuditQuery {
398 limit: 50,
399 ..AuditQuery::default()
400 },
401 db,
402 )
403 .await
404 .unwrap()
405 .0
406 }
407
408 #[test]
411 fn the_audit_cutoff_is_days_before_now_and_saturates_rather_than_overflowing() {
412 let now = crate::sqlite::nonce::now_secs();
413 assert!((audit_cutoff(0) - now).abs() <= 1);
414 let week = audit_cutoff(7);
415 assert!((now - week - 7 * 24 * 60 * 60).abs() <= 1, "{week}");
416 assert!(audit_cutoff(u64::MAX) <= now);
418 }
419
420 #[tokio::test]
423 async fn cleaning_the_audit_trail_is_confirm_gated_and_bounded_by_age() {
424 let db = Arc::new(Database::connect_in_memory().await.unwrap());
425 AuditEntry::insert(
426 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
427 &db,
428 )
429 .await
430 .unwrap();
431
432 let mut declined: &[u8] = b"n\n";
433 assert_eq!(
434 confirm_cleanup_audit(0, false, &mut declined, db.clone())
435 .await
436 .unwrap(),
437 None
438 );
439 assert_eq!(audit_rows(&db).await.len(), 1);
440
441 let mut reader: &[u8] = &[];
443 assert_eq!(
444 confirm_cleanup_audit(7, true, &mut reader, db.clone())
445 .await
446 .unwrap(),
447 Some(0)
448 );
449 assert_eq!(audit_rows(&db).await.len(), 1);
450
451 assert_eq!(cleanup_audit(0, db.clone()).await.unwrap(), 0);
452
453 assert_eq!(
455 AuditEntry::cleanup(audit_cutoff(0) + 3600, &db)
456 .await
457 .unwrap(),
458 1
459 );
460 assert!(audit_rows(&db).await.is_empty());
461 }
462
463 #[tokio::test]
467 async fn listing_and_finding_audit_rows_pages_and_resolves() {
468 let db = Arc::new(Database::connect_in_memory().await.unwrap());
469 let mut ids = Vec::new();
470 for _ in 0..3 {
471 ids.push(
472 AuditEntry::insert(
473 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
474 &db,
475 )
476 .await
477 .unwrap(),
478 );
479 }
480
481 let (page, total) = list_audit(
482 &AuditQuery {
483 limit: 2,
484 ..AuditQuery::default()
485 },
486 db.clone(),
487 )
488 .await
489 .unwrap();
490 assert_eq!(total, 3);
491 assert_eq!(page.len(), 2);
492
493 assert!(find_audit(ids[0], db.clone()).await.unwrap().is_some());
494 assert!(find_audit(9_999, db).await.unwrap().is_none());
495 }
496
497 #[tokio::test]
501 async fn revoking_writes_one_audit_row_naming_the_caller() {
502 let db = Arc::new(Database::connect_in_memory().await.unwrap());
503 let signer = in_memory_ca();
504 let order = finalized_order(db.clone(), &signer).await;
505
506 let outcome = revoke_order(
507 &order.id,
508 Some(1),
509 Actor::admin("root"),
510 ClientContext {
511 ip: Some("203.0.113.7".to_string()),
512 ptr: Some("desk.example.com".to_string()),
513 ..ClientContext::default()
514 },
515 db.clone(),
516 signer.clone(),
517 )
518 .await
519 .unwrap();
520 assert!(matches!(outcome, RevokeOutcome::Revoked(_)));
521
522 let rows = audit_rows(&db).await;
523 assert_eq!(rows.len(), 1, "{rows:?}");
524 let row = &rows[0];
525 assert_eq!(row.event, "certificate_revoked");
526 assert_eq!(row.outcome, "success");
527 assert_eq!(row.actor_kind, "admin");
528 assert_eq!(row.actor_id.as_deref(), Some("root"));
529 assert_eq!(row.account_id.as_deref(), Some(order.account_id.as_str()));
530 assert_eq!(row.order_id.as_deref(), Some(order.id.as_str()));
531 assert_eq!(row.cert_serial, order.cert_serial);
532 assert_eq!(row.client_ip.as_deref(), Some("203.0.113.7"));
533 assert_eq!(row.client_ptr.as_deref(), Some("desk.example.com"));
534 assert_eq!(row.reason.as_deref(), Some("1"));
535
536 let outcome = revoke_order(
539 &order.id,
540 None,
541 Actor::admin("root"),
542 ClientContext::default(),
543 db.clone(),
544 signer,
545 )
546 .await
547 .unwrap();
548 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
549 assert_eq!(audit_rows(&db).await.len(), 1);
550 }
551
552 #[tokio::test]
555 async fn a_revocation_with_no_reason_leaves_the_column_absent() {
556 let db = Arc::new(Database::connect_in_memory().await.unwrap());
557 let signer = in_memory_ca();
558 let order = finalized_order(db.clone(), &signer).await;
559
560 revoke_order(
561 &order.id,
562 None,
563 cli_actor(),
564 ClientContext::default(),
565 db.clone(),
566 signer,
567 )
568 .await
569 .unwrap();
570
571 let rows = audit_rows(&db).await;
572 assert_eq!(rows[0].reason, None);
573 assert_eq!(rows[0].actor_kind, "cli");
574 assert_eq!(rows[0].client_ip, None);
576 assert_eq!(rows[0].client_ptr, None);
577 }
578
579 #[tokio::test]
580 async fn delete_account_not_found() {
581 let db = Arc::new(Database::connect_in_memory().await.unwrap());
582 let mut reader: &[u8] = &[];
583 let outcome = confirm_delete_account("nope", true, &mut reader, db)
584 .await
585 .unwrap();
586 assert_eq!(outcome, DeleteOutcome::NotFound);
587 }
588
589 #[tokio::test]
590 async fn delete_account_cancelled_leaves_row() {
591 let db = Arc::new(Database::connect_in_memory().await.unwrap());
592 let acct = account_id(&db).await;
593
594 let mut reader = b"n\n".as_slice();
595 let outcome = confirm_delete_account(&acct, false, &mut reader, db.clone())
596 .await
597 .unwrap();
598 assert_eq!(outcome, DeleteOutcome::Cancelled);
599 assert!(
600 Account::find_by_id("default", &acct, &db)
601 .await
602 .unwrap()
603 .is_some()
604 );
605 }
606
607 #[tokio::test]
608 async fn delete_account_confirmed_deletes_and_cascades() {
609 let db = Arc::new(Database::connect_in_memory().await.unwrap());
610 let acct = account_id(&db).await;
611 let order = Order::create(
612 "default",
613 &acct,
614 vec![Identifier::dns("example.com")],
615 crate::sqlite::nonce::now_secs() + 3600,
616 None,
617 None,
618 &db,
619 )
620 .await
621 .unwrap();
622
623 let mut reader: &[u8] = &[];
624 let outcome = confirm_delete_account(&acct, true, &mut reader, db.clone())
625 .await
626 .unwrap();
627 assert_eq!(outcome, DeleteOutcome::Deleted);
628 assert!(
629 Account::find_by_id("default", &acct, &db)
630 .await
631 .unwrap()
632 .is_none()
633 );
634 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
635 }
636
637 #[tokio::test]
638 async fn delete_order_not_found() {
639 let db = Arc::new(Database::connect_in_memory().await.unwrap());
640 let mut reader: &[u8] = &[];
641 let outcome = confirm_delete_order("nope", true, &mut reader, db)
642 .await
643 .unwrap();
644 assert_eq!(outcome, DeleteOutcome::NotFound);
645 }
646
647 #[tokio::test]
648 async fn delete_order_cancelled_leaves_row() {
649 let db = Arc::new(Database::connect_in_memory().await.unwrap());
650 let acct = account_id(&db).await;
651 let order = Order::create(
652 "default",
653 &acct,
654 vec![Identifier::dns("example.com")],
655 crate::sqlite::nonce::now_secs() + 3600,
656 None,
657 None,
658 &db,
659 )
660 .await
661 .unwrap();
662
663 let mut reader = b"no\n".as_slice();
664 let outcome = confirm_delete_order(&order.id, false, &mut reader, db.clone())
665 .await
666 .unwrap();
667 assert_eq!(outcome, DeleteOutcome::Cancelled);
668 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_some());
669 }
670
671 #[tokio::test]
672 async fn delete_order_confirmed_deletes_and_cascades() {
673 let db = Arc::new(Database::connect_in_memory().await.unwrap());
674 let acct = account_id(&db).await;
675 let order = Order::create(
676 "default",
677 &acct,
678 vec![Identifier::dns("example.com")],
679 crate::sqlite::nonce::now_secs() + 3600,
680 None,
681 None,
682 &db,
683 )
684 .await
685 .unwrap();
686 let authz = Authorization::create(
687 &order.id,
688 Identifier::dns("example.com"),
689 crate::sqlite::nonce::now_secs() + 3600,
690 &db,
691 )
692 .await
693 .unwrap();
694
695 let mut reader: &[u8] = &[];
696 let outcome = confirm_delete_order(&order.id, true, &mut reader, db.clone())
697 .await
698 .unwrap();
699 assert_eq!(outcome, DeleteOutcome::Deleted);
700 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
701 assert!(
702 Authorization::find_by_id(&authz.id, &db)
703 .await
704 .unwrap()
705 .is_none()
706 );
707 }
708
709 #[tokio::test]
713 async fn bare_delete_account_reports_none_for_an_unknown_id() {
714 let db = Arc::new(Database::connect_in_memory().await.unwrap());
715 assert_eq!(delete_account("nope", db).await.unwrap(), None);
716 }
717
718 #[tokio::test]
719 async fn bare_delete_account_deletes_and_counts_the_cascade() {
720 let db = Arc::new(Database::connect_in_memory().await.unwrap());
721 let acct = account_id(&db).await;
722 for _ in 0..2 {
723 Order::create(
724 "default",
725 &acct,
726 vec![Identifier::dns("example.com")],
727 crate::sqlite::nonce::now_secs() + 3600,
728 None,
729 None,
730 &db,
731 )
732 .await
733 .unwrap();
734 }
735
736 assert_eq!(
737 delete_account(&acct, db.clone()).await.unwrap(),
738 Some(Deleted { cascaded: 2 })
739 );
740 assert!(
741 Account::find_by_id("default", &acct, &db)
742 .await
743 .unwrap()
744 .is_none()
745 );
746 }
747
748 #[tokio::test]
749 async fn bare_delete_order_reports_none_for_an_unknown_id() {
750 let db = Arc::new(Database::connect_in_memory().await.unwrap());
751 assert_eq!(delete_order("nope", db).await.unwrap(), None);
752 }
753
754 #[tokio::test]
755 async fn bare_delete_order_deletes_and_counts_the_cascade() {
756 let db = Arc::new(Database::connect_in_memory().await.unwrap());
757 let acct = account_id(&db).await;
758 let order = Order::create(
759 "default",
760 &acct,
761 vec![Identifier::dns("example.com")],
762 crate::sqlite::nonce::now_secs() + 3600,
763 None,
764 None,
765 &db,
766 )
767 .await
768 .unwrap();
769 Authorization::create(
770 &order.id,
771 Identifier::dns("example.com"),
772 crate::sqlite::nonce::now_secs() + 3600,
773 &db,
774 )
775 .await
776 .unwrap();
777
778 assert_eq!(
779 delete_order(&order.id, db.clone()).await.unwrap(),
780 Some(Deleted { cascaded: 1 })
781 );
782 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
783 }
784
785 #[tokio::test]
786 async fn bare_cleanup_nonces_removes_stale_rows_without_asking() {
787 let db = Arc::new(Database::connect_in_memory().await.unwrap());
788 let stale = Nonce {
789 value: "stale".to_string(),
790 created_at: crate::sqlite::nonce::now_secs() - 10_000,
791 };
792 stale.save(&db).await.unwrap();
793 Nonce::new().save(&db).await.unwrap();
794
795 assert_eq!(
796 cleanup_nonces(Duration::from_secs(300), db.clone())
797 .await
798 .unwrap(),
799 1
800 );
801 assert!(
802 !Nonce::verify("stale", &db, Duration::from_secs(300))
803 .await
804 .unwrap()
805 );
806 }
807
808 fn in_memory_ca() -> Arc<dyn SignerBackend> {
809 Arc::new(
810 crate::signer::local_ca::LocalCa::generate_in_memory("ecdsa-p256", 90)
811 .expect("in-memory CA"),
812 )
813 }
814
815 async fn finalized_order(db: Arc<Database>, signer: &Arc<dyn SignerBackend>) -> Order {
816 let acct = account_id(&db).await;
817 let mut order = Order::create(
818 "default",
819 &acct,
820 vec![Identifier::dns("example.com")],
821 crate::sqlite::nonce::now_secs() + 3600,
822 None,
823 None,
824 &db,
825 )
826 .await
827 .unwrap();
828
829 let key_pair = rcgen::KeyPair::generate().unwrap();
830 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
831 let csr = params.serialize_request(&key_pair).unwrap();
832 let chain = match signer
833 .issue(
834 &order.id,
835 csr.der(),
836 &order.identifiers,
837 crate::signer::RequestedValidity::default(),
838 )
839 .await
840 .unwrap()
841 {
842 crate::signer::IssueOutcome::Issued(chain) => chain,
843 crate::signer::IssueOutcome::Processing => {
844 panic!("the in-memory local CA issues synchronously")
845 }
846 };
847 let leaf = crate::cert::leaf_der_from_chain(&chain).unwrap();
848 let (serial, pubkey) = crate::cert::cert_serial_and_spki(&leaf).unwrap();
849 order.finalize(chain, serial, pubkey, &db).await.unwrap();
850 order
851 }
852
853 #[tokio::test]
854 async fn revoke_order_not_found() {
855 let db = Arc::new(Database::connect_in_memory().await.unwrap());
856 let outcome = revoke_order(
857 "nope",
858 None,
859 cli_actor(),
860 ClientContext::default(),
861 db,
862 in_memory_ca(),
863 )
864 .await
865 .unwrap();
866 assert!(matches!(outcome, RevokeOutcome::NotFound));
867 }
868
869 #[tokio::test]
870 async fn revoke_order_without_a_certificate_is_refused() {
871 let db = Arc::new(Database::connect_in_memory().await.unwrap());
872 let acct = account_id(&db).await;
873 let order = Order::create(
874 "default",
875 &acct,
876 vec![Identifier::dns("example.com")],
877 crate::sqlite::nonce::now_secs() + 3600,
878 None,
879 None,
880 &db,
881 )
882 .await
883 .unwrap();
884
885 let outcome = revoke_order(
886 &order.id,
887 None,
888 cli_actor(),
889 ClientContext::default(),
890 db,
891 in_memory_ca(),
892 )
893 .await
894 .unwrap();
895 assert!(matches!(outcome, RevokeOutcome::NotIssued));
896 }
897
898 #[tokio::test]
899 async fn revoke_order_persists() {
900 let db = Arc::new(Database::connect_in_memory().await.unwrap());
901 let signer = in_memory_ca();
902 let order = finalized_order(db.clone(), &signer).await;
903
904 let outcome = revoke_order(
905 &order.id,
906 Some(1),
907 cli_actor(),
908 ClientContext::default(),
909 db.clone(),
910 signer.clone(),
911 )
912 .await
913 .unwrap();
914 let RevokeOutcome::Revoked(revoked) = outcome else {
915 panic!("expected Revoked, got {outcome:?}");
916 };
917 assert!(revoked.revoked_at.is_some());
918 assert_eq!(revoked.revocation_reason, Some(1));
919
920 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
921 assert!(reloaded.revoked_at.is_some());
922
923 use x509_parser::prelude::FromDer;
924 let der = signer.crl_der().await.unwrap();
925 let (_, crl) =
926 x509_parser::revocation_list::CertificateRevocationList::from_der(&der).unwrap();
927 assert_eq!(crl.iter_revoked_certificates().count(), 1);
928 }
929
930 #[tokio::test]
931 async fn revoke_order_already_revoked() {
932 let db = Arc::new(Database::connect_in_memory().await.unwrap());
933 let signer = in_memory_ca();
934 let order = finalized_order(db.clone(), &signer).await;
935
936 revoke_order(
937 &order.id,
938 None,
939 cli_actor(),
940 ClientContext::default(),
941 db.clone(),
942 signer.clone(),
943 )
944 .await
945 .unwrap();
946 let outcome = revoke_order(
947 &order.id,
948 None,
949 cli_actor(),
950 ClientContext::default(),
951 db,
952 signer,
953 )
954 .await
955 .unwrap();
956 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
957 }
958
959 #[tokio::test]
960 async fn revoke_order_bad_reason_is_refused() {
961 let db = Arc::new(Database::connect_in_memory().await.unwrap());
962 let signer = in_memory_ca();
963 let order = finalized_order(db.clone(), &signer).await;
964
965 let error = revoke_order(
966 &order.id,
967 Some(999),
968 cli_actor(),
969 ClientContext::default(),
970 db,
971 signer,
972 )
973 .await
974 .unwrap_err();
975 assert!(matches!(error, RevokeError::BadReason(999)));
976 }
977
978 #[tokio::test]
979 async fn cleanup_nonces_cancelled_leaves_nonces() {
980 let db = Arc::new(Database::connect_in_memory().await.unwrap());
981 Nonce {
982 value: "stale".to_string(),
983 created_at: crate::sqlite::nonce::now_secs() - 600,
984 }
985 .save(&db)
986 .await
987 .unwrap();
988
989 let mut reader = b"n\n".as_slice();
990 let outcome =
991 confirm_cleanup_nonces(Duration::from_secs(300), false, &mut reader, db.clone())
992 .await
993 .unwrap();
994 assert_eq!(outcome, None);
995
996 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
997 .fetch_one(&db.pool)
998 .await
999 .unwrap();
1000 assert_eq!(count, 1);
1001 }
1002
1003 #[tokio::test]
1004 async fn cleanup_nonces_confirmed_removes_stale_and_reports_count() {
1005 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1006 Nonce {
1007 value: "stale".to_string(),
1008 created_at: crate::sqlite::nonce::now_secs() - 600,
1009 }
1010 .save(&db)
1011 .await
1012 .unwrap();
1013
1014 let mut reader: &[u8] = &[];
1015 let outcome =
1016 confirm_cleanup_nonces(Duration::from_secs(300), true, &mut reader, db.clone())
1017 .await
1018 .unwrap();
1019 assert_eq!(outcome, Some(1));
1020
1021 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1022 .fetch_one(&db.pool)
1023 .await
1024 .unwrap();
1025 assert_eq!(count, 0);
1026 }
1027
1028 #[tokio::test]
1029 async fn update_account_contact_not_found() {
1030 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1031 assert!(
1032 update_account_contact("nope", vec![], db)
1033 .await
1034 .unwrap()
1035 .is_none()
1036 );
1037 }
1038
1039 #[tokio::test]
1040 async fn update_account_contact_persists() {
1041 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1042 let acct = account_id(&db).await;
1043
1044 let contact = vec!["mailto:a@example.com".to_string()];
1045 let updated = update_account_contact(&acct, contact.clone(), db.clone())
1046 .await
1047 .unwrap()
1048 .unwrap();
1049 assert_eq!(updated.contact, contact);
1050
1051 let reloaded = Account::find_by_id("default", &acct, &db)
1052 .await
1053 .unwrap()
1054 .unwrap();
1055 assert_eq!(reloaded.contact, contact);
1056 }
1057
1058 #[tokio::test]
1059 async fn deactivate_account_not_found() {
1060 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1061 assert!(deactivate_account("nope", db).await.unwrap().is_none());
1062 }
1063
1064 #[tokio::test]
1065 async fn deactivate_account_persists() {
1066 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1067 let acct = account_id(&db).await;
1068
1069 let updated = deactivate_account(&acct, db.clone())
1070 .await
1071 .unwrap()
1072 .unwrap();
1073 assert_eq!(updated.status, "deactivated");
1074
1075 let reloaded = Account::find_by_id("default", &acct, &db)
1076 .await
1077 .unwrap()
1078 .unwrap();
1079 assert_eq!(reloaded.status, "deactivated");
1080 }
1081
1082 #[tokio::test]
1083 async fn load_order_detail_not_found() {
1084 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1085 assert!(load_order_detail("nope", db).await.unwrap().is_none());
1086 }
1087
1088 #[tokio::test]
1089 async fn load_order_detail_nests_authorizations_and_challenges() {
1090 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1091 let acct = account_id(&db).await;
1092 let order = Order::create(
1093 "default",
1094 &acct,
1095 vec![Identifier::dns("example.com")],
1096 crate::sqlite::nonce::now_secs() + 3600,
1097 None,
1098 None,
1099 &db,
1100 )
1101 .await
1102 .unwrap();
1103 let authz = Authorization::create(
1104 &order.id,
1105 Identifier::dns("example.com"),
1106 crate::sqlite::nonce::now_secs() + 3600,
1107 &db,
1108 )
1109 .await
1110 .unwrap();
1111 Challenge::create(&authz.id, "http-01", &db).await.unwrap();
1112
1113 let detail = load_order_detail(&order.id, db).await.unwrap().unwrap();
1114 assert_eq!(detail.order.id, order.id);
1115 assert_eq!(detail.authorizations.len(), 1);
1116 assert_eq!(detail.authorizations[0].0.id, authz.id);
1117 assert_eq!(detail.authorizations[0].1.len(), 1);
1118 assert_eq!(detail.authorizations[0].1[0].typ, "http-01");
1119 }
1120
1121 #[test]
1122 fn revoke_error_display_formatting() {
1123 let db_err: RevokeError = sqlx::Error::RowNotFound.into();
1124 assert!(format!("{db_err}").contains("database error"));
1125
1126 let signer_internal: RevokeError = SignerError::Internal("test".to_string()).into();
1127 assert!(format!("{signer_internal}").contains("signer error: test"));
1128
1129 let signer_bad_csr: RevokeError = SignerError::BadCsr.into();
1130 assert!(format!("{signer_bad_csr}").contains("unexpected badCsr"));
1131
1132 let internal = RevokeError::Internal("detail".to_string());
1133 assert!(format!("{internal}").contains("internal error: detail"));
1134
1135 let bad_reason = RevokeError::BadReason(7);
1136 assert!(format!("{bad_reason}").contains("unsupported revocation reason code 7"));
1137 }
1138}