1use std::path::PathBuf;
2
3use rusqlite::Connection;
4use thiserror::Error;
5
6use crate::config::HawkConfig;
7use crate::config_engine::LayeredConfig;
8use crate::db::init_database;
9use crate::platform::PlatformConfig;
10
11#[derive(Debug, Error)]
12pub enum DaemonError {
13 #[error("database error: {0}")]
14 Database(String),
15 #[error("config error: {0}")]
16 Config(String),
17}
18
19impl From<crate::error::HawkError> for DaemonError {
20 fn from(e: crate::error::HawkError) -> Self {
21 match e {
22 crate::error::HawkError::Database(msg) => DaemonError::Database(msg),
23 crate::error::HawkError::Config(msg) => DaemonError::Config(msg),
24 other => DaemonError::Config(other.to_string()),
25 }
26 }
27}
28
29pub struct DaemonContext {
30 pub db_path: PathBuf,
31 pub config: HawkConfig,
32 pub platform: PlatformConfig,
33}
34
35impl DaemonContext {
36 pub fn initialize(db_path: PathBuf) -> Result<Self, DaemonError> {
37 let platform = PlatformConfig::detect();
38
39 let layered = LayeredConfig::load(None)
40 .map_err(|e| DaemonError::Config(e.to_string()))?;
41 let config = layered.merged();
42
43 init_database(&db_path).map_err(|e| DaemonError::Database(e.to_string()))?;
44
45 Ok(Self { db_path, config, platform })
46 }
47
48 pub fn db(&self) -> Result<Connection, DaemonError> {
49 init_database(&self.db_path).map_err(|e| DaemonError::Database(e.to_string()))
50 }
51
52 pub fn is_air_gapped(&self) -> bool {
53 self.config.privacy.mode == "air-gapped"
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use tempfile::NamedTempFile;
61
62 #[test]
63 fn initialize_creates_db_and_returns_context() {
64 let f = NamedTempFile::new().unwrap();
65 let ctx = DaemonContext::initialize(f.path().to_path_buf()).unwrap();
66 assert_eq!(ctx.db_path, f.path());
67 }
68
69 #[test]
70 fn db_returns_usable_connection() {
71 let f = NamedTempFile::new().unwrap();
72 let ctx = DaemonContext::initialize(f.path().to_path_buf()).unwrap();
73 let conn = ctx.db().unwrap();
74 let count: i64 = conn.query_row(
75 "SELECT COUNT(*) FROM sqlite_master WHERE type='table'",
76 [], |row| row.get(0),
77 ).unwrap();
78 assert!(count > 0);
79 }
80
81 #[test]
82 fn is_air_gapped_false_by_default() {
83 let f = NamedTempFile::new().unwrap();
84 let mut ctx = DaemonContext::initialize(f.path().to_path_buf()).unwrap();
85 ctx.config.privacy.mode = "standard".to_string();
87 assert!(!ctx.is_air_gapped());
88 }
89
90 #[test]
91 fn is_air_gapped_true_when_mode_set() {
92 let f = NamedTempFile::new().unwrap();
93 let mut ctx = DaemonContext::initialize(f.path().to_path_buf()).unwrap();
94 ctx.config.privacy.mode = "air-gapped".to_string();
95 assert!(ctx.is_air_gapped());
96 }
97
98 #[test]
99 fn platform_detected_on_initialize() {
100 let f = NamedTempFile::new().unwrap();
101 let ctx = DaemonContext::initialize(f.path().to_path_buf()).unwrap();
102 let _ = &ctx.platform.os;
103 let _ = &ctx.platform.arch;
104 }
105}