Skip to main content

slack_rs/profile/
storage.rs

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
19/// Get the legacy config file path (slack-cli) for migration purposes
20fn 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
26/// Get the default config file path using the OS config directory
27pub 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    // Create directory if it doesn't exist
33    fs::create_dir_all(config_dir)?;
34
35    Ok(config_dir.join("profiles.json"))
36}
37
38/// Migrate legacy config file to new path if needed
39/// This function is only called when using the default config path
40fn migrate_legacy_config_internal() -> Result<bool> {
41    // Get new default path
42    let new_path = default_config_path()?;
43
44    // If new path already exists, no migration needed
45    if new_path.exists() {
46        return Ok(false);
47    }
48
49    // Try to get legacy path
50    let legacy_path = match legacy_config_path() {
51        Ok(path) => path,
52        Err(_) => return Ok(false),
53    };
54
55    // If legacy path doesn't exist, no migration needed
56    if !legacy_path.exists() {
57        return Ok(false);
58    }
59
60    // Create parent directory for new path if it doesn't exist
61    if let Some(parent) = new_path.parent() {
62        fs::create_dir_all(parent)?;
63    }
64
65    // Try to rename (move) the file first
66    match fs::rename(&legacy_path, &new_path) {
67        Ok(_) => Ok(true),
68        Err(_) => {
69            // If rename fails (e.g., different filesystems), copy and keep the old file
70            let content = fs::read_to_string(&legacy_path)?;
71            fs::write(&new_path, content)?;
72            Ok(true)
73        }
74    }
75}
76
77/// Migrate legacy config file for a specific path (used for testing)
78/// Returns true if migration was performed
79#[cfg(test)]
80fn migrate_legacy_config(legacy_path: &Path, new_path: &Path) -> Result<bool> {
81    // If new path already exists, no migration needed
82    if new_path.exists() {
83        return Ok(false);
84    }
85
86    // If legacy path doesn't exist, no migration needed
87    if !legacy_path.exists() {
88        return Ok(false);
89    }
90
91    // Create parent directory for new path if it doesn't exist
92    if let Some(parent) = new_path.parent() {
93        fs::create_dir_all(parent)?;
94    }
95
96    // Try to rename (move) the file first
97    match fs::rename(legacy_path, new_path) {
98        Ok(_) => Ok(true),
99        Err(_) => {
100            // If rename fails (e.g., different filesystems), copy and keep the old file
101            let content = fs::read_to_string(legacy_path)?;
102            fs::write(new_path, content)?;
103            Ok(true)
104        }
105    }
106}
107
108/// Load profiles config from a file
109pub fn load_config(path: &Path) -> Result<ProfilesConfig> {
110    // Try to migrate legacy config if this is the default path
111    // Only attempt migration when using default_config_path
112    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
127/// Save profiles config to a file
128pub fn save_config(path: &Path, config: &ProfilesConfig) -> Result<()> {
129    // Create parent directory if it doesn't exist
130    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
168        save_config(&config_path, &config).unwrap();
169        assert!(config_path.exists());
170
171        // Load config
172        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        // Just verify it doesn't panic and returns something
242        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                // This might happen in some test environments
250            }
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        // Create legacy config
262        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        // Perform migration
283        let migrated = migrate_legacy_config(&legacy_path, &new_path).unwrap();
284        assert!(migrated);
285        assert!(new_path.exists());
286
287        // Verify migrated content
288        let loaded = load_config(&new_path).unwrap();
289        assert_eq!(config, loaded);
290
291        // Test that migration is skipped if new path exists
292        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        // Create legacy config
307        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        // Manually trigger migration by calling migrate_legacy_config
327        migrate_legacy_config(&legacy_path, &new_path).unwrap();
328
329        // Load from new path should work
330        let loaded = load_config(&new_path).unwrap();
331        assert_eq!(config, loaded);
332    }
333}