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