Skip to main content

slack_rs/profile/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use thiserror::Error;
4
5use super::token_type::TokenType;
6
7#[derive(Debug, Error)]
8pub enum ProfileError {
9    #[error("Profile name already exists: {0}")]
10    DuplicateName(String),
11}
12
13/// Profile configuration data structure
14/// Contains non-secret information only
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct Profile {
17    pub team_id: String,
18    pub user_id: String,
19    pub team_name: Option<String>,
20    pub user_name: Option<String>,
21    /// OAuth client ID for this profile (optional for backward compatibility)
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub client_id: Option<String>,
24    /// OAuth redirect URI for this profile (optional for backward compatibility)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub redirect_uri: Option<String>,
27    /// OAuth scopes for this profile (legacy field, migrated to bot_scopes for backward compatibility)
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub scopes: Option<Vec<String>>,
30    /// Bot OAuth scopes (new field)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub bot_scopes: Option<Vec<String>>,
33    /// User OAuth scopes (new field)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub user_scopes: Option<Vec<String>>,
36    /// Default token type for this profile (optional for backward compatibility)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub default_token_type: Option<TokenType>,
39}
40
41impl Profile {
42    /// Get bot scopes, falling back to legacy scopes field if bot_scopes is None
43    pub fn get_bot_scopes(&self) -> Option<Vec<String>> {
44        self.bot_scopes.clone().or_else(|| self.scopes.clone())
45    }
46
47    /// Get user scopes
48    pub fn get_user_scopes(&self) -> Option<Vec<String>> {
49        self.user_scopes.clone()
50    }
51
52    /// Create a new profile with bot and user scopes
53    #[allow(clippy::too_many_arguments)]
54    pub fn with_scopes(
55        team_id: String,
56        user_id: String,
57        team_name: Option<String>,
58        user_name: Option<String>,
59        client_id: Option<String>,
60        redirect_uri: Option<String>,
61        bot_scopes: Option<Vec<String>>,
62        user_scopes: Option<Vec<String>>,
63    ) -> Self {
64        Self {
65            team_id,
66            user_id,
67            team_name,
68            user_name,
69            client_id,
70            redirect_uri,
71            scopes: None, // Deprecated field, kept for backward compatibility
72            bot_scopes,
73            user_scopes,
74            default_token_type: None,
75        }
76    }
77}
78
79/// Root configuration structure with versioning for future migration
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct ProfilesConfig {
82    pub version: u32,
83    pub profiles: HashMap<String, Profile>,
84}
85
86impl ProfilesConfig {
87    pub fn new() -> Self {
88        Self {
89            version: 1,
90            profiles: HashMap::new(),
91        }
92    }
93
94    /// Get profile by name
95    pub fn get(&self, name: &str) -> Option<&Profile> {
96        self.profiles.get(name)
97    }
98
99    /// Add or update a profile
100    /// This method allows overwriting existing profiles
101    pub fn set(&mut self, name: String, profile: Profile) {
102        self.profiles.insert(name, profile);
103    }
104
105    /// Add a new profile, returning an error if the name already exists
106    pub fn add(&mut self, name: String, profile: Profile) -> Result<(), ProfileError> {
107        if self.profiles.contains_key(&name) {
108            return Err(ProfileError::DuplicateName(name));
109        }
110        self.profiles.insert(name, profile);
111        Ok(())
112    }
113
114    /// Update or create a profile for a given (team_id, user_id) pair
115    /// If a profile with the same (team_id, user_id) exists, it will be updated
116    /// If the profile name already exists but points to a different (team_id, user_id), returns an error
117    /// PLACEHOLDER values are treated specially: they are always replaced by real values
118    pub fn set_or_update(&mut self, name: String, profile: Profile) -> Result<(), ProfileError> {
119        // Check if profile name already exists
120        if let Some(existing) = self.profiles.get(&name) {
121            // Special case: if existing has PLACEHOLDER values, allow replacement with real values
122            let existing_is_placeholder =
123                existing.team_id == "PLACEHOLDER" || existing.user_id == "PLACEHOLDER";
124            let profile_is_placeholder =
125                profile.team_id == "PLACEHOLDER" || profile.user_id == "PLACEHOLDER";
126
127            // If existing is placeholder, always allow update with real or placeholder values
128            if existing_is_placeholder {
129                self.profiles.insert(name, profile);
130                return Ok(());
131            }
132
133            // If new profile is placeholder but existing is real, keep existing (don't downgrade)
134            if profile_is_placeholder {
135                return Ok(());
136            }
137
138            // Both are real values - check if they match
139            if existing.team_id != profile.team_id || existing.user_id != profile.user_id {
140                return Err(ProfileError::DuplicateName(name));
141            }
142            // Same identity - just update
143            self.profiles.insert(name, profile);
144            return Ok(());
145        }
146
147        // Check if another profile with the same (team_id, user_id) exists
148        // Skip this check for PLACEHOLDER values
149        if profile.team_id != "PLACEHOLDER" && profile.user_id != "PLACEHOLDER" {
150            if let Some((existing_name, _)) = self.profiles.iter().find(|(_, p)| {
151                p.team_id != "PLACEHOLDER"
152                    && p.user_id != "PLACEHOLDER"
153                    && p.team_id == profile.team_id
154                    && p.user_id == profile.user_id
155            }) {
156                // Update the existing profile
157                let existing_name = existing_name.clone();
158                self.profiles.insert(existing_name, profile);
159                return Ok(());
160            }
161        }
162
163        // No conflicts - add new profile
164        self.profiles.insert(name, profile);
165        Ok(())
166    }
167
168    /// Remove a profile
169    pub fn remove(&mut self, name: &str) -> Option<Profile> {
170        self.profiles.remove(name)
171    }
172
173    /// List all profile names
174    pub fn list_names(&self) -> Vec<String> {
175        self.profiles.keys().cloned().collect()
176    }
177}
178
179impl Default for ProfilesConfig {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_profiles_config_new() {
191        let config = ProfilesConfig::new();
192        assert_eq!(config.version, 1);
193        assert!(config.profiles.is_empty());
194    }
195
196    #[test]
197    fn test_profiles_config_get_set() {
198        let mut config = ProfilesConfig::new();
199        let profile = Profile {
200            team_id: "T123".to_string(),
201            user_id: "U456".to_string(),
202            team_name: Some("Test Team".to_string()),
203            user_name: Some("Test User".to_string()),
204            client_id: None,
205            redirect_uri: None,
206            scopes: None,
207            bot_scopes: None,
208            user_scopes: None,
209            default_token_type: None,
210        };
211
212        config.set("default".to_string(), profile.clone());
213        assert_eq!(config.get("default"), Some(&profile));
214        assert_eq!(config.get("nonexistent"), None);
215    }
216
217    #[test]
218    fn test_profiles_config_remove() {
219        let mut config = ProfilesConfig::new();
220        let profile = Profile {
221            team_id: "T123".to_string(),
222            user_id: "U456".to_string(),
223            team_name: None,
224            user_name: None,
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        config.set("test".to_string(), profile.clone());
234        let removed = config.remove("test");
235        assert_eq!(removed, Some(profile));
236        assert_eq!(config.get("test"), None);
237    }
238
239    #[test]
240    fn test_profiles_config_list_names() {
241        let mut config = ProfilesConfig::new();
242        config.set(
243            "profile1".to_string(),
244            Profile {
245                team_id: "T1".to_string(),
246                user_id: "U1".to_string(),
247                team_name: None,
248                user_name: None,
249                client_id: None,
250                redirect_uri: None,
251                scopes: None,
252                bot_scopes: None,
253                user_scopes: None,
254                default_token_type: None,
255            },
256        );
257        config.set(
258            "profile2".to_string(),
259            Profile {
260                team_id: "T2".to_string(),
261                user_id: "U2".to_string(),
262                team_name: None,
263                user_name: None,
264                client_id: None,
265                redirect_uri: None,
266                scopes: None,
267                bot_scopes: None,
268                user_scopes: None,
269                default_token_type: None,
270            },
271        );
272
273        let mut names = config.list_names();
274        names.sort();
275        assert_eq!(names, vec!["profile1", "profile2"]);
276    }
277
278    #[test]
279    fn test_profile_serialization() {
280        let profile = Profile {
281            team_id: "T123".to_string(),
282            user_id: "U456".to_string(),
283            team_name: Some("Test Team".to_string()),
284            user_name: Some("Test User".to_string()),
285            client_id: None,
286            redirect_uri: None,
287            scopes: None,
288            bot_scopes: None,
289            user_scopes: None,
290            default_token_type: None,
291        };
292
293        let json = serde_json::to_string(&profile).unwrap();
294        let deserialized: Profile = serde_json::from_str(&json).unwrap();
295        assert_eq!(profile, deserialized);
296    }
297
298    #[test]
299    fn test_profiles_config_serialization() {
300        let mut config = ProfilesConfig::new();
301        config.set(
302            "default".to_string(),
303            Profile {
304                team_id: "T123".to_string(),
305                user_id: "U456".to_string(),
306                team_name: Some("Test Team".to_string()),
307                user_name: Some("Test User".to_string()),
308                client_id: None,
309                redirect_uri: None,
310                scopes: None,
311                bot_scopes: None,
312                user_scopes: None,
313                default_token_type: None,
314            },
315        );
316
317        let json = serde_json::to_string_pretty(&config).unwrap();
318        let deserialized: ProfilesConfig = serde_json::from_str(&json).unwrap();
319        assert_eq!(config, deserialized);
320    }
321
322    #[test]
323    fn test_profiles_config_add_duplicate_name() {
324        let mut config = ProfilesConfig::new();
325        let profile1 = Profile {
326            team_id: "T123".to_string(),
327            user_id: "U456".to_string(),
328            team_name: None,
329            user_name: None,
330            client_id: None,
331            redirect_uri: None,
332            scopes: None,
333            bot_scopes: None,
334            user_scopes: None,
335            default_token_type: None,
336        };
337        let profile2 = Profile {
338            team_id: "T789".to_string(),
339            user_id: "U012".to_string(),
340            team_name: None,
341            user_name: None,
342            client_id: None,
343            redirect_uri: None,
344            scopes: None,
345            bot_scopes: None,
346            user_scopes: None,
347            default_token_type: None,
348        };
349
350        // First add should succeed
351        assert!(config.add("default".to_string(), profile1).is_ok());
352
353        // Second add with same name should fail
354        let result = config.add("default".to_string(), profile2);
355        assert!(result.is_err());
356        match result {
357            Err(ProfileError::DuplicateName(name)) => {
358                assert_eq!(name, "default");
359            }
360            _ => panic!("Expected DuplicateName error"),
361        }
362    }
363
364    #[test]
365    fn test_profiles_config_set_or_update_new() {
366        let mut config = ProfilesConfig::new();
367        let profile = Profile {
368            team_id: "T123".to_string(),
369            user_id: "U456".to_string(),
370            team_name: Some("Test Team".to_string()),
371            user_name: Some("Test User".to_string()),
372            client_id: None,
373            redirect_uri: None,
374            scopes: None,
375            bot_scopes: None,
376            user_scopes: None,
377            default_token_type: None,
378        };
379
380        // Adding new profile should succeed
381        assert!(config
382            .set_or_update("default".to_string(), profile.clone())
383            .is_ok());
384        assert_eq!(config.get("default"), Some(&profile));
385    }
386
387    #[test]
388    fn test_profiles_config_set_or_update_same_identity() {
389        let mut config = ProfilesConfig::new();
390        let profile1 = Profile {
391            team_id: "T123".to_string(),
392            user_id: "U456".to_string(),
393            team_name: Some("Test Team".to_string()),
394            user_name: Some("Test User".to_string()),
395            client_id: None,
396            redirect_uri: None,
397            scopes: None,
398            bot_scopes: None,
399            user_scopes: None,
400            default_token_type: None,
401        };
402        let profile2 = Profile {
403            team_id: "T123".to_string(),
404            user_id: "U456".to_string(),
405            team_name: Some("Updated Team".to_string()),
406            user_name: Some("Updated User".to_string()),
407            client_id: None,
408            redirect_uri: None,
409            scopes: None,
410            bot_scopes: None,
411            user_scopes: None,
412            default_token_type: None,
413        };
414
415        config
416            .set_or_update("default".to_string(), profile1)
417            .unwrap();
418
419        // Updating with same identity should succeed
420        assert!(config
421            .set_or_update("default".to_string(), profile2.clone())
422            .is_ok());
423        assert_eq!(config.get("default"), Some(&profile2));
424    }
425
426    #[test]
427    fn test_profiles_config_set_or_update_different_identity() {
428        let mut config = ProfilesConfig::new();
429        let profile1 = Profile {
430            team_id: "T123".to_string(),
431            user_id: "U456".to_string(),
432            team_name: None,
433            user_name: None,
434            client_id: None,
435            redirect_uri: None,
436            scopes: None,
437            bot_scopes: None,
438            user_scopes: None,
439            default_token_type: None,
440        };
441        let profile2 = Profile {
442            team_id: "T789".to_string(),
443            user_id: "U012".to_string(),
444            team_name: None,
445            user_name: None,
446            client_id: None,
447            redirect_uri: None,
448            scopes: None,
449            bot_scopes: None,
450            user_scopes: None,
451            default_token_type: None,
452        };
453
454        config
455            .set_or_update("default".to_string(), profile1)
456            .unwrap();
457
458        // Trying to use same name with different identity should fail
459        let result = config.set_or_update("default".to_string(), profile2);
460        assert!(result.is_err());
461        match result {
462            Err(ProfileError::DuplicateName(_)) => {}
463            _ => panic!("Expected DuplicateName error"),
464        }
465    }
466
467    #[test]
468    fn test_profiles_config_set_or_update_same_identity_different_name() {
469        let mut config = ProfilesConfig::new();
470        let profile1 = Profile {
471            team_id: "T123".to_string(),
472            user_id: "U456".to_string(),
473            team_name: Some("Test Team".to_string()),
474            user_name: Some("Test User".to_string()),
475            client_id: None,
476            redirect_uri: None,
477            scopes: None,
478            bot_scopes: None,
479            user_scopes: None,
480            default_token_type: None,
481        };
482        let profile2 = Profile {
483            team_id: "T123".to_string(),
484            user_id: "U456".to_string(),
485            team_name: Some("Updated Team".to_string()),
486            user_name: Some("Updated User".to_string()),
487            client_id: None,
488            redirect_uri: None,
489            scopes: None,
490            bot_scopes: None,
491            user_scopes: None,
492            default_token_type: None,
493        };
494
495        config.set_or_update("old".to_string(), profile1).unwrap();
496
497        // Adding same identity with different name should update the old entry
498        assert!(config
499            .set_or_update("new".to_string(), profile2.clone())
500            .is_ok());
501
502        // Old name should still have the updated profile
503        assert_eq!(config.get("old"), Some(&profile2));
504        // New name should not exist
505        assert_eq!(config.get("new"), None);
506    }
507
508    #[test]
509    fn test_backward_compatibility_profile_without_client_id() {
510        // Test that old profiles.json without client_id can be deserialized
511        let json = r#"{
512            "version": 1,
513            "profiles": {
514                "default": {
515                    "team_id": "T123",
516                    "user_id": "U456",
517                    "team_name": "Test Team",
518                    "user_name": "Test User"
519                }
520            }
521        }"#;
522
523        let config: ProfilesConfig = serde_json::from_str(json).unwrap();
524        assert_eq!(config.version, 1);
525        assert_eq!(config.profiles.len(), 1);
526
527        let profile = config.get("default").unwrap();
528        assert_eq!(profile.team_id, "T123");
529        assert_eq!(profile.user_id, "U456");
530        assert_eq!(profile.client_id, None);
531    }
532
533    #[test]
534    fn test_profile_with_client_id_serialization() {
535        // Test that new profiles with client_id serialize correctly
536        let profile = Profile {
537            team_id: "T123".to_string(),
538            user_id: "U456".to_string(),
539            team_name: Some("Test Team".to_string()),
540            user_name: Some("Test User".to_string()),
541            client_id: Some("client-123".to_string()),
542            redirect_uri: None,
543            scopes: None,
544            bot_scopes: None,
545            user_scopes: None,
546            default_token_type: None,
547        };
548
549        let json = serde_json::to_string(&profile).unwrap();
550        let deserialized: Profile = serde_json::from_str(&json).unwrap();
551
552        assert_eq!(profile, deserialized);
553        assert_eq!(deserialized.client_id, Some("client-123".to_string()));
554    }
555
556    #[test]
557    fn test_profile_without_client_id_omits_field() {
558        // Test that profiles without client_id don't include the field in JSON
559        let profile = Profile {
560            team_id: "T123".to_string(),
561            user_id: "U456".to_string(),
562            team_name: Some("Test Team".to_string()),
563            user_name: Some("Test User".to_string()),
564            client_id: None,
565            redirect_uri: None,
566            scopes: None,
567            bot_scopes: None,
568            user_scopes: None,
569            default_token_type: None,
570        };
571
572        let json = serde_json::to_string(&profile).unwrap();
573        // The JSON should not contain "client_id" field due to skip_serializing_if
574        assert!(!json.contains("client_id"));
575    }
576
577    #[test]
578    fn test_profile_with_oauth_config_serialization() {
579        // Test that profiles with full OAuth config serialize correctly
580        let profile = Profile {
581            team_id: "T123".to_string(),
582            user_id: "U456".to_string(),
583            team_name: Some("Test Team".to_string()),
584            user_name: Some("Test User".to_string()),
585            client_id: Some("client-123".to_string()),
586            redirect_uri: Some("http://127.0.0.1:8765/callback".to_string()),
587            scopes: Some(vec!["chat:write".to_string(), "users:read".to_string()]),
588            bot_scopes: None,
589            user_scopes: None,
590            default_token_type: None,
591        };
592
593        let json = serde_json::to_string(&profile).unwrap();
594        let deserialized: Profile = serde_json::from_str(&json).unwrap();
595
596        assert_eq!(profile, deserialized);
597        assert_eq!(deserialized.client_id, Some("client-123".to_string()));
598        assert_eq!(
599            deserialized.redirect_uri,
600            Some("http://127.0.0.1:8765/callback".to_string())
601        );
602        assert_eq!(
603            deserialized.scopes,
604            Some(vec!["chat:write".to_string(), "users:read".to_string()])
605        );
606    }
607
608    #[test]
609    fn test_profile_without_oauth_config_omits_fields() {
610        // Test that profiles without OAuth config don't include the fields in JSON
611        let profile = Profile {
612            team_id: "T123".to_string(),
613            user_id: "U456".to_string(),
614            team_name: Some("Test Team".to_string()),
615            user_name: Some("Test User".to_string()),
616            client_id: None,
617            redirect_uri: None,
618            scopes: None,
619            bot_scopes: None,
620            user_scopes: None,
621            default_token_type: None,
622        };
623
624        let json = serde_json::to_string(&profile).unwrap();
625        // The JSON should not contain OAuth config fields due to skip_serializing_if
626        assert!(!json.contains("client_id"));
627        assert!(!json.contains("redirect_uri"));
628        assert!(!json.contains("scopes"));
629    }
630
631    #[test]
632    fn test_set_or_update_placeholder_to_real() {
633        // Test that a profile with PLACEHOLDER values can be updated with real values
634        let mut config = ProfilesConfig::new();
635
636        // First, set a profile with PLACEHOLDER (e.g., from oauth config set)
637        let placeholder_profile = Profile {
638            team_id: "PLACEHOLDER".to_string(),
639            user_id: "PLACEHOLDER".to_string(),
640            team_name: None,
641            user_name: None,
642            client_id: Some("client-123".to_string()),
643            redirect_uri: Some("http://localhost:8765/callback".to_string()),
644            scopes: Some(vec!["chat:write".to_string()]),
645            bot_scopes: None,
646            user_scopes: None,
647            default_token_type: None,
648        };
649
650        config
651            .set_or_update("work".to_string(), placeholder_profile)
652            .unwrap();
653
654        // Then, update with real values (e.g., from login)
655        let real_profile = Profile {
656            team_id: "T123".to_string(),
657            user_id: "U456".to_string(),
658            team_name: Some("Real Team".to_string()),
659            user_name: Some("Real User".to_string()),
660            client_id: Some("client-123".to_string()),
661            redirect_uri: Some("http://localhost:8765/callback".to_string()),
662            scopes: Some(vec!["chat:write".to_string()]),
663            bot_scopes: None,
664            user_scopes: None,
665            default_token_type: None,
666        };
667
668        // This should succeed and update the profile
669        assert!(config
670            .set_or_update("work".to_string(), real_profile.clone())
671            .is_ok());
672
673        // Verify the profile was updated with real values
674        let updated = config.get("work").unwrap();
675        assert_eq!(updated.team_id, "T123");
676        assert_eq!(updated.user_id, "U456");
677    }
678
679    #[test]
680    fn test_set_or_update_real_to_placeholder_keeps_real() {
681        // Test that trying to update a real profile with PLACEHOLDER doesn't downgrade it
682        let mut config = ProfilesConfig::new();
683
684        // First, set a real profile
685        let real_profile = Profile {
686            team_id: "T123".to_string(),
687            user_id: "U456".to_string(),
688            team_name: Some("Real Team".to_string()),
689            user_name: Some("Real User".to_string()),
690            client_id: Some("client-123".to_string()),
691            redirect_uri: Some("http://localhost:8765/callback".to_string()),
692            scopes: Some(vec!["chat:write".to_string()]),
693            bot_scopes: None,
694            user_scopes: None,
695            default_token_type: None,
696        };
697
698        config
699            .set_or_update("work".to_string(), real_profile.clone())
700            .unwrap();
701
702        // Then, try to update with PLACEHOLDER values
703        let placeholder_profile = Profile {
704            team_id: "PLACEHOLDER".to_string(),
705            user_id: "PLACEHOLDER".to_string(),
706            team_name: None,
707            user_name: None,
708            client_id: Some("client-456".to_string()),
709            redirect_uri: None,
710            scopes: None,
711            bot_scopes: None,
712            user_scopes: None,
713            default_token_type: None,
714        };
715
716        // This should succeed but keep the real values
717        assert!(config
718            .set_or_update("work".to_string(), placeholder_profile)
719            .is_ok());
720
721        // Verify the profile still has real values
722        let updated = config.get("work").unwrap();
723        assert_eq!(updated.team_id, "T123");
724        assert_eq!(updated.user_id, "U456");
725    }
726
727    #[test]
728    fn test_backward_compatibility_scopes_to_bot_scopes() {
729        // Test that old profiles with scopes field can be read as bot_scopes
730        let json = r#"{
731            "version": 1,
732            "profiles": {
733                "default": {
734                    "team_id": "T123",
735                    "user_id": "U456",
736                    "team_name": "Test Team",
737                    "user_name": "Test User",
738                    "scopes": ["chat:write", "users:read"]
739                }
740            }
741        }"#;
742
743        let config: ProfilesConfig = serde_json::from_str(json).unwrap();
744        let profile = config.get("default").unwrap();
745
746        // Old scopes field should be migrated to bot_scopes via get_bot_scopes()
747        assert_eq!(
748            profile.get_bot_scopes(),
749            Some(vec!["chat:write".to_string(), "users:read".to_string()])
750        );
751        assert_eq!(profile.get_user_scopes(), None);
752    }
753
754    #[test]
755    fn test_new_profile_with_bot_and_user_scopes() {
756        // Test new profiles with separate bot_scopes and user_scopes
757        let profile = Profile {
758            team_id: "T123".to_string(),
759            user_id: "U456".to_string(),
760            team_name: Some("Test Team".to_string()),
761            user_name: Some("Test User".to_string()),
762            client_id: Some("client-123".to_string()),
763            redirect_uri: Some("http://localhost:8765/callback".to_string()),
764            scopes: None,
765            bot_scopes: Some(vec!["chat:write".to_string()]),
766            user_scopes: Some(vec!["users:read".to_string()]),
767            default_token_type: None,
768        };
769
770        assert_eq!(
771            profile.get_bot_scopes(),
772            Some(vec!["chat:write".to_string()])
773        );
774        assert_eq!(
775            profile.get_user_scopes(),
776            Some(vec!["users:read".to_string()])
777        );
778    }
779
780    #[test]
781    fn test_backward_compatibility_profile_without_default_token_type() {
782        // Test that old profiles.json without default_token_type can be deserialized
783        let json = r#"{
784            "version": 1,
785            "profiles": {
786                "default": {
787                    "team_id": "T123",
788                    "user_id": "U456",
789                    "team_name": "Test Team",
790                    "user_name": "Test User"
791                }
792            }
793        }"#;
794
795        let config: ProfilesConfig = serde_json::from_str(json).unwrap();
796        assert_eq!(config.version, 1);
797        assert_eq!(config.profiles.len(), 1);
798
799        let profile = config.get("default").unwrap();
800        assert_eq!(profile.team_id, "T123");
801        assert_eq!(profile.user_id, "U456");
802        assert_eq!(profile.default_token_type, None);
803    }
804
805    #[test]
806    fn test_profile_with_default_token_type_serialization() {
807        // Test that new profiles with default_token_type serialize correctly
808        let profile = Profile {
809            team_id: "T123".to_string(),
810            user_id: "U456".to_string(),
811            team_name: Some("Test Team".to_string()),
812            user_name: Some("Test User".to_string()),
813            client_id: None,
814            redirect_uri: None,
815            scopes: None,
816            bot_scopes: None,
817            user_scopes: None,
818            default_token_type: Some(super::super::token_type::TokenType::Bot),
819        };
820
821        let json = serde_json::to_string(&profile).unwrap();
822        let deserialized: Profile = serde_json::from_str(&json).unwrap();
823
824        assert_eq!(profile.team_id, deserialized.team_id);
825        assert_eq!(
826            deserialized.default_token_type,
827            Some(super::super::token_type::TokenType::Bot)
828        );
829    }
830
831    #[test]
832    fn test_profile_without_default_token_type_omits_field() {
833        // Test that profiles without default_token_type don't include the field in JSON
834        let profile = Profile {
835            team_id: "T123".to_string(),
836            user_id: "U456".to_string(),
837            team_name: Some("Test Team".to_string()),
838            user_name: Some("Test User".to_string()),
839            client_id: None,
840            redirect_uri: None,
841            scopes: None,
842            bot_scopes: None,
843            user_scopes: None,
844            default_token_type: None,
845        };
846
847        let json = serde_json::to_string(&profile).unwrap();
848        // The JSON should not contain "default_token_type" field due to skip_serializing_if
849        assert!(!json.contains("default_token_type"));
850    }
851
852    #[test]
853    fn test_set_or_update_placeholder_no_conflict_with_other_profiles() {
854        // Test that PLACEHOLDER profiles don't conflict with other real profiles
855        let mut config = ProfilesConfig::new();
856
857        // Add a real profile
858        let real_profile = Profile {
859            team_id: "T123".to_string(),
860            user_id: "U456".to_string(),
861            team_name: Some("Real Team".to_string()),
862            user_name: None,
863            client_id: None,
864            redirect_uri: None,
865            scopes: None,
866            bot_scopes: None,
867            user_scopes: None,
868            default_token_type: None,
869        };
870        config
871            .set_or_update("existing".to_string(), real_profile)
872            .unwrap();
873
874        // Add a PLACEHOLDER profile with different name
875        let placeholder_profile = Profile {
876            team_id: "PLACEHOLDER".to_string(),
877            user_id: "PLACEHOLDER".to_string(),
878            team_name: None,
879            user_name: None,
880            client_id: Some("client-789".to_string()),
881            redirect_uri: None,
882            scopes: None,
883            bot_scopes: None,
884            user_scopes: None,
885            default_token_type: None,
886        };
887
888        // This should succeed without conflicts
889        assert!(config
890            .set_or_update("new".to_string(), placeholder_profile)
891            .is_ok());
892
893        // Both profiles should exist
894        assert!(config.get("existing").is_some());
895        assert!(config.get("new").is_some());
896    }
897}
898
899#[cfg(test)]
900mod backward_compat_tests {
901    use super::*;
902
903    #[test]
904    fn test_get_bot_scopes_from_legacy_scopes() {
905        // Test that scopes field is treated as bot_scopes for backward compatibility
906        let profile = Profile {
907            team_id: "T123".to_string(),
908            user_id: "U456".to_string(),
909            team_name: None,
910            user_name: None,
911            client_id: None,
912            redirect_uri: None,
913            scopes: Some(vec!["chat:write".to_string(), "users:read".to_string()]),
914            bot_scopes: None,
915            user_scopes: None,
916            default_token_type: None,
917        };
918
919        let bot_scopes = profile.get_bot_scopes();
920        assert_eq!(
921            bot_scopes,
922            Some(vec!["chat:write".to_string(), "users:read".to_string()])
923        );
924    }
925
926    #[test]
927    fn test_get_bot_scopes_prefers_bot_scopes_over_scopes() {
928        // Test that bot_scopes takes precedence over scopes
929        let profile = Profile {
930            team_id: "T123".to_string(),
931            user_id: "U456".to_string(),
932            team_name: None,
933            user_name: None,
934            client_id: None,
935            redirect_uri: None,
936            scopes: Some(vec!["old:scope".to_string()]),
937            bot_scopes: Some(vec!["new:scope".to_string()]),
938            user_scopes: None,
939            default_token_type: None,
940        };
941
942        let bot_scopes = profile.get_bot_scopes();
943        assert_eq!(bot_scopes, Some(vec!["new:scope".to_string()]));
944    }
945
946    #[test]
947    fn test_get_user_scopes_returns_none_for_legacy() {
948        // Test that user_scopes returns None for legacy profiles
949        let profile = Profile {
950            team_id: "T123".to_string(),
951            user_id: "U456".to_string(),
952            team_name: None,
953            user_name: None,
954            client_id: None,
955            redirect_uri: None,
956            scopes: Some(vec!["chat:write".to_string()]),
957            bot_scopes: None,
958            user_scopes: None,
959            default_token_type: None,
960        };
961
962        let user_scopes = profile.get_user_scopes();
963        assert_eq!(user_scopes, None);
964    }
965
966    #[test]
967    fn test_deserialize_old_profile_format() {
968        // Test that old profile.json without bot_scopes/user_scopes can be deserialized
969        let json = r#"{
970            "team_id": "T123",
971            "user_id": "U456",
972            "team_name": "Test Team",
973            "user_name": "Test User",
974            "scopes": ["chat:write", "users:read"]
975        }"#;
976
977        let profile: Profile = serde_json::from_str(json).unwrap();
978        assert_eq!(profile.team_id, "T123");
979        assert_eq!(profile.user_id, "U456");
980        assert_eq!(
981            profile.scopes,
982            Some(vec!["chat:write".to_string(), "users:read".to_string()])
983        );
984        assert_eq!(profile.bot_scopes, None);
985        assert_eq!(profile.user_scopes, None);
986
987        // Verify backward compatibility: scopes should be accessible as bot_scopes
988        let bot_scopes = profile.get_bot_scopes();
989        assert_eq!(
990            bot_scopes,
991            Some(vec!["chat:write".to_string(), "users:read".to_string()])
992        );
993    }
994}