acme_proxy/cli/
profile.rs1use std::sync::Arc;
17
18use clap::Subcommand;
19
20use crate::admin::{self, ProfileSummary};
21use crate::cli::CliError;
22use crate::cli::render;
23use crate::cli::style::Palette;
24use crate::config::Config;
25
26#[derive(Subcommand)]
27pub enum ProfileCommand {
28 List {
30 #[arg(long)]
31 json: bool,
32 },
33}
34
35pub async fn run_profile_command(
36 command: ProfileCommand,
37 palette: Palette,
38 config: &Arc<Config>,
39) -> Result<(), CliError> {
40 match command {
41 ProfileCommand::List { json } => {
42 let resolved = config
48 .resolve_profiles()
49 .map_err(|error| CliError(format!("configuration error: {error}")))?;
50
51 let profiles: Vec<ProfileSummary> = resolved
52 .iter()
53 .map(|profile| ProfileSummary::configured(&config.server.base_url, profile))
54 .collect();
55
56 if json {
61 let rendered: Vec<_> = profiles.iter().map(admin::render_profile_json).collect();
62 println!("{}", serde_json::Value::Array(rendered));
63 } else {
64 for profile in &profiles {
65 println!("{}", render::render_profile_line(profile, palette));
66 }
67 }
68 }
69 }
70 Ok(())
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::config::ENV_LOCK;
77
78 fn config_from(body: &str) -> Arc<Config> {
82 let _lock = ENV_LOCK
83 .lock()
84 .unwrap_or_else(std::sync::PoisonError::into_inner);
85 let dir = crate::testutil::TempDir::new("profile");
86 std::fs::write(dir.join("config.toml"), body).unwrap();
87 unsafe {
89 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
90 }
91 let config = Config::load().expect("the configuration must load");
92 unsafe {
93 std::env::remove_var("ACME_PROXY_CONFIG");
94 }
95 Arc::new(config)
96 }
97
98 #[test]
101 fn the_listing_is_the_profiles_this_configuration_would_mount() {
102 let config = config_from(
103 r#"
104 [server]
105 base_url = "https://ca.example.com"
106
107 [challenge]
108 bypass = false
109
110 [profiles.staging]
111 challenge.bypass = true
112
113 [profiles.le]
114 eab.enabled = true
115
116 [profiles.parked]
117 enabled = false
118 "#,
119 );
120
121 let resolved = config.resolve_profiles().unwrap();
122 let summaries: Vec<ProfileSummary> = resolved
123 .iter()
124 .map(|profile| ProfileSummary::configured(&config.server.base_url, profile))
125 .collect();
126
127 let names: Vec<&str> = summaries.iter().map(|p| p.name.as_str()).collect();
128 assert_eq!(names, ["le", "staging"], "parked is not mounted");
129
130 let le = &summaries[0];
131 assert_eq!(le.base_url, "https://ca.example.com/profile/le");
132 assert_eq!(
133 le.directory_url(),
134 "https://ca.example.com/profile/le/directory"
135 );
136 assert!(le.eab_enabled);
137 assert!(!le.challenge_bypass);
140 assert!(summaries[1].challenge_bypass, "staging overrode it");
141 }
142
143 #[tokio::test]
148 async fn both_shapes_render_and_a_profileless_configuration_is_refused() {
149 let config = config_from("[profiles.default]\n");
150 for json in [false, true] {
151 run_profile_command(ProfileCommand::List { json }, Palette::plain(), &config)
152 .await
153 .unwrap();
154 }
155
156 let empty = Arc::new(Config::default());
157 let error = run_profile_command(
158 ProfileCommand::List { json: false },
159 Palette::plain(),
160 &empty,
161 )
162 .await
163 .expect_err("a configuration mounting nothing is not a listing of nothing");
164 assert!(
165 error.to_string().starts_with("configuration error: "),
166 "{error}"
167 );
168 }
169}