ironpost_core/
plugin.rs

1//! 플러그인 시스템 — 모듈 등록, 생명주기 관리, 동적 확장
2//!
3//! [`Plugin`] trait은 [`Pipeline`](crate::pipeline::Pipeline)의 상위 추상화로,
4//! 모듈 메타데이터와 초기화 단계를 추가합니다.
5//!
6//! [`PluginRegistry`]는 플러그인의 등록, 해제, 생명주기 관리를 담당합니다.
7//!
8//! # 생명주기
9//! ```text
10//! Created → init() → Initialized → start() → Running → stop() → Stopped
11//! ```
12
13use std::fmt;
14use std::future::Future;
15
16use serde::{Deserialize, Serialize};
17
18use crate::error::{IronpostError, PluginError};
19use crate::pipeline::{BoxFuture, HealthStatus};
20
21// ─── PluginType ──────────────────────────────────────────────────────
22
23/// 플러그인 유형
24///
25/// 기본 제공 모듈 유형과 사용자 정의 유형을 구분합니다.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum PluginType {
28    /// 네트워크 탐지 (eBPF 등)
29    Detector,
30    /// 로그 수집/분석 파이프라인
31    LogPipeline,
32    /// SBOM/취약점 스캐너
33    Scanner,
34    /// 컨테이너 격리/정책 집행
35    Enforcer,
36    /// 사용자 정의 플러그인
37    Custom(String),
38}
39
40impl fmt::Display for PluginType {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Detector => write!(f, "detector"),
44            Self::LogPipeline => write!(f, "log-pipeline"),
45            Self::Scanner => write!(f, "scanner"),
46            Self::Enforcer => write!(f, "enforcer"),
47            Self::Custom(name) => write!(f, "custom:{name}"),
48        }
49    }
50}
51
52// ─── PluginInfo ──────────────────────────────────────────────────────
53
54/// 플러그인 메타데이터
55///
56/// 플러그인 등록 시 고유 이름, 버전, 설명, 유형 정보를 제공합니다.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PluginInfo {
59    /// 플러그인 고유 이름 (예: `"ebpf-engine"`)
60    pub name: String,
61    /// 플러그인 버전 (semver, 예: `"0.1.0"`)
62    pub version: String,
63    /// 플러그인 설명
64    pub description: String,
65    /// 플러그인 유형
66    pub plugin_type: PluginType,
67}
68
69// ─── PluginState ─────────────────────────────────────────────────────
70
71/// 플러그인 생명주기 상태
72///
73/// 상태 전환:
74/// - `Created` → `init()` → `Initialized`
75/// - `Initialized` → `start()` → `Running`
76/// - `Running` → `stop()` → `Stopped`
77/// - 에러 발생 시 → `Failed`
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum PluginState {
80    /// 생성됨 (init 전)
81    Created,
82    /// 초기화 완료 (start 가능)
83    Initialized,
84    /// 실행 중
85    Running,
86    /// 정지됨
87    Stopped,
88    /// 오류 상태
89    Failed,
90}
91
92impl fmt::Display for PluginState {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::Created => write!(f, "created"),
96            Self::Initialized => write!(f, "initialized"),
97            Self::Running => write!(f, "running"),
98            Self::Stopped => write!(f, "stopped"),
99            Self::Failed => write!(f, "failed"),
100        }
101    }
102}
103
104// ─── Plugin Trait ────────────────────────────────────────────────────
105
106/// 모든 모듈이 구현하는 플러그인 trait
107///
108/// [`Pipeline`](crate::pipeline::Pipeline)의 상위 추상화로,
109/// 메타데이터 조회와 초기화 단계를 추가합니다.
110///
111/// # 생명주기
112/// ```text
113/// Created → init() → Initialized → start() → Running → stop() → Stopped
114/// ```
115///
116/// # 구현 예시
117/// ```ignore
118/// struct MyPlugin {
119///     info: PluginInfo,
120///     state: PluginState,
121/// }
122///
123/// impl Plugin for MyPlugin {
124///     fn info(&self) -> &PluginInfo { &self.info }
125///     fn state(&self) -> PluginState { self.state }
126///
127///     async fn init(&mut self) -> Result<(), IronpostError> {
128///         self.state = PluginState::Initialized;
129///         Ok(())
130///     }
131///     async fn start(&mut self) -> Result<(), IronpostError> {
132///         self.state = PluginState::Running;
133///         Ok(())
134///     }
135///     async fn stop(&mut self) -> Result<(), IronpostError> {
136///         self.state = PluginState::Stopped;
137///         Ok(())
138///     }
139///     async fn health_check(&self) -> HealthStatus {
140///         HealthStatus::Healthy
141///     }
142/// }
143/// ```
144pub trait Plugin: Send + Sync {
145    /// 플러그인 메타데이터를 반환합니다.
146    fn info(&self) -> &PluginInfo;
147
148    /// 현재 플러그인 상태를 반환합니다.
149    fn state(&self) -> PluginState;
150
151    /// 플러그인을 초기화합니다.
152    ///
153    /// 리소스 할당, 설정 검증 등을 수행합니다.
154    /// `Created` 상태에서만 호출 가능합니다.
155    fn init(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
156
157    /// 플러그인을 시작합니다.
158    ///
159    /// `Initialized` 또는 `Stopped` 상태에서만 호출 가능합니다.
160    fn start(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
161
162    /// 플러그인을 정지합니다.
163    ///
164    /// `Running` 상태에서만 호출 가능합니다.
165    /// Graceful shutdown을 수행합니다.
166    fn stop(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
167
168    /// 플러그인의 건강 상태를 확인합니다.
169    fn health_check(&self) -> impl Future<Output = HealthStatus> + Send;
170}
171
172// ─── DynPlugin Trait ─────────────────────────────────────────────────
173
174/// dyn-compatible 플러그인 trait
175///
176/// `Plugin` trait은 RPITIT를 사용하므로 `dyn Plugin`이 불가합니다.
177/// `DynPlugin`은 `BoxFuture`를 반환하여 `Vec<Box<dyn DynPlugin>>`으로
178/// 플러그인을 동적 관리할 수 있게 합니다.
179pub trait DynPlugin: Send + Sync {
180    /// 플러그인 메타데이터를 반환합니다.
181    fn info(&self) -> &PluginInfo;
182
183    /// 현재 플러그인 상태를 반환합니다.
184    fn state(&self) -> PluginState;
185
186    /// 플러그인을 초기화합니다.
187    fn init(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
188
189    /// 플러그인을 시작합니다.
190    fn start(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
191
192    /// 플러그인을 정지합니다.
193    fn stop(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
194
195    /// 플러그인의 건강 상태를 확인합니다.
196    fn health_check(&self) -> BoxFuture<'_, HealthStatus>;
197}
198
199/// Plugin을 구현한 타입은 자동으로 DynPlugin도 구현됩니다.
200impl<T: Plugin> DynPlugin for T {
201    fn info(&self) -> &PluginInfo {
202        Plugin::info(self)
203    }
204
205    fn state(&self) -> PluginState {
206        Plugin::state(self)
207    }
208
209    fn init(&mut self) -> BoxFuture<'_, Result<(), IronpostError>> {
210        Box::pin(Plugin::init(self))
211    }
212
213    fn start(&mut self) -> BoxFuture<'_, Result<(), IronpostError>> {
214        Box::pin(Plugin::start(self))
215    }
216
217    fn stop(&mut self) -> BoxFuture<'_, Result<(), IronpostError>> {
218        Box::pin(Plugin::stop(self))
219    }
220
221    fn health_check(&self) -> BoxFuture<'_, HealthStatus> {
222        Box::pin(Plugin::health_check(self))
223    }
224}
225
226// ─── PluginRegistry ──────────────────────────────────────────────────
227
228/// 플러그인 레지스트리
229///
230/// 플러그인의 등록, 해제, 생명주기 관리를 담당합니다.
231/// 등록 순서가 보존되며, 생산자를 먼저 등록하고 소비자를 나중에 등록합니다.
232///
233/// # 사용 예시
234/// ```ignore
235/// let mut registry = PluginRegistry::new();
236/// registry.register(Box::new(ebpf_plugin))?;
237/// registry.register(Box::new(log_plugin))?;
238///
239/// registry.init_all().await?;
240/// registry.start_all().await?;
241///
242/// // ... 실행 중 ...
243///
244/// registry.stop_all().await?;
245/// ```
246pub struct PluginRegistry {
247    plugins: Vec<Box<dyn DynPlugin>>,
248}
249
250impl PluginRegistry {
251    /// 빈 레지스트리를 생성합니다.
252    pub fn new() -> Self {
253        Self {
254            plugins: Vec::new(),
255        }
256    }
257
258    /// 플러그인을 등록합니다.
259    ///
260    /// 동일한 이름의 플러그인이 이미 등록되어 있으면 에러를 반환합니다.
261    /// 등록 순서가 보존되며, 생산자를 먼저 등록해야 합니다.
262    pub fn register(&mut self, plugin: Box<dyn DynPlugin>) -> Result<(), IronpostError> {
263        let name = plugin.info().name.clone();
264        if self.plugins.iter().any(|p| p.info().name == name) {
265            return Err(PluginError::AlreadyRegistered { name }.into());
266        }
267        self.plugins.push(plugin);
268        Ok(())
269    }
270
271    /// 플러그인을 해제합니다.
272    ///
273    /// 존재하지 않는 플러그인이면 에러를 반환합니다.
274    /// 해제된 플러그인의 소유권을 반환합니다.
275    pub fn unregister(&mut self, name: &str) -> Result<Box<dyn DynPlugin>, IronpostError> {
276        let pos = self.plugins.iter().position(|p| p.info().name == name);
277        match pos {
278            Some(idx) => Ok(self.plugins.remove(idx)),
279            None => Err(PluginError::NotFound {
280                name: name.to_owned(),
281            }
282            .into()),
283        }
284    }
285
286    /// 이름으로 플러그인을 조회합니다.
287    pub fn get(&self, name: &str) -> Option<&dyn DynPlugin> {
288        self.plugins
289            .iter()
290            .find(|p| p.info().name == name)
291            .map(|p| p.as_ref())
292    }
293
294    /// 이름으로 플러그인을 가변 조회합니다.
295    pub fn get_mut(&mut self, name: &str) -> Option<&mut dyn DynPlugin> {
296        for plugin in &mut self.plugins {
297            if plugin.info().name == name {
298                return Some(&mut **plugin);
299            }
300        }
301        None
302    }
303
304    /// 모든 플러그인을 등록 순서대로 초기화합니다.
305    ///
306    /// 첫 번째 실패 시 즉시 반환합니다 (fail-fast).
307    pub async fn init_all(&mut self) -> Result<(), IronpostError> {
308        for plugin in &mut self.plugins {
309            plugin.init().await?;
310        }
311        Ok(())
312    }
313
314    /// 모든 플러그인을 등록 순서대로 시작합니다.
315    ///
316    /// 첫 번째 실패 시 즉시 반환합니다 (fail-fast).
317    /// 이미 시작된 플러그인은 롤백하지 않으므로, 호출자가 `stop_all`을 호출해야 합니다.
318    pub async fn start_all(&mut self) -> Result<(), IronpostError> {
319        for plugin in &mut self.plugins {
320            plugin.start().await?;
321        }
322        Ok(())
323    }
324
325    /// 모든 플러그인을 등록 순서대로 정지합니다.
326    ///
327    /// 생산자가 먼저 정지하여 소비자가 잔여 이벤트를 드레인할 수 있습니다.
328    /// 개별 플러그인 정지 실패 시에도 나머지 플러그인의 정지를 계속합니다.
329    /// 모든 에러를 수집하여 반환합니다.
330    pub async fn stop_all(&mut self) -> Result<(), IronpostError> {
331        let mut errors = Vec::new();
332        for plugin in &mut self.plugins {
333            if let Err(e) = plugin.stop().await {
334                errors.push(format!("{}: {}", plugin.info().name, e));
335            }
336        }
337        if errors.is_empty() {
338            Ok(())
339        } else {
340            Err(PluginError::StopFailed(errors.join("; ")).into())
341        }
342    }
343
344    /// 등록된 플러그인 수를 반환합니다.
345    pub fn count(&self) -> usize {
346        self.plugins.len()
347    }
348
349    /// 등록된 모든 플러그인의 정보를 반환합니다.
350    pub fn list(&self) -> Vec<&PluginInfo> {
351        self.plugins.iter().map(|p| p.info()).collect()
352    }
353
354    /// 모든 플러그인의 건강 상태를 조회합니다.
355    pub async fn health_check_all(&self) -> Vec<(String, PluginState, HealthStatus)> {
356        let mut statuses = Vec::new();
357        for plugin in &self.plugins {
358            let name = plugin.info().name.clone();
359            let state = plugin.state();
360            let health = plugin.health_check().await;
361            statuses.push((name, state, health));
362        }
363        statuses
364    }
365}
366
367impl Default for PluginRegistry {
368    fn default() -> Self {
369        Self::new()
370    }
371}
372
373// ─── Tests ───────────────────────────────────────────────────────────
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::error::PipelineError;
379
380    /// 테스트용 Mock 플러그인
381    struct MockPlugin {
382        info: PluginInfo,
383        state: PluginState,
384        fail_on_init: bool,
385        fail_on_start: bool,
386        fail_on_stop: bool,
387    }
388
389    impl MockPlugin {
390        fn new(name: &str, plugin_type: PluginType) -> Self {
391            Self {
392                info: PluginInfo {
393                    name: name.to_owned(),
394                    version: "0.1.0".to_owned(),
395                    description: format!("Mock plugin: {name}"),
396                    plugin_type,
397                },
398                state: PluginState::Created,
399                fail_on_init: false,
400                fail_on_start: false,
401                fail_on_stop: false,
402            }
403        }
404
405        fn failing_init(mut self) -> Self {
406            self.fail_on_init = true;
407            self
408        }
409
410        fn failing_start(mut self) -> Self {
411            self.fail_on_start = true;
412            self
413        }
414
415        fn failing_stop(mut self) -> Self {
416            self.fail_on_stop = true;
417            self
418        }
419    }
420
421    impl Plugin for MockPlugin {
422        fn info(&self) -> &PluginInfo {
423            &self.info
424        }
425
426        fn state(&self) -> PluginState {
427            self.state
428        }
429
430        async fn init(&mut self) -> Result<(), IronpostError> {
431            if self.fail_on_init {
432                self.state = PluginState::Failed;
433                return Err(PipelineError::InitFailed("mock init failure".to_owned()).into());
434            }
435            self.state = PluginState::Initialized;
436            Ok(())
437        }
438
439        async fn start(&mut self) -> Result<(), IronpostError> {
440            if self.fail_on_start {
441                self.state = PluginState::Failed;
442                return Err(PipelineError::InitFailed("mock start failure".to_owned()).into());
443            }
444            self.state = PluginState::Running;
445            Ok(())
446        }
447
448        async fn stop(&mut self) -> Result<(), IronpostError> {
449            if self.fail_on_stop {
450                self.state = PluginState::Failed;
451                return Err(PipelineError::InitFailed("mock stop failure".to_owned()).into());
452            }
453            self.state = PluginState::Stopped;
454            Ok(())
455        }
456
457        async fn health_check(&self) -> HealthStatus {
458            match self.state {
459                PluginState::Running => HealthStatus::Healthy,
460                PluginState::Failed => HealthStatus::Unhealthy("failed".to_owned()),
461                _ => HealthStatus::Degraded("not running".to_owned()),
462            }
463        }
464    }
465
466    // ── PluginType tests ──
467
468    #[test]
469    fn plugin_type_display() {
470        assert_eq!(PluginType::Detector.to_string(), "detector");
471        assert_eq!(PluginType::LogPipeline.to_string(), "log-pipeline");
472        assert_eq!(PluginType::Scanner.to_string(), "scanner");
473        assert_eq!(PluginType::Enforcer.to_string(), "enforcer");
474        assert_eq!(
475            PluginType::Custom("my-plugin".to_owned()).to_string(),
476            "custom:my-plugin"
477        );
478    }
479
480    #[test]
481    fn plugin_type_equality() {
482        assert_eq!(PluginType::Detector, PluginType::Detector);
483        assert_ne!(PluginType::Detector, PluginType::Scanner);
484        assert_eq!(
485            PluginType::Custom("a".to_owned()),
486            PluginType::Custom("a".to_owned())
487        );
488        assert_ne!(
489            PluginType::Custom("a".to_owned()),
490            PluginType::Custom("b".to_owned())
491        );
492    }
493
494    #[test]
495    fn plugin_type_serialize_deserialize() {
496        let pt = PluginType::Scanner;
497        let json = serde_json::to_string(&pt).unwrap();
498        let deserialized: PluginType = serde_json::from_str(&json).unwrap();
499        assert_eq!(pt, deserialized);
500
501        let custom = PluginType::Custom("ext".to_owned());
502        let json = serde_json::to_string(&custom).unwrap();
503        let deserialized: PluginType = serde_json::from_str(&json).unwrap();
504        assert_eq!(custom, deserialized);
505    }
506
507    // ── PluginState tests ──
508
509    #[test]
510    fn plugin_state_display() {
511        assert_eq!(PluginState::Created.to_string(), "created");
512        assert_eq!(PluginState::Initialized.to_string(), "initialized");
513        assert_eq!(PluginState::Running.to_string(), "running");
514        assert_eq!(PluginState::Stopped.to_string(), "stopped");
515        assert_eq!(PluginState::Failed.to_string(), "failed");
516    }
517
518    #[test]
519    fn plugin_state_equality() {
520        assert_eq!(PluginState::Created, PluginState::Created);
521        assert_ne!(PluginState::Created, PluginState::Running);
522    }
523
524    #[test]
525    fn plugin_state_serialize_deserialize() {
526        let state = PluginState::Running;
527        let json = serde_json::to_string(&state).unwrap();
528        let deserialized: PluginState = serde_json::from_str(&json).unwrap();
529        assert_eq!(state, deserialized);
530    }
531
532    // ── PluginInfo tests ──
533
534    #[test]
535    fn plugin_info_clone() {
536        let info = PluginInfo {
537            name: "test".to_owned(),
538            version: "1.0.0".to_owned(),
539            description: "Test plugin".to_owned(),
540            plugin_type: PluginType::Detector,
541        };
542        let cloned = info.clone();
543        assert_eq!(info.name, cloned.name);
544        assert_eq!(info.version, cloned.version);
545    }
546
547    #[test]
548    fn plugin_info_serialize_deserialize() {
549        let info = PluginInfo {
550            name: "ebpf-engine".to_owned(),
551            version: "0.1.0".to_owned(),
552            description: "eBPF network detection".to_owned(),
553            plugin_type: PluginType::Detector,
554        };
555        let json = serde_json::to_string(&info).unwrap();
556        let deserialized: PluginInfo = serde_json::from_str(&json).unwrap();
557        assert_eq!(info.name, deserialized.name);
558        assert_eq!(info.version, deserialized.version);
559        assert_eq!(info.plugin_type, deserialized.plugin_type);
560    }
561
562    // ── Plugin trait lifecycle tests ──
563
564    #[tokio::test]
565    async fn plugin_lifecycle_init_start_stop() {
566        let mut plugin = MockPlugin::new("test", PluginType::Detector);
567        assert_eq!(Plugin::state(&plugin), PluginState::Created);
568
569        Plugin::init(&mut plugin).await.unwrap();
570        assert_eq!(Plugin::state(&plugin), PluginState::Initialized);
571
572        Plugin::start(&mut plugin).await.unwrap();
573        assert_eq!(Plugin::state(&plugin), PluginState::Running);
574
575        Plugin::stop(&mut plugin).await.unwrap();
576        assert_eq!(Plugin::state(&plugin), PluginState::Stopped);
577    }
578
579    #[tokio::test]
580    async fn plugin_health_check_reflects_state() {
581        let mut plugin = MockPlugin::new("test", PluginType::Detector);
582
583        // Created → not running
584        let health = Plugin::health_check(&plugin).await;
585        assert!(!health.is_healthy());
586
587        Plugin::init(&mut plugin).await.unwrap();
588        Plugin::start(&mut plugin).await.unwrap();
589
590        // Running → healthy
591        let health = Plugin::health_check(&plugin).await;
592        assert!(health.is_healthy());
593    }
594
595    #[tokio::test]
596    async fn plugin_init_failure_sets_failed_state() {
597        let mut plugin = MockPlugin::new("test", PluginType::Detector).failing_init();
598
599        let result = Plugin::init(&mut plugin).await;
600        assert!(result.is_err());
601        assert_eq!(Plugin::state(&plugin), PluginState::Failed);
602    }
603
604    // ── DynPlugin tests ──
605
606    #[tokio::test]
607    async fn dyn_plugin_can_be_boxed() {
608        let mut plugin: Box<dyn DynPlugin> =
609            Box::new(MockPlugin::new("boxed", PluginType::Scanner));
610
611        assert_eq!(plugin.info().name, "boxed");
612        assert_eq!(plugin.state(), PluginState::Created);
613
614        plugin.init().await.unwrap();
615        assert_eq!(plugin.state(), PluginState::Initialized);
616
617        plugin.start().await.unwrap();
618        assert_eq!(plugin.state(), PluginState::Running);
619
620        let health = plugin.health_check().await;
621        assert!(health.is_healthy());
622
623        plugin.stop().await.unwrap();
624        assert_eq!(plugin.state(), PluginState::Stopped);
625    }
626
627    // ── PluginRegistry tests ──
628
629    #[test]
630    fn registry_new_is_empty() {
631        let registry = PluginRegistry::new();
632        assert_eq!(registry.count(), 0);
633        assert!(registry.list().is_empty());
634    }
635
636    #[test]
637    fn registry_default_is_empty() {
638        let registry = PluginRegistry::default();
639        assert_eq!(registry.count(), 0);
640    }
641
642    #[test]
643    fn registry_register_increases_count() {
644        let mut registry = PluginRegistry::new();
645        let plugin = MockPlugin::new("test", PluginType::Detector);
646        registry.register(Box::new(plugin)).unwrap();
647        assert_eq!(registry.count(), 1);
648    }
649
650    #[test]
651    fn registry_register_duplicate_name_fails() {
652        let mut registry = PluginRegistry::new();
653        let plugin1 = MockPlugin::new("dup", PluginType::Detector);
654        let plugin2 = MockPlugin::new("dup", PluginType::Scanner);
655
656        registry.register(Box::new(plugin1)).unwrap();
657        let err = registry.register(Box::new(plugin2)).unwrap_err();
658        assert!(err.to_string().contains("already registered"));
659        assert!(err.to_string().contains("dup"));
660        assert_eq!(registry.count(), 1);
661    }
662
663    #[test]
664    fn registry_unregister_removes_plugin() {
665        let mut registry = PluginRegistry::new();
666        let plugin = MockPlugin::new("removable", PluginType::Detector);
667        registry.register(Box::new(plugin)).unwrap();
668        assert_eq!(registry.count(), 1);
669
670        let removed = registry.unregister("removable").unwrap();
671        assert_eq!(removed.info().name, "removable");
672        assert_eq!(registry.count(), 0);
673    }
674
675    #[test]
676    fn registry_unregister_not_found_fails() {
677        let mut registry = PluginRegistry::new();
678        let err = registry
679            .unregister("nonexistent")
680            .err()
681            .expect("should return error");
682        assert!(err.to_string().contains("not found"));
683        assert!(err.to_string().contains("nonexistent"));
684    }
685
686    #[test]
687    fn registry_get_returns_plugin() {
688        let mut registry = PluginRegistry::new();
689        let plugin = MockPlugin::new("lookup", PluginType::Enforcer);
690        registry.register(Box::new(plugin)).unwrap();
691
692        let found = registry.get("lookup");
693        assert!(found.is_some());
694        assert_eq!(found.unwrap().info().name, "lookup");
695    }
696
697    #[test]
698    fn registry_get_not_found_returns_none() {
699        let registry = PluginRegistry::new();
700        assert!(registry.get("missing").is_none());
701    }
702
703    #[test]
704    fn registry_get_mut_returns_mutable_plugin() {
705        let mut registry = PluginRegistry::new();
706        let plugin = MockPlugin::new("mutable", PluginType::Detector);
707        registry.register(Box::new(plugin)).unwrap();
708
709        let found = registry.get_mut("mutable");
710        assert!(found.is_some());
711        assert_eq!(found.unwrap().info().name, "mutable");
712    }
713
714    #[test]
715    fn registry_list_returns_all_info() {
716        let mut registry = PluginRegistry::new();
717        registry
718            .register(Box::new(MockPlugin::new("a", PluginType::Detector)))
719            .unwrap();
720        registry
721            .register(Box::new(MockPlugin::new("b", PluginType::Scanner)))
722            .unwrap();
723        registry
724            .register(Box::new(MockPlugin::new("c", PluginType::Enforcer)))
725            .unwrap();
726
727        let list = registry.list();
728        assert_eq!(list.len(), 3);
729        assert_eq!(list[0].name, "a");
730        assert_eq!(list[1].name, "b");
731        assert_eq!(list[2].name, "c");
732    }
733
734    #[tokio::test]
735    async fn registry_init_all_initializes_plugins() {
736        let mut registry = PluginRegistry::new();
737        registry
738            .register(Box::new(MockPlugin::new("p1", PluginType::Detector)))
739            .unwrap();
740        registry
741            .register(Box::new(MockPlugin::new("p2", PluginType::Scanner)))
742            .unwrap();
743
744        registry.init_all().await.unwrap();
745
746        assert_eq!(
747            registry.get("p1").unwrap().state(),
748            PluginState::Initialized
749        );
750        assert_eq!(
751            registry.get("p2").unwrap().state(),
752            PluginState::Initialized
753        );
754    }
755
756    #[tokio::test]
757    async fn registry_init_all_fails_fast() {
758        let mut registry = PluginRegistry::new();
759        registry
760            .register(Box::new(MockPlugin::new("ok", PluginType::Detector)))
761            .unwrap();
762        registry
763            .register(Box::new(
764                MockPlugin::new("fail", PluginType::Scanner).failing_init(),
765            ))
766            .unwrap();
767        registry
768            .register(Box::new(MockPlugin::new("skipped", PluginType::Enforcer)))
769            .unwrap();
770
771        let result = registry.init_all().await;
772        assert!(result.is_err());
773
774        // First plugin was initialized, second failed, third was skipped
775        assert_eq!(
776            registry.get("ok").unwrap().state(),
777            PluginState::Initialized
778        );
779        assert_eq!(registry.get("fail").unwrap().state(), PluginState::Failed);
780        assert_eq!(
781            registry.get("skipped").unwrap().state(),
782            PluginState::Created
783        );
784    }
785
786    #[tokio::test]
787    async fn registry_start_all_starts_plugins() {
788        let mut registry = PluginRegistry::new();
789        registry
790            .register(Box::new(MockPlugin::new("p1", PluginType::Detector)))
791            .unwrap();
792        registry
793            .register(Box::new(MockPlugin::new("p2", PluginType::Scanner)))
794            .unwrap();
795
796        registry.init_all().await.unwrap();
797        registry.start_all().await.unwrap();
798
799        assert_eq!(registry.get("p1").unwrap().state(), PluginState::Running);
800        assert_eq!(registry.get("p2").unwrap().state(), PluginState::Running);
801    }
802
803    #[tokio::test]
804    async fn registry_start_all_fails_fast() {
805        let mut registry = PluginRegistry::new();
806        registry
807            .register(Box::new(MockPlugin::new("ok", PluginType::Detector)))
808            .unwrap();
809        registry
810            .register(Box::new(
811                MockPlugin::new("fail", PluginType::Scanner).failing_start(),
812            ))
813            .unwrap();
814
815        registry.init_all().await.unwrap();
816        let result = registry.start_all().await;
817        assert!(result.is_err());
818
819        assert_eq!(registry.get("ok").unwrap().state(), PluginState::Running);
820        assert_eq!(registry.get("fail").unwrap().state(), PluginState::Failed);
821    }
822
823    #[tokio::test]
824    async fn registry_stop_all_stops_plugins() {
825        let mut registry = PluginRegistry::new();
826        registry
827            .register(Box::new(MockPlugin::new("p1", PluginType::Detector)))
828            .unwrap();
829        registry
830            .register(Box::new(MockPlugin::new("p2", PluginType::Scanner)))
831            .unwrap();
832
833        registry.init_all().await.unwrap();
834        registry.start_all().await.unwrap();
835        registry.stop_all().await.unwrap();
836
837        assert_eq!(registry.get("p1").unwrap().state(), PluginState::Stopped);
838        assert_eq!(registry.get("p2").unwrap().state(), PluginState::Stopped);
839    }
840
841    #[tokio::test]
842    async fn registry_stop_all_continues_on_error() {
843        let mut registry = PluginRegistry::new();
844        registry
845            .register(Box::new(
846                MockPlugin::new("fail", PluginType::Detector).failing_stop(),
847            ))
848            .unwrap();
849        registry
850            .register(Box::new(MockPlugin::new("ok", PluginType::Scanner)))
851            .unwrap();
852
853        registry.init_all().await.unwrap();
854        registry.start_all().await.unwrap();
855
856        let result = registry.stop_all().await;
857        assert!(result.is_err());
858        assert!(result.unwrap_err().to_string().contains("fail"));
859
860        // Second plugin should still have been stopped
861        assert_eq!(registry.get("ok").unwrap().state(), PluginState::Stopped);
862    }
863
864    #[tokio::test]
865    async fn registry_health_check_all() {
866        let mut registry = PluginRegistry::new();
867        registry
868            .register(Box::new(MockPlugin::new("running", PluginType::Detector)))
869            .unwrap();
870        registry
871            .register(Box::new(MockPlugin::new("created", PluginType::Scanner)))
872            .unwrap();
873
874        // Only init+start the first one
875        if let Some(p) = registry.get_mut("running") {
876            p.init().await.unwrap();
877            p.start().await.unwrap();
878        }
879
880        let statuses = registry.health_check_all().await;
881        assert_eq!(statuses.len(), 2);
882
883        let (name1, state1, health1) = &statuses[0];
884        assert_eq!(name1, "running");
885        assert_eq!(*state1, PluginState::Running);
886        assert!(health1.is_healthy());
887
888        let (name2, state2, _health2) = &statuses[1];
889        assert_eq!(name2, "created");
890        assert_eq!(*state2, PluginState::Created);
891    }
892
893    #[tokio::test]
894    async fn registry_full_lifecycle() {
895        let mut registry = PluginRegistry::new();
896        registry
897            .register(Box::new(MockPlugin::new("ebpf", PluginType::Detector)))
898            .unwrap();
899        registry
900            .register(Box::new(MockPlugin::new("log", PluginType::LogPipeline)))
901            .unwrap();
902        registry
903            .register(Box::new(MockPlugin::new("sbom", PluginType::Scanner)))
904            .unwrap();
905        registry
906            .register(Box::new(MockPlugin::new("guard", PluginType::Enforcer)))
907            .unwrap();
908
909        assert_eq!(registry.count(), 4);
910
911        // Init all
912        registry.init_all().await.unwrap();
913        for info in registry.list() {
914            assert_eq!(
915                registry.get(&info.name).unwrap().state(),
916                PluginState::Initialized
917            );
918        }
919
920        // Start all
921        registry.start_all().await.unwrap();
922        for info in registry.list() {
923            assert_eq!(
924                registry.get(&info.name).unwrap().state(),
925                PluginState::Running
926            );
927        }
928
929        // Health check
930        let statuses = registry.health_check_all().await;
931        assert!(statuses.iter().all(|(_, _, h)| h.is_healthy()));
932
933        // Stop all
934        registry.stop_all().await.unwrap();
935        for info in registry.list() {
936            assert_eq!(
937                registry.get(&info.name).unwrap().state(),
938                PluginState::Stopped
939            );
940        }
941    }
942
943    #[test]
944    fn registry_preserves_registration_order() {
945        let mut registry = PluginRegistry::new();
946        let names = ["alpha", "beta", "gamma", "delta"];
947
948        for name in &names {
949            let plugin = MockPlugin::new(name, PluginType::Detector);
950            registry.register(Box::new(plugin)).unwrap();
951        }
952
953        let list: Vec<&str> = registry
954            .list()
955            .iter()
956            .map(|info| info.name.as_str())
957            .collect();
958        assert_eq!(list, names);
959    }
960
961    #[test]
962    fn registry_unregister_middle_preserves_order() {
963        let mut registry = PluginRegistry::new();
964        registry
965            .register(Box::new(MockPlugin::new("a", PluginType::Detector)))
966            .unwrap();
967        registry
968            .register(Box::new(MockPlugin::new("b", PluginType::Scanner)))
969            .unwrap();
970        registry
971            .register(Box::new(MockPlugin::new("c", PluginType::Enforcer)))
972            .unwrap();
973
974        registry.unregister("b").unwrap();
975
976        let list: Vec<&str> = registry
977            .list()
978            .iter()
979            .map(|info| info.name.as_str())
980            .collect();
981        assert_eq!(list, vec!["a", "c"]);
982    }
983
984    // ── PluginError tests ──
985
986    #[test]
987    fn plugin_error_already_registered_display() {
988        let err = PluginError::AlreadyRegistered {
989            name: "test".to_owned(),
990        };
991        assert_eq!(err.to_string(), "plugin already registered: test");
992    }
993
994    #[test]
995    fn plugin_error_not_found_display() {
996        let err = PluginError::NotFound {
997            name: "missing".to_owned(),
998        };
999        assert_eq!(err.to_string(), "plugin not found: missing");
1000    }
1001
1002    #[test]
1003    fn plugin_error_invalid_state_display() {
1004        let err = PluginError::InvalidState {
1005            name: "test".to_owned(),
1006            current: "created".to_owned(),
1007            expected: "initialized".to_owned(),
1008        };
1009        assert!(err.to_string().contains("test"));
1010        assert!(err.to_string().contains("created"));
1011        assert!(err.to_string().contains("initialized"));
1012    }
1013
1014    #[test]
1015    fn plugin_error_stop_failed_display() {
1016        let err = PluginError::StopFailed("p1: timeout; p2: connection lost".to_owned());
1017        assert!(err.to_string().contains("p1: timeout"));
1018        assert!(err.to_string().contains("p2: connection lost"));
1019    }
1020
1021    #[test]
1022    fn plugin_error_converts_to_ironpost_error() {
1023        let plugin_err = PluginError::NotFound {
1024            name: "test".to_owned(),
1025        };
1026        let err: IronpostError = plugin_err.into();
1027        assert!(matches!(err, IronpostError::Plugin(_)));
1028        assert!(err.to_string().contains("test"));
1029    }
1030}