1use std::io::BufRead;
2use std::sync::Arc;
3use std::time::Duration;
4
5use clap::Subcommand;
6
7use crate::admin;
8use crate::cli::CliError;
9use crate::config::Config;
10use crate::sqlite::db::Database;
11use crate::sqlite::nonce::Nonce;
12
13#[derive(Subcommand)]
14pub enum NonceCommand {
15 Cleanup {
17 #[arg(long = "ttl-seconds")]
18 ttl_seconds: Option<u64>,
19 },
20 Count {
22 #[arg(long)]
23 json: bool,
24 },
25}
26
27pub async fn run_nonce_command(
28 command: NonceCommand,
29 yes: bool,
30 reader: &mut impl BufRead,
31 config: &Config,
32 database: Arc<Database>,
33) -> Result<(), CliError> {
34 match command {
35 NonceCommand::Cleanup { ttl_seconds } => {
36 let ttl = Duration::from_secs(ttl_seconds.unwrap_or(config.nonce.ttl_seconds));
37 match admin::confirm_cleanup_nonces(ttl, yes, reader, database).await? {
38 None => println!("Cancelled."),
39 Some(removed) => println!("Removed {removed} nonce(s)."),
40 }
41 }
42 NonceCommand::Count { json } => {
43 let count = Nonce::count(&database).await?;
48 let ttl = config.nonce.ttl_seconds;
49 if json {
50 println!("{}", admin::render_nonce_stats_json(count, ttl));
51 } else {
52 println!("{count} nonce(s), ttl {ttl}s.");
53 }
54 }
55 }
56 Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[tokio::test]
66 async fn count_reports_the_table_and_the_configured_ttl() {
67 let database = Arc::new(Database::connect_in_memory().await.unwrap());
68 let mut config = Config::default();
69 config.nonce.ttl_seconds = 42;
70
71 for _ in 0..3 {
72 Nonce::new().save(&database).await.unwrap();
73 }
74 assert_eq!(Nonce::count(&database).await.unwrap(), 3);
75
76 assert_eq!(
77 admin::render_nonce_stats_json(3, config.nonce.ttl_seconds),
78 serde_json::json!({ "count": 3, "ttlSeconds": 42 }),
79 );
80
81 for json in [false, true] {
82 let mut reader: &[u8] = &[];
83 run_nonce_command(
84 NonceCommand::Count { json },
85 true,
86 &mut reader,
87 &config,
88 database.clone(),
89 )
90 .await
91 .unwrap();
92 }
93 }
94
95 #[tokio::test]
98 async fn cleanup_honours_the_configured_ttl_and_the_prompt() {
99 let database = Arc::new(
100 crate::sqlite::db::Database::connect_in_memory()
101 .await
102 .unwrap(),
103 );
104 let mut config = Config::default();
105 config.nonce.ttl_seconds = 1;
106
107 let mut declined: &[u8] = b"n\n";
108 run_nonce_command(
109 NonceCommand::Cleanup { ttl_seconds: None },
110 false,
111 &mut declined,
112 &config,
113 database.clone(),
114 )
115 .await
116 .unwrap();
117
118 let mut reader: &[u8] = &[];
119 run_nonce_command(
120 NonceCommand::Cleanup {
121 ttl_seconds: Some(60),
122 },
123 true,
124 &mut reader,
125 &config,
126 database,
127 )
128 .await
129 .unwrap();
130 }
131}