1use anyhow::{bail, Context, Result};
2use secrecy::SecretString;
3use std::{fs, path::PathBuf};
4
5pub fn get_opt_secret(
9 base_opt_name: &str,
10 path: Option<PathBuf>,
11 val: Option<SecretString>,
12) -> Result<SecretString> {
13 match (path, val) {
14 (Some(_), Some(_)) => unreachable!("options should conflict"),
15 (Some(path), None) => fs::read_to_string(&path)
16 .with_context(|| format!("failed to read file `{path}`", path = path.display()))
17 .map(Into::into),
18 (None, Some(val)) => Ok(val),
19 (None, None) => {
20 bail!("either option `{base_opt_name}-file` or `{base_opt_name}` needs to be specified")
21 }
22 }
23}
24
25#[cfg(test)]
26mod tests {
27 use secrecy::ExposeSecret;
28
29 use super::*;
30
31 const BASE_OPT_NAME: &str = "db-password";
32
33 #[test]
34 fn test_missing_file_arg() {
35 let path_opt = Some(PathBuf::from("tests/welcome456.txt"));
36 let val_opt = None;
37
38 assert_eq!(
39 get_opt_secret(BASE_OPT_NAME, path_opt, val_opt)
40 .unwrap_err()
41 .to_string(),
42 "failed to read file `tests/welcome456.txt`"
43 );
44 }
45
46 #[test]
47 fn test_cli_arg_priority() {
48 let path_opt: Option<PathBuf> = None;
49 let val_opt = Some(String::from("welcome456").into());
50
51 let content = get_opt_secret(BASE_OPT_NAME, path_opt, val_opt).unwrap();
52 assert_eq!(content.expose_secret(), "welcome456");
53 }
54
55 #[test]
56 fn test_no_arg_set() {
57 let path_opt: Option<PathBuf> = None;
58 let val_opt: Option<SecretString> = None;
59
60 assert_eq!(
61 get_opt_secret(BASE_OPT_NAME, path_opt, val_opt)
62 .unwrap_err()
63 .to_string(),
64 "either option `db-password-file` or `db-password` needs to be specified"
65 );
66 }
67}