1use std::io::BufRead;
20use std::sync::Arc;
21use std::time::Duration;
22
23use crate::admin::prompt::confirm;
24use acme_proxy_core::audit::Actor;
25use acme_proxy_core::audit::ClientContext;
26use acme_proxy_jobs::auditor::Auditor;
27use acme_proxy_signer::SignerError;
28use acme_proxy_signer::relay::RELAY_JOB_KIND;
29use acme_proxy_signer::relay::abandon_relayed_order;
30use acme_proxy_store::account::Account;
31use acme_proxy_store::audit::AuditEntry;
32use acme_proxy_store::audit::AuditQuery;
33use acme_proxy_store::authz::Authorization;
34use acme_proxy_store::authz::Challenge;
35use acme_proxy_store::db::Database;
36use acme_proxy_store::eab::BoundAccounts;
37use acme_proxy_store::eab::DeletedEab;
38use acme_proxy_store::eab::Eab;
39use acme_proxy_store::eab::EabDeletion;
40use acme_proxy_store::job::Job;
41use acme_proxy_store::nonce::Nonce;
42use acme_proxy_store::order::GuardedDelete;
43use acme_proxy_store::order::Order;
44use acme_proxy_store::status::JobStatus;
45use acme_proxy_store::upstream_order::UpstreamOrder;
46use acme_proxy_store::upstream_order::UpstreamOrderRow;
47
48#[derive(Debug, PartialEq, Eq)]
54pub enum DeleteOutcome {
55 NotFound,
56 Cancelled,
57 Deleted(Deleted),
66 LiveCertificates(u64),
74}
75
76#[derive(Debug, PartialEq, Eq)]
79pub enum Deletion {
80 NotFound,
81 LiveCertificates(u64),
83 Deleted(Deleted),
84}
85
86impl Deletion {
87 fn from_guarded(outcome: GuardedDelete, cascaded: u64) -> Self {
88 match outcome {
89 GuardedDelete::NotFound => Self::NotFound,
90 GuardedDelete::LiveCertificates(live) => Self::LiveCertificates(live),
91 GuardedDelete::Deleted => Self::Deleted(Deleted { cascaded }),
92 }
93 }
94}
95
96impl From<Deletion> for DeleteOutcome {
97 fn from(deletion: Deletion) -> Self {
98 match deletion {
99 Deletion::NotFound => Self::NotFound,
100 Deletion::LiveCertificates(live) => Self::LiveCertificates(live),
101 Deletion::Deleted(deleted) => Self::Deleted(deleted),
102 }
103 }
104}
105
106#[must_use]
114pub fn live_certificates_refusal(subject: &str, live: u64) -> String {
115 format!(
116 "{subject} holds {live} live certificate(s) (issued, not revoked, not expired); \
117 deleting it would leave them impossible to revoke — revoke them first, or wait \
118 for them to expire"
119 )
120}
121
122#[must_use]
125pub fn eab_live_certificates_refusal(kid: &str, accounts: u64, certificates: u64) -> String {
126 format!(
127 "{accounts} account(s) bound to EAB credential {kid} hold {certificates} live \
128 certificate(s) (issued, not revoked, not expired); deleting them would leave those \
129 certificates impossible to revoke — revoke them first, wait for them to expire, or \
130 deactivate the accounts instead of deleting them"
131 )
132}
133
134#[must_use]
145pub fn unmounted_profile_refusal(
146 mounted: impl Fn(&str) -> bool,
147 profile: Option<&str>,
148 hint: &str,
149) -> Option<String> {
150 let name = profile?;
151 if mounted(name) {
152 return None;
153 }
154 Some(format!(
155 "no profile named `{name}` is mounted; {hint} for a credential valid at every endpoint"
156 ))
157}
158
159#[derive(Debug, PartialEq, Eq)]
165pub struct Deleted {
166 pub cascaded: u64,
169}
170
171#[derive(Debug)]
173pub struct OrderDetail {
174 pub order: Order,
175 pub authorizations: Vec<(Authorization, Vec<Challenge>)>,
176}
177
178#[derive(Debug)]
180pub enum RevokeOutcome {
181 NotFound,
182 NotIssued,
183 AlreadyRevoked,
184 Revoked(Box<Order>),
185 Queued(uuid::Uuid),
189}
190
191#[derive(Debug, thiserror::Error)]
193pub enum RevokeError {
194 #[error("database error: {0}")]
195 Database(sqlx::Error),
196 #[error("signer error: {}", acme_proxy_protocol::acme::revoke::signer_detail(.0))]
197 Signer(SignerError),
198 #[error("internal error: {0}")]
199 Internal(String),
200 #[error("unsupported revocation reason code {0}")]
201 BadReason(u32),
202 #[error("the revocation failed (job {job}): {reason}")]
205 Abandoned { job: uuid::Uuid, reason: String },
206}
207impl From<sqlx::Error> for RevokeError {
208 fn from(error: sqlx::Error) -> Self {
209 Self::Database(error)
210 }
211}
212
213impl From<SignerError> for RevokeError {
214 fn from(error: SignerError) -> Self {
215 Self::Signer(error)
216 }
217}
218
219pub async fn delete_account(id: &str, database: Arc<Database>) -> Result<Deletion, sqlx::Error> {
230 let Some(cascaded) = account_cascade(id, database.clone()).await? else {
231 return Ok(Deletion::NotFound);
232 };
233 let outcome = Account::delete(id, &database).await?;
234 Ok(Deletion::from_guarded(outcome, cascaded))
235}
236
237pub async fn confirm_delete_account(
244 id: &str,
245 assume_yes: bool,
246 reader: &mut impl BufRead,
247 database: Arc<Database>,
248) -> Result<DeleteOutcome, sqlx::Error> {
249 let Some(account) = Account::find_any_by_id(id, &database).await? else {
250 return Ok(DeleteOutcome::NotFound);
251 };
252 let live = Account::count_live_certificates(account.id, &database).await?;
253 if live > 0 {
254 return Ok(DeleteOutcome::LiveCertificates(live));
255 }
256 let order_count = Order::count_by_account(account.id, &database).await?;
257 let prompt = format!(
258 "Delete account {id} (status: {}, {order_count} order(s) will cascade)?",
259 account.status
260 );
261 if !confirm(&prompt, assume_yes, reader) {
262 return Ok(DeleteOutcome::Cancelled);
263 }
264 let outcome = Account::delete(id, &database).await?;
265 Ok(Deletion::from_guarded(outcome, order_count as u64).into())
266}
267
268pub async fn delete_order(id: &str, database: Arc<Database>) -> Result<Deletion, sqlx::Error> {
271 let Some(cascaded) = order_cascade(id, database.clone()).await? else {
272 return Ok(Deletion::NotFound);
273 };
274 let outcome = Order::delete(id, &database).await?;
275 Ok(Deletion::from_guarded(outcome, cascaded))
276}
277
278pub async fn confirm_delete_order(
280 id: &str,
281 assume_yes: bool,
282 reader: &mut impl BufRead,
283 database: Arc<Database>,
284) -> Result<DeleteOutcome, sqlx::Error> {
285 let Some(order) = Order::find_by_id(id, &database).await? else {
286 return Ok(DeleteOutcome::NotFound);
287 };
288 let live = Order::count_live_certificates(order.id, &database).await?;
289 if live > 0 {
290 return Ok(DeleteOutcome::LiveCertificates(live));
291 }
292 let authz_count = Authorization::count_by_order(order.id, &database).await?;
293 let prompt = format!(
294 "Delete order {id} (status: {}, {authz_count} authorization(s) will cascade)?",
295 order.status
296 );
297 if !confirm(&prompt, assume_yes, reader) {
298 return Ok(DeleteOutcome::Cancelled);
299 }
300 let outcome = Order::delete(id, &database).await?;
301 Ok(Deletion::from_guarded(outcome, authz_count as u64).into())
302}
303
304#[derive(Debug)]
306pub enum EabDeleteOutcome {
307 NotFound,
308 Cancelled,
309 LiveCertificates {
311 accounts: u64,
312 certificates: u64,
313 },
314 Deleted(DeletedEab),
315}
316
317impl From<EabDeletion> for EabDeleteOutcome {
318 fn from(deletion: EabDeletion) -> Self {
319 match deletion {
320 EabDeletion::NotFound => Self::NotFound,
321 EabDeletion::LiveCertificates {
322 accounts,
323 certificates,
324 } => Self::LiveCertificates {
325 accounts,
326 certificates,
327 },
328 EabDeletion::Deleted(deleted) => Self::Deleted(deleted),
329 }
330 }
331}
332
333pub async fn delete_eab(
336 kid: &str,
337 accounts: BoundAccounts,
338 database: Arc<Database>,
339) -> Result<EabDeletion, sqlx::Error> {
340 Eab::delete(kid, accounts, &database).await
341}
342
343pub async fn confirm_delete_eab(
349 kid: &str,
350 accounts: BoundAccounts,
351 assume_yes: bool,
352 reader: &mut impl BufRead,
353 database: Arc<Database>,
354) -> Result<EabDeleteOutcome, sqlx::Error> {
355 let Some(eab) = Eab::find_any_by_kid(kid, &database).await? else {
356 return Ok(EabDeleteOutcome::NotFound);
357 };
358 let bound = Account::eab_summary(eab.kid, &database).await?;
359 if accounts == BoundAccounts::Delete && bound.live_certificates > 0 {
360 return Ok(EabDeleteOutcome::LiveCertificates {
361 accounts: bound.accounts_with_live_certificates,
362 certificates: bound.live_certificates,
363 });
364 }
365 let prompt = match accounts {
366 BoundAccounts::Keep => format!(
367 "Delete EAB credential {kid} (status: {})? Its {} account(s) are kept, and will \
368 fail any eab filter check from now on.",
369 eab.status, bound.accounts
370 ),
371 BoundAccounts::Deactivate => format!(
372 "Delete EAB credential {kid} and deactivate its {} account(s)? Their {} order(s) \
373 are kept.",
374 bound.accounts, bound.orders
375 ),
376 BoundAccounts::Delete => format!(
377 "Delete EAB credential {kid} and its {} account(s) ({} order(s) will cascade)?",
378 bound.accounts, bound.orders
379 ),
380 };
381 if !confirm(&prompt, assume_yes, reader) {
382 return Ok(EabDeleteOutcome::Cancelled);
383 }
384 Ok(Eab::delete(kid, accounts, &database).await?.into())
385}
386
387pub async fn cleanup_nonces(ttl: Duration, database: Arc<Database>) -> Result<u64, sqlx::Error> {
389 Nonce::cleanup(&database, ttl).await
390}
391
392pub async fn confirm_cleanup_nonces(
394 ttl: Duration,
395 assume_yes: bool,
396 reader: &mut impl BufRead,
397 database: Arc<Database>,
398) -> Result<Option<u64>, sqlx::Error> {
399 let prompt = format!("Delete all nonces older than {}s?", ttl.as_secs());
400 if !confirm(&prompt, assume_yes, reader) {
401 return Ok(None);
402 }
403 Ok(Some(cleanup_nonces(ttl, database).await?))
404}
405
406async fn account_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
409 let Some(account) = Account::find_any_by_id(id, &database).await? else {
410 return Ok(None);
411 };
412 Ok(Some(
413 Order::count_by_account(account.id, &database).await? as u64,
414 ))
415}
416
417async fn order_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
419 let Some(order) = Order::find_by_id(id, &database).await? else {
420 return Ok(None);
421 };
422 Ok(Some(
423 Authorization::count_by_order(order.id, &database).await? as u64,
424 ))
425}
426
427#[derive(Debug, thiserror::Error)]
429pub enum ContactError {
430 #[error("{0}")]
432 Invalid(String),
433 #[error("database error: {0}")]
434 Database(#[from] sqlx::Error),
435}
436
437pub async fn update_account_contact(
441 id: &str,
442 contact: Vec<String>,
443 database: Arc<Database>,
444) -> Result<Option<Account>, ContactError> {
445 use acme_proxy_protocol::acme::account::ContactUpdateError;
446 use acme_proxy_protocol::acme::account::update_contact;
447
448 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
449 return Ok(None);
450 };
451 update_contact(&mut account, contact, &database)
452 .await
453 .map_err(|error| match error {
454 ContactUpdateError::Refused(problem) => ContactError::Invalid(
455 problem.to_value()["detail"]
456 .as_str()
457 .unwrap_or_default()
458 .to_string(),
459 ),
460 ContactUpdateError::Database(error) => ContactError::Database(error),
461 })?;
462 Ok(Some(account))
463}
464
465pub async fn deactivate_account(
472 id: &str,
473 database: Arc<Database>,
474 notifier: impl Fn(&str) -> Option<Arc<acme_proxy_jobs::notify::NotifyDispatcher>>,
475 client_ip: Option<String>,
476) -> Result<Option<Account>, sqlx::Error> {
477 let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
478 return Ok(None);
479 };
480 let dispatcher = notifier(&account.profile);
481 acme_proxy_protocol::acme::account::deactivate(
482 &mut account,
483 &database,
484 dispatcher.as_deref(),
485 client_ip,
486 )
487 .await?;
488 Ok(Some(account))
489}
490
491pub async fn revoke_order(
511 id: &str,
512 reason: Option<u32>,
513 actor: Actor,
514 client: ClientContext,
515 revocations: acme_proxy_protocol::acme::revoke::Revocations<'_>,
516) -> Result<RevokeOutcome, RevokeError> {
517 use acme_proxy_protocol::acme::revoke::RevokeError as Refusal;
518
519 match revocations.revoke_order(id, reason, actor, client).await {
520 Ok(order) => Ok(RevokeOutcome::Revoked(Box::new(order))),
521 Err(Refusal::NotFound) => Ok(RevokeOutcome::NotFound),
522 Err(Refusal::NotIssued) => Ok(RevokeOutcome::NotIssued),
523 Err(Refusal::AlreadyRevoked) => Ok(RevokeOutcome::AlreadyRevoked),
524 Err(Refusal::BadReason(code)) => Err(RevokeError::BadReason(code)),
525 Err(Refusal::Database(error)) => Err(RevokeError::Database(error)),
526 Err(Refusal::Internal(detail)) => Err(RevokeError::Internal(detail)),
527 Err(Refusal::Signer(error)) => Err(RevokeError::Signer(error)),
528 Err(Refusal::Pending { job }) => Ok(RevokeOutcome::Queued(job)),
529 Err(Refusal::Abandoned { job, reason }) => Err(RevokeError::Abandoned { job, reason }),
530 Err(Refusal::Refused(problem)) => Err(RevokeError::Internal(problem.detail().to_owned())),
532 }
533}
534
535pub async fn list_audit(
537 query: &AuditQuery,
538 database: Arc<Database>,
539) -> Result<(Vec<AuditEntry>, i64), sqlx::Error> {
540 AuditEntry::search(query, &database).await
541}
542
543pub async fn find_audit(
545 id: i64,
546 database: Arc<Database>,
547) -> Result<Option<AuditEntry>, sqlx::Error> {
548 AuditEntry::find_by_id(id, &database).await
549}
550
551pub async fn cleanup_audit(days: u64, database: Arc<Database>) -> Result<u64, sqlx::Error> {
553 AuditEntry::cleanup(acme_proxy_store::audit::audit_cutoff(days), &database).await
554}
555
556pub async fn confirm_cleanup_audit(
562 days: u64,
563 assume_yes: bool,
564 reader: &mut impl BufRead,
565 database: Arc<Database>,
566) -> Result<Option<u64>, sqlx::Error> {
567 let cutoff = acme_proxy_store::audit::audit_cutoff(days);
568 let doomed = AuditEntry::count_older_than(cutoff, &database).await?;
569 let prompt =
570 format!("Delete {doomed} audit row(s) older than {days} day(s)? This cannot be undone.");
571 if !confirm(&prompt, assume_yes, reader) {
572 return Ok(None);
573 }
574 Ok(Some(AuditEntry::cleanup(cutoff, &database).await?))
575}
576
577pub async fn orders_json(
584 orders: &[Order],
585 base_url: &str,
586 database: &Database,
587) -> Result<Vec<serde_json::Value>, sqlx::Error> {
588 let ids: Vec<uuid::Uuid> = orders.iter().map(|order| order.id).collect();
589 let mut grouped = Authorization::find_ids_by_orders(&ids, database).await?;
590 Ok(orders
591 .iter()
592 .map(|order| {
593 crate::admin::render_order_json(
594 order,
595 base_url,
596 &grouped.remove(&order.id).unwrap_or_default(),
597 )
598 })
599 .collect())
600}
601
602pub async fn load_order_detail(
604 id: &str,
605 database: Arc<Database>,
606) -> Result<Option<OrderDetail>, sqlx::Error> {
607 let Some(order) = Order::find_by_id(id, &database).await? else {
608 return Ok(None);
609 };
610 let authzs = Authorization::find_by_order(order.id, &database).await?;
611 let mut authorizations = Vec::with_capacity(authzs.len());
612 for authz in authzs {
613 let challenges = Challenge::find_by_authz(authz.id, &database).await?;
614 authorizations.push((authz, challenges));
615 }
616 Ok(Some(OrderDetail {
617 order,
618 authorizations,
619 }))
620}
621
622#[derive(Debug)]
634pub struct JobDetail {
635 pub job: Job,
636 pub upstream_order: Option<UpstreamOrderRow>,
637}
638
639#[derive(Debug)]
642pub struct UpstreamOrderDetail {
643 pub upstream_order: UpstreamOrderRow,
644 pub job: Option<Job>,
645}
646
647const PERIODIC_JOB_KINDS: &[&str] = &[
651 acme_proxy_jobs::jobs::sweep::RETENTION_JOB_KIND,
652 acme_proxy_jobs::jobs::sweep::NONCE_SWEEP_KIND,
653 acme_proxy_jobs::jobs::sweep::AUDIT_SWEEP_KIND,
654 acme_proxy_jobs::jobs::sweep::ADMIN_SESSION_SWEEP_KIND,
655 acme_proxy_jobs::jobs::sweep::ORDER_SWEEP_KIND,
656 acme_proxy_signer::local_ca::sweep::CRL_SWEEP_KIND,
657 acme_proxy_jobs::notify::expiry::EXPIRY_JOB_KIND,
658];
659
660#[must_use]
662pub fn is_periodic_job_kind(kind: &str) -> bool {
663 PERIODIC_JOB_KINDS.contains(&kind)
664}
665
666pub async fn load_job_detail(
669 id: &str,
670 database: Arc<Database>,
671) -> Result<Option<JobDetail>, sqlx::Error> {
672 let Some(job_id) = acme_proxy_store::id::parse(id) else {
673 return Ok(None);
674 };
675 let Some(job) = Job::find_by_id(job_id, &database).await? else {
676 return Ok(None);
677 };
678 let upstream_order = if job.kind == RELAY_JOB_KIND {
679 UpstreamOrder::find_row_by_order_id(&job.dedup_key, &database).await?
680 } else {
681 None
682 };
683 Ok(Some(JobDetail {
684 job,
685 upstream_order,
686 }))
687}
688
689pub async fn load_upstream_order_detail(
693 order_id: &str,
694 database: Arc<Database>,
695) -> Result<Option<UpstreamOrderDetail>, sqlx::Error> {
696 let Some(upstream_order) = UpstreamOrder::find_row_by_order_id(order_id, &database).await?
697 else {
698 return Ok(None);
699 };
700 let job = Job::find_latest_by_dedup(
701 RELAY_JOB_KIND,
702 &upstream_order.order_id.to_string(),
703 &database,
704 )
705 .await?;
706 Ok(Some(UpstreamOrderDetail {
707 upstream_order,
708 job,
709 }))
710}
711
712#[derive(Debug)]
714pub enum CancelJobOutcome {
715 NotFound,
716 NotCancellable(String),
719 Cancelled(Box<Job>),
721 CancelledAndOrderAbandoned {
725 job: Box<Job>,
726 order_id: String,
727 },
728}
729
730#[derive(Debug)]
732pub enum RunJobNowOutcome {
733 NotFound,
734 Refused(String),
737 Nudged(Box<Job>),
739 Revived(Box<Job>),
741}
742
743#[derive(Debug, thiserror::Error)]
745pub enum CancelJobError {
746 #[error("database error: {0}")]
747 Database(#[from] sqlx::Error),
748}
749
750pub async fn cancel_job(
775 id: &str,
776 actor: Actor,
777 client: ClientContext,
778 audit: &Auditor,
779 database: Arc<Database>,
780) -> Result<CancelJobOutcome, CancelJobError> {
781 let Some(job_id) = acme_proxy_store::id::parse(id) else {
782 return Ok(CancelJobOutcome::NotFound);
783 };
784
785 let (job, was_in_flight) = match Job::cancel_row(job_id, JobStatus::Ready, &database).await? {
794 Some(job) => (job, true),
795 None => match Job::cancel_row(job_id, JobStatus::Failed, &database).await? {
796 Some(job) => (job, false),
797 None => {
798 return Ok(match Job::find_by_id(job_id, &database).await? {
801 None => CancelJobOutcome::NotFound,
802 Some(job) => CancelJobOutcome::NotCancellable(job.status),
803 });
804 }
805 },
806 };
807
808 if job.kind == RELAY_JOB_KIND && was_in_flight {
809 let order_id = job.dedup_key.clone();
817 if let Some(mut order) = Order::find_by_id(&order_id, &database).await? {
818 abandon_relayed_order(
819 &mut order,
820 "issuance cancelled by operator",
821 actor,
822 client,
823 audit,
824 &database,
825 )
826 .await?;
827 return Ok(CancelJobOutcome::CancelledAndOrderAbandoned {
828 job: Box::new(job),
829 order_id,
830 });
831 }
832 }
833
834 if job.kind == acme_proxy_protocol::acme::issue::SIGNER_ISSUE_KIND && was_in_flight {
835 let order_id = job.dedup_key.clone();
840 if let Some(mut order) = Order::find_by_id(&order_id, &database).await?
841 && order.status == acme_proxy_store::status::OrderStatus::Processing
842 {
843 acme_proxy_signer::issuance::record_issue_failure(
844 &mut order,
845 &acme_proxy_core::error::Problem::server_internal("Certificate issuance failed"),
846 "issuance cancelled by operator",
847 actor,
848 client,
849 audit,
850 &database,
851 )
852 .await?;
853 return Ok(CancelJobOutcome::CancelledAndOrderAbandoned {
854 job: Box::new(job),
855 order_id,
856 });
857 }
858 }
859
860 audit
866 .record(acme_proxy_jobs::auditor::admin::job_cancelled(
867 actor,
868 client,
869 &job.kind,
870 &job.id.to_string(),
871 ))
872 .await;
873 Ok(CancelJobOutcome::Cancelled(Box::new(job)))
874}
875
876pub async fn confirm_cancel_job(
879 id: &str,
880 assume_yes: bool,
881 reader: &mut impl BufRead,
882 actor: Actor,
883 client: ClientContext,
884 audit: &Auditor,
885 database: Arc<Database>,
886) -> Result<Option<CancelJobOutcome>, CancelJobError> {
887 let Some(job_id) = acme_proxy_store::id::parse(id) else {
892 return Ok(Some(CancelJobOutcome::NotFound));
893 };
894 let Some(job) = Job::find_by_id(job_id, &database).await? else {
895 return Ok(Some(CancelJobOutcome::NotFound));
896 };
897
898 let prompt = if job.kind == RELAY_JOB_KIND && job.status == JobStatus::Ready.as_str() {
903 format!(
904 "Cancel relay job {id}? Order {} will be marked invalid.",
905 job.dedup_key
906 )
907 } else if is_periodic_job_kind(&job.kind) {
908 format!(
909 "Cancel job {id} ({})? This periodic sweep will not run again until \
910 the server restarts.",
911 job.kind
912 )
913 } else {
914 format!("Cancel job {id} ({})?", job.kind)
915 };
916 if !confirm(&prompt, assume_yes, reader) {
917 return Ok(None);
918 }
919 Ok(Some(cancel_job(id, actor, client, audit, database).await?))
920}
921
922pub async fn run_job_now(
940 id: &str,
941 actor: Actor,
942 client: ClientContext,
943 audit: &Auditor,
944 database: Arc<Database>,
945) -> Result<RunJobNowOutcome, sqlx::Error> {
946 let Some(job_id) = acme_proxy_store::id::parse(id) else {
947 return Ok(RunJobNowOutcome::NotFound);
948 };
949 if let Some(job) = Job::advance_row(job_id, &database).await? {
950 audit
951 .record(acme_proxy_jobs::auditor::admin::job_advanced(
952 actor,
953 client,
954 &job.id.to_string(),
955 false,
956 ))
957 .await;
958 return Ok(RunJobNowOutcome::Nudged(Box::new(job)));
959 }
960 if let Some(job) = Job::revive_row(job_id, &database).await? {
961 audit
962 .record(acme_proxy_jobs::auditor::admin::job_advanced(
963 actor,
964 client,
965 &job.id.to_string(),
966 true,
967 ))
968 .await;
969 return Ok(RunJobNowOutcome::Revived(Box::new(job)));
970 }
971 Ok(match Job::find_by_id(job_id, &database).await? {
972 None => RunJobNowOutcome::NotFound,
973 Some(job) => RunJobNowOutcome::Refused(job.status),
974 })
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980 use acme_proxy_core::audit::AuditEvent;
981 use acme_proxy_core::audit::AuditRecord;
982 use acme_proxy_core::identifier::Identifier;
983 use acme_proxy_signer::SignerBackend;
984 use acme_proxy_store::nonce::now_secs;
985 use acme_proxy_store::testutil::account_id;
986
987 async fn db() -> Arc<Database> {
988 Arc::new(Database::connect_in_memory().await.unwrap())
989 }
990
991 fn cli_actor() -> Actor {
995 Actor::cli()
996 }
997
998 async fn audit_rows(db: &Arc<Database>) -> Vec<AuditEntry> {
999 AuditEntry::search(
1000 &AuditQuery {
1001 limit: 50,
1002 ..AuditQuery::default()
1003 },
1004 db,
1005 )
1006 .await
1007 .unwrap()
1008 .0
1009 }
1010
1011 #[tokio::test]
1014 async fn cleaning_the_audit_trail_is_confirm_gated_and_bounded_by_age() {
1015 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1016 AuditEntry::insert(
1017 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
1018 &db,
1019 )
1020 .await
1021 .unwrap();
1022
1023 let mut declined: &[u8] = b"n\n";
1024 assert_eq!(
1025 confirm_cleanup_audit(0, false, &mut declined, db.clone())
1026 .await
1027 .unwrap(),
1028 None
1029 );
1030 assert_eq!(audit_rows(&db).await.len(), 1);
1031
1032 let mut reader: &[u8] = &[];
1034 assert_eq!(
1035 confirm_cleanup_audit(7, true, &mut reader, db.clone())
1036 .await
1037 .unwrap(),
1038 Some(0)
1039 );
1040 assert_eq!(audit_rows(&db).await.len(), 1);
1041
1042 assert_eq!(cleanup_audit(1, db.clone()).await.unwrap(), 0);
1046
1047 assert_eq!(
1049 AuditEntry::cleanup(acme_proxy_store::audit::audit_cutoff(0) + 3600, &db)
1050 .await
1051 .unwrap(),
1052 1
1053 );
1054 assert!(audit_rows(&db).await.is_empty());
1055 }
1056
1057 #[tokio::test]
1061 async fn listing_and_finding_audit_rows_pages_and_resolves() {
1062 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1063 let mut ids = Vec::new();
1064 for _ in 0..3 {
1065 ids.push(
1066 AuditEntry::insert(
1067 AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
1068 &db,
1069 )
1070 .await
1071 .unwrap(),
1072 );
1073 }
1074
1075 let (page, total) = list_audit(
1076 &AuditQuery {
1077 limit: 2,
1078 ..AuditQuery::default()
1079 },
1080 db.clone(),
1081 )
1082 .await
1083 .unwrap();
1084 assert_eq!(total, 3);
1085 assert_eq!(page.len(), 2);
1086
1087 assert!(find_audit(ids[0], db.clone()).await.unwrap().is_some());
1088 assert!(find_audit(9_999, db).await.unwrap().is_none());
1089 }
1090
1091 #[tokio::test]
1095 async fn revoking_writes_one_audit_row_naming_the_caller() {
1096 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1097 let signer = in_memory_ca(&db);
1098 let order = finalized_order(db.clone(), &signer).await;
1099
1100 let outcome = revoke_order(
1101 order.id.to_string().as_str(),
1102 Some(1),
1103 Actor::admin("root"),
1104 ClientContext {
1105 ip: Some("203.0.113.7".to_string()),
1106 ptr: Some("desk.example.com".to_string()),
1107 ..ClientContext::default()
1108 },
1109 acme_proxy_protocol::acme::revoke::Revocations {
1110 database: &db,
1111 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1112 notify: None,
1113 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1114 },
1115 )
1116 .await
1117 .unwrap();
1118 assert!(matches!(outcome, RevokeOutcome::Revoked(_)));
1119
1120 let rows = audit_rows(&db).await;
1121 assert_eq!(rows.len(), 1, "{rows:?}");
1122 let row = &rows[0];
1123 assert_eq!(row.event, "certificate_revoked");
1124 assert_eq!(row.outcome, "success");
1125 assert_eq!(row.actor_kind, "admin");
1126 assert_eq!(row.actor_id.as_deref(), Some("root"));
1127 assert_eq!(row.account_id, Some(order.account_id.to_string()));
1128 assert_eq!(row.order_id, Some(order.id.to_string()));
1129 assert_eq!(row.cert_serial, order.cert_serial);
1130 assert_eq!(row.client_ip.as_deref(), Some("203.0.113.7"));
1131 assert_eq!(row.client_ptr.as_deref(), Some("desk.example.com"));
1132 assert_eq!(row.reason.as_deref(), Some("1"));
1133
1134 let outcome = revoke_order(
1137 order.id.to_string().as_str(),
1138 None,
1139 Actor::admin("root"),
1140 ClientContext::default(),
1141 acme_proxy_protocol::acme::revoke::Revocations {
1142 database: &db,
1143 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1144 notify: None,
1145 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1146 },
1147 )
1148 .await
1149 .unwrap();
1150 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
1151 assert_eq!(audit_rows(&db).await.len(), 1);
1152 }
1153
1154 struct Wanting;
1157
1158 #[async_trait::async_trait]
1159 impl acme_proxy_jobs::notify::NotifyBackend for Wanting {
1160 fn name(&self) -> &'static str {
1161 "custom"
1162 }
1163 async fn send(
1164 &self,
1165 _event: &acme_proxy_jobs::notify::NotifyEvent,
1166 ) -> Result<(), acme_proxy_jobs::notify::NotifyError> {
1167 Ok(())
1168 }
1169 }
1170
1171 #[tokio::test]
1174 async fn an_operator_revocation_queues_a_certificate_revoked_notification() {
1175 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1176 let signer = in_memory_ca(&db);
1177 let order = finalized_order(db.clone(), &signer).await;
1178 let dispatcher = acme_proxy_jobs::notify::NotifyDispatcher::new(
1179 "default",
1180 vec![acme_proxy_jobs::notify::BackendSlot::new(
1181 "custom:test",
1182 Arc::new(Wanting),
1183 &["certificate_revoked".to_string()],
1184 )],
1185 acme_proxy_jobs::testutil::idle_job_queue(db.clone()),
1186 );
1187
1188 let outcome = revoke_order(
1189 order.id.to_string().as_str(),
1190 None,
1191 cli_actor(),
1192 ClientContext::default(),
1193 acme_proxy_protocol::acme::revoke::Revocations {
1194 database: &db,
1195 audit: &Auditor::offline(db.clone()),
1196 notify: Some(&dispatcher),
1197 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1198 },
1199 )
1200 .await
1201 .unwrap();
1202 assert!(matches!(outcome, RevokeOutcome::Revoked(_)));
1203
1204 let queued = Job::count_live(acme_proxy_jobs::notify::NOTIFY_JOB_KIND, &db)
1205 .await
1206 .unwrap();
1207 assert_eq!(queued, 1);
1208 }
1209
1210 #[tokio::test]
1213 async fn a_revocation_with_no_reason_leaves_the_column_absent() {
1214 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1215 let signer = in_memory_ca(&db);
1216 let order = finalized_order(db.clone(), &signer).await;
1217
1218 revoke_order(
1219 order.id.to_string().as_str(),
1220 None,
1221 cli_actor(),
1222 ClientContext::default(),
1223 acme_proxy_protocol::acme::revoke::Revocations {
1224 database: &db,
1225 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1226 notify: None,
1227 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1228 },
1229 )
1230 .await
1231 .unwrap();
1232
1233 let rows = audit_rows(&db).await;
1234 assert_eq!(rows[0].reason, None);
1235 assert_eq!(rows[0].actor_kind, "cli");
1236 assert_eq!(rows[0].client_ip, None);
1238 assert_eq!(rows[0].client_ptr, None);
1239 }
1240
1241 #[tokio::test]
1242 async fn delete_account_not_found() {
1243 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1244 let mut reader: &[u8] = &[];
1245 let outcome = confirm_delete_account("nope", true, &mut reader, db)
1246 .await
1247 .unwrap();
1248 assert_eq!(outcome, DeleteOutcome::NotFound);
1249 }
1250
1251 #[tokio::test]
1252 async fn delete_account_cancelled_leaves_row() {
1253 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1254 let acct = account_id(&db).await;
1255
1256 let mut reader = b"n\n".as_slice();
1257 let outcome =
1258 confirm_delete_account(acct.to_string().as_str(), false, &mut reader, db.clone())
1259 .await
1260 .unwrap();
1261 assert_eq!(outcome, DeleteOutcome::Cancelled);
1262 assert!(
1263 Account::find_by_id("default", acct.to_string().as_str(), &db)
1264 .await
1265 .unwrap()
1266 .is_some()
1267 );
1268 }
1269
1270 #[tokio::test]
1271 async fn delete_account_confirmed_deletes_and_cascades() {
1272 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1273 let acct = account_id(&db).await;
1274 let order = Order::create(
1275 "default",
1276 acct,
1277 vec![Identifier::dns("example.com")],
1278 acme_proxy_store::nonce::now_secs() + 3600,
1279 None,
1280 None,
1281 &db,
1282 )
1283 .await
1284 .unwrap();
1285
1286 let mut reader: &[u8] = &[];
1287 let outcome =
1288 confirm_delete_account(acct.to_string().as_str(), true, &mut reader, db.clone())
1289 .await
1290 .unwrap();
1291 assert!(matches!(outcome, DeleteOutcome::Deleted(_)));
1292 assert!(
1293 Account::find_by_id("default", acct.to_string().as_str(), &db)
1294 .await
1295 .unwrap()
1296 .is_none()
1297 );
1298 assert!(
1299 Order::find_by_id(order.id.to_string().as_str(), &db)
1300 .await
1301 .unwrap()
1302 .is_none()
1303 );
1304 }
1305
1306 #[tokio::test]
1307 async fn delete_order_not_found() {
1308 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1309 let mut reader: &[u8] = &[];
1310 let outcome = confirm_delete_order("nope", true, &mut reader, db)
1311 .await
1312 .unwrap();
1313 assert_eq!(outcome, DeleteOutcome::NotFound);
1314 }
1315
1316 #[tokio::test]
1317 async fn delete_order_cancelled_leaves_row() {
1318 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1319 let acct = account_id(&db).await;
1320 let order = Order::create(
1321 "default",
1322 acct,
1323 vec![Identifier::dns("example.com")],
1324 acme_proxy_store::nonce::now_secs() + 3600,
1325 None,
1326 None,
1327 &db,
1328 )
1329 .await
1330 .unwrap();
1331
1332 let mut reader = b"no\n".as_slice();
1333 let outcome = confirm_delete_order(
1334 order.id.to_string().as_str(),
1335 false,
1336 &mut reader,
1337 db.clone(),
1338 )
1339 .await
1340 .unwrap();
1341 assert_eq!(outcome, DeleteOutcome::Cancelled);
1342 assert!(
1343 Order::find_by_id(order.id.to_string().as_str(), &db)
1344 .await
1345 .unwrap()
1346 .is_some()
1347 );
1348 }
1349
1350 #[tokio::test]
1351 async fn delete_order_confirmed_deletes_and_cascades() {
1352 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1353 let acct = account_id(&db).await;
1354 let order = Order::create(
1355 "default",
1356 acct,
1357 vec![Identifier::dns("example.com")],
1358 acme_proxy_store::nonce::now_secs() + 3600,
1359 None,
1360 None,
1361 &db,
1362 )
1363 .await
1364 .unwrap();
1365 let authz = Authorization::create(
1366 order.id,
1367 Identifier::dns("example.com"),
1368 acme_proxy_store::nonce::now_secs() + 3600,
1369 &db,
1370 )
1371 .await
1372 .unwrap();
1373
1374 let mut reader: &[u8] = &[];
1375 let outcome =
1376 confirm_delete_order(order.id.to_string().as_str(), true, &mut reader, db.clone())
1377 .await
1378 .unwrap();
1379 assert!(matches!(outcome, DeleteOutcome::Deleted(_)));
1380 assert!(
1381 Order::find_by_id(order.id.to_string().as_str(), &db)
1382 .await
1383 .unwrap()
1384 .is_none()
1385 );
1386 assert!(
1387 Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1388 .await
1389 .unwrap()
1390 .is_none()
1391 );
1392 }
1393
1394 #[tokio::test]
1398 async fn bare_delete_account_reports_not_found_for_an_unknown_id() {
1399 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1400 assert_eq!(
1401 delete_account("nope", db).await.unwrap(),
1402 Deletion::NotFound
1403 );
1404 }
1405
1406 #[tokio::test]
1407 async fn bare_delete_account_deletes_and_counts_the_cascade() {
1408 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1409 let acct = account_id(&db).await;
1410 for _ in 0..2 {
1411 Order::create(
1412 "default",
1413 acct,
1414 vec![Identifier::dns("example.com")],
1415 acme_proxy_store::nonce::now_secs() + 3600,
1416 None,
1417 None,
1418 &db,
1419 )
1420 .await
1421 .unwrap();
1422 }
1423
1424 assert_eq!(
1425 delete_account(acct.to_string().as_str(), db.clone())
1426 .await
1427 .unwrap(),
1428 Deletion::Deleted(Deleted { cascaded: 2 })
1429 );
1430 assert!(
1431 Account::find_by_id("default", acct.to_string().as_str(), &db)
1432 .await
1433 .unwrap()
1434 .is_none()
1435 );
1436 }
1437
1438 #[tokio::test]
1439 async fn bare_delete_order_reports_not_found_for_an_unknown_id() {
1440 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1441 assert_eq!(delete_order("nope", db).await.unwrap(), Deletion::NotFound);
1442 }
1443
1444 #[tokio::test]
1445 async fn bare_delete_order_deletes_and_counts_the_cascade() {
1446 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1447 let acct = account_id(&db).await;
1448 let order = Order::create(
1449 "default",
1450 acct,
1451 vec![Identifier::dns("example.com")],
1452 acme_proxy_store::nonce::now_secs() + 3600,
1453 None,
1454 None,
1455 &db,
1456 )
1457 .await
1458 .unwrap();
1459 Authorization::create(
1460 order.id,
1461 Identifier::dns("example.com"),
1462 acme_proxy_store::nonce::now_secs() + 3600,
1463 &db,
1464 )
1465 .await
1466 .unwrap();
1467
1468 assert_eq!(
1469 delete_order(order.id.to_string().as_str(), db.clone())
1470 .await
1471 .unwrap(),
1472 Deletion::Deleted(Deleted { cascaded: 1 })
1473 );
1474 assert!(
1475 Order::find_by_id(order.id.to_string().as_str(), &db)
1476 .await
1477 .unwrap()
1478 .is_none()
1479 );
1480 }
1481
1482 #[tokio::test]
1486 async fn a_live_certificate_refuses_account_and_order_deletes_before_asking() {
1487 let db = db().await;
1488 let acct = account_id(&db).await;
1489 let order = acme_proxy_store::testutil::certified_order(&db, acct, None).await;
1490 let (acct, order) = (acct.to_string(), order.id.to_string());
1491
1492 let mut reader: &[u8] = &[];
1493 assert_eq!(
1494 confirm_delete_account(&acct, false, &mut reader, db.clone())
1495 .await
1496 .unwrap(),
1497 DeleteOutcome::LiveCertificates(1)
1498 );
1499 assert_eq!(
1500 confirm_delete_order(&order, false, &mut reader, db.clone())
1501 .await
1502 .unwrap(),
1503 DeleteOutcome::LiveCertificates(1)
1504 );
1505 assert_eq!(
1506 delete_account(&acct, db.clone()).await.unwrap(),
1507 Deletion::LiveCertificates(1)
1508 );
1509 assert_eq!(
1510 delete_order(&order, db.clone()).await.unwrap(),
1511 Deletion::LiveCertificates(1)
1512 );
1513 assert!(Order::find_by_id(&order, &db).await.unwrap().is_some());
1514 }
1515
1516 #[test]
1517 fn the_refusals_name_the_subject_the_count_and_the_way_out() {
1518 let message = live_certificates_refusal("account a-1", 2);
1519 assert!(message.starts_with("account a-1 holds 2 live certificate(s)"));
1520 assert!(message.contains("revoke them first"));
1521
1522 let message = eab_live_certificates_refusal("k-1", 1, 3);
1523 assert!(message.starts_with("1 account(s) bound to EAB credential k-1 hold 3 live"));
1524 assert!(message.contains("deactivate the accounts instead"));
1525 }
1526
1527 async fn eab_with_live_account(db: &Arc<Database>) -> (Eab, uuid::Uuid) {
1529 let eab = Eab::create(None, None, db).await.unwrap();
1530 let (mut account, _) =
1531 Account::find_or_create("default", &[42u8], vec![], &ClientContext::default(), db)
1532 .await
1533 .unwrap();
1534 account.set_eab_kid(eab.kid, db).await.unwrap();
1535 acme_proxy_store::testutil::certified_order(db, account.id, None).await;
1536 (eab, account.id)
1537 }
1538
1539 #[tokio::test]
1540 async fn confirm_delete_eab_not_found_cancelled_and_refused() {
1541 let db = db().await;
1542 let mut reader: &[u8] = &[];
1543 assert!(matches!(
1544 confirm_delete_eab("nope", BoundAccounts::Keep, true, &mut reader, db.clone())
1545 .await
1546 .unwrap(),
1547 EabDeleteOutcome::NotFound
1548 ));
1549
1550 let (eab, _) = eab_with_live_account(&db).await;
1551 let kid = eab.kid.to_string();
1552 let mut declined: &[u8] = b"n\n";
1553 assert!(matches!(
1554 confirm_delete_eab(&kid, BoundAccounts::Keep, false, &mut declined, db.clone())
1555 .await
1556 .unwrap(),
1557 EabDeleteOutcome::Cancelled
1558 ));
1559 assert!(Eab::find_any_by_kid(&kid, &db).await.unwrap().is_some());
1560
1561 let mut reader: &[u8] = &[];
1563 assert!(matches!(
1564 confirm_delete_eab(&kid, BoundAccounts::Delete, false, &mut reader, db.clone())
1565 .await
1566 .unwrap(),
1567 EabDeleteOutcome::LiveCertificates {
1568 accounts: 1,
1569 certificates: 1
1570 }
1571 ));
1572 assert!(Eab::find_any_by_kid(&kid, &db).await.unwrap().is_some());
1573 }
1574
1575 #[tokio::test]
1578 async fn confirm_delete_eab_deactivating_keeps_the_live_certificate() {
1579 let db = db().await;
1580 let (eab, account) = eab_with_live_account(&db).await;
1581 let mut reader: &[u8] = &[];
1582
1583 let EabDeleteOutcome::Deleted(deleted) = confirm_delete_eab(
1584 &eab.kid.to_string(),
1585 BoundAccounts::Deactivate,
1586 true,
1587 &mut reader,
1588 db.clone(),
1589 )
1590 .await
1591 .unwrap() else {
1592 panic!("deactivating is never refused");
1593 };
1594 assert_eq!(deleted.deactivated.len(), 1);
1595 assert_eq!(Order::find_by_account(account, &db).await.unwrap().len(), 1);
1596 assert!(
1597 delete_eab(&eab.kid.to_string(), BoundAccounts::Keep, db.clone())
1598 .await
1599 .map(|deletion| matches!(deletion, EabDeletion::NotFound))
1600 .unwrap()
1601 );
1602 }
1603
1604 #[tokio::test]
1605 async fn bare_cleanup_nonces_removes_stale_rows_without_asking() {
1606 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1607 let stale = Nonce {
1608 value: "stale".to_string(),
1609 created_at: acme_proxy_store::nonce::now_secs() - 10_000,
1610 };
1611 stale.save(&db).await.unwrap();
1612 Nonce::new().save(&db).await.unwrap();
1613
1614 assert_eq!(
1615 cleanup_nonces(Duration::from_secs(300), db.clone())
1616 .await
1617 .unwrap(),
1618 1
1619 );
1620 assert!(
1621 !Nonce::verify("stale", &db, Duration::from_secs(300))
1622 .await
1623 .unwrap()
1624 );
1625 }
1626
1627 fn in_memory_ca(database: &Arc<Database>) -> Arc<dyn SignerBackend> {
1628 Arc::new(
1629 acme_proxy_signer::local_ca::LocalCa::generate_in_memory(
1630 "ecdsa-p256",
1631 90,
1632 database.clone(),
1633 )
1634 .expect("in-memory CA"),
1635 )
1636 }
1637
1638 async fn finalized_order(db: Arc<Database>, signer: &Arc<dyn SignerBackend>) -> Order {
1639 let acct = account_id(&db).await;
1640 let mut order = Order::create(
1641 "default",
1642 acct,
1643 vec![Identifier::dns("example.com")],
1644 acme_proxy_store::nonce::now_secs() + 3600,
1645 None,
1646 None,
1647 &db,
1648 )
1649 .await
1650 .unwrap();
1651
1652 let key_pair = rcgen::KeyPair::generate().unwrap();
1653 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
1654 let csr = params.serialize_request(&key_pair).unwrap();
1655 let chain = match signer
1656 .issue(
1657 order.id.to_string().as_str(),
1658 csr.der(),
1659 &order.identifiers,
1660 acme_proxy_signer::RequestedValidity::default(),
1661 )
1662 .await
1663 .unwrap()
1664 {
1665 acme_proxy_signer::IssueOutcome::Issued(chain) => chain,
1666 acme_proxy_signer::IssueOutcome::Processing => {
1667 panic!("the in-memory local CA issues synchronously")
1668 }
1669 };
1670 let leaf = acme_proxy_core::cert::leaf_der_from_chain(&chain).unwrap();
1671 let (serial, pubkey) = acme_proxy_core::cert::cert_serial_and_spki(&leaf).unwrap();
1672 let not_after = acme_proxy_core::cert::cert_validity(&leaf)
1673 .ok()
1674 .map(|(_, na)| na);
1675 order
1676 .finalize(chain, serial, pubkey, not_after, &db)
1677 .await
1678 .unwrap();
1679 order
1680 }
1681
1682 #[tokio::test]
1683 async fn revoke_order_not_found() {
1684 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1685 let outcome = revoke_order(
1686 "nope",
1687 None,
1688 cli_actor(),
1689 ClientContext::default(),
1690 acme_proxy_protocol::acme::revoke::Revocations {
1691 database: &db,
1692 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1693 notify: None,
1694 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(
1695 in_memory_ca(&db).as_ref(),
1696 ),
1697 },
1698 )
1699 .await
1700 .unwrap();
1701 assert!(matches!(outcome, RevokeOutcome::NotFound));
1702 }
1703
1704 #[tokio::test]
1705 async fn revoke_order_without_a_certificate_is_refused() {
1706 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1707 let acct = account_id(&db).await;
1708 let order = Order::create(
1709 "default",
1710 acct,
1711 vec![Identifier::dns("example.com")],
1712 acme_proxy_store::nonce::now_secs() + 3600,
1713 None,
1714 None,
1715 &db,
1716 )
1717 .await
1718 .unwrap();
1719
1720 let outcome = revoke_order(
1721 order.id.to_string().as_str(),
1722 None,
1723 cli_actor(),
1724 ClientContext::default(),
1725 acme_proxy_protocol::acme::revoke::Revocations {
1726 database: &db,
1727 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1728 notify: None,
1729 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(
1730 in_memory_ca(&db).as_ref(),
1731 ),
1732 },
1733 )
1734 .await
1735 .unwrap();
1736 assert!(matches!(outcome, RevokeOutcome::NotIssued));
1737 }
1738
1739 #[tokio::test]
1740 async fn revoke_order_persists() {
1741 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1742 let signer = in_memory_ca(&db);
1743 let order = finalized_order(db.clone(), &signer).await;
1744
1745 let outcome = revoke_order(
1746 order.id.to_string().as_str(),
1747 Some(1),
1748 cli_actor(),
1749 ClientContext::default(),
1750 acme_proxy_protocol::acme::revoke::Revocations {
1751 database: &db,
1752 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1753 notify: None,
1754 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1755 },
1756 )
1757 .await
1758 .unwrap();
1759 let RevokeOutcome::Revoked(revoked) = outcome else {
1760 panic!("expected Revoked, got {outcome:?}");
1761 };
1762 assert!(revoked.revoked_at.is_some());
1763 assert_eq!(revoked.revocation_reason, Some(1));
1764
1765 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1766 .await
1767 .unwrap()
1768 .unwrap();
1769 assert!(reloaded.revoked_at.is_some());
1770
1771 use x509_parser::prelude::FromDer;
1772 let der = signer.info().crl_der().await.unwrap().unwrap();
1773 let (_, crl) =
1774 x509_parser::revocation_list::CertificateRevocationList::from_der(&der).unwrap();
1775 assert_eq!(crl.iter_revoked_certificates().count(), 1);
1776 }
1777
1778 #[tokio::test]
1779 async fn revoke_order_already_revoked() {
1780 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1781 let signer = in_memory_ca(&db);
1782 let order = finalized_order(db.clone(), &signer).await;
1783
1784 revoke_order(
1785 order.id.to_string().as_str(),
1786 None,
1787 cli_actor(),
1788 ClientContext::default(),
1789 acme_proxy_protocol::acme::revoke::Revocations {
1790 database: &db,
1791 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1792 notify: None,
1793 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1794 },
1795 )
1796 .await
1797 .unwrap();
1798 let outcome = revoke_order(
1799 order.id.to_string().as_str(),
1800 None,
1801 cli_actor(),
1802 ClientContext::default(),
1803 acme_proxy_protocol::acme::revoke::Revocations {
1804 database: &db,
1805 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1806 notify: None,
1807 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1808 },
1809 )
1810 .await
1811 .unwrap();
1812 assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
1813 }
1814
1815 #[tokio::test]
1816 async fn revoke_order_bad_reason_is_refused() {
1817 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1818 let signer = in_memory_ca(&db);
1819 let order = finalized_order(db.clone(), &signer).await;
1820
1821 let error = revoke_order(
1822 order.id.to_string().as_str(),
1823 Some(999),
1824 cli_actor(),
1825 ClientContext::default(),
1826 acme_proxy_protocol::acme::revoke::Revocations {
1827 database: &db,
1828 audit: &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
1829 notify: None,
1830 revoker: acme_proxy_protocol::acme::revoke::Revoker::Backend(signer.as_ref()),
1831 },
1832 )
1833 .await
1834 .unwrap_err();
1835 assert!(matches!(error, RevokeError::BadReason(999)));
1836 }
1837
1838 #[tokio::test]
1839 async fn cleanup_nonces_cancelled_leaves_nonces() {
1840 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1841 Nonce {
1842 value: "stale".to_string(),
1843 created_at: acme_proxy_store::nonce::now_secs() - 600,
1844 }
1845 .save(&db)
1846 .await
1847 .unwrap();
1848
1849 let mut reader = b"n\n".as_slice();
1850 let outcome =
1851 confirm_cleanup_nonces(Duration::from_secs(300), false, &mut reader, db.clone())
1852 .await
1853 .unwrap();
1854 assert_eq!(outcome, None);
1855
1856 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1857 .fetch_one(db.raw_pool())
1858 .await
1859 .unwrap();
1860 assert_eq!(count, 1);
1861 }
1862
1863 #[tokio::test]
1864 async fn cleanup_nonces_confirmed_removes_stale_and_reports_count() {
1865 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1866 Nonce {
1867 value: "stale".to_string(),
1868 created_at: acme_proxy_store::nonce::now_secs() - 600,
1869 }
1870 .save(&db)
1871 .await
1872 .unwrap();
1873
1874 let mut reader: &[u8] = &[];
1875 let outcome =
1876 confirm_cleanup_nonces(Duration::from_secs(300), true, &mut reader, db.clone())
1877 .await
1878 .unwrap();
1879 assert_eq!(outcome, Some(1));
1880
1881 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1882 .fetch_one(db.raw_pool())
1883 .await
1884 .unwrap();
1885 assert_eq!(count, 0);
1886 }
1887
1888 #[tokio::test]
1889 async fn update_account_contact_not_found() {
1890 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1891 assert!(
1892 update_account_contact("nope", vec![], db)
1893 .await
1894 .unwrap()
1895 .is_none()
1896 );
1897 }
1898
1899 #[tokio::test]
1900 async fn update_account_contact_persists() {
1901 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1902 let acct = account_id(&db).await;
1903
1904 let contact = vec!["mailto:a@example.com".to_string()];
1905 let updated =
1906 update_account_contact(acct.to_string().as_str(), contact.clone(), db.clone())
1907 .await
1908 .unwrap()
1909 .unwrap();
1910 assert_eq!(updated.contact, contact);
1911
1912 let reloaded = Account::find_by_id("default", acct.to_string().as_str(), &db)
1913 .await
1914 .unwrap()
1915 .unwrap();
1916 assert_eq!(reloaded.contact, contact);
1917 }
1918
1919 #[tokio::test]
1920 async fn deactivate_account_not_found() {
1921 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1922 assert!(
1923 deactivate_account("nope", db, |_| None, None)
1924 .await
1925 .unwrap()
1926 .is_none()
1927 );
1928 }
1929
1930 #[tokio::test]
1932 async fn deactivate_account_persists() {
1933 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1934 let acct = account_id(&db).await;
1935 let dispatcher = Arc::new(acme_proxy_jobs::notify::NotifyDispatcher::new(
1936 "default",
1937 vec![acme_proxy_jobs::notify::BackendSlot::new(
1938 "custom:test",
1939 Arc::new(Wanting),
1940 &["account_deactivated".to_string()],
1941 )],
1942 acme_proxy_jobs::testutil::idle_job_queue(db.clone()),
1943 ));
1944
1945 let updated = deactivate_account(
1946 acct.to_string().as_str(),
1947 db.clone(),
1948 |profile| (profile == "default").then(|| dispatcher.clone()),
1949 Some("203.0.113.7".to_string()),
1950 )
1951 .await
1952 .unwrap()
1953 .unwrap();
1954 assert_eq!(updated.status, "deactivated");
1955 assert_eq!(
1956 Job::count_live(acme_proxy_jobs::notify::NOTIFY_JOB_KIND, &db)
1957 .await
1958 .unwrap(),
1959 1
1960 );
1961
1962 let reloaded = Account::find_by_id("default", acct.to_string().as_str(), &db)
1963 .await
1964 .unwrap()
1965 .unwrap();
1966 assert_eq!(reloaded.status, "deactivated");
1967 }
1968
1969 #[tokio::test]
1970 async fn load_order_detail_not_found() {
1971 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1972 assert!(load_order_detail("nope", db).await.unwrap().is_none());
1973 }
1974
1975 #[tokio::test]
1976 async fn load_order_detail_nests_authorizations_and_challenges() {
1977 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1978 let acct = account_id(&db).await;
1979 let order = Order::create(
1980 "default",
1981 acct,
1982 vec![Identifier::dns("example.com")],
1983 acme_proxy_store::nonce::now_secs() + 3600,
1984 None,
1985 None,
1986 &db,
1987 )
1988 .await
1989 .unwrap();
1990 let authz = Authorization::create(
1991 order.id,
1992 Identifier::dns("example.com"),
1993 acme_proxy_store::nonce::now_secs() + 3600,
1994 &db,
1995 )
1996 .await
1997 .unwrap();
1998 Challenge::create(authz.id, "http-01", &db).await.unwrap();
1999
2000 let detail = load_order_detail(order.id.to_string().as_str(), db)
2001 .await
2002 .unwrap()
2003 .unwrap();
2004 assert_eq!(detail.order.id, order.id);
2005 assert_eq!(detail.authorizations.len(), 1);
2006 assert_eq!(detail.authorizations[0].0.id, authz.id);
2007 assert_eq!(detail.authorizations[0].1.len(), 1);
2008 assert_eq!(detail.authorizations[0].1[0].typ, "http-01");
2009 }
2010
2011 use acme_proxy_store::job::Job;
2014 use acme_proxy_store::job::NewJob;
2015
2016 async fn relay_job(db: &Arc<Database>) -> (Order, Job) {
2019 let acct = account_id(db).await;
2020 let order = Order::create(
2021 "default",
2022 acct,
2023 vec![Identifier::dns("example.com")],
2024 now_secs() + 3600,
2025 None,
2026 None,
2027 db,
2028 )
2029 .await
2030 .unwrap();
2031 UpstreamOrder::create(
2032 order.id.to_string().as_str(),
2033 "https://up.example/o/1",
2034 None,
2035 b"csr",
2036 db,
2037 )
2038 .await
2039 .unwrap();
2040 let id = acme_proxy_store::id::mint();
2041 Job::enqueue(
2042 NewJob {
2043 id,
2044 kind: RELAY_JOB_KIND,
2045 dedup_key: &order.id.to_string(),
2046 payload: &serde_json::json!({ "order_id": order.id.to_string(), "profile": "default" }),
2047 run_at: now_secs(),
2048 deadline: Some(order.expires),
2049 max_attempts: 5,
2050 },
2051 db,
2052 )
2053 .await
2054 .unwrap();
2055 let job = Job::find_by_id(id, db).await.unwrap().unwrap();
2056 (order, job)
2057 }
2058
2059 async fn sweep_job(db: &Arc<Database>) -> Job {
2061 let id = acme_proxy_store::id::mint();
2062 Job::enqueue(
2063 NewJob {
2064 id,
2065 kind: "nonce_sweep",
2066 dedup_key: "nonce_sweep",
2067 payload: &serde_json::json!({}),
2068 run_at: now_secs() + 3600,
2069 deadline: None,
2070 max_attempts: 5,
2071 },
2072 db,
2073 )
2074 .await
2075 .unwrap();
2076 Job::find_by_id(id, db).await.unwrap().unwrap()
2077 }
2078
2079 #[tokio::test]
2080 async fn load_job_detail_attaches_the_upstream_order_only_for_a_relay_job() {
2081 let db = db().await;
2082 let (order, job) = relay_job(&db).await;
2083
2084 let detail = load_job_detail(job.id.to_string().as_str(), db.clone())
2085 .await
2086 .unwrap()
2087 .unwrap();
2088 assert_eq!(detail.job.id, job.id);
2089 assert_eq!(detail.upstream_order.as_ref().unwrap().order_id, order.id);
2090
2091 let sweep = sweep_job(&db).await;
2092 let detail = load_job_detail(sweep.id.to_string().as_str(), db.clone())
2093 .await
2094 .unwrap()
2095 .unwrap();
2096 assert!(detail.upstream_order.is_none());
2097
2098 assert!(load_job_detail("nope", db.clone()).await.unwrap().is_none());
2100 assert!(
2101 load_job_detail(acme_proxy_store::id::mint().to_string().as_str(), db)
2102 .await
2103 .unwrap()
2104 .is_none()
2105 );
2106 }
2107
2108 #[tokio::test]
2109 async fn load_job_detail_resolves_the_cross_link_after_the_job_is_done() {
2110 let db = db().await;
2111 let (_order, job) = relay_job(&db).await;
2112 sqlx::query("UPDATE jobs SET status = 'done' WHERE id = ?;")
2114 .bind(job.id)
2115 .execute(db.raw_pool())
2116 .await
2117 .unwrap();
2118
2119 let detail = load_job_detail(job.id.to_string().as_str(), db)
2120 .await
2121 .unwrap()
2122 .unwrap();
2123 assert!(detail.upstream_order.is_some());
2124 }
2125
2126 #[tokio::test]
2127 async fn load_upstream_order_detail_finds_the_latest_job_even_when_terminal() {
2128 let db = db().await;
2129 let (order, job) = relay_job(&db).await;
2130 sqlx::query("UPDATE jobs SET status = 'failed' WHERE id = ?;")
2131 .bind(job.id)
2132 .execute(db.raw_pool())
2133 .await
2134 .unwrap();
2135
2136 let detail = load_upstream_order_detail(order.id.to_string().as_str(), db.clone())
2137 .await
2138 .unwrap()
2139 .unwrap();
2140 assert_eq!(detail.upstream_order.order_id, order.id);
2141 assert_eq!(detail.job.as_ref().unwrap().id, job.id);
2142
2143 assert!(
2144 load_upstream_order_detail("nope", db)
2145 .await
2146 .unwrap()
2147 .is_none()
2148 );
2149 }
2150
2151 #[tokio::test]
2152 async fn cancel_job_on_a_ready_sweep_writes_a_plain_job_cancelled_row_and_no_order_change() {
2153 let db = db().await;
2154 let sweep = sweep_job(&db).await;
2155
2156 let outcome = cancel_job(
2157 sweep.id.to_string().as_str(),
2158 cli_actor(),
2159 ClientContext::default(),
2160 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2161 db.clone(),
2162 )
2163 .await
2164 .unwrap();
2165 assert!(matches!(outcome, CancelJobOutcome::Cancelled(_)));
2166 assert_eq!(
2167 Job::find_by_id(sweep.id, &db)
2168 .await
2169 .unwrap()
2170 .unwrap()
2171 .status,
2172 "cancelled"
2173 );
2174
2175 let rows = audit_rows(&db).await;
2178 assert_eq!(rows.len(), 1);
2179 assert_eq!(rows[0].event, "job_cancelled");
2180 assert_eq!(rows[0].outcome, "success");
2181 assert_eq!(rows[0].actor_kind, "cli");
2182 assert!(rows[0].order_id.is_none());
2183 assert!(rows[0].account_id.is_none());
2184 }
2185
2186 #[tokio::test]
2187 async fn cancel_job_on_a_ready_relay_job_abandons_the_order_and_audits_the_operator() {
2188 let db = db().await;
2189 let (order, job) = relay_job(&db).await;
2190
2191 let outcome = cancel_job(
2192 job.id.to_string().as_str(),
2193 Actor::admin("root"),
2194 ClientContext {
2195 ip: Some("203.0.113.7".to_string()),
2196 ..ClientContext::default()
2197 },
2198 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2199 db.clone(),
2200 )
2201 .await
2202 .unwrap();
2203 let CancelJobOutcome::CancelledAndOrderAbandoned { order_id, .. } = outcome else {
2204 panic!("expected CancelledAndOrderAbandoned, got {outcome:?}");
2205 };
2206 assert_eq!(order_id, order.id.to_string());
2207
2208 assert_eq!(
2209 Job::find_by_id(job.id, &db).await.unwrap().unwrap().status,
2210 "cancelled"
2211 );
2212 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
2213 .await
2214 .unwrap()
2215 .unwrap();
2216 assert_eq!(reloaded.status.as_str(), "invalid");
2217 let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
2218 .await
2219 .unwrap()
2220 .unwrap();
2221 assert_eq!(mapping.status, "invalid");
2222
2223 let rows = audit_rows(&db).await;
2224 assert_eq!(rows.len(), 1);
2225 assert_eq!(rows[0].event, "certificate_issue_failed");
2226 assert_eq!(rows[0].actor_kind, "admin");
2227 assert_eq!(rows[0].actor_id.as_deref(), Some("root"));
2228 assert_eq!(rows[0].client_ip.as_deref(), Some("203.0.113.7"));
2229 }
2230
2231 #[tokio::test]
2243 async fn cancel_job_on_a_failed_relay_job_keeps_the_upstream_error_and_writes_one_row() {
2244 let db = db().await;
2245 let (order, job) = relay_job(&db).await;
2246
2247 acme_proxy_signer::relay::abandon_relayed_order(
2250 &mut Order::find_by_id(order.id.to_string().as_str(), &db)
2251 .await
2252 .unwrap()
2253 .unwrap(),
2254 "urn:ietf:params:acme:error:rejectedIdentifier from the upstream",
2255 Actor::cli(),
2256 ClientContext::default(),
2257 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2258 &db,
2259 )
2260 .await
2261 .unwrap();
2262 Job::claim_next(
2265 "runner-a",
2266 &[RELAY_JOB_KIND],
2267 now_secs() + 60,
2268 now_secs(),
2269 &db,
2270 )
2271 .await
2272 .unwrap()
2273 .expect("the relay job is claimable");
2274 assert!(
2275 Job::abandon(job.id, "runner-a", "gave up", &db)
2276 .await
2277 .unwrap()
2278 );
2279 let rows_before = audit_rows(&db).await.len();
2280
2281 let outcome = cancel_job(
2282 job.id.to_string().as_str(),
2283 Actor::admin("root"),
2284 ClientContext::default(),
2285 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2286 db.clone(),
2287 )
2288 .await
2289 .unwrap();
2290 assert!(
2291 matches!(outcome, CancelJobOutcome::Cancelled(_)),
2292 "a failed relay job has nothing left to abandon: {outcome:?}"
2293 );
2294
2295 let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
2296 .await
2297 .unwrap()
2298 .unwrap();
2299 assert_eq!(
2300 mapping.error.as_deref(),
2301 Some("urn:ietf:params:acme:error:rejectedIdentifier from the upstream"),
2302 "the upstream's own reason must survive the cancellation"
2303 );
2304
2305 let rows = audit_rows(&db).await;
2306 assert_eq!(
2307 rows.len(),
2308 rows_before + 1,
2309 "one issuance, one certificate_issue_failed row"
2310 );
2311 assert_eq!(rows[0].event, "job_cancelled");
2312 }
2313
2314 #[tokio::test]
2318 async fn cancel_job_on_a_relay_job_with_no_order_says_so_and_still_audits() {
2319 let db = db().await;
2320 let (order, job) = relay_job(&db).await;
2321 Order::delete(order.id.to_string().as_str(), &db)
2322 .await
2323 .unwrap();
2324
2325 let outcome = cancel_job(
2326 job.id.to_string().as_str(),
2327 Actor::admin("root"),
2328 ClientContext::default(),
2329 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2330 db.clone(),
2331 )
2332 .await
2333 .unwrap();
2334 assert!(
2335 matches!(outcome, CancelJobOutcome::Cancelled(_)),
2336 "nothing was abandoned, so nothing may say it was: {outcome:?}"
2337 );
2338
2339 let rows = audit_rows(&db).await;
2340 assert_eq!(rows.len(), 1, "the cancellation is still an admin action");
2341 assert_eq!(rows[0].event, "job_cancelled");
2342 }
2343
2344 #[tokio::test]
2345 async fn cancel_job_refuses_a_running_job_and_leaves_the_order_alone() {
2346 let db = db().await;
2347 let (order, job) = relay_job(&db).await;
2348 sqlx::query("UPDATE jobs SET status = 'running' WHERE id = ?;")
2349 .bind(job.id)
2350 .execute(db.raw_pool())
2351 .await
2352 .unwrap();
2353
2354 let outcome = cancel_job(
2355 job.id.to_string().as_str(),
2356 cli_actor(),
2357 ClientContext::default(),
2358 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2359 db.clone(),
2360 )
2361 .await
2362 .unwrap();
2363 assert!(matches!(
2364 outcome,
2365 CancelJobOutcome::NotCancellable(s) if s == "running"
2366 ));
2367 assert_ne!(
2369 Order::find_by_id(order.id.to_string().as_str(), &db)
2370 .await
2371 .unwrap()
2372 .unwrap()
2373 .status
2374 .as_str(),
2375 "invalid"
2376 );
2377 }
2378
2379 #[tokio::test]
2380 async fn cancel_job_not_found_versus_not_cancellable() {
2381 let db = db().await;
2382 assert!(matches!(
2383 cancel_job(
2384 "nope",
2385 cli_actor(),
2386 ClientContext::default(),
2387 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2388 db.clone()
2389 )
2390 .await
2391 .unwrap(),
2392 CancelJobOutcome::NotFound
2393 ));
2394 assert!(matches!(
2395 cancel_job(
2396 acme_proxy_store::id::mint().to_string().as_str(),
2397 cli_actor(),
2398 ClientContext::default(),
2399 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2400 db.clone()
2401 )
2402 .await
2403 .unwrap(),
2404 CancelJobOutcome::NotFound
2405 ));
2406
2407 let sweep = sweep_job(&db).await;
2408 sqlx::query("UPDATE jobs SET status = 'done' WHERE id = ?;")
2409 .bind(sweep.id)
2410 .execute(db.raw_pool())
2411 .await
2412 .unwrap();
2413 assert!(matches!(
2414 cancel_job(
2415 sweep.id.to_string().as_str(),
2416 cli_actor(),
2417 ClientContext::default(),
2418 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2419 db,
2420 )
2421 .await
2422 .unwrap(),
2423 CancelJobOutcome::NotCancellable(s) if s == "done"
2424 ));
2425 }
2426
2427 #[tokio::test]
2428 async fn confirm_cancel_job_declined_leaves_the_job() {
2429 let db = db().await;
2430 let sweep = sweep_job(&db).await;
2431 let mut reader = b"n\n".as_slice();
2432 assert!(
2433 confirm_cancel_job(
2434 sweep.id.to_string().as_str(),
2435 false,
2436 &mut reader,
2437 cli_actor(),
2438 ClientContext::default(),
2439 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2440 db.clone(),
2441 )
2442 .await
2443 .unwrap()
2444 .is_none()
2445 );
2446 assert_eq!(
2447 Job::find_by_id(sweep.id, &db)
2448 .await
2449 .unwrap()
2450 .unwrap()
2451 .status,
2452 "ready"
2453 );
2454 }
2455
2456 #[tokio::test]
2457 async fn run_job_now_nudges_ready_revives_failed_and_refuses_the_rest() {
2458 let db = db().await;
2459
2460 let sweep = sweep_job(&db).await; let outcome = run_job_now(
2463 sweep.id.to_string().as_str(),
2464 Actor::cli(),
2465 ClientContext::default(),
2466 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2467 db.clone(),
2468 )
2469 .await
2470 .unwrap();
2471 let RunJobNowOutcome::Nudged(job) = outcome else {
2472 panic!("expected Nudged, got {outcome:?}");
2473 };
2474 assert!(job.run_at <= now_secs() + 1);
2475
2476 let (_order, relay) = relay_job(&db).await;
2478 sqlx::query(
2479 "UPDATE jobs SET status = 'failed', attempts = 5, last_error = 'boom' WHERE id = ?;",
2480 )
2481 .bind(relay.id)
2482 .execute(db.raw_pool())
2483 .await
2484 .unwrap();
2485 let outcome = run_job_now(
2486 relay.id.to_string().as_str(),
2487 Actor::cli(),
2488 ClientContext::default(),
2489 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2490 db.clone(),
2491 )
2492 .await
2493 .unwrap();
2494 let RunJobNowOutcome::Revived(job) = outcome else {
2495 panic!("expected Revived, got {outcome:?}");
2496 };
2497 assert_eq!(job.status, "ready");
2498 assert_eq!(job.attempts, 4, "max_attempts - 1");
2499
2500 sqlx::query("UPDATE jobs SET status = 'done' WHERE id = ?;")
2502 .bind(sweep.id)
2503 .execute(db.raw_pool())
2504 .await
2505 .unwrap();
2506 assert!(matches!(
2507 run_job_now(
2508 sweep.id.to_string().as_str(),
2509 Actor::cli(),
2510 ClientContext::default(),
2511 &acme_proxy_jobs::auditor::Auditor::offline(db.clone()),
2512 db.clone(),
2513 ).await.unwrap(),
2514 RunJobNowOutcome::Refused(s) if s == "done"
2515 ));
2516
2517 assert!(matches!(
2518 run_job_now(
2519 "nope",
2520 Actor::cli(),
2521 ClientContext::default(),
2522 &Auditor::offline(db.clone()),
2523 db
2524 )
2525 .await
2526 .unwrap(),
2527 RunJobNowOutcome::NotFound
2528 ));
2529 }
2530
2531 #[test]
2532 fn revoke_error_display_formatting() {
2533 let db_err: RevokeError = sqlx::Error::RowNotFound.into();
2534 assert!(format!("{db_err}").contains("database error"));
2535
2536 let signer_internal: RevokeError = SignerError::Internal("test".to_string()).into();
2537 assert!(format!("{signer_internal}").contains("signer error: test"));
2538
2539 let signer_bad_csr: RevokeError = SignerError::BadCsr.into();
2540 assert!(format!("{signer_bad_csr}").contains("unexpected badCsr"));
2541
2542 let internal = RevokeError::Internal("detail".to_string());
2543 assert!(format!("{internal}").contains("internal error: detail"));
2544
2545 let bad_reason = RevokeError::BadReason(7);
2546 assert!(format!("{bad_reason}").contains("unsupported revocation reason code 7"));
2547 }
2548}