Skip to main content

terraphim_update/
config.rs

1//! Configuration types for automatic update functionality
2//!
3//! This module defines all configuration and state types needed for
4//! automatic updates, including UpdateConfig, UpdateInfo, UpdateHistory,
5//! and related types.
6
7use jiff::Timestamp;
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11/// Configuration for automatic updates
12///
13/// Controls how and when automatic update checks are performed.
14///
15/// # Example
16/// ```no_run
17/// use terraphim_update::config::UpdateConfig;
18///
19/// let config = UpdateConfig::default();
20/// println!("Auto-update enabled: {}", config.auto_update_enabled);
21/// println!("Check interval: {:?}", config.auto_update_check_interval);
22/// ```
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub struct UpdateConfig {
25    /// Enable automatic update checking
26    ///
27    /// When true, the application will automatically check for updates
28    /// according to the configured interval. When false, updates must
29    /// be triggered manually.
30    pub auto_update_enabled: bool,
31
32    /// Interval between update checks
33    ///
34    /// The application will check for updates at most once per this interval.
35    /// Default is 24 hours (daily).
36    pub auto_update_check_interval: Duration,
37}
38
39impl Default for UpdateConfig {
40    fn default() -> Self {
41        Self {
42            auto_update_enabled: true,
43            auto_update_check_interval: Duration::from_secs(86400), // 24 hours
44        }
45    }
46}
47
48/// Information about an available update
49///
50/// Contains all metadata needed to display and install an update.
51///
52/// # Example
53/// ```no_run
54/// use terraphim_update::config::UpdateInfo;
55/// use jiff::Timestamp;
56///
57/// let info = UpdateInfo {
58///     version: "1.1.0".to_string(),
59///     release_date: Timestamp::now(),
60///     notes: "Bug fixes and improvements".to_string(),
61///     download_url: "https://example.com/binary".to_string(),
62///     signature_url: "https://example.com/binary.sig".to_string(),
63///     arch: "x86_64".to_string(),
64/// };
65/// ```
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct UpdateInfo {
68    /// Version number of the update
69    ///
70    /// Format: "X.Y.Z" (e.g., "1.2.3")
71    pub version: String,
72
73    /// Release date of the update
74    pub release_date: Timestamp,
75
76    /// Release notes or changelog
77    ///
78    /// May contain markdown-formatted text describing changes in this release.
79    pub notes: String,
80
81    /// Download URL for the binary
82    ///
83    /// URL pointing to the binary file on GitHub Releases.
84    pub download_url: String,
85
86    /// PGP signature URL for verification
87    ///
88    /// URL pointing to the detached PGP signature for the binary.
89    /// Used to verify authenticity and integrity of the download.
90    pub signature_url: String,
91
92    /// Binary architecture
93    ///
94    /// Target architecture (e.g., "x86_64", "aarch64").
95    pub arch: String,
96}
97
98/// Persistent update history state
99///
100/// Tracks the state of updates over time to avoid redundant checks
101/// and maintain backup version information.
102///
103/// # Example
104/// ```no_run
105/// use terraphim_update::config::UpdateHistory;
106///
107/// let history = UpdateHistory::default();
108/// println!("Last check: {:?}", history.last_check);
109/// println!("Current version: {}", history.current_version);
110/// ```
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
112pub struct UpdateHistory {
113    /// Last time an update check was performed
114    pub last_check: Timestamp,
115
116    /// Version currently installed
117    pub current_version: String,
118
119    /// Pending update notification (if user hasn't acted on it)
120    ///
121    /// When an update is available but not yet installed, this field
122    /// stores the update info. After installation or user dismissal,
123    /// this field should be cleared.
124    pub pending_update: Option<UpdateInfo>,
125
126    /// Backup versions available for rollback
127    ///
128    /// List of version strings that have been backed up. The newest
129    /// backup is at the end of the list.
130    pub backup_versions: Vec<String>,
131
132    /// Update check history (last 10 checks)
133    ///
134    /// Maintains a log of recent update check attempts for debugging
135    /// and analytics purposes.
136    pub check_history: Vec<UpdateCheckEntry>,
137}
138
139impl Default for UpdateHistory {
140    fn default() -> Self {
141        Self {
142            last_check: Timestamp::now(),
143            current_version: String::new(),
144            pending_update: None,
145            backup_versions: Vec::new(),
146            check_history: Vec::new(),
147        }
148    }
149}
150
151impl UpdateHistory {
152    /// Add a check entry to history
153    ///
154    /// Maintains at most 10 entries, removing oldest when limit is exceeded.
155    ///
156    /// # Arguments
157    /// * `entry` - Check entry to add
158    pub fn add_check_entry(&mut self, entry: UpdateCheckEntry) {
159        self.check_history.push(entry);
160        // Keep only last 10 entries
161        if self.check_history.len() > 10 {
162            self.check_history.remove(0);
163        }
164    }
165
166    /// Add a backup version to the list
167    ///
168    /// Maintains at most 3 backup versions (configurable).
169    ///
170    /// # Arguments
171    /// * `version` - Version string to add as backup
172    /// * `max_backups` - Maximum number of backups to keep (default: 3)
173    pub fn add_backup_version(&mut self, version: String, max_backups: usize) {
174        self.backup_versions.push(version);
175        // Keep only the last N backups
176        if self.backup_versions.len() > max_backups {
177            self.backup_versions.remove(0);
178        }
179    }
180
181    /// Get the most recent backup version
182    ///
183    /// Returns the latest backup version or None if no backups exist.
184    pub fn latest_backup(&self) -> Option<&String> {
185        self.backup_versions.last()
186    }
187}
188
189/// Single update check entry
190///
191/// Records the result of a single update check attempt.
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
193pub struct UpdateCheckEntry {
194    /// When the check was performed
195    pub timestamp: Timestamp,
196
197    /// Result of the check
198    pub result: UpdateCheckResult,
199}
200
201/// Result of an update check
202///
203/// Describes whether an update is available, the system is up to date,
204/// or the check failed.
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206pub enum UpdateCheckResult {
207    /// No update available
208    ///
209    /// The current version is the latest.
210    UpToDate,
211
212    /// Update available
213    ///
214    /// A newer version is available. The `notified` flag indicates
215    /// whether the user has been notified about this update.
216    UpdateAvailable { version: String, notified: bool },
217
218    /// Update check failed
219    ///
220    /// The check could not be completed due to network or other errors.
221    CheckFailed { error: String },
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_update_config_default() {
230        let config = UpdateConfig::default();
231        assert!(config.auto_update_enabled, "Default should be enabled");
232        assert_eq!(
233            config.auto_update_check_interval,
234            Duration::from_secs(86400),
235            "Default should be 24 hours"
236        );
237    }
238
239    #[test]
240    fn test_update_config_serialize() {
241        let config = UpdateConfig {
242            auto_update_enabled: false,
243            auto_update_check_interval: Duration::from_secs(3600), // 1 hour
244        };
245
246        let serialized = serde_json::to_string(&config).unwrap();
247        let deserialized: UpdateConfig = serde_json::from_str(&serialized).unwrap();
248
249        assert_eq!(config, deserialized);
250    }
251
252    #[test]
253    fn test_update_info_serialization() {
254        let info = UpdateInfo {
255            version: "1.2.3".to_string(),
256            release_date: Timestamp::now(),
257            notes: "Test release".to_string(),
258            download_url: "https://example.com/binary".to_string(),
259            signature_url: "https://example.com/binary.sig".to_string(),
260            arch: "x86_64".to_string(),
261        };
262
263        let serialized = serde_json::to_string(&info).unwrap();
264        let deserialized: UpdateInfo = serde_json::from_str(&serialized).unwrap();
265
266        assert_eq!(info.version, deserialized.version);
267        assert_eq!(info.download_url, deserialized.download_url);
268        assert_eq!(info.signature_url, deserialized.signature_url);
269        assert_eq!(info.arch, deserialized.arch);
270    }
271
272    #[test]
273    fn test_update_history_default() {
274        let history = UpdateHistory::default();
275        assert_eq!(history.current_version, String::new());
276        assert!(history.pending_update.is_none());
277        assert!(history.backup_versions.is_empty());
278        assert!(history.check_history.is_empty());
279    }
280
281    #[test]
282    fn test_update_history_add_entry() {
283        let mut history = UpdateHistory::default();
284        let entry = UpdateCheckEntry {
285            timestamp: Timestamp::now(),
286            result: UpdateCheckResult::UpToDate,
287        };
288
289        history.add_check_entry(entry);
290        assert_eq!(history.check_history.len(), 1);
291    }
292
293    #[test]
294    fn test_update_history_limit_entries() {
295        let mut history = UpdateHistory::default();
296
297        // Add 15 entries (should be limited to 10)
298        for _i in 0..15 {
299            history.add_check_entry(UpdateCheckEntry {
300                timestamp: Timestamp::now(),
301                result: UpdateCheckResult::UpToDate,
302            });
303        }
304
305        assert_eq!(
306            history.check_history.len(),
307            10,
308            "Should keep only last 10 entries"
309        );
310    }
311
312    #[test]
313    fn test_update_history_add_backup() {
314        let mut history = UpdateHistory::default();
315        let max_backups = 3;
316
317        // Add 5 backups (should be limited to 3)
318        for i in 0..5 {
319            history.add_backup_version(format!("1.0.{}", i), max_backups);
320        }
321
322        assert_eq!(
323            history.backup_versions.len(),
324            3,
325            "Should keep only 3 backups"
326        );
327        assert_eq!(
328            history.backup_versions,
329            vec![
330                "1.0.2".to_string(),
331                "1.0.3".to_string(),
332                "1.0.4".to_string()
333            ]
334        );
335    }
336
337    #[test]
338    fn test_update_history_latest_backup() {
339        let mut history = UpdateHistory::default();
340        history.add_backup_version("1.0.0".to_string(), 3);
341        history.add_backup_version("1.0.1".to_string(), 3);
342
343        assert_eq!(history.latest_backup(), Some(&"1.0.1".to_string()));
344    }
345
346    #[test]
347    fn test_update_check_entry_serialization() {
348        let entry = UpdateCheckEntry {
349            timestamp: Timestamp::now(),
350            result: UpdateCheckResult::UpdateAvailable {
351                version: "1.1.0".to_string(),
352                notified: false,
353            },
354        };
355
356        let serialized = serde_json::to_string(&entry).unwrap();
357        let deserialized: UpdateCheckEntry = serde_json::from_str(&serialized).unwrap();
358
359        match deserialized.result {
360            UpdateCheckResult::UpdateAvailable { version, notified } => {
361                assert_eq!(version, "1.1.0");
362                assert!(!notified);
363            }
364            _ => panic!("Expected UpdateAvailable variant"),
365        }
366    }
367
368    #[test]
369    fn test_update_check_result_variants() {
370        let up_to_date = UpdateCheckResult::UpToDate;
371        let update_available = UpdateCheckResult::UpdateAvailable {
372            version: "1.2.0".to_string(),
373            notified: true,
374        };
375        let check_failed = UpdateCheckResult::CheckFailed {
376            error: "Network error".to_string(),
377        };
378
379        // Test that variants are distinct
380        assert_ne!(up_to_date, update_available);
381        assert_ne!(up_to_date, check_failed);
382        assert_ne!(update_available, check_failed);
383
384        // Test serialization for each variant
385        for result in [up_to_date, update_available, check_failed] {
386            let serialized = serde_json::to_string(&result).unwrap();
387            let deserialized: UpdateCheckResult = serde_json::from_str(&serialized).unwrap();
388            assert_eq!(result, deserialized);
389        }
390    }
391}