1use std::io::BufRead;
2use std::sync::Arc;
3
4use clap::Subcommand;
5
6use crate::admin::{self, DeleteOutcome};
7use crate::cli::CliError;
8use crate::cli::render;
9use crate::cli::style::Palette;
10use crate::config::Config;
11use crate::sqlite::account::Account;
12use crate::sqlite::db::Database;
13
14#[derive(Subcommand)]
15pub enum AccountCommand {
16 List {
18 #[arg(long)]
20 profile: Option<String>,
21 #[arg(long)]
22 json: bool,
23 },
24 Show {
26 id: String,
27 #[arg(long)]
28 json: bool,
29 },
30 UpdateContact {
32 id: String,
33 #[arg(long = "contact")]
34 contact: Vec<String>,
35 },
36 Deactivate { id: String },
38 Delete { id: String },
40}
41
42pub async fn run_account_command(
43 command: AccountCommand,
44 yes: bool,
45 palette: Palette,
46 reader: &mut impl BufRead,
47 config: &Config,
48 database: Arc<Database>,
49) -> Result<(), CliError> {
50 match command {
51 AccountCommand::List { profile, json } => {
52 let accounts = Account::list_all(profile.as_deref(), &database).await?;
53 render::print_rows(
54 &accounts,
55 json,
56 |a| admin::render_account_json(a, &config.server.base_url),
57 |a| render::render_account_line(a, palette),
58 );
59 }
60 AccountCommand::Show { id, json } => match Account::find_any_by_id(&id, &database).await? {
61 None => return Err(not_found(&id)),
62 Some(account) if json => {
63 println!(
64 "{}",
65 admin::render_account_json(&account, &config.server.base_url)
66 );
67 }
68 Some(account) => print!("{}", render::render_account_detail_text(&account, palette)),
69 },
70 AccountCommand::UpdateContact { id, contact } => {
71 match admin::update_account_contact(&id, contact, database).await? {
72 None => return Err(not_found(&id)),
73 Some(account) => println!("{}", render::render_account_line(&account, palette)),
74 }
75 }
76 AccountCommand::Deactivate { id } => {
77 match admin::deactivate_account(&id, database).await? {
78 None => return Err(not_found(&id)),
79 Some(account) => println!("{}", render::render_account_line(&account, palette)),
80 }
81 }
82 AccountCommand::Delete { id } => {
83 match admin::confirm_delete_account(&id, yes, reader, database).await? {
84 DeleteOutcome::NotFound => return Err(not_found(&id)),
85 DeleteOutcome::Cancelled => println!("Cancelled."),
86 DeleteOutcome::Deleted => println!("Deleted account {id}."),
87 }
88 }
89 }
90 Ok(())
91}
92
93fn not_found(id: &str) -> CliError {
94 CliError(format!("no such account: {id}"))
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::audit::ClientContext;
101
102 #[tokio::test]
105 async fn every_arm_refuses_an_unknown_account() {
106 let database = Arc::new(Database::connect_in_memory().await.unwrap());
107 let config = Config::default();
108 let expected = CliError("no such account: acct-nope".to_string());
109
110 let commands = vec![
111 AccountCommand::Show {
112 id: "acct-nope".to_string(),
113 json: false,
114 },
115 AccountCommand::UpdateContact {
116 id: "acct-nope".to_string(),
117 contact: vec!["mailto:someone@example.com".to_string()],
118 },
119 AccountCommand::Deactivate {
120 id: "acct-nope".to_string(),
121 },
122 AccountCommand::Delete {
123 id: "acct-nope".to_string(),
124 },
125 ];
126 for command in commands {
127 let mut reader: &[u8] = &[];
128 let error = run_account_command(
129 command,
130 true,
131 Palette::plain(),
132 &mut reader,
133 &config,
134 database.clone(),
135 )
136 .await
137 .expect_err("an unknown account must fail");
138 assert_eq!(error, expected);
139 }
140 }
141
142 #[tokio::test]
145 async fn a_declined_delete_is_not_a_failure() {
146 let database = Arc::new(Database::connect_in_memory().await.unwrap());
147 let config = Config::default();
148 let (account, _) = Account::find_or_create(
149 "default",
150 &[7, 7, 7],
151 vec![],
152 &ClientContext::default(),
153 &database,
154 )
155 .await
156 .unwrap();
157
158 let mut reader: &[u8] = b"n\n";
159 run_account_command(
160 AccountCommand::Delete {
161 id: account.id.clone(),
162 },
163 false,
164 Palette::plain(),
165 &mut reader,
166 &config,
167 database.clone(),
168 )
169 .await
170 .unwrap();
171
172 assert!(
173 Account::find_any_by_id(&account.id, &database)
174 .await
175 .unwrap()
176 .is_some(),
177 "a declined delete must leave the account in place"
178 );
179 }
180
181 #[tokio::test]
184 async fn the_json_arms_render() {
185 let database = Arc::new(Database::connect_in_memory().await.unwrap());
186 let config = Config::default();
187 let (account, _) = Account::find_or_create(
188 "default",
189 &[9, 9, 9],
190 vec![],
191 &ClientContext::default(),
192 &database,
193 )
194 .await
195 .unwrap();
196
197 let mut reader: &[u8] = &[];
198 run_account_command(
199 AccountCommand::List {
200 profile: Some("default".to_string()),
201 json: true,
202 },
203 true,
204 Palette::plain(),
205 &mut reader,
206 &config,
207 database.clone(),
208 )
209 .await
210 .unwrap();
211
212 run_account_command(
213 AccountCommand::Show {
214 id: account.id,
215 json: true,
216 },
217 true,
218 Palette::plain(),
219 &mut reader,
220 &config,
221 database,
222 )
223 .await
224 .unwrap();
225 }
226}