envx_core/
snapshot.rs

1use crate::EnvVar;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Snapshot {
8    pub id: String,
9    pub name: String,
10    pub description: Option<String>,
11    pub created_at: DateTime<Utc>,
12    pub variables: HashMap<String, EnvVar>,
13    pub metadata: HashMap<String, String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Profile {
18    pub name: String,
19    pub description: Option<String>,
20    pub created_at: DateTime<Utc>,
21    pub updated_at: DateTime<Utc>,
22    pub variables: HashMap<String, ProfileVar>,
23    pub parent: Option<String>,
24    pub metadata: HashMap<String, String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ProfileVar {
29    pub value: String,
30    pub enabled: bool,
31    pub override_system: bool,
32}
33
34impl Snapshot {
35    #[must_use]
36    pub fn new(name: String, description: Option<String>) -> Self {
37        Self {
38            id: uuid::Uuid::new_v4().to_string(),
39            name,
40            description,
41            created_at: Utc::now(),
42            variables: HashMap::new(),
43            metadata: HashMap::new(),
44        }
45    }
46
47    #[must_use]
48    pub fn from_vars(name: String, description: Option<String>, vars: Vec<EnvVar>) -> Self {
49        let mut snapshot = Self::new(name, description);
50        for var in vars {
51            snapshot.variables.insert(var.name.clone(), var);
52        }
53        snapshot
54    }
55}
56
57impl Profile {
58    #[must_use]
59    pub fn new(name: String, description: Option<String>) -> Self {
60        Self {
61            name,
62            description,
63            created_at: Utc::now(),
64            updated_at: Utc::now(),
65            variables: HashMap::new(),
66            parent: None,
67            metadata: HashMap::new(),
68        }
69    }
70
71    pub fn add_var(&mut self, name: String, value: String, override_system: bool) {
72        self.variables.insert(
73            name,
74            ProfileVar {
75                value,
76                enabled: true,
77                override_system,
78            },
79        );
80        self.updated_at = Utc::now();
81    }
82
83    pub fn remove_var(&mut self, name: &str) -> Option<ProfileVar> {
84        self.updated_at = Utc::now();
85        self.variables.remove(name)
86    }
87
88    #[must_use]
89    pub fn get_active_vars(&self) -> HashMap<String, String> {
90        self.variables
91            .iter()
92            .filter(|(_, var)| var.enabled)
93            .map(|(name, var)| (name.clone(), var.value.clone()))
94            .collect()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::{EnvVar, EnvVarSource};
102
103    fn create_test_env_var(name: &str, value: &str) -> EnvVar {
104        EnvVar {
105            name: name.to_string(),
106            value: value.to_string(),
107            source: EnvVarSource::User,
108            modified: Utc::now(),
109            original_value: None,
110        }
111    }
112
113    #[test]
114    fn test_snapshot_new() {
115        let name = "test-snapshot".to_string();
116        let description = Some("Test description".to_string());
117        let snapshot = Snapshot::new(name.clone(), description.clone());
118
119        assert_eq!(snapshot.name, name);
120        assert_eq!(snapshot.description, description);
121        assert!(!snapshot.id.is_empty());
122        assert!(snapshot.variables.is_empty());
123        assert!(snapshot.metadata.is_empty());
124        assert!(snapshot.created_at <= Utc::now());
125    }
126
127    #[test]
128    fn test_snapshot_new_without_description() {
129        let name = "test-snapshot".to_string();
130        let snapshot = Snapshot::new(name.clone(), None);
131
132        assert_eq!(snapshot.name, name);
133        assert!(snapshot.description.is_none());
134    }
135
136    #[test]
137    fn test_snapshot_unique_ids() {
138        let snapshot1 = Snapshot::new("snap1".to_string(), None);
139        let snapshot2 = Snapshot::new("snap2".to_string(), None);
140
141        assert_ne!(snapshot1.id, snapshot2.id);
142    }
143
144    #[test]
145    fn test_snapshot_from_vars_empty() {
146        let vars = vec![];
147        let snapshot = Snapshot::from_vars("empty-snapshot".to_string(), None, vars);
148
149        assert_eq!(snapshot.name, "empty-snapshot");
150        assert!(snapshot.variables.is_empty());
151    }
152
153    #[test]
154    fn test_snapshot_from_vars_single() {
155        let var = create_test_env_var("TEST_VAR", "test_value");
156        let vars = vec![var.clone()];
157        let snapshot = Snapshot::from_vars("single-var-snapshot".to_string(), None, vars);
158
159        assert_eq!(snapshot.variables.len(), 1);
160        assert!(snapshot.variables.contains_key("TEST_VAR"));
161
162        let stored_var = snapshot.variables.get("TEST_VAR").unwrap();
163        assert_eq!(stored_var.name, var.name);
164        assert_eq!(stored_var.value, var.value);
165    }
166
167    #[test]
168    fn test_snapshot_from_vars_multiple() {
169        let vars = vec![
170            create_test_env_var("VAR1", "value1"),
171            create_test_env_var("VAR2", "value2"),
172            create_test_env_var("VAR3", "value3"),
173        ];
174        let snapshot = Snapshot::from_vars(
175            "multi-var-snapshot".to_string(),
176            Some("Multiple variables".to_string()),
177            vars,
178        );
179
180        assert_eq!(snapshot.variables.len(), 3);
181        assert!(snapshot.variables.contains_key("VAR1"));
182        assert!(snapshot.variables.contains_key("VAR2"));
183        assert!(snapshot.variables.contains_key("VAR3"));
184        assert_eq!(snapshot.description, Some("Multiple variables".to_string()));
185    }
186
187    #[test]
188    fn test_snapshot_from_vars_duplicate_names() {
189        // Test that later values override earlier ones for same variable name
190        let vars = vec![
191            create_test_env_var("DUPLICATE", "first_value"),
192            create_test_env_var("DUPLICATE", "second_value"),
193        ];
194        let snapshot = Snapshot::from_vars("duplicate-test".to_string(), None, vars);
195
196        assert_eq!(snapshot.variables.len(), 1);
197        assert_eq!(snapshot.variables.get("DUPLICATE").unwrap().value, "second_value");
198    }
199
200    #[test]
201    fn test_snapshot_serialization() {
202        let mut snapshot = Snapshot::new("serialize-test".to_string(), Some("Test serialization".to_string()));
203        snapshot.metadata.insert("key1".to_string(), "value1".to_string());
204        snapshot.metadata.insert("key2".to_string(), "value2".to_string());
205
206        let var = create_test_env_var("TEST_VAR", "test_value");
207        snapshot.variables.insert(var.name.clone(), var);
208
209        // Serialize to JSON
210        let json = serde_json::to_string(&snapshot).expect("Failed to serialize snapshot to JSON");
211
212        // Deserialize back
213        let deserialized: Snapshot = serde_json::from_str(&json).expect("Failed to deserialize snapshot from JSON");
214
215        assert_eq!(deserialized.id, snapshot.id);
216        assert_eq!(deserialized.name, snapshot.name);
217        assert_eq!(deserialized.description, snapshot.description);
218        assert_eq!(deserialized.variables.len(), snapshot.variables.len());
219        assert_eq!(deserialized.metadata.len(), snapshot.metadata.len());
220        assert_eq!(deserialized.metadata.get("key1"), Some(&"value1".to_string()));
221    }
222
223    #[test]
224    fn test_snapshot_with_special_characters() {
225        let name = "snapshot-with-special-chars!@#$%^&*()".to_string();
226        let description = Some("Description with\nnewlines\tand\ttabs".to_string());
227        let snapshot = Snapshot::new(name.clone(), description.clone());
228
229        assert_eq!(snapshot.name, name);
230        assert_eq!(snapshot.description, description);
231    }
232
233    #[test]
234    fn test_snapshot_with_empty_name() {
235        // This test documents current behavior - empty names are allowed
236        let snapshot = Snapshot::new(String::new(), None);
237        assert_eq!(snapshot.name, "");
238    }
239
240    #[test]
241    fn test_snapshot_from_vars_preserves_all_env_var_fields() {
242        let mut var = create_test_env_var("FULL_VAR", "full_value");
243        var.source = EnvVarSource::System;
244        var.original_value = Some("original".to_string());
245
246        let vars = vec![var];
247        let snapshot = Snapshot::from_vars("preserve-test".to_string(), None, vars);
248
249        let stored_var = snapshot.variables.get("FULL_VAR").unwrap();
250        assert_eq!(stored_var.source, EnvVarSource::System);
251        assert_eq!(stored_var.original_value, Some("original".to_string()));
252    }
253
254    #[test]
255    fn test_snapshot_metadata_operations() {
256        let mut snapshot = Snapshot::new("metadata-test".to_string(), None);
257
258        // Test empty metadata
259        assert!(snapshot.metadata.is_empty());
260
261        // Add metadata
262        snapshot.metadata.insert("author".to_string(), "test_user".to_string());
263        snapshot.metadata.insert("version".to_string(), "1.0.0".to_string());
264
265        assert_eq!(snapshot.metadata.len(), 2);
266        assert_eq!(snapshot.metadata.get("author"), Some(&"test_user".to_string()));
267        assert_eq!(snapshot.metadata.get("version"), Some(&"1.0.0".to_string()));
268    }
269
270    #[test]
271    fn test_snapshot_clone() {
272        let mut original = Snapshot::new("original".to_string(), Some("Original snapshot".to_string()));
273        original
274            .variables
275            .insert("VAR1".to_string(), create_test_env_var("VAR1", "value1"));
276        original.metadata.insert("key".to_string(), "value".to_string());
277
278        let cloned = original.clone();
279
280        assert_eq!(cloned.id, original.id);
281        assert_eq!(cloned.name, original.name);
282        assert_eq!(cloned.description, original.description);
283        assert_eq!(cloned.created_at, original.created_at);
284        assert_eq!(cloned.variables.len(), original.variables.len());
285        assert_eq!(cloned.metadata.len(), original.metadata.len());
286    }
287
288    #[test]
289    fn test_snapshot_debug_format() {
290        let snapshot = Snapshot::new("debug-test".to_string(), None);
291        let debug_str = format!("{snapshot:?}");
292
293        assert!(debug_str.contains("Snapshot"));
294        assert!(debug_str.contains("debug-test"));
295        assert!(debug_str.contains(&snapshot.id));
296    }
297}