Skip to main content

koi_embedded/
config.rs

1use std::net::IpAddr;
2use std::path::PathBuf;
3
4use koi_common::firewall::{FirewallPort, FirewallProtocol};
5use koi_dns::DnsConfig;
6use koi_runtime::RuntimeBackendKind;
7
8#[derive(Debug, Clone)]
9pub struct KoiConfig {
10    pub data_dir: Option<PathBuf>,
11    pub service_endpoint: String,
12    pub service_mode: ServiceMode,
13    pub http_enabled: bool,
14    pub mdns_enabled: bool,
15    pub dns_enabled: bool,
16    pub health_enabled: bool,
17    pub certmesh_enabled: bool,
18    pub proxy_enabled: bool,
19    pub udp_enabled: bool,
20    pub runtime_enabled: bool,
21    pub runtime_backend: RuntimeBackendKind,
22    pub http_port: u16,
23    pub dashboard_enabled: bool,
24    pub api_docs_enabled: bool,
25    pub mdns_browser_enabled: bool,
26    pub announce_http: bool,
27    pub dns_config: DnsConfig,
28    pub dns_auto_start: bool,
29    pub health_auto_start: bool,
30    pub proxy_auto_start: bool,
31}
32
33impl KoiConfig {
34    /// Collect firewall ports required by the currently-enabled capabilities.
35    ///
36    /// This mirrors the logic in the standalone Koi daemon's
37    /// `firewall_ports_for_config`, but derives from the embedded config.
38    pub fn firewall_ports(&self) -> Vec<FirewallPort> {
39        use std::collections::HashSet;
40
41        let mut ports = Vec::new();
42        if self.mdns_enabled {
43            ports.extend(koi_mdns::firewall_ports());
44        }
45        if self.http_enabled {
46            ports.push(FirewallPort::new(
47                "HTTP",
48                FirewallProtocol::Tcp,
49                self.http_port,
50            ));
51        }
52        if self.dns_enabled {
53            ports.extend(koi_dns::firewall_ports(&self.dns_config));
54        }
55
56        // Deduplicate by (protocol, port)
57        let mut seen = HashSet::new();
58        ports
59            .into_iter()
60            .filter(|p| seen.insert((p.protocol, p.port)))
61            .collect()
62    }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ServiceMode {
67    Auto,
68    EmbeddedOnly,
69    ClientOnly,
70}
71
72impl Default for KoiConfig {
73    fn default() -> Self {
74        Self {
75            data_dir: None,
76            service_endpoint: "http://127.0.0.1:5641".to_string(),
77            service_mode: ServiceMode::Auto,
78            http_enabled: false,
79            mdns_enabled: true,
80            dns_enabled: true,
81            health_enabled: false,
82            certmesh_enabled: false,
83            proxy_enabled: false,
84            udp_enabled: false,
85            runtime_enabled: false,
86            runtime_backend: RuntimeBackendKind::Auto,
87            http_port: 5641,
88            dashboard_enabled: false,
89            api_docs_enabled: false,
90            mdns_browser_enabled: false,
91            announce_http: false,
92            dns_config: DnsConfig::default(),
93            dns_auto_start: false,
94            health_auto_start: false,
95            proxy_auto_start: false,
96        }
97    }
98}
99
100pub struct DnsConfigBuilder {
101    config: DnsConfig,
102}
103
104impl DnsConfigBuilder {
105    pub fn new(config: DnsConfig) -> Self {
106        Self { config }
107    }
108
109    pub fn bind_addr(mut self, addr: IpAddr) -> Self {
110        self.config.bind_addr = addr;
111        self
112    }
113
114    pub fn port(mut self, port: u16) -> Self {
115        self.config.port = port;
116        self
117    }
118
119    pub fn zone(mut self, zone: impl Into<String>) -> Self {
120        self.config.zone = zone.into();
121        self
122    }
123
124    pub fn local_ttl(mut self, ttl: u32) -> Self {
125        self.config.local_ttl = ttl;
126        self
127    }
128
129    pub fn allow_public_clients(mut self, allow: bool) -> Self {
130        self.config.allow_public_clients = allow;
131        self
132    }
133
134    pub fn max_qps(mut self, max_qps: u32) -> Self {
135        self.config.max_qps = max_qps;
136        self
137    }
138
139    pub fn local_zone(mut self, enabled: bool) -> Self {
140        self.config.local_zone = enabled;
141        self
142    }
143
144    pub fn build(self) -> DnsConfig {
145        self.config
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::net::{IpAddr, Ipv4Addr};
153
154    // ── KoiConfig defaults ─────────────────────────────────────────
155
156    #[test]
157    fn default_config_has_expected_values() {
158        let cfg = KoiConfig::default();
159        assert_eq!(cfg.service_endpoint, "http://127.0.0.1:5641");
160        assert_eq!(cfg.service_mode, ServiceMode::Auto);
161        assert!(!cfg.http_enabled);
162        assert!(cfg.mdns_enabled);
163        assert!(cfg.dns_enabled);
164        assert!(!cfg.health_enabled);
165        assert!(!cfg.certmesh_enabled);
166        assert!(!cfg.proxy_enabled);
167        assert!(!cfg.udp_enabled);
168        assert!(!cfg.runtime_enabled);
169        assert_eq!(cfg.runtime_backend, RuntimeBackendKind::Auto);
170        assert_eq!(cfg.http_port, 5641);
171        assert!(!cfg.dashboard_enabled);
172        assert!(!cfg.api_docs_enabled);
173        assert!(!cfg.mdns_browser_enabled);
174        assert!(!cfg.announce_http);
175        assert!(!cfg.dns_auto_start);
176        assert!(!cfg.health_auto_start);
177        assert!(!cfg.proxy_auto_start);
178        assert!(cfg.data_dir.is_none());
179    }
180
181    #[test]
182    fn default_config_clone_is_equal() {
183        let cfg = KoiConfig::default();
184        let cloned = cfg.clone();
185        assert_eq!(cfg.http_port, cloned.http_port);
186        assert_eq!(cfg.mdns_enabled, cloned.mdns_enabled);
187        assert_eq!(cfg.service_endpoint, cloned.service_endpoint);
188    }
189
190    #[test]
191    fn default_config_debug_does_not_panic() {
192        let cfg = KoiConfig::default();
193        let debug = format!("{cfg:?}");
194        assert!(debug.contains("KoiConfig"));
195    }
196
197    // ── Firewall ports ─────────────────────────────────────────────
198
199    #[test]
200    fn firewall_ports_includes_http_when_enabled() {
201        let mut cfg = KoiConfig::default();
202        cfg.http_enabled = true;
203        cfg.mdns_enabled = false;
204        cfg.dns_enabled = false;
205        let ports = cfg.firewall_ports();
206        assert!(
207            ports.iter().any(|p| p.port == 5641),
208            "expected HTTP port 5641 in firewall ports"
209        );
210    }
211
212    #[test]
213    fn firewall_ports_respects_custom_http_port() {
214        let mut cfg = KoiConfig::default();
215        cfg.http_enabled = true;
216        cfg.http_port = 9999;
217        cfg.mdns_enabled = false;
218        cfg.dns_enabled = false;
219        let ports = cfg.firewall_ports();
220        assert!(
221            ports.iter().any(|p| p.port == 9999),
222            "expected custom HTTP port 9999"
223        );
224        assert!(
225            !ports.iter().any(|p| p.port == 5641),
226            "should not have default port when overridden"
227        );
228    }
229
230    #[test]
231    fn firewall_ports_empty_when_all_disabled() {
232        let mut cfg = KoiConfig::default();
233        cfg.http_enabled = false;
234        cfg.mdns_enabled = false;
235        cfg.dns_enabled = false;
236        let ports = cfg.firewall_ports();
237        assert!(ports.is_empty(), "all disabled should yield no ports");
238    }
239
240    #[test]
241    fn firewall_ports_deduplicates() {
242        let mut cfg = KoiConfig::default();
243        // DNS default port is 53 (TCP+UDP), mDNS is 5353 (UDP).
244        // With both enabled we should not have duplicate (protocol, port) pairs.
245        cfg.http_enabled = false;
246        cfg.mdns_enabled = true;
247        cfg.dns_enabled = true;
248        let ports = cfg.firewall_ports();
249        let mut seen = std::collections::HashSet::new();
250        for p in &ports {
251            assert!(
252                seen.insert((p.protocol, p.port)),
253                "duplicate firewall port: {:?} {}",
254                p.protocol,
255                p.port
256            );
257        }
258    }
259
260    // ── ServiceMode ────────────────────────────────────────────────
261
262    #[test]
263    fn service_mode_equality() {
264        assert_eq!(ServiceMode::Auto, ServiceMode::Auto);
265        assert_eq!(ServiceMode::EmbeddedOnly, ServiceMode::EmbeddedOnly);
266        assert_eq!(ServiceMode::ClientOnly, ServiceMode::ClientOnly);
267        assert_ne!(ServiceMode::Auto, ServiceMode::EmbeddedOnly);
268        assert_ne!(ServiceMode::Auto, ServiceMode::ClientOnly);
269    }
270
271    #[test]
272    fn service_mode_is_copy() {
273        let mode = ServiceMode::Auto;
274        let copy = mode;
275        assert_eq!(mode, copy);
276    }
277
278    #[test]
279    fn service_mode_debug() {
280        let debug = format!("{:?}", ServiceMode::EmbeddedOnly);
281        assert!(debug.contains("EmbeddedOnly"));
282    }
283
284    // ── DnsConfigBuilder ───────────────────────────────────────────
285
286    #[test]
287    fn dns_config_builder_defaults_match_dns_config() {
288        let dns_default = DnsConfig::default();
289        let built = DnsConfigBuilder::new(DnsConfig::default()).build();
290        assert_eq!(built.port, dns_default.port);
291        assert_eq!(built.zone, dns_default.zone);
292        assert_eq!(built.local_ttl, dns_default.local_ttl);
293        assert_eq!(built.allow_public_clients, dns_default.allow_public_clients);
294        assert_eq!(built.max_qps, dns_default.max_qps);
295        assert_eq!(built.local_zone, dns_default.local_zone);
296    }
297
298    #[test]
299    fn dns_config_builder_port() {
300        let cfg = DnsConfigBuilder::new(DnsConfig::default())
301            .port(5353)
302            .build();
303        assert_eq!(cfg.port, 5353);
304    }
305
306    #[test]
307    fn dns_config_builder_bind_addr() {
308        let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
309        let cfg = DnsConfigBuilder::new(DnsConfig::default())
310            .bind_addr(addr)
311            .build();
312        assert_eq!(cfg.bind_addr, addr);
313    }
314
315    #[test]
316    fn dns_config_builder_zone() {
317        let cfg = DnsConfigBuilder::new(DnsConfig::default())
318            .zone("home")
319            .build();
320        assert_eq!(cfg.zone, "home");
321    }
322
323    #[test]
324    fn dns_config_builder_local_ttl() {
325        let cfg = DnsConfigBuilder::new(DnsConfig::default())
326            .local_ttl(120)
327            .build();
328        assert_eq!(cfg.local_ttl, 120);
329    }
330
331    #[test]
332    fn dns_config_builder_allow_public_clients() {
333        let cfg = DnsConfigBuilder::new(DnsConfig::default())
334            .allow_public_clients(true)
335            .build();
336        assert!(cfg.allow_public_clients);
337    }
338
339    #[test]
340    fn dns_config_builder_max_qps() {
341        let cfg = DnsConfigBuilder::new(DnsConfig::default())
342            .max_qps(500)
343            .build();
344        assert_eq!(cfg.max_qps, 500);
345    }
346
347    #[test]
348    fn dns_config_builder_local_zone() {
349        let cfg = DnsConfigBuilder::new(DnsConfig::default())
350            .local_zone(false)
351            .build();
352        assert!(!cfg.local_zone);
353    }
354
355    #[test]
356    fn dns_config_builder_chaining() {
357        let cfg = DnsConfigBuilder::new(DnsConfig::default())
358            .port(5353)
359            .zone("office")
360            .local_ttl(300)
361            .allow_public_clients(true)
362            .max_qps(1000)
363            .local_zone(false)
364            .build();
365        assert_eq!(cfg.port, 5353);
366        assert_eq!(cfg.zone, "office");
367        assert_eq!(cfg.local_ttl, 300);
368        assert!(cfg.allow_public_clients);
369        assert_eq!(cfg.max_qps, 1000);
370        assert!(!cfg.local_zone);
371    }
372}