1use std::io::BufRead;
2use std::sync::Arc;
3use uuid::Uuid;
4
5use clap::Subcommand;
6
7use crate::admin::{self, DeleteOutcome};
8use crate::cli::CliError;
9use crate::cli::render;
10use crate::cli::style::Palette;
11use crate::cli::window::{DEFAULT_LIMIT, Window};
12use crate::config::Config;
13use crate::signer;
14use crate::sqlite::authz::Authorization;
15use crate::sqlite::db::Database;
16use crate::sqlite::order::{Order, OrderQuery};
17use crate::sqlite::status::OrderStatus;
18
19#[derive(Subcommand)]
20pub enum OrderCommand {
21 List {
23 #[arg(long)]
25 profile: Option<String>,
26 #[arg(long = "account-id")]
27 account_id: Option<String>,
28 #[arg(long)]
29 status: Option<String>,
30 #[arg(long = "expiring-in")]
33 expiring_in: Option<u64>,
34 #[arg(long = "hide-superseded")]
37 hide_superseded: bool,
38 #[arg(long, default_value_t = DEFAULT_LIMIT)]
39 limit: i64,
40 #[arg(long, default_value_t = 0)]
41 offset: i64,
42 #[arg(long)]
43 json: bool,
44 },
45 Show {
47 id: String,
48 #[arg(long)]
49 json: bool,
50 },
51 Chain { id: String },
53 Delete { id: String },
55 Revoke {
57 id: String,
58 #[arg(long)]
59 reason: Option<u32>,
60 },
61}
62
63pub async fn run_order_command(
64 command: OrderCommand,
65 yes: bool,
66 palette: Palette,
67 reader: &mut impl BufRead,
68 config: &Config,
69 database: Arc<Database>,
70) -> Result<(), CliError> {
71 match command {
72 OrderCommand::List {
73 profile,
74 account_id,
75 status,
76 expiring_in,
77 hide_superseded,
78 limit,
79 offset,
80 json,
81 } => {
82 let window = Window::resolve(limit, offset);
83
84 if let Some(days) = expiring_in {
92 return run_expiring(
93 days,
94 profile,
95 account_id.as_deref(),
96 status.as_deref(),
97 hide_superseded,
98 window,
99 json,
100 palette,
101 database,
102 )
103 .await;
104 }
105 if hide_superseded {
106 return Err(CliError(
107 "--hide-superseded needs --expiring-in: it filters on the supersession \
108 annotation, which only the expiry listing carries"
109 .to_string(),
110 ));
111 }
112
113 let status = status
117 .map(|value| value.parse::<OrderStatus>())
118 .transpose()
119 .map_err(|error| CliError(format!("--status: {error}")))?;
120
121 let query = OrderQuery {
126 profile,
127 account_id,
128 status,
129 limit: window.limit,
130 offset: window.offset,
131 };
132 let (orders, total) = Order::search(&query, &database).await?;
133 if json {
142 let ids: Vec<Uuid> = orders.iter().map(|o| o.id).collect();
143 let mut authz_ids = Authorization::find_ids_by_orders(&ids, &database).await?;
144 let rendered: Vec<_> = orders
145 .iter()
146 .map(|order| {
147 admin::render_order_json(
148 order,
149 &config.server.base_url,
150 &authz_ids.remove(&order.id).unwrap_or_default(),
151 )
152 })
153 .collect();
154 println!("{}", render::json_page(rendered, total, window));
155 } else {
156 for order in &orders {
157 println!("{}", render::render_order_line(order, palette));
158 }
159 render::print_footer(orders.len(), total);
160 }
161 }
162 OrderCommand::Show { id, json } => match admin::load_order_detail(&id, database).await? {
163 None => return Err(not_found(&id)),
164 Some(detail) if json => {
165 println!(
166 "{}",
167 admin::render_order_detail_json(&detail, &config.server.base_url)
168 );
169 }
170 Some(detail) => print!("{}", render::render_order_detail_text(&detail, palette)),
171 },
172 OrderCommand::Chain { id } => {
173 let Some(order) = Order::find_by_id(&id, &database).await? else {
179 return Err(not_found(&id));
180 };
181 let Some(pem) = order.certificate else {
185 return Err(CliError(format!(
186 "order {id} has no certificate: it has not been finalized"
187 )));
188 };
189 print!("{pem}");
192 }
193 OrderCommand::Delete { id } => {
194 match admin::confirm_delete_order(&id, yes, reader, database).await? {
195 DeleteOutcome::NotFound => return Err(not_found(&id)),
196 DeleteOutcome::Cancelled => println!("Cancelled."),
197 DeleteOutcome::Deleted => println!("Deleted order {id}."),
198 }
199 }
200 OrderCommand::Revoke { id, reason } => {
201 let Some(order) = Order::find_by_id(&id, &database).await? else {
204 return Err(not_found(&id));
205 };
206 let profiles = config
207 .resolve_profiles()
208 .map_err(|error| CliError(format!("configuration error: {error}")))?;
209 let Some(profile) = profiles.iter().find(|p| p.name == order.profile) else {
210 return Err(CliError(format!(
211 "order {id} was issued by profile `{}`, which this configuration does not \
212 define — revoking it needs the endpoint that signed it",
213 order.profile
214 )));
215 };
216 let egress = Arc::new(
224 crate::Egress::from_config(config)
225 .map_err(|error| CliError(format!("configuration error: {error}")))?,
226 );
227 let jobs = crate::jobs::JobQueue::new(database.clone(), &config.jobs);
232 let metrics = Arc::new(crate::metrics::Metrics::new(database.clone()));
236 let signer = signer::from_config(
237 &profile.sections.signer,
238 &signer::SignerParts {
239 database: database.clone(),
240 notifiers: std::collections::HashMap::new().into(),
241 metrics,
242 egress,
243 jobs,
244 },
245 &signer::CarriedState::new(),
248 )
249 .map_err(|error| CliError(format!("signer error: {error}")))?;
250 match admin::revoke_order(
253 &id,
254 reason,
255 crate::audit::Actor::cli(),
256 crate::audit::ClientContext::default(),
257 database,
258 signer,
259 )
260 .await
261 .map_err(|error| CliError(error.to_string()))?
262 {
263 admin::RevokeOutcome::NotFound => return Err(not_found(&id)),
264 admin::RevokeOutcome::NotIssued => {
265 return Err(CliError(format!("order {id} has no issued certificate")));
266 }
267 admin::RevokeOutcome::AlreadyRevoked => {
268 return Err(CliError(format!(
269 "order {id}'s certificate is already revoked"
270 )));
271 }
272 admin::RevokeOutcome::Revoked(order) => {
273 println!("{}", render::render_order_line(&order, palette));
274 }
275 }
276 }
277 }
278 Ok(())
279}
280
281#[allow(clippy::too_many_arguments)]
296async fn run_expiring(
297 days: u64,
298 profile: Option<String>,
299 account_id: Option<&str>,
300 status: Option<&str>,
301 hide_superseded: bool,
302 window: Window,
303 json: bool,
304 palette: Palette,
305 database: Arc<Database>,
306) -> Result<(), CliError> {
307 if status.is_some() {
308 return Err(CliError(
309 "--status does not apply with --expiring-in: the expiry listing is issued, \
310 unrevoked certificates by definition, so a status filter here would mean \
311 something other than it does everywhere else"
312 .to_string(),
313 ));
314 }
315 if account_id.is_some() {
316 return Err(CliError(
317 "--account-id does not apply with --expiring-in: the expiry listing has no \
318 account predicate, and answering as though it did would report one \
319 subscriber's certificates as every subscriber's"
320 .to_string(),
321 ));
322 }
323
324 let query = admin::ExpiringQuery {
325 profile,
326 before: admin::expiring_horizon(days),
327 include_superseded: !hide_superseded,
328 limit: window.limit,
329 offset: window.offset,
330 };
331 let (entries, total, hidden) = admin::list_expiring(&query, database).await?;
332 if json {
333 let items = entries.iter().map(admin::render_expiring_json).collect();
334 let mut envelope = render::json_page(items, total, window);
335 if let Some(object) = envelope.as_object_mut() {
336 object.insert("hidden".to_string(), serde_json::json!(hidden));
340 object.insert("days".to_string(), serde_json::json!(days));
341 }
342 println!("{envelope}");
343 } else {
344 for entry in &entries {
345 println!("{}", render::render_expiring_line(entry, palette));
346 }
347 render::print_expiring_footer(entries.len(), total, hidden);
348 }
349 Ok(())
350}
351
352fn not_found(id: &str) -> CliError {
353 CliError(format!("no such order: {id}"))
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::audit::ClientContext;
360 use crate::signer::{IssueOutcome, RequestedValidity, SignerBackend};
361 use crate::sqlite::account::Account;
362
363 fn config_in(dir: impl AsRef<std::path::Path>, profile: &str) -> Config {
367 let dir = dir.as_ref();
368 let _lock = crate::config::ENV_LOCK
369 .lock()
370 .unwrap_or_else(std::sync::PoisonError::into_inner);
371 let ca = dir.join("ca");
372 std::fs::write(
373 dir.join("config.toml"),
374 format!(
375 r#"
376 [profiles.{profile}]
377 signer.local_ca.cert_path = "{ca}.pem"
378 signer.local_ca.key_path = "{ca}.key"
379 signer.local_ca.crl_path = "{ca}.crl"
380 "#,
381 ca = ca.display(),
382 ),
383 )
384 .unwrap();
385 unsafe {
388 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
389 }
390 let config = Config::load().expect("the configuration must load");
391 unsafe {
392 std::env::remove_var("ACME_PROXY_CONFIG");
393 }
394 config
395 }
396
397 fn temp_dir() -> crate::testutil::TempDir {
398 crate::testutil::TempDir::new("cli-order")
399 }
400
401 async fn seed_order(database: &Arc<Database>, profile: &str) -> Order {
402 let (account, _) = Account::find_or_create(
403 profile,
404 &[4, 5, 6],
405 vec![],
406 &ClientContext::default(),
407 database,
408 )
409 .await
410 .unwrap();
411 Order::create(
412 profile,
413 account.id,
414 vec![crate::sqlite::order::Identifier::dns("example.com")],
415 crate::sqlite::nonce::now_secs() + 3600,
416 None,
417 None,
418 database,
419 )
420 .await
421 .unwrap()
422 }
423
424 async fn issue_onto(order: &mut Order, config: &Config, database: Arc<Database>) {
427 let profile = &config.resolve_profiles().unwrap()[0];
428 let resolver = crate::dns::resolver_addr(&config.dns)
429 .and_then(crate::challenge::build_resolver)
430 .expect("the default dns configuration must build a resolver");
431 let signer: Arc<dyn SignerBackend> = signer::from_config(
432 &profile.sections.signer,
433 &crate::testutil::signer_parts(database.clone(), resolver),
434 &signer::CarriedState::new(),
435 )
436 .unwrap();
437
438 let key_pair = rcgen::KeyPair::generate().unwrap();
439 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
440 let csr = params.serialize_request(&key_pair).unwrap();
441 let chain = match signer
442 .issue(
443 order.id.to_string().as_str(),
444 csr.der(),
445 &order.identifiers,
446 RequestedValidity::default(),
447 )
448 .await
449 .unwrap()
450 {
451 IssueOutcome::Issued(chain) => chain,
452 IssueOutcome::Processing => panic!("the local CA issues synchronously"),
453 };
454 let leaf = crate::cert::leaf_der_from_chain(&chain).unwrap();
455 let (serial, pubkey) = crate::cert::cert_serial_and_spki(&leaf).unwrap();
456 let not_after = crate::cert::cert_validity(&leaf).ok().map(|(_, na)| na);
457 order
458 .finalize(chain, serial, pubkey, not_after, &database)
459 .await
460 .unwrap();
461 }
462
463 #[tokio::test]
464 async fn every_arm_refuses_an_unknown_order() {
465 let database = Arc::new(Database::connect_in_memory().await.unwrap());
466 let config = Config::default();
467 let expected = CliError("no such order: ord-nope".to_string());
468
469 let commands = vec![
470 OrderCommand::Show {
471 id: "ord-nope".to_string(),
472 json: false,
473 },
474 OrderCommand::Chain {
475 id: "ord-nope".to_string(),
476 },
477 OrderCommand::Delete {
478 id: "ord-nope".to_string(),
479 },
480 OrderCommand::Revoke {
481 id: "ord-nope".to_string(),
482 reason: None,
483 },
484 ];
485 for command in commands {
486 let mut reader: &[u8] = &[];
487 let error = run_order_command(
488 command,
489 true,
490 Palette::plain(),
491 &mut reader,
492 &config,
493 database.clone(),
494 )
495 .await
496 .expect_err("an unknown order must fail");
497 assert_eq!(error, expected);
498 }
499 }
500
501 #[tokio::test]
506 async fn chain_prints_the_issued_pem_and_refuses_an_order_with_none() {
507 let dir = temp_dir();
508 let config = config_in(&dir, "default");
509 let database = Arc::new(Database::connect_in_memory().await.unwrap());
510 let mut order = seed_order(&database, "default").await;
511
512 let mut reader: &[u8] = &[];
513 let error = run_order_command(
514 OrderCommand::Chain {
515 id: order.id.to_string(),
516 },
517 true,
518 Palette::plain(),
519 &mut reader,
520 &config,
521 database.clone(),
522 )
523 .await
524 .expect_err("an order with no certificate has no chain to print");
525 assert_eq!(
526 error,
527 CliError(format!(
528 "order {} has no certificate: it has not been finalized",
529 order.id
530 ))
531 );
532
533 issue_onto(&mut order, &config, database.clone()).await;
534
535 let stored = Order::find_by_id(order.id.to_string().as_str(), &database)
539 .await
540 .unwrap()
541 .unwrap()
542 .certificate
543 .expect("finalize stored the chain");
544 assert!(stored.starts_with("-----BEGIN CERTIFICATE-----"));
545
546 let mut reader: &[u8] = &[];
547 run_order_command(
548 OrderCommand::Chain {
549 id: order.id.to_string(),
550 },
551 true,
552 Palette::plain(),
553 &mut reader,
554 &config,
555 database,
556 )
557 .await
558 .unwrap();
559 }
560
561 #[tokio::test]
565 async fn revoking_an_order_from_an_undefined_profile_is_refused() {
566 let dir = temp_dir();
567 let database = Arc::new(Database::connect_in_memory().await.unwrap());
568 let order = seed_order(&database, "default").await;
570 let config = config_in(&dir, "other");
571
572 let mut reader: &[u8] = &[];
573 let error = run_order_command(
574 OrderCommand::Revoke {
575 id: order.id.to_string(),
576 reason: None,
577 },
578 true,
579 Palette::plain(),
580 &mut reader,
581 &config,
582 database,
583 )
584 .await
585 .expect_err("a profile this configuration does not define must be refused");
586 assert!(
587 error.to_string().contains("which this configuration"),
588 "{error}"
589 );
590 }
591
592 #[tokio::test]
594 async fn revoking_without_a_resolvable_configuration_is_refused() {
595 let database = Arc::new(Database::connect_in_memory().await.unwrap());
596 let order = seed_order(&database, "default").await;
597
598 let mut reader: &[u8] = &[];
599 let error = run_order_command(
600 OrderCommand::Revoke {
601 id: order.id.to_string(),
602 reason: None,
603 },
604 true,
605 Palette::plain(),
606 &mut reader,
607 &Config::default(),
608 database,
609 )
610 .await
611 .expect_err("a configuration mounting nothing must be refused");
612 assert!(
613 error.to_string().starts_with("configuration error: "),
614 "{error}"
615 );
616 }
617
618 #[tokio::test]
619 async fn revoking_an_order_with_no_certificate_is_refused() {
620 let dir = temp_dir();
621 let database = Arc::new(Database::connect_in_memory().await.unwrap());
622 let order = seed_order(&database, "default").await;
623 let config = config_in(&dir, "default");
624
625 let mut reader: &[u8] = &[];
626 let error = run_order_command(
627 OrderCommand::Revoke {
628 id: order.id.to_string(),
629 reason: None,
630 },
631 true,
632 Palette::plain(),
633 &mut reader,
634 &config,
635 database,
636 )
637 .await
638 .expect_err("there is nothing to revoke");
639 assert_eq!(
640 error,
641 CliError(format!("order {} has no issued certificate", order.id))
642 );
643 }
644
645 #[tokio::test]
648 async fn an_issued_order_revokes_once() {
649 let dir = temp_dir();
650 let database = Arc::new(Database::connect_in_memory().await.unwrap());
651 let config = config_in(&dir, "default");
652 let mut order = seed_order(&database, "default").await;
653 issue_onto(&mut order, &config, database.clone()).await;
654
655 let mut reader: &[u8] = &[];
656 run_order_command(
657 OrderCommand::Revoke {
658 id: order.id.to_string(),
659 reason: Some(1),
660 },
661 true,
662 Palette::plain(),
663 &mut reader,
664 &config,
665 database.clone(),
666 )
667 .await
668 .expect("a certificate issued by this profile's CA must revoke");
669
670 assert!(
671 Order::find_by_id(order.id.to_string().as_str(), &database)
672 .await
673 .unwrap()
674 .unwrap()
675 .revoked_at
676 .is_some()
677 );
678
679 let error = run_order_command(
680 OrderCommand::Revoke {
681 id: order.id.to_string(),
682 reason: None,
683 },
684 true,
685 Palette::plain(),
686 &mut reader,
687 &config,
688 database.clone(),
689 )
690 .await
691 .expect_err("a second revocation has nothing left to do");
692 assert_eq!(
693 error,
694 CliError(format!(
695 "order {}'s certificate is already revoked",
696 order.id
697 ))
698 );
699 }
700
701 #[tokio::test]
704 async fn an_invalid_revocation_reason_is_refused() {
705 let dir = temp_dir();
706 let database = Arc::new(Database::connect_in_memory().await.unwrap());
707 let config = config_in(&dir, "default");
708 let mut order = seed_order(&database, "default").await;
709 issue_onto(&mut order, &config, database.clone()).await;
710
711 let mut reader: &[u8] = &[];
712 let error = run_order_command(
713 OrderCommand::Revoke {
714 id: order.id.to_string(),
715 reason: Some(7),
716 },
717 true,
718 Palette::plain(),
719 &mut reader,
720 &config,
721 database,
722 )
723 .await
724 .expect_err("7 is not a defined CRLReason");
725 assert!(error.to_string().contains('7'), "{error}");
726 }
727
728 #[tokio::test]
730 async fn a_declined_delete_is_not_a_failure() {
731 let database = Arc::new(Database::connect_in_memory().await.unwrap());
732 let order = seed_order(&database, "default").await;
733
734 let mut reader: &[u8] = b"n\n";
735 run_order_command(
736 OrderCommand::Delete {
737 id: order.id.to_string(),
738 },
739 false,
740 Palette::plain(),
741 &mut reader,
742 &Config::default(),
743 database.clone(),
744 )
745 .await
746 .unwrap();
747
748 assert!(
749 Order::find_by_id(order.id.to_string().as_str(), &database)
750 .await
751 .unwrap()
752 .is_some()
753 );
754 }
755
756 #[tokio::test]
759 async fn the_json_arms_render() {
760 let database = Arc::new(Database::connect_in_memory().await.unwrap());
761 let order = seed_order(&database, "default").await;
762
763 let mut reader: &[u8] = &[];
764 for command in [
765 OrderCommand::List {
766 profile: Some("default".to_string()),
767 account_id: None,
768 status: None,
769 expiring_in: None,
770 hide_superseded: false,
771 limit: DEFAULT_LIMIT,
772 offset: 0,
773 json: true,
774 },
775 OrderCommand::Show {
776 id: order.id.to_string(),
777 json: true,
778 },
779 OrderCommand::Show {
780 id: order.id.to_string(),
781 json: false,
782 },
783 ] {
784 run_order_command(
785 command,
786 true,
787 Palette::plain(),
788 &mut reader,
789 &Config::default(),
790 database.clone(),
791 )
792 .await
793 .unwrap();
794 }
795 }
796
797 #[tokio::test]
803 async fn an_unknown_status_is_refused_by_name_rather_than_matching_nothing() {
804 let database = Arc::new(Database::connect_in_memory().await.unwrap());
805 seed_order(&database, "default").await;
806
807 let mut reader: &[u8] = &[];
808 let error = run_order_command(
809 OrderCommand::List {
810 profile: None,
811 account_id: None,
812 status: Some("readyy".to_string()),
813 expiring_in: None,
814 hide_superseded: false,
815 limit: DEFAULT_LIMIT,
816 offset: 0,
817 json: false,
818 },
819 true,
820 Palette::plain(),
821 &mut reader,
822 &Config::default(),
823 database.clone(),
824 )
825 .await
826 .unwrap_err();
827
828 assert!(error.0.contains("--status"), "{error}");
829 assert!(error.0.contains("`readyy`"), "{error}");
830 assert!(
832 error
833 .0
834 .contains("pending, ready, processing, valid, invalid"),
835 "{error}"
836 );
837 }
838
839 #[tokio::test]
844 async fn every_order_status_is_accepted_as_a_filter() {
845 let database = Arc::new(Database::connect_in_memory().await.unwrap());
846 seed_order(&database, "default").await;
847
848 let mut reader: &[u8] = &[];
849 for status in OrderStatus::ALL {
850 run_order_command(
851 OrderCommand::List {
852 profile: None,
853 account_id: None,
854 status: Some(status.as_str().to_string()),
855 expiring_in: None,
856 hide_superseded: false,
857 limit: DEFAULT_LIMIT,
858 offset: 0,
859 json: false,
860 },
861 true,
862 Palette::plain(),
863 &mut reader,
864 &Config::default(),
865 database.clone(),
866 )
867 .await
868 .unwrap_or_else(|error| panic!("--status {status} was refused: {error}"));
869 }
870 }
871
872 async fn list_with(
875 expiring_in: Option<u64>,
876 account_id: Option<&str>,
877 status: Option<&str>,
878 hide_superseded: bool,
879 json: bool,
880 database: Arc<Database>,
881 ) -> Result<(), CliError> {
882 let mut reader: &[u8] = &[];
883 run_order_command(
884 OrderCommand::List {
885 profile: None,
886 account_id: account_id.map(str::to_string),
887 status: status.map(str::to_string),
888 expiring_in,
889 hide_superseded,
890 limit: DEFAULT_LIMIT,
891 offset: 0,
892 json,
893 },
894 true,
895 Palette::plain(),
896 &mut reader,
897 &Config::default(),
898 database,
899 )
900 .await
901 }
902
903 #[tokio::test]
906 async fn the_expiring_arm_lists_and_renders_both_ways() {
907 let database = Arc::new(Database::connect_in_memory().await.unwrap());
908 let acct = crate::testutil::account_id(&database).await;
909 crate::testutil::issued_order(&database, "default", acct, &["a.example.com"], 3).await;
910 crate::testutil::issued_order(&database, "default", acct, &["b.example.com"], 5).await;
911 crate::testutil::issued_order(&database, "default", acct, &["a.example.com"], 90).await;
913
914 for json in [false, true] {
915 list_with(Some(30), None, None, false, json, database.clone())
916 .await
917 .unwrap();
918 list_with(Some(30), None, None, true, json, database.clone())
920 .await
921 .unwrap();
922 }
923 }
924
925 #[tokio::test]
931 async fn both_listings_take_a_window_and_clamp_a_nonsense_one() {
932 let database = Arc::new(Database::connect_in_memory().await.unwrap());
933 seed_order(&database, "default").await;
934
935 let mut reader: &[u8] = &[];
936 for expiring_in in [None, Some(30)] {
937 for (limit, offset, json) in
938 [(1, 0, false), (1, 1, false), (1, 0, true), (0, -5, false)]
939 {
940 run_order_command(
941 OrderCommand::List {
942 profile: None,
943 account_id: None,
944 status: None,
945 expiring_in,
946 hide_superseded: false,
947 limit,
948 offset,
949 json,
950 },
951 true,
952 Palette::plain(),
953 &mut reader,
954 &Config::default(),
955 database.clone(),
956 )
957 .await
958 .unwrap_or_else(|error| {
959 panic!(
960 "--expiring-in {expiring_in:?} --limit {limit} --offset {offset}: {error}"
961 )
962 });
963 }
964 }
965 }
966
967 #[tokio::test]
974 async fn the_flags_that_do_not_compose_with_expiring_in_are_refused_by_name() {
975 let database = Arc::new(Database::connect_in_memory().await.unwrap());
976 seed_order(&database, "default").await;
977
978 let error = list_with(
979 Some(30),
980 None,
981 Some("valid"),
982 false,
983 false,
984 database.clone(),
985 )
986 .await
987 .unwrap_err();
988 assert!(error.0.contains("--status"), "{error}");
989 assert!(error.0.contains("--expiring-in"), "{error}");
990
991 let error = list_with(
992 Some(30),
993 Some("acct-1"),
994 None,
995 false,
996 false,
997 database.clone(),
998 )
999 .await
1000 .unwrap_err();
1001 assert!(error.0.contains("--account-id"), "{error}");
1002 assert!(error.0.contains("--expiring-in"), "{error}");
1003
1004 let error = list_with(None, None, None, true, false, database.clone())
1005 .await
1006 .unwrap_err();
1007 assert!(error.0.contains("--hide-superseded"), "{error}");
1008 assert!(error.0.contains("--expiring-in"), "{error}");
1009
1010 list_with(None, None, Some("valid"), false, false, database)
1012 .await
1013 .unwrap();
1014 }
1015}