Skip to main content

koi_embedded/
events.rs

1use koi_common::posture::Posture;
2use koi_common::types::ServiceRecord;
3use koi_health::HealthStatus;
4use koi_proxy::ProxyEntry;
5
6#[derive(Debug, Clone)]
7pub enum KoiEvent {
8    MdnsFound(ServiceRecord),
9    MdnsResolved(ServiceRecord),
10    MdnsRemoved {
11        name: String,
12        service_type: String,
13    },
14    DnsEntryUpdated {
15        name: String,
16        ip: String,
17    },
18    DnsEntryRemoved {
19        name: String,
20    },
21    HealthChanged {
22        name: String,
23        status: HealthStatus,
24    },
25    CertmeshMemberJoined {
26        hostname: String,
27        fingerprint: String,
28    },
29    CertmeshMemberRevoked {
30        hostname: String,
31    },
32    CertmeshDestroyed,
33    /// This node's trust posture changed (ADR-020 §5/§13). Emitted on every
34    /// Open↔Authenticated transition. The **degrade** direction (identity lost →
35    /// fell back to Open) is surfaced as loudly as the upgrade — exactly where
36    /// silent expiry/fallback loses operators.
37    PostureChanged {
38        from: Posture,
39        to: Posture,
40    },
41    ProxyEntryUpdated {
42        entry: ProxyEntry,
43    },
44    ProxyEntryRemoved {
45        name: String,
46    },
47    RuntimeInstanceStarted {
48        name: String,
49        backend: String,
50    },
51    RuntimeInstanceStopped {
52        name: String,
53    },
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use std::collections::HashMap;
60
61    fn sample_record() -> ServiceRecord {
62        ServiceRecord {
63            name: "My App".to_string(),
64            service_type: "_http._tcp".to_string(),
65            host: Some("server.local".to_string()),
66            ip: Some("192.168.1.42".to_string()),
67            port: Some(8080),
68            txt: HashMap::new(),
69        }
70    }
71
72    #[test]
73    fn mdns_found_variant_construction() {
74        let event = KoiEvent::MdnsFound(sample_record());
75        assert!(matches!(event, KoiEvent::MdnsFound(ref r) if r.name == "My App"));
76    }
77
78    #[test]
79    fn mdns_resolved_variant_construction() {
80        let event = KoiEvent::MdnsResolved(sample_record());
81        assert!(matches!(event, KoiEvent::MdnsResolved(ref r) if r.port == Some(8080)));
82    }
83
84    #[test]
85    fn mdns_removed_variant_construction() {
86        let event = KoiEvent::MdnsRemoved {
87            name: "Old Service".to_string(),
88            service_type: "_http._tcp".to_string(),
89        };
90        assert!(matches!(event, KoiEvent::MdnsRemoved { ref name, .. } if name == "Old Service"));
91    }
92
93    #[test]
94    fn dns_entry_updated_variant() {
95        let event = KoiEvent::DnsEntryUpdated {
96            name: "grafana".to_string(),
97            ip: "10.0.0.5".to_string(),
98        };
99        assert!(
100            matches!(event, KoiEvent::DnsEntryUpdated { ref name, ref ip } if name == "grafana" && ip == "10.0.0.5")
101        );
102    }
103
104    #[test]
105    fn dns_entry_removed_variant() {
106        let event = KoiEvent::DnsEntryRemoved {
107            name: "grafana".to_string(),
108        };
109        assert!(matches!(event, KoiEvent::DnsEntryRemoved { ref name } if name == "grafana"));
110    }
111
112    #[test]
113    fn health_changed_variant() {
114        let event = KoiEvent::HealthChanged {
115            name: "web-api".to_string(),
116            status: HealthStatus::Up,
117        };
118        assert!(
119            matches!(event, KoiEvent::HealthChanged { ref name, status: HealthStatus::Up } if name == "web-api")
120        );
121    }
122
123    #[test]
124    fn certmesh_member_joined_variant() {
125        let event = KoiEvent::CertmeshMemberJoined {
126            hostname: "node1".to_string(),
127            fingerprint: "abc123".to_string(),
128        };
129        assert!(
130            matches!(event, KoiEvent::CertmeshMemberJoined { ref hostname, .. } if hostname == "node1")
131        );
132    }
133
134    #[test]
135    fn certmesh_member_revoked_variant() {
136        let event = KoiEvent::CertmeshMemberRevoked {
137            hostname: "node2".to_string(),
138        };
139        assert!(
140            matches!(event, KoiEvent::CertmeshMemberRevoked { ref hostname } if hostname == "node2")
141        );
142    }
143
144    #[test]
145    fn certmesh_destroyed_variant() {
146        let event = KoiEvent::CertmeshDestroyed;
147        assert!(matches!(event, KoiEvent::CertmeshDestroyed));
148    }
149
150    #[test]
151    fn posture_changed_variant() {
152        let event = KoiEvent::PostureChanged {
153            from: Posture::OPEN,
154            to: Posture::new(true, false),
155        };
156        assert!(
157            matches!(event, KoiEvent::PostureChanged { from, to } if !from.signed && to.signed)
158        );
159    }
160
161    #[test]
162    fn proxy_entry_updated_variant() {
163        let entry = ProxyEntry {
164            name: "grafana".to_string(),
165            listen_port: 443,
166            backend: "http://localhost:3000".to_string(),
167            allow_remote: false,
168        };
169        let event = KoiEvent::ProxyEntryUpdated {
170            entry: entry.clone(),
171        };
172        assert!(
173            matches!(event, KoiEvent::ProxyEntryUpdated { ref entry } if entry.name == "grafana")
174        );
175    }
176
177    #[test]
178    fn proxy_entry_removed_variant() {
179        let event = KoiEvent::ProxyEntryRemoved {
180            name: "grafana".to_string(),
181        };
182        assert!(matches!(event, KoiEvent::ProxyEntryRemoved { ref name } if name == "grafana"));
183    }
184
185    #[test]
186    fn runtime_instance_started_variant() {
187        let event = KoiEvent::RuntimeInstanceStarted {
188            name: "nginx".to_string(),
189            backend: "docker".to_string(),
190        };
191        assert!(
192            matches!(event, KoiEvent::RuntimeInstanceStarted { ref name, ref backend } if name == "nginx" && backend == "docker")
193        );
194    }
195
196    #[test]
197    fn runtime_instance_stopped_variant() {
198        let event = KoiEvent::RuntimeInstanceStopped {
199            name: "nginx".to_string(),
200        };
201        assert!(matches!(event, KoiEvent::RuntimeInstanceStopped { ref name } if name == "nginx"));
202    }
203
204    #[test]
205    fn clone_preserves_data() {
206        let event = KoiEvent::MdnsFound(sample_record());
207        let cloned = event.clone();
208        match (&event, &cloned) {
209            (KoiEvent::MdnsFound(a), KoiEvent::MdnsFound(b)) => {
210                assert_eq!(a.name, b.name);
211                assert_eq!(a.port, b.port);
212                assert_eq!(a.service_type, b.service_type);
213            }
214            _ => panic!("clone should preserve variant"),
215        }
216    }
217
218    #[test]
219    fn debug_does_not_panic() {
220        let events = vec![
221            KoiEvent::MdnsFound(sample_record()),
222            KoiEvent::MdnsRemoved {
223                name: "x".to_string(),
224                service_type: "y".to_string(),
225            },
226            KoiEvent::DnsEntryUpdated {
227                name: "a".to_string(),
228                ip: "1.2.3.4".to_string(),
229            },
230            KoiEvent::DnsEntryRemoved {
231                name: "a".to_string(),
232            },
233            KoiEvent::HealthChanged {
234                name: "svc".to_string(),
235                status: HealthStatus::Down,
236            },
237            KoiEvent::CertmeshMemberJoined {
238                hostname: "h".to_string(),
239                fingerprint: "f".to_string(),
240            },
241            KoiEvent::CertmeshMemberRevoked {
242                hostname: "h".to_string(),
243            },
244            KoiEvent::CertmeshDestroyed,
245            KoiEvent::PostureChanged {
246                from: Posture::OPEN,
247                to: Posture::new(true, true),
248            },
249            KoiEvent::ProxyEntryUpdated {
250                entry: ProxyEntry {
251                    name: "p".to_string(),
252                    listen_port: 443,
253                    backend: "http://localhost".to_string(),
254                    allow_remote: false,
255                },
256            },
257            KoiEvent::ProxyEntryRemoved {
258                name: "p".to_string(),
259            },
260            KoiEvent::RuntimeInstanceStarted {
261                name: "web".to_string(),
262                backend: "docker".to_string(),
263            },
264            KoiEvent::RuntimeInstanceStopped {
265                name: "web".to_string(),
266            },
267        ];
268        for event in &events {
269            let _ = format!("{event:?}");
270        }
271    }
272}