Skip to main content

kftray_commons/utils/
settings.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4use std::sync::LazyLock;
5use std::sync::Mutex;
6
7use log::info;
8use log::warn;
9use serde::{
10    Deserialize,
11    Serialize,
12};
13use sqlx::{
14    Row,
15    SqlitePool,
16};
17use tokio::sync::RwLock;
18
19use crate::models::settings_model::AppSettings;
20use crate::utils::db::get_db_pool;
21use crate::utils::db_mode::{
22    DatabaseManager,
23    DatabaseMode,
24};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Setting {
28    pub key: String,
29    pub value: String,
30    pub updated_at: Option<String>,
31}
32
33pub struct SettingsManager {
34    cache: Arc<RwLock<HashMap<String, String>>>,
35}
36
37impl SettingsManager {
38    pub fn new() -> Self {
39        Self {
40            cache: Arc::new(RwLock::new(HashMap::new())),
41        }
42    }
43
44    pub async fn load_settings(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
45        let pool = get_db_pool().await?;
46        let settings = load_all_settings(&pool).await?;
47
48        let mut cache = self.cache.write().await;
49        cache.clear();
50        for setting in settings {
51            cache.insert(setting.key, setting.value);
52        }
53        info!("Loaded {} settings from database", cache.len());
54        Ok(())
55    }
56
57    pub async fn get_setting(&self, key: &str) -> Option<String> {
58        let cache = self.cache.read().await;
59        cache.get(key).cloned()
60    }
61
62    pub async fn set_setting(
63        &self, key: &str, value: &str,
64    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
65        let pool = get_db_pool().await?;
66        upsert_setting(&pool, key, value).await?;
67
68        let mut cache = self.cache.write().await;
69        cache.insert(key.to_string(), value.to_string());
70        info!("Set setting: {key} = {value}");
71        Ok(())
72    }
73
74    pub async fn get_disconnect_timeout(&self) -> Option<u32> {
75        if let Some(value) = self.get_setting("disconnect_timeout_minutes").await {
76            value.parse::<u32>().ok()
77        } else {
78            Some(0) // Default to 0 (no timeout)
79        }
80    }
81
82    pub async fn set_disconnect_timeout(
83        &self, minutes: u32,
84    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
85        self.set_setting("disconnect_timeout_minutes", &minutes.to_string())
86            .await
87    }
88
89    pub async fn get_network_monitor(&self) -> bool {
90        if let Some(value) = self.get_setting("network_monitor").await {
91            value.parse::<bool>().unwrap_or(true)
92        } else {
93            true // Default to true
94        }
95    }
96
97    pub async fn set_network_monitor(
98        &self, enabled: bool,
99    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
100        self.set_setting("network_monitor", &enabled.to_string())
101            .await
102    }
103
104    pub async fn get_http_logs_default_enabled(&self) -> bool {
105        if let Some(value) = self.get_setting("http_logs_default_enabled").await {
106            value.parse::<bool>().unwrap_or(false)
107        } else {
108            false // Default to false
109        }
110    }
111
112    pub async fn set_http_logs_default_enabled(
113        &self, enabled: bool,
114    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
115        self.set_setting("http_logs_default_enabled", &enabled.to_string())
116            .await
117    }
118
119    pub async fn get_http_logs_max_file_size(&self) -> u64 {
120        if let Some(value) = self.get_setting("http_logs_max_file_size").await {
121            value.parse::<u64>().unwrap_or(10 * 1024 * 1024)
122        } else {
123            10 * 1024 * 1024 // Default 10MB
124        }
125    }
126
127    pub async fn set_http_logs_max_file_size(
128        &self, size: u64,
129    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
130        self.set_setting("http_logs_max_file_size", &size.to_string())
131            .await
132    }
133
134    pub async fn get_http_logs_retention_days(&self) -> u64 {
135        if let Some(value) = self.get_setting("http_logs_retention_days").await {
136            value.parse::<u64>().unwrap_or(7)
137        } else {
138            7 // Default 7 days
139        }
140    }
141
142    pub async fn set_http_logs_retention_days(
143        &self, days: u64,
144    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
145        self.set_setting("http_logs_retention_days", &days.to_string())
146            .await
147    }
148
149    pub async fn get_auto_update_enabled(&self) -> bool {
150        if let Some(value) = self.get_setting("auto_update_enabled").await {
151            value.parse::<bool>().unwrap_or(true)
152        } else {
153            true
154        }
155    }
156
157    pub async fn set_auto_update_enabled(
158        &self, enabled: bool,
159    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
160        self.set_setting("auto_update_enabled", &enabled.to_string())
161            .await
162    }
163
164    pub async fn get_last_update_check(&self) -> Option<i64> {
165        if let Some(value) = self.get_setting("last_update_check").await {
166            value.parse::<i64>().ok()
167        } else {
168            None
169        }
170    }
171
172    pub async fn set_last_update_check(
173        &self, timestamp: i64,
174    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
175        self.set_setting("last_update_check", &timestamp.to_string())
176            .await
177    }
178
179    pub async fn get_all_settings(&self) -> HashMap<String, String> {
180        let cache = self.cache.read().await;
181        cache.clone()
182    }
183
184    pub async fn get_app_settings(&self) -> AppSettings {
185        let settings = self.get_all_settings().await;
186        AppSettings::from_settings_manager(&settings)
187    }
188
189    pub async fn set_app_settings(
190        &self, app_settings: &AppSettings,
191    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
192        let settings_map = app_settings.to_settings_map();
193
194        for (key, value) in settings_map.iter() {
195            self.set_setting(key, value).await?;
196        }
197
198        Ok(())
199    }
200
201    pub async fn get_ssl_enabled(&self) -> bool {
202        if let Some(value) = self.get_setting("ssl_enabled").await {
203            value.parse::<bool>().unwrap_or(false)
204        } else {
205            false
206        }
207    }
208
209    pub async fn set_ssl_enabled(
210        &self, enabled: bool,
211    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
212        self.set_setting("ssl_enabled", &enabled.to_string()).await
213    }
214
215    pub async fn get_ssl_cert_validity_days(&self) -> u16 {
216        if let Some(value) = self.get_setting("ssl_cert_validity_days").await {
217            value.parse::<u16>().unwrap_or(365)
218        } else {
219            365
220        }
221    }
222
223    pub async fn set_ssl_cert_validity_days(
224        &self, days: u16,
225    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
226        self.set_setting("ssl_cert_validity_days", &days.to_string())
227            .await
228    }
229
230    pub async fn get_ssl_auto_regenerate(&self) -> bool {
231        if let Some(value) = self.get_setting("ssl_auto_regenerate").await {
232            value.parse::<bool>().unwrap_or(true)
233        } else {
234            true
235        }
236    }
237
238    pub async fn set_ssl_auto_regenerate(
239        &self, enabled: bool,
240    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
241        self.set_setting("ssl_auto_regenerate", &enabled.to_string())
242            .await
243    }
244
245    pub async fn get_ssl_ca_auto_install(&self) -> bool {
246        if let Some(value) = self.get_setting("ssl_ca_auto_install").await {
247            value.parse::<bool>().unwrap_or(false)
248        } else {
249            false
250        }
251    }
252
253    pub async fn set_ssl_ca_auto_install(
254        &self, enabled: bool,
255    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
256        self.set_setting("ssl_ca_auto_install", &enabled.to_string())
257            .await
258    }
259
260    pub async fn get_global_shortcut(&self) -> String {
261        if let Some(value) = self.get_setting("global_shortcut").await {
262            value
263        } else {
264            "Ctrl+Shift+F1".to_string()
265        }
266    }
267
268    pub async fn set_global_shortcut(
269        &self, shortcut: &str,
270    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
271        self.set_setting("global_shortcut", shortcut).await
272    }
273
274    pub async fn get_mcp_server_enabled(&self) -> bool {
275        if let Some(value) = self.get_setting("mcp_server_enabled").await {
276            value.parse::<bool>().unwrap_or(false)
277        } else {
278            false
279        }
280    }
281
282    pub async fn set_mcp_server_enabled(
283        &self, enabled: bool,
284    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
285        self.set_setting("mcp_server_enabled", &enabled.to_string())
286            .await
287    }
288
289    pub async fn get_mcp_server_port(&self) -> u16 {
290        if let Some(value) = self.get_setting("mcp_server_port").await {
291            value.parse::<u16>().unwrap_or(3000)
292        } else {
293            3000
294        }
295    }
296
297    pub async fn set_mcp_server_port(
298        &self, port: u16,
299    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
300        self.set_setting("mcp_server_port", &port.to_string()).await
301    }
302}
303
304impl Default for SettingsManager {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310async fn load_all_settings(pool: &SqlitePool) -> Result<Vec<Setting>, sqlx::Error> {
311    let mut conn = pool.acquire().await?;
312    let rows = sqlx::query("SELECT key, value, updated_at FROM settings ORDER BY key")
313        .fetch_all(&mut *conn)
314        .await?;
315
316    let settings = rows
317        .into_iter()
318        .map(|row| Setting {
319            key: row.get("key"),
320            value: row.get("value"),
321            updated_at: row.get("updated_at"),
322        })
323        .collect();
324
325    Ok(settings)
326}
327
328pub async fn upsert_setting(pool: &SqlitePool, key: &str, value: &str) -> Result<(), sqlx::Error> {
329    let mut conn = pool.acquire().await?;
330    sqlx::query(
331        "INSERT INTO settings (key, value, updated_at)
332         VALUES (?, ?, CURRENT_TIMESTAMP)
333         ON CONFLICT(key) DO UPDATE SET
334         value = excluded.value,
335         updated_at = CURRENT_TIMESTAMP",
336    )
337    .bind(key)
338    .bind(value)
339    .execute(&mut *conn)
340    .await?;
341    Ok(())
342}
343
344/// Removes a setting from a specific database.
345pub async fn delete_setting_with_mode(
346    key: &str, mode: DatabaseMode,
347) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
348    let context = DatabaseManager::get_context(mode).await?;
349    let mut conn = context.pool.acquire().await?;
350    sqlx::query("DELETE FROM settings WHERE key = ?")
351        .bind(key)
352        .execute(&mut *conn)
353        .await?;
354    Ok(())
355}
356
357/// Reads every setting whose key starts with `prefix`, from a specific
358/// database.
359pub async fn get_settings_with_prefix_and_mode(
360    prefix: &str, mode: DatabaseMode,
361) -> Result<Vec<(String, String)>, Box<dyn std::error::Error + Send + Sync>> {
362    let context = DatabaseManager::get_context(mode).await?;
363    let mut conn = context.pool.acquire().await?;
364    // `LIKE` treats `_` and `%` as wildcards and ignores ASCII case, so it is
365    // used only to narrow the scan; the prefix itself is enforced below.
366    let escaped = prefix
367        .replace('\\', "\\\\")
368        .replace('%', "\\%")
369        .replace('_', "\\_");
370    let rows = sqlx::query("SELECT key, value FROM settings WHERE key LIKE ? ESCAPE '\\'")
371        .bind(format!("{escaped}%"))
372        .fetch_all(&mut *conn)
373        .await?;
374
375    Ok(rows
376        .iter()
377        .map(|row| {
378            (
379                sqlx::Row::get::<String, _>(row, "key"),
380                sqlx::Row::get::<String, _>(row, "value"),
381            )
382        })
383        .filter(|(key, _)| key.starts_with(prefix))
384        .collect())
385}
386
387pub async fn get_setting(
388    key: &str,
389) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
390    let pool = get_db_pool().await?;
391    get_setting_with_pool(&pool, key).await
392}
393
394pub async fn get_setting_with_pool(
395    pool: &SqlitePool, key: &str,
396) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
397    let mut conn = pool.acquire().await?;
398    let result = sqlx::query("SELECT value FROM settings WHERE key = ?")
399        .bind(key)
400        .fetch_optional(&mut *conn)
401        .await?;
402
403    Ok(result.map(|row| row.get("value")))
404}
405
406pub async fn set_setting(
407    key: &str, value: &str,
408) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
409    let pool = get_db_pool().await?;
410    upsert_setting(&pool, key, value).await?;
411    Ok(())
412}
413
414pub async fn get_disconnect_timeout()
415-> Result<Option<u32>, Box<dyn std::error::Error + Send + Sync>> {
416    if let Some(value) = get_setting("disconnect_timeout_minutes").await? {
417        Ok(value.parse::<u32>().ok())
418    } else {
419        Ok(Some(0)) // Default to 0 (no timeout)
420    }
421}
422
423pub async fn set_disconnect_timeout(
424    minutes: u32,
425) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
426    set_setting("disconnect_timeout_minutes", &minutes.to_string()).await
427}
428
429pub async fn get_network_monitor() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
430    if let Some(value) = get_setting("network_monitor").await? {
431        Ok(value.parse::<bool>().unwrap_or(true))
432    } else {
433        Ok(true) // Default to true
434    }
435}
436
437pub async fn set_network_monitor(
438    enabled: bool,
439) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
440    set_setting("network_monitor", &enabled.to_string()).await
441}
442
443pub async fn get_setting_with_mode(
444    key: &str, mode: DatabaseMode,
445) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
446    let context = DatabaseManager::get_context(mode).await?;
447    let mut conn = context.pool.acquire().await?;
448    let result = sqlx::query("SELECT value FROM settings WHERE key = ?")
449        .bind(key)
450        .fetch_optional(&mut *conn)
451        .await?;
452    Ok(result.map(|row| row.get("value")))
453}
454
455pub async fn set_setting_with_mode(
456    key: &str, value: &str, mode: DatabaseMode,
457) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
458    let context = DatabaseManager::get_context(mode).await?;
459    upsert_setting(&context.pool, key, value).await?;
460    Ok(())
461}
462
463/// Atomically increments the integer value of a setting, creating it at `1`
464/// if it does not exist yet, and returns the new value.
465///
466/// The read, increment and write happen in a single statement, so concurrent
467/// callers each observe a distinct result instead of racing a separate get
468/// and set. A value that is not numeric, or that is already at `u32::MAX`,
469/// is rejected without mutating the row: incrementing past `u32::MAX` would
470/// store a value that no longer round-trips through `u32::parse`, wedging
471/// every future increment.
472pub async fn increment_setting_with_mode(
473    key: &str, mode: DatabaseMode,
474) -> Result<u32, Box<dyn std::error::Error + Send + Sync>> {
475    let context = DatabaseManager::get_context(mode).await?;
476    let mut conn = context.pool.acquire().await?;
477    let row = sqlx::query(
478        "INSERT INTO settings (key, value, updated_at)
479         VALUES (?, '1', CURRENT_TIMESTAMP)
480         ON CONFLICT(key) DO UPDATE SET
481         value = CAST(value AS INTEGER) + 1,
482         updated_at = CURRENT_TIMESTAMP
483         WHERE value NOT GLOB '*[^0-9]*' AND value != '' AND CAST(value AS INTEGER) < 4294967295
484         RETURNING value",
485    )
486    .bind(key)
487    .fetch_optional(&mut *conn)
488    .await?;
489    let Some(row) = row else {
490        return Err(format!(
491            "setting {key} holds a non-numeric value, or is already at its maximum, and cannot \
492             be incremented"
493        )
494        .into());
495    };
496    let value: String = row.get("value");
497    Ok(value.parse()?)
498}
499
500pub async fn get_disconnect_timeout_with_mode(
501    mode: DatabaseMode,
502) -> Result<Option<u32>, Box<dyn std::error::Error + Send + Sync>> {
503    if let Some(value) = get_setting_with_mode("disconnect_timeout_minutes", mode).await? {
504        Ok(value.parse::<u32>().ok())
505    } else {
506        Ok(Some(0))
507    }
508}
509
510pub async fn set_disconnect_timeout_with_mode(
511    minutes: u32, mode: DatabaseMode,
512) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
513    set_setting_with_mode("disconnect_timeout_minutes", &minutes.to_string(), mode).await
514}
515
516pub async fn get_network_monitor_with_mode(
517    mode: DatabaseMode,
518) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
519    if let Some(value) = get_setting_with_mode("network_monitor", mode).await? {
520        Ok(value.parse::<bool>().unwrap_or(true))
521    } else {
522        Ok(true)
523    }
524}
525
526pub async fn set_network_monitor_with_mode(
527    enabled: bool, mode: DatabaseMode,
528) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
529    set_setting_with_mode("network_monitor", &enabled.to_string(), mode).await
530}
531
532pub async fn get_auto_update_enabled() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
533    if let Some(value) = get_setting("auto_update_enabled").await? {
534        Ok(value.parse::<bool>().unwrap_or(true))
535    } else {
536        Ok(true)
537    }
538}
539
540pub async fn set_auto_update_enabled(
541    enabled: bool,
542) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
543    set_setting("auto_update_enabled", &enabled.to_string()).await
544}
545
546pub async fn get_last_update_check() -> Result<Option<i64>, Box<dyn std::error::Error + Send + Sync>>
547{
548    if let Some(value) = get_setting("last_update_check").await? {
549        Ok(value.parse::<i64>().ok())
550    } else {
551        Ok(None)
552    }
553}
554
555pub async fn set_last_update_check(
556    timestamp: i64,
557) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
558    set_setting("last_update_check", &timestamp.to_string()).await
559}
560
561pub async fn get_auto_update_enabled_with_mode(
562    mode: DatabaseMode,
563) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
564    if let Some(value) = get_setting_with_mode("auto_update_enabled", mode).await? {
565        Ok(value.parse::<bool>().unwrap_or(true))
566    } else {
567        Ok(true)
568    }
569}
570
571pub async fn set_auto_update_enabled_with_mode(
572    enabled: bool, mode: DatabaseMode,
573) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
574    set_setting_with_mode("auto_update_enabled", &enabled.to_string(), mode).await
575}
576
577pub async fn get_app_settings() -> Result<AppSettings, Box<dyn std::error::Error + Send + Sync>> {
578    let pool = get_db_pool().await?;
579    let settings = load_all_settings(&pool).await?;
580
581    let settings_map: HashMap<String, String> =
582        settings.into_iter().map(|s| (s.key, s.value)).collect();
583
584    Ok(AppSettings::from_settings_manager(&settings_map))
585}
586
587pub async fn set_app_settings(
588    app_settings: &AppSettings,
589) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
590    let pool = get_db_pool().await?;
591    let settings_map = app_settings.to_settings_map();
592
593    for (key, value) in settings_map.iter() {
594        upsert_setting(&pool, key, value).await?;
595    }
596
597    Ok(())
598}
599
600pub async fn get_ssl_enabled() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
601    if let Some(value) = get_setting("ssl_enabled").await? {
602        Ok(value.parse::<bool>().unwrap_or(false))
603    } else {
604        Ok(false)
605    }
606}
607
608pub async fn set_ssl_enabled(
609    enabled: bool,
610) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
611    set_setting("ssl_enabled", &enabled.to_string()).await
612}
613
614pub async fn get_ssl_cert_validity_days() -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
615    if let Some(value) = get_setting("ssl_cert_validity_days").await? {
616        Ok(value.parse::<u16>().unwrap_or(365))
617    } else {
618        Ok(365)
619    }
620}
621
622pub async fn set_ssl_cert_validity_days(
623    days: u16,
624) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
625    set_setting("ssl_cert_validity_days", &days.to_string()).await
626}
627
628pub async fn get_ssl_auto_regenerate() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
629    if let Some(value) = get_setting("ssl_auto_regenerate").await? {
630        Ok(value.parse::<bool>().unwrap_or(true))
631    } else {
632        Ok(true)
633    }
634}
635
636pub async fn set_ssl_auto_regenerate(
637    enabled: bool,
638) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
639    set_setting("ssl_auto_regenerate", &enabled.to_string()).await
640}
641
642pub async fn get_ssl_ca_auto_install() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
643    if let Some(value) = get_setting("ssl_ca_auto_install").await? {
644        Ok(value.parse::<bool>().unwrap_or(false))
645    } else {
646        Ok(false)
647    }
648}
649
650pub async fn set_ssl_ca_auto_install(
651    enabled: bool,
652) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
653    set_setting("ssl_ca_auto_install", &enabled.to_string()).await
654}
655
656pub async fn get_global_shortcut() -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
657    if let Some(value) = get_setting("global_shortcut").await? {
658        Ok(value)
659    } else {
660        Ok("Ctrl+Shift+F1".to_string())
661    }
662}
663
664pub async fn set_global_shortcut(
665    shortcut: &str,
666) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
667    set_setting("global_shortcut", shortcut).await
668}
669
670pub async fn get_mcp_server_enabled() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
671    if let Some(value) = get_setting("mcp_server_enabled").await? {
672        Ok(value.parse::<bool>().unwrap_or(false))
673    } else {
674        Ok(false)
675    }
676}
677
678pub async fn set_mcp_server_enabled(
679    enabled: bool,
680) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
681    set_setting("mcp_server_enabled", &enabled.to_string()).await
682}
683
684pub async fn get_mcp_server_port() -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
685    if let Some(value) = get_setting("mcp_server_port").await? {
686        Ok(value.parse::<u16>().unwrap_or(3000))
687    } else {
688        Ok(3000)
689    }
690}
691
692pub async fn set_mcp_server_port(
693    port: u16,
694) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
695    set_setting("mcp_server_port", &port.to_string()).await
696}
697
698pub async fn get_ssl_enabled_with_mode(
699    mode: DatabaseMode,
700) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
701    if let Some(value) = get_setting_with_mode("ssl_enabled", mode).await? {
702        Ok(value.parse::<bool>().unwrap_or(false))
703    } else {
704        Ok(false)
705    }
706}
707
708pub async fn set_ssl_enabled_with_mode(
709    enabled: bool, mode: DatabaseMode,
710) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
711    set_setting_with_mode("ssl_enabled", &enabled.to_string(), mode).await
712}
713
714pub async fn get_ssl_cert_validity_days_with_mode(
715    mode: DatabaseMode,
716) -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
717    if let Some(value) = get_setting_with_mode("ssl_cert_validity_days", mode).await? {
718        Ok(value.parse::<u16>().unwrap_or(365))
719    } else {
720        Ok(365)
721    }
722}
723
724pub async fn set_ssl_cert_validity_days_with_mode(
725    days: u16, mode: DatabaseMode,
726) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
727    set_setting_with_mode("ssl_cert_validity_days", &days.to_string(), mode).await
728}
729
730pub async fn get_ssl_auto_regenerate_with_mode(
731    mode: DatabaseMode,
732) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
733    if let Some(value) = get_setting_with_mode("ssl_auto_regenerate", mode).await? {
734        Ok(value.parse::<bool>().unwrap_or(true))
735    } else {
736        Ok(true)
737    }
738}
739
740pub async fn set_ssl_auto_regenerate_with_mode(
741    enabled: bool, mode: DatabaseMode,
742) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
743    set_setting_with_mode("ssl_auto_regenerate", &enabled.to_string(), mode).await
744}
745
746pub async fn get_ssl_ca_auto_install_with_mode(
747    mode: DatabaseMode,
748) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
749    if let Some(value) = get_setting_with_mode("ssl_ca_auto_install", mode).await? {
750        Ok(value.parse::<bool>().unwrap_or(false))
751    } else {
752        Ok(false)
753    }
754}
755
756pub async fn set_ssl_ca_auto_install_with_mode(
757    enabled: bool, mode: DatabaseMode,
758) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
759    set_setting_with_mode("ssl_ca_auto_install", &enabled.to_string(), mode).await
760}
761
762pub async fn get_app_settings_with_mode(
763    mode: DatabaseMode,
764) -> Result<AppSettings, Box<dyn std::error::Error + Send + Sync>> {
765    let context = DatabaseManager::get_context(mode).await?;
766    let settings = load_all_settings(&context.pool).await?;
767
768    let settings_map: HashMap<String, String> =
769        settings.into_iter().map(|s| (s.key, s.value)).collect();
770
771    Ok(AppSettings::from_settings_manager(&settings_map))
772}
773
774/// Scope prefix of a database mode inside a setting key.
775pub fn mode_scope(mode: DatabaseMode) -> &'static str {
776    match mode {
777        DatabaseMode::File => "file",
778        DatabaseMode::Memory => "memory",
779    }
780}
781
782/// Key under which an exposure is marked as one whose ingress history was
783/// never recorded, because it predates the recording.
784pub fn expose_legacy_key(config_id: &str, mode: DatabaseMode) -> String {
785    format!("expose_legacy_possible:{}:{config_id}", mode_scope(mode))
786}
787
788/// Key under which a database records that its baseline has been taken.
789pub fn expose_history_baseline_key(mode: DatabaseMode) -> String {
790    format!("expose_history_baseline:{}", mode_scope(mode))
791}
792
793/// Key under which the ids snapshotted by a failed init baseline attempt are
794/// persisted, so a lazy baseline establishment in a later process restarted
795/// on this database still restricts itself to configs that existed before
796/// the failed attempt, instead of the in-memory snapshot being lost with the
797/// process and every currently existing row being treated as unrestricted.
798fn expose_baseline_pending_ids_key(mode: DatabaseMode) -> String {
799    format!("expose_baseline_pending_ids:{}", mode_scope(mode))
800}
801
802/// Config ids seen at the moment this process gave up retrying the
803/// baseline, keyed by database mode.
804///
805/// A later lazy call (from `ensure_expose_history_baseline`) uses this to
806/// mark only ids that already existed back then as having no recoverable
807/// history: without it, a row inserted after the failed init would be swept
808/// up by whatever the lazy call happens to see in the `configs` table and
809/// wrongly marked as predating ingress history.
810static FAILED_BASELINE_SNAPSHOT: LazyLock<Mutex<HashMap<DatabaseMode, HashSet<i64>>>> =
811    LazyLock::new(|| Mutex::new(HashMap::new()));
812
813const BASELINE_INIT_ATTEMPTS: u32 = 3;
814const BASELINE_INIT_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(50);
815
816/// Establishes the expose history baseline at database initialisation,
817/// retrying a transient failure (e.g. `SQLITE_BUSY` from another kftray
818/// process sharing the file database) a few times before giving up.
819///
820/// If every attempt fails, the ids of the configs that exist right now are
821/// snapshotted so a later lazy call marks only those ids as legacy, never
822/// one inserted afterwards.
823pub async fn establish_expose_history_baseline_at_init(
824    pool: &SqlitePool, mode: DatabaseMode,
825) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
826    let mut last_error = None;
827    for attempt in 0..BASELINE_INIT_ATTEMPTS {
828        if attempt > 0 {
829            tokio::time::sleep(BASELINE_INIT_RETRY_DELAY * attempt).await;
830        }
831        match establish_expose_history_baseline(pool, mode).await {
832            Ok(()) => return Ok(()),
833            Err(error) => last_error = Some(error),
834        }
835    }
836
837    if let Ok(rows) = sqlx::query("SELECT id FROM configs").fetch_all(pool).await {
838        let ids: HashSet<i64> = rows
839            .iter()
840            .filter_map(|row| row.try_get("id").ok())
841            .collect();
842        let serialized =
843            serde_json::to_string(&ids.iter().copied().collect::<Vec<_>>()).unwrap_or_default();
844        FAILED_BASELINE_SNAPSHOT.lock().unwrap().insert(mode, ids);
845        if let Err(error) =
846            upsert_setting(pool, &expose_baseline_pending_ids_key(mode), &serialized).await
847        {
848            warn!(
849                "failed to persist the expose baseline pending id allow-list: {error}; \
850                 relying on the in-memory snapshot for this process only"
851            );
852        }
853    }
854    // If the snapshot query itself fails, no entry is recorded for `mode`:
855    // the lazy path then treats every row it later sees as unrestricted,
856    // instead of an empty allow-list that would mark nothing.
857
858    Err(last_error.expect("loop runs at least once"))
859}
860
861pub async fn establish_expose_history_baseline(
862    pool: &SqlitePool, mode: DatabaseMode,
863) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
864    let baseline = expose_history_baseline_key(mode);
865    // The common case, on every expose start and stop once a baseline has
866    // been taken, is that the marker is already present: check it with a
867    // plain read against the pool first, so that case never pays for the
868    // exclusive write lock below.
869    if sqlx::query("SELECT 1 FROM settings WHERE key = ?")
870        .bind(&baseline)
871        .fetch_optional(pool)
872        .await?
873        .is_some()
874    {
875        return Ok(());
876    }
877    // One write transaction from the check to the marker: two processes
878    // sharing the file database can both initialise it, and a baseline taken
879    // by the second after the first finished would mark rows inserted in
880    // between, which do have a history, as ones that never had one. An
881    // immediate transaction takes the write lock up front, so the second
882    // initialiser sees the marker the first wrote.
883    // Through the transaction guard: a future dropped partway, or a failed
884    // commit, must not hand the pooled connection back with a write
885    // transaction still open on it.
886    let mut tx = pool.begin_with("BEGIN IMMEDIATE").await?;
887    let in_memory_snapshot = FAILED_BASELINE_SNAPSHOT.lock().unwrap().get(&mode).cloned();
888    let allowed_ids = match in_memory_snapshot {
889        Some(ids) => Some(ids),
890        // The in-memory snapshot only survives within the process that took
891        // it; a process restarted after a failed init still needs the
892        // allow-list it persisted, or it would treat every row it now sees
893        // as unrestricted and mark rows created after the failed attempt.
894        None => read_pending_baseline_ids(&mut tx, mode).await?,
895    };
896    take_expose_history_baseline(&mut tx, &baseline, mode, allowed_ids.as_ref()).await?;
897    clear_pending_baseline_ids(&mut tx, mode).await?;
898    tx.commit().await?;
899    Ok(())
900}
901
902async fn read_pending_baseline_ids(
903    conn: &mut sqlx::SqliteConnection, mode: DatabaseMode,
904) -> Result<Option<HashSet<i64>>, Box<dyn std::error::Error + Send + Sync>> {
905    let row = sqlx::query("SELECT value FROM settings WHERE key = ?")
906        .bind(expose_baseline_pending_ids_key(mode))
907        .fetch_optional(&mut *conn)
908        .await?;
909    let Some(row) = row else {
910        return Ok(None);
911    };
912    let value: String = row.try_get("value")?;
913    let ids: Vec<i64> = serde_json::from_str(&value)?;
914    Ok(Some(ids.into_iter().collect()))
915}
916
917/// Deletes the persisted allow-list once the baseline marker has been
918/// written (or was already present), so it never lingers to restrict a
919/// baseline this database will never take again.
920async fn clear_pending_baseline_ids(
921    conn: &mut sqlx::SqliteConnection, mode: DatabaseMode,
922) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
923    sqlx::query("DELETE FROM settings WHERE key = ?")
924        .bind(expose_baseline_pending_ids_key(mode))
925        .execute(&mut *conn)
926        .await?;
927    Ok(())
928}
929
930async fn take_expose_history_baseline(
931    conn: &mut sqlx::SqliteConnection, baseline: &str, mode: DatabaseMode,
932    allowed_ids: Option<&HashSet<i64>>,
933) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
934    const UPSERT: &str = "INSERT INTO settings (key, value, updated_at) VALUES (?, '1', \
935                          CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = \
936                          excluded.value, updated_at = CURRENT_TIMESTAMP";
937
938    let taken = sqlx::query("SELECT value FROM settings WHERE key = ?")
939        .bind(baseline)
940        .fetch_optional(&mut *conn)
941        .await?;
942    if taken.is_some() {
943        return Ok(());
944    }
945    let rows = sqlx::query("SELECT id, data FROM configs")
946        .fetch_all(&mut *conn)
947        .await?;
948    for row in rows {
949        let id: i64 = row.try_get("id")?;
950        if allowed_ids.is_some_and(|allowed| !allowed.contains(&id)) {
951            continue;
952        }
953        let data: String = row.try_get("data")?;
954        // Only the one field matters, read on its own: a row whose other
955        // fields do not decode is still an exposure if this one says so, and
956        // the baseline is taken once, so a row misread now would be treated
957        // ever after as one that has a history it never had. A row that is
958        // not even JSON is marked too, on the same reasoning.
959        let is_exposure = match serde_json::from_str::<serde_json::Value>(&data) {
960            Ok(value) => {
961                value.get("workload_type").and_then(|kind| kind.as_str()) == Some("expose")
962            }
963            Err(_) => true,
964        };
965        if !is_exposure {
966            continue;
967        }
968        sqlx::query(UPSERT)
969            .bind(expose_legacy_key(&id.to_string(), mode))
970            .execute(&mut *conn)
971            .await?;
972    }
973    sqlx::query(UPSERT)
974        .bind(baseline)
975        .execute(&mut *conn)
976        .await?;
977    Ok(())
978}
979
980#[cfg(test)]
981mod tests {
982    use sqlx::SqlitePool;
983
984    use super::*;
985    use crate::utils::db::create_db_table;
986
987    async fn create_test_pool() -> SqlitePool {
988        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
989        create_db_table(&pool).await.unwrap();
990        pool
991    }
992
993    #[tokio::test]
994    async fn test_upsert_setting() {
995        let pool = create_test_pool().await;
996
997        upsert_setting(&pool, "test_key", "test_value")
998            .await
999            .unwrap();
1000
1001        let result: String = sqlx::query("SELECT value FROM settings WHERE key = 'test_key'")
1002            .fetch_one(&pool)
1003            .await
1004            .unwrap()
1005            .get("value");
1006        assert_eq!(result, "test_value");
1007
1008        upsert_setting(&pool, "test_key", "updated_value")
1009            .await
1010            .unwrap();
1011
1012        let result: String = sqlx::query("SELECT value FROM settings WHERE key = 'test_key'")
1013            .fetch_one(&pool)
1014            .await
1015            .unwrap()
1016            .get("value");
1017        assert_eq!(result, "updated_value");
1018    }
1019
1020    #[tokio::test]
1021    async fn test_load_all_settings() {
1022        let pool = create_test_pool().await;
1023
1024        upsert_setting(&pool, "key1", "value1").await.unwrap();
1025        upsert_setting(&pool, "key2", "value2").await.unwrap();
1026
1027        let settings = load_all_settings(&pool).await.unwrap();
1028
1029        assert_eq!(settings.len(), 2);
1030        assert_eq!(settings[0].key, "key1");
1031        assert_eq!(settings[0].value, "value1");
1032        assert_eq!(settings[1].key, "key2");
1033        assert_eq!(settings[1].value, "value2");
1034    }
1035
1036    #[tokio::test]
1037    async fn test_settings_manager() {
1038        let pool = create_test_pool().await;
1039
1040        upsert_setting(&pool, "test_timeout", "30").await.unwrap();
1041        let settings = load_all_settings(&pool).await.unwrap();
1042        assert_eq!(settings.len(), 1);
1043        assert_eq!(settings[0].key, "test_timeout");
1044        assert_eq!(settings[0].value, "30");
1045
1046        upsert_setting(&pool, "test_timeout", "60").await.unwrap();
1047        let settings = load_all_settings(&pool).await.unwrap();
1048        assert_eq!(settings.len(), 1);
1049        assert_eq!(settings[0].value, "60");
1050
1051        let _manager = SettingsManager::new();
1052    }
1053
1054    #[tokio::test]
1055    async fn test_settings_with_mode_memory() {
1056        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1057        set_setting_with_mode("memory_test", "test_value", DatabaseMode::Memory)
1058            .await
1059            .unwrap();
1060
1061        let value = get_setting_with_mode("memory_test", DatabaseMode::Memory)
1062            .await
1063            .unwrap();
1064        assert_eq!(value, Some("test_value".to_string()));
1065
1066        set_disconnect_timeout_with_mode(120, DatabaseMode::Memory)
1067            .await
1068            .unwrap();
1069        let timeout = get_disconnect_timeout_with_mode(DatabaseMode::Memory)
1070            .await
1071            .unwrap();
1072        assert_eq!(timeout, Some(120));
1073
1074        set_network_monitor_with_mode(false, DatabaseMode::Memory)
1075            .await
1076            .unwrap();
1077        let monitor = get_network_monitor_with_mode(DatabaseMode::Memory)
1078            .await
1079            .unwrap();
1080        assert!(!monitor);
1081    }
1082
1083    #[tokio::test]
1084    async fn test_settings_isolation_between_modes() {
1085        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1086        let memory_context = DatabaseManager::get_context(DatabaseMode::Memory)
1087            .await
1088            .unwrap();
1089        let memory_key = "isolation_test_memory";
1090
1091        upsert_setting(&memory_context.pool, memory_key, "memory_value")
1092            .await
1093            .unwrap();
1094
1095        let memory_result = get_setting_with_pool(&memory_context.pool, memory_key)
1096            .await
1097            .unwrap();
1098        assert_eq!(memory_result, Some("memory_value".to_string()));
1099
1100        let different_key = "isolation_test_different";
1101        let different_result = get_setting_with_pool(&memory_context.pool, different_key)
1102            .await
1103            .unwrap();
1104        assert!(
1105            different_result.is_none(),
1106            "Different key should not have data"
1107        );
1108
1109        let same_result = get_setting_with_pool(&memory_context.pool, memory_key)
1110            .await
1111            .unwrap();
1112        assert_eq!(same_result, Some("memory_value".to_string()));
1113    }
1114
1115    #[tokio::test]
1116    async fn test_lazy_baseline_never_marks_row_created_after_failed_init_memory_mode() {
1117        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1118
1119        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1120            .await
1121            .unwrap();
1122        let pool = context.pool.clone();
1123
1124        crate::utils::config::delete_all_configs_with_pool(&pool)
1125            .await
1126            .unwrap();
1127        let _ = delete_setting_with_mode(
1128            &expose_history_baseline_key(DatabaseMode::Memory),
1129            DatabaseMode::Memory,
1130        )
1131        .await;
1132
1133        let existing_config = crate::models::config_model::Config {
1134            workload_type: Some("expose".to_string()),
1135            ..Default::default()
1136        };
1137        let existing_id = crate::utils::config::insert_config_with_pool_and_mode(
1138            existing_config,
1139            &pool,
1140            DatabaseMode::Memory,
1141        )
1142        .await
1143        .unwrap();
1144
1145        // Simulate a process that failed every baseline retry at init: at
1146        // that moment only `existing_id` existed.
1147        FAILED_BASELINE_SNAPSHOT
1148            .lock()
1149            .unwrap()
1150            .insert(DatabaseMode::Memory, HashSet::from([existing_id]));
1151
1152        let created_after_failed_init = crate::models::config_model::Config {
1153            workload_type: Some("expose".to_string()),
1154            ..Default::default()
1155        };
1156        let after_id = crate::utils::config::insert_config_with_pool_and_mode(
1157            created_after_failed_init,
1158            &pool,
1159            DatabaseMode::Memory,
1160        )
1161        .await
1162        .unwrap();
1163
1164        establish_expose_history_baseline(&pool, DatabaseMode::Memory)
1165            .await
1166            .unwrap();
1167
1168        let existing_marked = get_setting_with_mode(
1169            &expose_legacy_key(&existing_id.to_string(), DatabaseMode::Memory),
1170            DatabaseMode::Memory,
1171        )
1172        .await
1173        .unwrap();
1174        assert!(
1175            existing_marked.is_some(),
1176            "a config that existed at the failed-init snapshot must be marked legacy"
1177        );
1178
1179        let after_marked = get_setting_with_mode(
1180            &expose_legacy_key(&after_id.to_string(), DatabaseMode::Memory),
1181            DatabaseMode::Memory,
1182        )
1183        .await
1184        .unwrap();
1185        assert!(
1186            after_marked.is_none(),
1187            "a config created after the failed init must never be marked legacy"
1188        );
1189
1190        FAILED_BASELINE_SNAPSHOT
1191            .lock()
1192            .unwrap()
1193            .remove(&DatabaseMode::Memory);
1194        let _ = delete_setting_with_mode(
1195            &expose_history_baseline_key(DatabaseMode::Memory),
1196            DatabaseMode::Memory,
1197        )
1198        .await;
1199        let _ = crate::utils::config::delete_all_configs_with_pool(&pool).await;
1200    }
1201
1202    #[tokio::test]
1203    async fn test_failed_snapshot_query_leaves_lazy_baseline_unrestricted_memory_mode() {
1204        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1205
1206        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1207            .await
1208            .unwrap();
1209        let pool = context.pool.clone();
1210
1211        crate::utils::config::delete_all_configs_with_pool(&pool)
1212            .await
1213            .unwrap();
1214        let _ = delete_setting_with_mode(
1215            &expose_history_baseline_key(DatabaseMode::Memory),
1216            DatabaseMode::Memory,
1217        )
1218        .await;
1219        FAILED_BASELINE_SNAPSHOT
1220            .lock()
1221            .unwrap()
1222            .remove(&DatabaseMode::Memory);
1223
1224        // Force every baseline attempt, including the fallback snapshot
1225        // query, to fail.
1226        sqlx::query("DROP TABLE configs")
1227            .execute(&*pool)
1228            .await
1229            .unwrap();
1230
1231        let init_result =
1232            establish_expose_history_baseline_at_init(&pool, DatabaseMode::Memory).await;
1233        assert!(
1234            init_result.is_err(),
1235            "every attempt must fail with no configs table"
1236        );
1237        assert!(
1238            FAILED_BASELINE_SNAPSHOT
1239                .lock()
1240                .unwrap()
1241                .get(&DatabaseMode::Memory)
1242                .is_none(),
1243            "a failed snapshot query must not record an empty allow-list"
1244        );
1245
1246        // Recreate the schema, as `db::init` would on the next run, and
1247        // insert a pre-existing exposure the failed init never saw.
1248        crate::utils::db::create_db_table(&pool).await.unwrap();
1249
1250        let pre_existing = crate::models::config_model::Config {
1251            workload_type: Some("expose".to_string()),
1252            ..Default::default()
1253        };
1254        let pre_existing_id = crate::utils::config::insert_config_with_pool_and_mode(
1255            pre_existing,
1256            &pool,
1257            DatabaseMode::Memory,
1258        )
1259        .await
1260        .unwrap();
1261
1262        establish_expose_history_baseline(&pool, DatabaseMode::Memory)
1263            .await
1264            .unwrap();
1265
1266        let marked = get_setting_with_mode(
1267            &expose_legacy_key(&pre_existing_id.to_string(), DatabaseMode::Memory),
1268            DatabaseMode::Memory,
1269        )
1270        .await
1271        .unwrap();
1272        assert!(
1273            marked.is_some(),
1274            "the lazy baseline must mark a pre-existing exposure when init's snapshot failed"
1275        );
1276
1277        FAILED_BASELINE_SNAPSHOT
1278            .lock()
1279            .unwrap()
1280            .remove(&DatabaseMode::Memory);
1281        let _ = delete_setting_with_mode(
1282            &expose_history_baseline_key(DatabaseMode::Memory),
1283            DatabaseMode::Memory,
1284        )
1285        .await;
1286        let _ = crate::utils::config::delete_all_configs_with_pool(&pool).await;
1287    }
1288
1289    #[tokio::test]
1290    async fn test_snapshot_is_recorded_in_memory_even_when_persisting_it_fails_memory_mode() {
1291        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1292
1293        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1294            .await
1295            .unwrap();
1296        let pool = context.pool.clone();
1297
1298        crate::utils::config::delete_all_configs_with_pool(&pool)
1299            .await
1300            .unwrap();
1301        let _ = delete_setting_with_mode(
1302            &expose_history_baseline_key(DatabaseMode::Memory),
1303            DatabaseMode::Memory,
1304        )
1305        .await;
1306        FAILED_BASELINE_SNAPSHOT
1307            .lock()
1308            .unwrap()
1309            .remove(&DatabaseMode::Memory);
1310
1311        let existing_config = crate::models::config_model::Config {
1312            workload_type: Some("expose".to_string()),
1313            ..Default::default()
1314        };
1315        let existing_id = crate::utils::config::insert_config_with_pool_and_mode(
1316            existing_config,
1317            &pool,
1318            DatabaseMode::Memory,
1319        )
1320        .await
1321        .unwrap();
1322
1323        // `configs` is left intact so the fallback snapshot query still
1324        // succeeds; only `settings` is gone, so both
1325        // `establish_expose_history_baseline` and the allow-list persist
1326        // attempt fail.
1327        sqlx::query("DROP TABLE settings")
1328            .execute(&*pool)
1329            .await
1330            .unwrap();
1331
1332        let init_result =
1333            establish_expose_history_baseline_at_init(&pool, DatabaseMode::Memory).await;
1334        assert!(
1335            init_result.is_err(),
1336            "every attempt must fail with no settings table"
1337        );
1338        assert_eq!(
1339            FAILED_BASELINE_SNAPSHOT
1340                .lock()
1341                .unwrap()
1342                .get(&DatabaseMode::Memory)
1343                .cloned(),
1344            Some(HashSet::from([existing_id])),
1345            "the in-memory snapshot must be recorded even though persisting the allow-list \
1346             failed"
1347        );
1348
1349        // Recreate the schema, as `db::init` would on the next run.
1350        crate::utils::db::create_db_table(&pool).await.unwrap();
1351
1352        FAILED_BASELINE_SNAPSHOT
1353            .lock()
1354            .unwrap()
1355            .remove(&DatabaseMode::Memory);
1356        let _ = crate::utils::config::delete_all_configs_with_pool(&pool).await;
1357    }
1358
1359    #[tokio::test]
1360    async fn test_persisted_baseline_allow_list_survives_a_restart_memory_mode() {
1361        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1362
1363        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1364            .await
1365            .unwrap();
1366        let pool = context.pool.clone();
1367
1368        crate::utils::config::delete_all_configs_with_pool(&pool)
1369            .await
1370            .unwrap();
1371        let _ = delete_setting_with_mode(
1372            &expose_history_baseline_key(DatabaseMode::Memory),
1373            DatabaseMode::Memory,
1374        )
1375        .await;
1376        let _ = delete_setting_with_mode(
1377            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1378            DatabaseMode::Memory,
1379        )
1380        .await;
1381        FAILED_BASELINE_SNAPSHOT
1382            .lock()
1383            .unwrap()
1384            .remove(&DatabaseMode::Memory);
1385
1386        let existing_config = crate::models::config_model::Config {
1387            workload_type: Some("expose".to_string()),
1388            ..Default::default()
1389        };
1390        let existing_id = crate::utils::config::insert_config_with_pool_and_mode(
1391            existing_config,
1392            &pool,
1393            DatabaseMode::Memory,
1394        )
1395        .await
1396        .unwrap();
1397
1398        // A process that failed every baseline retry at init persists the
1399        // ids it saw to settings, on top of the in-memory snapshot.
1400        upsert_setting(
1401            &pool,
1402            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1403            &serde_json::to_string(&[existing_id]).unwrap(),
1404        )
1405        .await
1406        .unwrap();
1407
1408        // Simulate a restart: a new process starts with an empty static, so
1409        // only the persisted settings key carries the allow-list forward.
1410        FAILED_BASELINE_SNAPSHOT
1411            .lock()
1412            .unwrap()
1413            .remove(&DatabaseMode::Memory);
1414
1415        let created_after_failed_init = crate::models::config_model::Config {
1416            workload_type: Some("expose".to_string()),
1417            ..Default::default()
1418        };
1419        let after_id = crate::utils::config::insert_config_with_pool_and_mode(
1420            created_after_failed_init,
1421            &pool,
1422            DatabaseMode::Memory,
1423        )
1424        .await
1425        .unwrap();
1426
1427        establish_expose_history_baseline(&pool, DatabaseMode::Memory)
1428            .await
1429            .unwrap();
1430
1431        let existing_marked = get_setting_with_mode(
1432            &expose_legacy_key(&existing_id.to_string(), DatabaseMode::Memory),
1433            DatabaseMode::Memory,
1434        )
1435        .await
1436        .unwrap();
1437        assert!(
1438            existing_marked.is_some(),
1439            "a config recorded in the persisted allow-list must be marked legacy after a restart"
1440        );
1441
1442        let after_marked = get_setting_with_mode(
1443            &expose_legacy_key(&after_id.to_string(), DatabaseMode::Memory),
1444            DatabaseMode::Memory,
1445        )
1446        .await
1447        .unwrap();
1448        assert!(
1449            after_marked.is_none(),
1450            "a config created after the failed init must never be marked legacy, even after a \
1451             restart lost the in-memory snapshot"
1452        );
1453
1454        let pending_after = get_setting_with_mode(
1455            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1456            DatabaseMode::Memory,
1457        )
1458        .await
1459        .unwrap();
1460        assert!(
1461            pending_after.is_none(),
1462            "the persisted allow-list must be deleted once the baseline marker is written"
1463        );
1464
1465        FAILED_BASELINE_SNAPSHOT
1466            .lock()
1467            .unwrap()
1468            .remove(&DatabaseMode::Memory);
1469        let _ = delete_setting_with_mode(
1470            &expose_history_baseline_key(DatabaseMode::Memory),
1471            DatabaseMode::Memory,
1472        )
1473        .await;
1474        let _ = crate::utils::config::delete_all_configs_with_pool(&pool).await;
1475    }
1476
1477    #[tokio::test]
1478    async fn test_malformed_persisted_allow_list_errors_and_leaves_the_pending_key_memory_mode() {
1479        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1480
1481        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1482            .await
1483            .unwrap();
1484        let pool = context.pool.clone();
1485
1486        let _ = delete_setting_with_mode(
1487            &expose_history_baseline_key(DatabaseMode::Memory),
1488            DatabaseMode::Memory,
1489        )
1490        .await;
1491        let _ = delete_setting_with_mode(
1492            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1493            DatabaseMode::Memory,
1494        )
1495        .await;
1496        FAILED_BASELINE_SNAPSHOT
1497            .lock()
1498            .unwrap()
1499            .remove(&DatabaseMode::Memory);
1500
1501        // Not valid JSON: what a corrupted or hand-edited settings row
1502        // leaves behind.
1503        upsert_setting(
1504            &pool,
1505            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1506            "not json",
1507        )
1508        .await
1509        .unwrap();
1510
1511        let result = establish_expose_history_baseline(&pool, DatabaseMode::Memory).await;
1512        assert!(
1513            result.is_err(),
1514            "a malformed persisted allow-list must error instead of silently becoming an empty \
1515             list"
1516        );
1517
1518        let pending = get_setting_with_mode(
1519            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1520            DatabaseMode::Memory,
1521        )
1522        .await
1523        .unwrap();
1524        assert_eq!(
1525            pending,
1526            Some("not json".to_string()),
1527            "a rejected malformed allow-list must be left in place, not cleared"
1528        );
1529
1530        let baseline_taken = get_setting_with_mode(
1531            &expose_history_baseline_key(DatabaseMode::Memory),
1532            DatabaseMode::Memory,
1533        )
1534        .await
1535        .unwrap();
1536        assert!(
1537            baseline_taken.is_none(),
1538            "the baseline marker must not be written when the allow-list read failed"
1539        );
1540
1541        let _ = delete_setting_with_mode(
1542            &expose_baseline_pending_ids_key(DatabaseMode::Memory),
1543            DatabaseMode::Memory,
1544        )
1545        .await;
1546    }
1547
1548    #[tokio::test]
1549    async fn test_increment_setting_with_mode_errors_on_a_non_numeric_value_memory_mode() {
1550        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1551
1552        for junk in ["not-a-number", "12abc"] {
1553            let key = format!("junk_counter_test:{junk}:memory");
1554            let _ = delete_setting_with_mode(&key, DatabaseMode::Memory).await;
1555
1556            let context = DatabaseManager::get_context(DatabaseMode::Memory)
1557                .await
1558                .unwrap();
1559            upsert_setting(&context.pool, &key, junk).await.unwrap();
1560
1561            let result = increment_setting_with_mode(&key, DatabaseMode::Memory).await;
1562            assert!(
1563                result.is_err(),
1564                "a junk counter value ({junk}) must error instead of silently incrementing from a CAST-to-0"
1565            );
1566
1567            let value = get_setting_with_mode(&key, DatabaseMode::Memory)
1568                .await
1569                .unwrap();
1570            assert_eq!(
1571                value,
1572                Some(junk.to_string()),
1573                "a rejected increment must leave the junk value ({junk}) untouched"
1574            );
1575
1576            let _ = delete_setting_with_mode(&key, DatabaseMode::Memory).await;
1577        }
1578    }
1579
1580    #[tokio::test]
1581    async fn test_increment_setting_with_mode_increments_a_numeric_value_memory_mode() {
1582        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1583
1584        let key = "numeric_counter_test:memory";
1585        let _ = delete_setting_with_mode(key, DatabaseMode::Memory).await;
1586
1587        let first = increment_setting_with_mode(key, DatabaseMode::Memory)
1588            .await
1589            .unwrap();
1590        assert_eq!(first, 1);
1591
1592        let second = increment_setting_with_mode(key, DatabaseMode::Memory)
1593            .await
1594            .unwrap();
1595        assert_eq!(second, 2);
1596
1597        let _ = delete_setting_with_mode(key, DatabaseMode::Memory).await;
1598    }
1599
1600    #[tokio::test]
1601    async fn test_increment_setting_with_mode_rejects_an_already_maxed_counter_memory_mode() {
1602        let _lock = crate::test_utils::MEMORY_MODE_TEST_MUTEX.lock().await;
1603
1604        let key = "maxed_counter_test:memory";
1605        let _ = delete_setting_with_mode(key, DatabaseMode::Memory).await;
1606
1607        let context = DatabaseManager::get_context(DatabaseMode::Memory)
1608            .await
1609            .unwrap();
1610        upsert_setting(&context.pool, key, &u32::MAX.to_string())
1611            .await
1612            .unwrap();
1613
1614        let result = increment_setting_with_mode(key, DatabaseMode::Memory).await;
1615        assert!(
1616            result.is_err(),
1617            "a counter already at u32::MAX must be rejected instead of overflowing on increment"
1618        );
1619
1620        let value = get_setting_with_mode(key, DatabaseMode::Memory)
1621            .await
1622            .unwrap();
1623        assert_eq!(
1624            value,
1625            Some(u32::MAX.to_string()),
1626            "a rejected increment must leave the maxed counter untouched"
1627        );
1628
1629        let _ = delete_setting_with_mode(key, DatabaseMode::Memory).await;
1630    }
1631}