1use std::fmt;
14use std::future::Future;
15
16use serde::{Deserialize, Serialize};
17
18use crate::error::{IronpostError, PluginError};
19use crate::pipeline::{BoxFuture, HealthStatus};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum PluginType {
28 Detector,
30 LogPipeline,
32 Scanner,
34 Enforcer,
36 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#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PluginInfo {
59 pub name: String,
61 pub version: String,
63 pub description: String,
65 pub plugin_type: PluginType,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum PluginState {
80 Created,
82 Initialized,
84 Running,
86 Stopped,
88 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
104pub trait Plugin: Send + Sync {
145 fn info(&self) -> &PluginInfo;
147
148 fn state(&self) -> PluginState;
150
151 fn init(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
156
157 fn start(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
161
162 fn stop(&mut self) -> impl Future<Output = Result<(), IronpostError>> + Send;
167
168 fn health_check(&self) -> impl Future<Output = HealthStatus> + Send;
170}
171
172pub trait DynPlugin: Send + Sync {
180 fn info(&self) -> &PluginInfo;
182
183 fn state(&self) -> PluginState;
185
186 fn init(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
188
189 fn start(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
191
192 fn stop(&mut self) -> BoxFuture<'_, Result<(), IronpostError>>;
194
195 fn health_check(&self) -> BoxFuture<'_, HealthStatus>;
197}
198
199impl<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
226pub struct PluginRegistry {
247 plugins: Vec<Box<dyn DynPlugin>>,
248}
249
250impl PluginRegistry {
251 pub fn new() -> Self {
253 Self {
254 plugins: Vec::new(),
255 }
256 }
257
258 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 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 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 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 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 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 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 pub fn count(&self) -> usize {
346 self.plugins.len()
347 }
348
349 pub fn list(&self) -> Vec<&PluginInfo> {
351 self.plugins.iter().map(|p| p.info()).collect()
352 }
353
354 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#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::error::PipelineError;
379
380 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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 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 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 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 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 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 let statuses = registry.health_check_all().await;
931 assert!(statuses.iter().all(|(_, _, h)| h.is_healthy()));
932
933 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 #[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}