1use crate::profile::types::ProfilesConfig;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum StorageError {
9 #[error("IO error: {0}")]
10 Io(#[from] io::Error),
11 #[error("JSON serialization error: {0}")]
12 Json(#[from] serde_json::Error),
13 #[error("Config directory not found")]
14 ConfigDirNotFound,
15}
16
17pub type Result<T> = std::result::Result<T, StorageError>;
18
19fn legacy_config_path() -> Result<PathBuf> {
21 directories::ProjectDirs::from("", "", "slack-cli")
22 .map(|dirs| dirs.config_dir().join("profiles.json"))
23 .ok_or(StorageError::ConfigDirNotFound)
24}
25
26pub fn default_config_path() -> Result<PathBuf> {
28 let dirs = directories::ProjectDirs::from("", "", "slack-rs")
29 .ok_or(StorageError::ConfigDirNotFound)?;
30 let config_dir = dirs.config_dir();
31
32 fs::create_dir_all(config_dir)?;
34
35 Ok(config_dir.join("profiles.json"))
36}
37
38fn migrate_legacy_config_internal() -> Result<bool> {
41 let new_path = default_config_path()?;
43
44 if new_path.exists() {
46 return Ok(false);
47 }
48
49 let legacy_path = match legacy_config_path() {
51 Ok(path) => path,
52 Err(_) => return Ok(false),
53 };
54
55 if !legacy_path.exists() {
57 return Ok(false);
58 }
59
60 if let Some(parent) = new_path.parent() {
62 fs::create_dir_all(parent)?;
63 }
64
65 match fs::rename(&legacy_path, &new_path) {
67 Ok(_) => Ok(true),
68 Err(_) => {
69 let content = fs::read_to_string(&legacy_path)?;
71 fs::write(&new_path, content)?;
72 Ok(true)
73 }
74 }
75}
76
77#[cfg(test)]
80fn migrate_legacy_config(legacy_path: &Path, new_path: &Path) -> Result<bool> {
81 if new_path.exists() {
83 return Ok(false);
84 }
85
86 if !legacy_path.exists() {
88 return Ok(false);
89 }
90
91 if let Some(parent) = new_path.parent() {
93 fs::create_dir_all(parent)?;
94 }
95
96 match fs::rename(legacy_path, new_path) {
98 Ok(_) => Ok(true),
99 Err(_) => {
100 let content = fs::read_to_string(legacy_path)?;
102 fs::write(new_path, content)?;
103 Ok(true)
104 }
105 }
106}
107
108pub fn load_config(path: &Path) -> Result<ProfilesConfig> {
110 if let Ok(default_path) = default_config_path() {
113 if path == default_path {
114 let _ = migrate_legacy_config_internal();
115 }
116 }
117
118 if !path.exists() {
119 return Ok(ProfilesConfig::new());
120 }
121
122 let content = fs::read_to_string(path)?;
123 let config: ProfilesConfig = serde_json::from_str(&content)?;
124 Ok(config)
125}
126
127pub fn save_config(path: &Path, config: &ProfilesConfig) -> Result<()> {
129 if let Some(parent) = path.parent() {
131 fs::create_dir_all(parent)?;
132 }
133
134 let content = serde_json::to_string_pretty(config)?;
135 fs::write(path, content)?;
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::profile::types::Profile;
143 use tempfile::TempDir;
144
145 #[test]
146 fn test_save_and_load_config() {
147 let temp_dir = TempDir::new().unwrap();
148 let config_path = temp_dir.path().join("profiles.json");
149
150 let mut config = ProfilesConfig::new();
151 config.set(
152 "default".to_string(),
153 Profile {
154 team_id: "T123".to_string(),
155 user_id: "U456".to_string(),
156 team_name: Some("Test Team".to_string()),
157 user_name: Some("Test User".to_string()),
158 client_id: None,
159 redirect_uri: None,
160 scopes: None,
161 bot_scopes: None,
162 user_scopes: None,
163 default_token_type: None,
164 },
165 );
166
167 save_config(&config_path, &config).unwrap();
169 assert!(config_path.exists());
170
171 let loaded = load_config(&config_path).unwrap();
173 assert_eq!(config, loaded);
174 }
175
176 #[test]
177 fn test_load_nonexistent_config() {
178 let temp_dir = TempDir::new().unwrap();
179 let config_path = temp_dir.path().join("nonexistent.json");
180
181 let loaded = load_config(&config_path).unwrap();
182 assert_eq!(loaded, ProfilesConfig::new());
183 }
184
185 #[test]
186 fn test_save_creates_parent_directory() {
187 let temp_dir = TempDir::new().unwrap();
188 let config_path = temp_dir.path().join("nested/dir/profiles.json");
189
190 let config = ProfilesConfig::new();
191 save_config(&config_path, &config).unwrap();
192
193 assert!(config_path.exists());
194 assert!(config_path.parent().unwrap().exists());
195 }
196
197 #[test]
198 fn test_load_save_round_trip() {
199 let temp_dir = TempDir::new().unwrap();
200 let config_path = temp_dir.path().join("profiles.json");
201
202 let mut config = ProfilesConfig::new();
203 config.set(
204 "profile1".to_string(),
205 Profile {
206 team_id: "T1".to_string(),
207 user_id: "U1".to_string(),
208 team_name: None,
209 user_name: None,
210 client_id: None,
211 redirect_uri: None,
212 scopes: None,
213 bot_scopes: None,
214 user_scopes: None,
215 default_token_type: None,
216 },
217 );
218 config.set(
219 "profile2".to_string(),
220 Profile {
221 team_id: "T2".to_string(),
222 user_id: "U2".to_string(),
223 team_name: Some("Team 2".to_string()),
224 user_name: Some("User 2".to_string()),
225 client_id: None,
226 redirect_uri: None,
227 scopes: None,
228 bot_scopes: None,
229 user_scopes: None,
230 default_token_type: None,
231 },
232 );
233
234 save_config(&config_path, &config).unwrap();
235 let loaded = load_config(&config_path).unwrap();
236 assert_eq!(config, loaded);
237 }
238
239 #[test]
240 fn test_default_config_path() {
241 let result = default_config_path();
243 match result {
244 Ok(path) => {
245 assert!(path.to_string_lossy().contains("slack-rs"));
246 assert!(path.to_string_lossy().contains("profiles.json"));
247 }
248 Err(StorageError::ConfigDirNotFound) => {
249 }
251 Err(e) => panic!("Unexpected error: {}", e),
252 }
253 }
254
255 #[test]
256 fn test_migrate_legacy_config_path() {
257 let temp_dir = TempDir::new().unwrap();
258 let legacy_path = temp_dir.path().join("legacy").join("profiles.json");
259 let new_path = temp_dir.path().join("new").join("profiles.json");
260
261 let mut config = ProfilesConfig::new();
263 config.set(
264 "legacy".to_string(),
265 Profile {
266 team_id: "T123".to_string(),
267 user_id: "U456".to_string(),
268 team_name: Some("Legacy Team".to_string()),
269 user_name: Some("Legacy User".to_string()),
270 client_id: None,
271 redirect_uri: None,
272 scopes: None,
273 bot_scopes: None,
274 user_scopes: None,
275 default_token_type: None,
276 },
277 );
278 fs::create_dir_all(legacy_path.parent().unwrap()).unwrap();
279 save_config(&legacy_path, &config).unwrap();
280 assert!(legacy_path.exists());
281
282 let migrated = migrate_legacy_config(&legacy_path, &new_path).unwrap();
284 assert!(migrated);
285 assert!(new_path.exists());
286
287 let loaded = load_config(&new_path).unwrap();
289 assert_eq!(config, loaded);
290
291 let legacy_path2 = temp_dir.path().join("legacy2").join("profiles.json");
293 fs::create_dir_all(legacy_path2.parent().unwrap()).unwrap();
294 save_config(&legacy_path2, &config).unwrap();
295
296 let migrated_again = migrate_legacy_config(&legacy_path2, &new_path).unwrap();
297 assert!(!migrated_again);
298 }
299
300 #[test]
301 fn test_load_config_with_migration() {
302 let temp_dir = TempDir::new().unwrap();
303 let legacy_path = temp_dir.path().join("legacy").join("profiles.json");
304 let new_path = temp_dir.path().join("new").join("profiles.json");
305
306 let mut config = ProfilesConfig::new();
308 config.set(
309 "test".to_string(),
310 Profile {
311 team_id: "T999".to_string(),
312 user_id: "U888".to_string(),
313 team_name: None,
314 user_name: None,
315 client_id: None,
316 redirect_uri: None,
317 scopes: None,
318 bot_scopes: None,
319 user_scopes: None,
320 default_token_type: None,
321 },
322 );
323 fs::create_dir_all(legacy_path.parent().unwrap()).unwrap();
324 save_config(&legacy_path, &config).unwrap();
325
326 migrate_legacy_config(&legacy_path, &new_path).unwrap();
328
329 let loaded = load_config(&new_path).unwrap();
331 assert_eq!(config, loaded);
332 }
333}