Skip to main content

openhawk_core/
daemon.rs

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        // force standard mode regardless of what ~/.hawk/config.toml says
86        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}