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 if let Ok(config_path) = std::env::var("SLACK_RS_CONFIG_PATH") {
30 return Ok(PathBuf::from(config_path));
31 }
32
33 let dirs = directories::ProjectDirs::from("", "", "slack-rs")
34 .ok_or(StorageError::ConfigDirNotFound)?;
35 let config_dir = dirs.config_dir();
36
37 fs::create_dir_all(config_dir)?;
39
40 Ok(config_dir.join("profiles.json"))
41}
42
43fn migrate_legacy_config_internal() -> Result<bool> {
46 let new_path = default_config_path()?;
48
49 if new_path.exists() {
51 return Ok(false);
52 }
53
54 let legacy_path = match legacy_config_path() {
56 Ok(path) => path,
57 Err(_) => return Ok(false),
58 };
59
60 if !legacy_path.exists() {
62 return Ok(false);
63 }
64
65 if let Some(parent) = new_path.parent() {
67 fs::create_dir_all(parent)?;
68 }
69
70 match fs::rename(&legacy_path, &new_path) {
72 Ok(_) => Ok(true),
73 Err(_) => {
74 let content = fs::read_to_string(&legacy_path)?;
76 fs::write(&new_path, content)?;
77 Ok(true)
78 }
79 }
80}
81
82#[cfg(test)]
85fn migrate_legacy_config(legacy_path: &Path, new_path: &Path) -> Result<bool> {
86 if new_path.exists() {
88 return Ok(false);
89 }
90
91 if !legacy_path.exists() {
93 return Ok(false);
94 }
95
96 if let Some(parent) = new_path.parent() {
98 fs::create_dir_all(parent)?;
99 }
100
101 match fs::rename(legacy_path, new_path) {
103 Ok(_) => Ok(true),
104 Err(_) => {
105 let content = fs::read_to_string(legacy_path)?;
107 fs::write(new_path, content)?;
108 Ok(true)
109 }
110 }
111}
112
113pub fn load_config(path: &Path) -> Result<ProfilesConfig> {
115 if let Ok(default_path) = default_config_path() {
118 if path == default_path {
119 let _ = migrate_legacy_config_internal();
120 }
121 }
122
123 if !path.exists() {
124 return Ok(ProfilesConfig::new());
125 }
126
127 let content = fs::read_to_string(path)?;
128 let config: ProfilesConfig = serde_json::from_str(&content)?;
129 Ok(config)
130}
131
132pub fn save_config(path: &Path, config: &ProfilesConfig) -> Result<()> {
134 if let Some(parent) = path.parent() {
136 fs::create_dir_all(parent)?;
137 }
138
139 let content = serde_json::to_string_pretty(config)?;
140 fs::write(path, content)?;
141 Ok(())
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::profile::types::Profile;
148 use tempfile::TempDir;
149
150 #[test]
151 fn test_save_and_load_config() {
152 let temp_dir = TempDir::new().unwrap();
153 let config_path = temp_dir.path().join("profiles.json");
154
155 let mut config = ProfilesConfig::new();
156 config.set(
157 "default".to_string(),
158 Profile {
159 team_id: "T123".to_string(),
160 user_id: "U456".to_string(),
161 team_name: Some("Test Team".to_string()),
162 user_name: Some("Test User".to_string()),
163 client_id: None,
164 redirect_uri: None,
165 scopes: None,
166 bot_scopes: None,
167 user_scopes: None,
168 default_token_type: None,
169 },
170 );
171
172 save_config(&config_path, &config).unwrap();
174 assert!(config_path.exists());
175
176 let loaded = load_config(&config_path).unwrap();
178 assert_eq!(config, loaded);
179 }
180
181 #[test]
182 fn test_load_nonexistent_config() {
183 let temp_dir = TempDir::new().unwrap();
184 let config_path = temp_dir.path().join("nonexistent.json");
185
186 let loaded = load_config(&config_path).unwrap();
187 assert_eq!(loaded, ProfilesConfig::new());
188 }
189
190 #[test]
191 fn test_save_creates_parent_directory() {
192 let temp_dir = TempDir::new().unwrap();
193 let config_path = temp_dir.path().join("nested/dir/profiles.json");
194
195 let config = ProfilesConfig::new();
196 save_config(&config_path, &config).unwrap();
197
198 assert!(config_path.exists());
199 assert!(config_path.parent().unwrap().exists());
200 }
201
202 #[test]
203 fn test_load_save_round_trip() {
204 let temp_dir = TempDir::new().unwrap();
205 let config_path = temp_dir.path().join("profiles.json");
206
207 let mut config = ProfilesConfig::new();
208 config.set(
209 "profile1".to_string(),
210 Profile {
211 team_id: "T1".to_string(),
212 user_id: "U1".to_string(),
213 team_name: None,
214 user_name: None,
215 client_id: None,
216 redirect_uri: None,
217 scopes: None,
218 bot_scopes: None,
219 user_scopes: None,
220 default_token_type: None,
221 },
222 );
223 config.set(
224 "profile2".to_string(),
225 Profile {
226 team_id: "T2".to_string(),
227 user_id: "U2".to_string(),
228 team_name: Some("Team 2".to_string()),
229 user_name: Some("User 2".to_string()),
230 client_id: None,
231 redirect_uri: None,
232 scopes: None,
233 bot_scopes: None,
234 user_scopes: None,
235 default_token_type: None,
236 },
237 );
238
239 save_config(&config_path, &config).unwrap();
240 let loaded = load_config(&config_path).unwrap();
241 assert_eq!(config, loaded);
242 }
243
244 #[test]
245 fn test_default_config_path() {
246 let result = default_config_path();
248 match result {
249 Ok(path) => {
250 assert!(path.to_string_lossy().contains("slack-rs"));
251 assert!(path.to_string_lossy().contains("profiles.json"));
252 }
253 Err(StorageError::ConfigDirNotFound) => {
254 }
256 Err(e) => panic!("Unexpected error: {}", e),
257 }
258 }
259
260 #[test]
261 fn test_migrate_legacy_config_path() {
262 let temp_dir = TempDir::new().unwrap();
263 let legacy_path = temp_dir.path().join("legacy").join("profiles.json");
264 let new_path = temp_dir.path().join("new").join("profiles.json");
265
266 let mut config = ProfilesConfig::new();
268 config.set(
269 "legacy".to_string(),
270 Profile {
271 team_id: "T123".to_string(),
272 user_id: "U456".to_string(),
273 team_name: Some("Legacy Team".to_string()),
274 user_name: Some("Legacy User".to_string()),
275 client_id: None,
276 redirect_uri: None,
277 scopes: None,
278 bot_scopes: None,
279 user_scopes: None,
280 default_token_type: None,
281 },
282 );
283 fs::create_dir_all(legacy_path.parent().unwrap()).unwrap();
284 save_config(&legacy_path, &config).unwrap();
285 assert!(legacy_path.exists());
286
287 let migrated = migrate_legacy_config(&legacy_path, &new_path).unwrap();
289 assert!(migrated);
290 assert!(new_path.exists());
291
292 let loaded = load_config(&new_path).unwrap();
294 assert_eq!(config, loaded);
295
296 let legacy_path2 = temp_dir.path().join("legacy2").join("profiles.json");
298 fs::create_dir_all(legacy_path2.parent().unwrap()).unwrap();
299 save_config(&legacy_path2, &config).unwrap();
300
301 let migrated_again = migrate_legacy_config(&legacy_path2, &new_path).unwrap();
302 assert!(!migrated_again);
303 }
304
305 #[test]
306 fn test_load_config_with_migration() {
307 let temp_dir = TempDir::new().unwrap();
308 let legacy_path = temp_dir.path().join("legacy").join("profiles.json");
309 let new_path = temp_dir.path().join("new").join("profiles.json");
310
311 let mut config = ProfilesConfig::new();
313 config.set(
314 "test".to_string(),
315 Profile {
316 team_id: "T999".to_string(),
317 user_id: "U888".to_string(),
318 team_name: None,
319 user_name: None,
320 client_id: None,
321 redirect_uri: None,
322 scopes: None,
323 bot_scopes: None,
324 user_scopes: None,
325 default_token_type: None,
326 },
327 );
328 fs::create_dir_all(legacy_path.parent().unwrap()).unwrap();
329 save_config(&legacy_path, &config).unwrap();
330
331 migrate_legacy_config(&legacy_path, &new_path).unwrap();
333
334 let loaded = load_config(&new_path).unwrap();
336 assert_eq!(config, loaded);
337 }
338}