Skip to main content

opentalk_client_data_persistence/
config_manager.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use std::{fs, path::PathBuf};
6
7use snafu::{OptionExt, ResultExt as _};
8
9use crate::{
10    ConfigError,
11    config::Config,
12    config_error::{
13        FolderNotCreatableSnafu, NotLoadableSnafu, NotReadableSnafu, NotStorableSnafu,
14        NotWriteableSnafu, SystemConfigHomeNotSetSnafu,
15    },
16};
17
18/// The [ConfigManager] stores and loads configs from providered pathes
19#[derive(Debug)]
20pub struct ConfigManager {
21    path: PathBuf,
22}
23
24impl ConfigManager {
25    /// Create a new [ConfigManager] instance.
26    pub fn new() -> Result<Self, ConfigError> {
27        let config_path = dirs::config_dir()
28            .context(SystemConfigHomeNotSetSnafu)?
29            .join("opentalk/cli.toml");
30
31        Ok(Self { path: config_path })
32    }
33    /// Load config if exist
34    pub fn load(&self) -> Result<Config, ConfigError> {
35        let file = fs::read_to_string(&self.path).context(NotLoadableSnafu {
36            path: self.path.clone(),
37        })?;
38        let config = toml::from_str(file.as_str()).context(NotReadableSnafu {
39            path: self.path.clone(),
40        })?;
41
42        Ok(config)
43    }
44
45    /// Store config
46    pub fn store(&self, config: &Config) -> Result<(), ConfigError> {
47        if let Some(config_dir) = self.path.parent() {
48            fs::create_dir_all(config_dir).context(FolderNotCreatableSnafu { path: config_dir })?;
49        }
50
51        let config_str = toml::to_string_pretty(config).context(NotWriteableSnafu {
52            path: self.path.clone(),
53        })?;
54
55        fs::write(&self.path, config_str).context(NotStorableSnafu {
56            path: self.path.clone(),
57        })?;
58
59        Ok(())
60    }
61}
62
63#[cfg(test)]
64mod tests {
65
66    use std::{collections::BTreeMap, io::Write as _, path::Path};
67
68    use pretty_assertions::{assert_eq, assert_matches};
69    use tempfile::tempdir;
70
71    use super::Config;
72    use crate::{
73        OpenTalkAccountConfig,
74        config_manager::{ConfigError, ConfigManager},
75        opentalk_instance_config::OpenTalkInstanceConfig,
76    };
77
78    const EXAMPLE_CONFIG: &str = r#"default_instance = "https://ot.example.com/"
79
80[instances."https://ot.example.com/"]
81default_account = "one"
82
83[instances."https://ot.example.com/".accounts.one]
84oidc_client_id = "device"
85
86[instances."https://ot.example.com/".accounts.two]
87oidc_client_id = "device"
88
89[instances."https://ot01.example.com/"]
90default_account = "three"
91
92[instances."https://ot01.example.com/".accounts.three]
93oidc_client_id = "device"
94"#;
95
96    fn example() -> Config {
97        Config {
98            default_instance: Some("https://ot.example.com".parse().unwrap()),
99            instances: BTreeMap::from_iter([
100                (
101                    "https://ot.example.com".parse().unwrap(),
102                    OpenTalkInstanceConfig {
103                        default_account: "one".parse().unwrap(),
104                        accounts: BTreeMap::from_iter([
105                            (
106                                "one".parse().unwrap(),
107                                OpenTalkAccountConfig {
108                                    oidc_client_id: "device".to_string(),
109                                },
110                            ),
111                            (
112                                "two".parse().unwrap(),
113                                OpenTalkAccountConfig {
114                                    oidc_client_id: "device".to_string(),
115                                },
116                            ),
117                        ]),
118                    },
119                ),
120                (
121                    "https://ot01.example.com".parse().unwrap(),
122                    OpenTalkInstanceConfig {
123                        default_account: "three".parse().unwrap(),
124                        accounts: BTreeMap::from_iter([(
125                            "three".parse().unwrap(),
126                            OpenTalkAccountConfig {
127                                oidc_client_id: "device".to_string(),
128                            },
129                        )]),
130                    },
131                ),
132            ]),
133        }
134    }
135
136    #[test]
137    fn success_load_with_empty_config() {
138        // Create a directory inside of `env::temp_dir()`.
139        let temp_dir = tempdir().unwrap();
140
141        let config_path = temp_dir.path().join("opentalk-cli.toml");
142        {
143            let _ = std::fs::File::create(&config_path).unwrap();
144        }
145        let config_manager = ConfigManager {
146            path: config_path.clone(),
147        };
148        let config = config_manager.load().unwrap();
149
150        assert_eq!(Config::default(), config);
151    }
152
153    #[test]
154    fn error_load_with_missing_file() {
155        // Create a directory inside of `env::temp_dir()`.
156        let temp_dir = tempdir().unwrap();
157
158        let config_path = temp_dir.path().join("opentalk-cli.toml");
159        let config_manager = ConfigManager {
160            path: config_path.clone(),
161        };
162        let config = config_manager.load();
163
164        assert_matches!(config, Err(ConfigError::NotLoadable { path, source: _ }) if path == config_path);
165    }
166
167    #[test]
168    fn success_load_example_config() {
169        // Create a directory inside of `env::temp_dir()`.
170        let temp_dir = tempdir().unwrap();
171
172        let config_path = temp_dir.path().join("opentalk-cli.toml");
173        {
174            let mut file = std::fs::File::create(&config_path).unwrap();
175            write!(file, "{EXAMPLE_CONFIG}").unwrap()
176        }
177        let config_manager = ConfigManager {
178            path: config_path.clone(),
179        };
180        let config = config_manager.load().unwrap();
181
182        assert_eq!(example(), config);
183    }
184
185    #[test]
186    fn success_store_config() {
187        let config = example();
188
189        let temp_dir = tempdir().unwrap();
190        let config_path = temp_dir.path().join("config/opentalk-cli.toml");
191
192        let config_manager = ConfigManager {
193            path: config_path.clone(),
194        };
195
196        config_manager.store(&config).unwrap();
197
198        let stored_config = std::fs::read_to_string(&config_path).unwrap();
199
200        assert_eq!(EXAMPLE_CONFIG, stored_config);
201    }
202
203    #[test]
204    fn success_new() {
205        #[allow(unsafe_code)]
206        unsafe {
207            // We only run this inside a single test, we just need to make sure
208            // that we don't set `XDG_CONFIG_HOME` anywhere else.
209            std::env::set_var("XDG_CONFIG_HOME", "/home/example/.config");
210        }
211        let config_manager = ConfigManager::new().unwrap();
212        assert_eq!(
213            Path::new("/home/example/.config/opentalk/cli.toml"),
214            config_manager.path,
215        );
216    }
217}