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 CertRenewed {
35 expires_at: chrono::DateTime<chrono::Utc>,
36 },
37 CertExpiringSoon {
39 days_left: i64,
40 },
41 CertRenewalFailed {
43 reason: String,
44 consecutive_failures: u32,
45 },
46 BundleUpdated {
48 self_revoked: bool,
49 },
50 PostureChanged {
55 from: Posture,
56 to: Posture,
57 },
58 ProxyEntryUpdated {
59 entry: ProxyEntry,
60 },
61 ProxyEntryRemoved {
62 name: String,
63 },
64 RuntimeInstanceStarted {
65 name: String,
66 backend: String,
67 },
68 RuntimeInstanceStopped {
69 name: String,
70 },
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::collections::HashMap;
77
78 fn sample_record() -> ServiceRecord {
79 ServiceRecord {
80 name: "My App".to_string(),
81 service_type: "_http._tcp".to_string(),
82 host: Some("server.local".to_string()),
83 ip: Some("192.168.1.42".to_string()),
84 port: Some(8080),
85 txt: HashMap::new(),
86 }
87 }
88
89 #[test]
90 fn mdns_found_variant_construction() {
91 let event = KoiEvent::MdnsFound(sample_record());
92 assert!(matches!(event, KoiEvent::MdnsFound(ref r) if r.name == "My App"));
93 }
94
95 #[test]
96 fn mdns_resolved_variant_construction() {
97 let event = KoiEvent::MdnsResolved(sample_record());
98 assert!(matches!(event, KoiEvent::MdnsResolved(ref r) if r.port == Some(8080)));
99 }
100
101 #[test]
102 fn mdns_removed_variant_construction() {
103 let event = KoiEvent::MdnsRemoved {
104 name: "Old Service".to_string(),
105 service_type: "_http._tcp".to_string(),
106 };
107 assert!(matches!(event, KoiEvent::MdnsRemoved { ref name, .. } if name == "Old Service"));
108 }
109
110 #[test]
111 fn dns_entry_updated_variant() {
112 let event = KoiEvent::DnsEntryUpdated {
113 name: "grafana".to_string(),
114 ip: "10.0.0.5".to_string(),
115 };
116 assert!(
117 matches!(event, KoiEvent::DnsEntryUpdated { ref name, ref ip } if name == "grafana" && ip == "10.0.0.5")
118 );
119 }
120
121 #[test]
122 fn dns_entry_removed_variant() {
123 let event = KoiEvent::DnsEntryRemoved {
124 name: "grafana".to_string(),
125 };
126 assert!(matches!(event, KoiEvent::DnsEntryRemoved { ref name } if name == "grafana"));
127 }
128
129 #[test]
130 fn health_changed_variant() {
131 let event = KoiEvent::HealthChanged {
132 name: "web-api".to_string(),
133 status: HealthStatus::Up,
134 };
135 assert!(
136 matches!(event, KoiEvent::HealthChanged { ref name, status: HealthStatus::Up } if name == "web-api")
137 );
138 }
139
140 #[test]
141 fn certmesh_member_joined_variant() {
142 let event = KoiEvent::CertmeshMemberJoined {
143 hostname: "node1".to_string(),
144 fingerprint: "abc123".to_string(),
145 };
146 assert!(
147 matches!(event, KoiEvent::CertmeshMemberJoined { ref hostname, .. } if hostname == "node1")
148 );
149 }
150
151 #[test]
152 fn certmesh_member_revoked_variant() {
153 let event = KoiEvent::CertmeshMemberRevoked {
154 hostname: "node2".to_string(),
155 };
156 assert!(
157 matches!(event, KoiEvent::CertmeshMemberRevoked { ref hostname } if hostname == "node2")
158 );
159 }
160
161 #[test]
162 fn certmesh_destroyed_variant() {
163 let event = KoiEvent::CertmeshDestroyed;
164 assert!(matches!(event, KoiEvent::CertmeshDestroyed));
165 }
166
167 #[test]
168 fn posture_changed_variant() {
169 let event = KoiEvent::PostureChanged {
170 from: Posture::OPEN,
171 to: Posture::new(true, false),
172 };
173 assert!(
174 matches!(event, KoiEvent::PostureChanged { from, to } if !from.signed && to.signed)
175 );
176 }
177
178 #[test]
179 fn proxy_entry_updated_variant() {
180 let entry = ProxyEntry {
181 name: "grafana".to_string(),
182 listen_port: 443,
183 backend: "http://localhost:3000".to_string(),
184 allow_remote: false,
185 };
186 let event = KoiEvent::ProxyEntryUpdated {
187 entry: entry.clone(),
188 };
189 assert!(
190 matches!(event, KoiEvent::ProxyEntryUpdated { ref entry } if entry.name == "grafana")
191 );
192 }
193
194 #[test]
195 fn proxy_entry_removed_variant() {
196 let event = KoiEvent::ProxyEntryRemoved {
197 name: "grafana".to_string(),
198 };
199 assert!(matches!(event, KoiEvent::ProxyEntryRemoved { ref name } if name == "grafana"));
200 }
201
202 #[test]
203 fn runtime_instance_started_variant() {
204 let event = KoiEvent::RuntimeInstanceStarted {
205 name: "nginx".to_string(),
206 backend: "docker".to_string(),
207 };
208 assert!(
209 matches!(event, KoiEvent::RuntimeInstanceStarted { ref name, ref backend } if name == "nginx" && backend == "docker")
210 );
211 }
212
213 #[test]
214 fn runtime_instance_stopped_variant() {
215 let event = KoiEvent::RuntimeInstanceStopped {
216 name: "nginx".to_string(),
217 };
218 assert!(matches!(event, KoiEvent::RuntimeInstanceStopped { ref name } if name == "nginx"));
219 }
220
221 #[test]
222 fn clone_preserves_data() {
223 let event = KoiEvent::MdnsFound(sample_record());
224 let cloned = event.clone();
225 match (&event, &cloned) {
226 (KoiEvent::MdnsFound(a), KoiEvent::MdnsFound(b)) => {
227 assert_eq!(a.name, b.name);
228 assert_eq!(a.port, b.port);
229 assert_eq!(a.service_type, b.service_type);
230 }
231 _ => panic!("clone should preserve variant"),
232 }
233 }
234
235 #[test]
236 fn debug_does_not_panic() {
237 let events = vec![
238 KoiEvent::MdnsFound(sample_record()),
239 KoiEvent::MdnsRemoved {
240 name: "x".to_string(),
241 service_type: "y".to_string(),
242 },
243 KoiEvent::DnsEntryUpdated {
244 name: "a".to_string(),
245 ip: "1.2.3.4".to_string(),
246 },
247 KoiEvent::DnsEntryRemoved {
248 name: "a".to_string(),
249 },
250 KoiEvent::HealthChanged {
251 name: "svc".to_string(),
252 status: HealthStatus::Down,
253 },
254 KoiEvent::CertmeshMemberJoined {
255 hostname: "h".to_string(),
256 fingerprint: "f".to_string(),
257 },
258 KoiEvent::CertmeshMemberRevoked {
259 hostname: "h".to_string(),
260 },
261 KoiEvent::CertmeshDestroyed,
262 KoiEvent::CertRenewed {
263 expires_at: chrono::Utc::now(),
264 },
265 KoiEvent::CertExpiringSoon { days_left: 3 },
266 KoiEvent::CertRenewalFailed {
267 reason: "timeout".to_string(),
268 consecutive_failures: 2,
269 },
270 KoiEvent::BundleUpdated {
271 self_revoked: false,
272 },
273 KoiEvent::PostureChanged {
274 from: Posture::OPEN,
275 to: Posture::new(true, true),
276 },
277 KoiEvent::ProxyEntryUpdated {
278 entry: ProxyEntry {
279 name: "p".to_string(),
280 listen_port: 443,
281 backend: "http://localhost".to_string(),
282 allow_remote: false,
283 },
284 },
285 KoiEvent::ProxyEntryRemoved {
286 name: "p".to_string(),
287 },
288 KoiEvent::RuntimeInstanceStarted {
289 name: "web".to_string(),
290 backend: "docker".to_string(),
291 },
292 KoiEvent::RuntimeInstanceStopped {
293 name: "web".to_string(),
294 },
295 ];
296 for event in &events {
297 let _ = format!("{event:?}");
298 }
299 }
300}