#[cfg(test)]
mod tests {
use forge_guard::core::{Finding, ProjectConfig, Severity};
use forge_guard::plugins::{
register_default_plugins, ExamplePlugin, OfflineGuardPlugin, Plugin, PluginContext,
PluginExecutionStats, PluginInfo, PluginIpcFinding, PluginIpcInput, PluginIpcOutput,
PluginRegistry, PluginType,
};
use std::path::PathBuf;
#[test]
fn test_example_plugin_meta() {
let plugin = ExamplePlugin;
assert_eq!(plugin.name(), "forge-guard-example");
assert_eq!(plugin.version(), "0.1.0");
assert_eq!(
plugin.description(),
"Example plugin demonstrating the plugin API"
);
assert!(plugin.supports_offline());
assert!(!plugin.requires_rpc());
}
#[test]
fn test_offline_guard_plugin_meta() {
let plugin = OfflineGuardPlugin;
assert_eq!(plugin.name(), "forge-guard-offline-guard");
assert!(plugin.supports_offline());
assert!(!plugin.requires_rpc());
}
#[test]
fn test_example_plugin_execution() {
let plugin = ExamplePlugin;
let ctx = PluginContext::new(&ProjectConfig::default(), vec![]);
let result = plugin.execute(&ctx);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0); }
#[test]
fn test_offline_guard_execution_offline() {
let plugin = OfflineGuardPlugin;
let ctx =
PluginContext::new(&ProjectConfig::default(), vec![]).with_metadata("offline", "true");
let result = plugin.execute(&ctx);
assert!(result.is_ok());
}
#[test]
fn test_registry_create_empty() {
let config = ProjectConfig::default();
let registry = PluginRegistry::new(&config);
assert!(registry.is_ok());
let registry = registry.unwrap();
assert_eq!(registry.plugin_count(), 0);
assert!(registry.list_plugins().is_empty());
}
#[test]
fn test_registry_register_builtin() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
assert_eq!(registry.plugin_count(), 0);
registry.register_builtin(Box::new(ExamplePlugin));
assert_eq!(registry.plugin_count(), 1);
registry.register_builtin(Box::new(OfflineGuardPlugin));
assert_eq!(registry.plugin_count(), 2);
}
#[test]
fn test_register_default_plugins_helper() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
assert_eq!(registry.plugin_count(), 2);
let names: Vec<String> = registry
.list_plugins()
.iter()
.map(|p| p.name.clone())
.collect();
assert!(names.contains(&"forge-guard-example".to_string()));
assert!(names.contains(&"forge-guard-offline-guard".to_string()));
}
#[test]
fn test_registry_list_and_get_plugin() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
let plugins = registry.list_plugins();
assert_eq!(plugins.len(), 2);
assert!(plugins
.iter()
.all(|p| matches!(p.plugin_type, PluginType::Builtin)));
let example = registry.get_plugin("forge-guard-example");
assert!(example.is_some());
assert_eq!(example.unwrap().version, "0.1.0");
assert!(registry.get_plugin("non-existent").is_none());
}
#[test]
fn test_enable_disable_plugin() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
assert_eq!(registry.enabled_count(), 2);
assert!(registry.disable_plugin("forge-guard-example"));
assert_eq!(registry.enabled_count(), 1);
let example = registry.get_plugin("forge-guard-example").unwrap();
assert!(!example.enabled);
assert!(registry.enable_plugin("forge-guard-example"));
let example = registry.get_plugin("forge-guard-example").unwrap();
assert!(example.enabled);
assert!(!registry.enable_plugin("non-existent"));
}
#[test]
fn test_registry_disabled_from_config() {
let mut config = ProjectConfig::default();
config
.plugins
.disabled
.push("forge-guard-example".to_string());
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
let example = registry.get_plugin("forge-guard-example").unwrap();
assert!(!example.enabled, "Should be disabled via config");
let guard = registry.get_plugin("forge-guard-offline-guard").unwrap();
assert!(guard.enabled, "Should still be enabled");
}
#[test]
fn test_execute_all_builtins() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
let ctx = PluginContext::new(&config, vec![]);
let results = registry.execute_all(&ctx);
assert_eq!(results.len(), 2, "Should execute both plugins");
for result in &results {
assert!(
result.success,
"Plugin '{}' should succeed",
result.plugin_name
);
assert!(
result.findings.is_empty(),
"Plugin '{}' should have no findings",
result.plugin_name
);
assert!(
result.error.is_none(),
"Plugin '{}' should have no error",
result.plugin_name
);
}
}
#[test]
fn test_execute_skips_disabled_plugins() {
let mut config = ProjectConfig::default();
config
.plugins
.disabled
.push("forge-guard-example".to_string());
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
let ctx = PluginContext::new(&config, vec![]);
let results = registry.execute_all(&ctx);
assert_eq!(results.len(), 1, "Only one plugin should execute");
assert_eq!(results[0].plugin_name, "forge-guard-offline-guard");
}
#[test]
fn test_execute_with_source_files() {
let config = ProjectConfig::default();
let mut registry = PluginRegistry::new(&config).unwrap();
register_default_plugins(&mut registry);
let source_files = vec![PathBuf::from("test-contracts/secure/src/Counter.sol")];
let ctx = PluginContext::new(&config, source_files);
let results = registry.execute_all(&ctx);
assert_eq!(results.len(), 2);
for result in &results {
assert!(result.success);
}
}
#[test]
fn test_plugin_info_display() {
let info = PluginInfo {
name: "test-plugin".to_string(),
version: "0.1.0".to_string(),
description: "A test plugin".to_string(),
enabled: true,
plugin_type: PluginType::Builtin,
path: None,
};
assert_eq!(info.name, "test-plugin");
assert_eq!(info.version, "0.1.0");
assert!(info.enabled);
assert_eq!(info.plugin_type, PluginType::Builtin);
}
#[test]
fn test_plugin_type_equality() {
assert_eq!(PluginType::Builtin, PluginType::Builtin);
assert_eq!(PluginType::External, PluginType::External);
assert_ne!(PluginType::Builtin, PluginType::External);
}
#[test]
fn test_ipc_input_serialization() {
let input = PluginIpcInput {
protocol_version: "1.0".to_string(),
plugin_name: "test-plugin".to_string(),
context: PluginContext::new(&ProjectConfig::default(), vec![]),
};
let json = serde_json::to_string(&input).unwrap();
assert!(json.contains("\"protocol_version\":\"1.0\""));
assert!(json.contains("\"plugin_name\":\"test-plugin\""));
let deserialized: PluginIpcInput = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.protocol_version, "1.0");
assert_eq!(deserialized.plugin_name, "test-plugin");
}
#[test]
fn test_ipc_output_success() {
let output = PluginIpcOutput {
success: true,
findings: vec![],
error: None,
stats: PluginExecutionStats {
files_analyzed: 5,
duration_ms: 42,
},
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"success\":true"));
let deserialized: PluginIpcOutput = serde_json::from_str(&json).unwrap();
assert!(deserialized.success);
assert!(deserialized.findings.is_empty());
assert!(deserialized.error.is_none());
assert_eq!(deserialized.stats.files_analyzed, 5);
}
#[test]
fn test_ipc_output_error() {
let output = PluginIpcOutput {
success: false,
findings: vec![],
error: Some("Something went wrong".to_string()),
stats: PluginExecutionStats::default(),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"success\":false"));
assert!(json.contains("\"error\":\"Something went wrong\""));
let deserialized: PluginIpcOutput = serde_json::from_str(&json).unwrap();
assert!(!deserialized.success);
assert_eq!(deserialized.error.unwrap(), "Something went wrong");
}
#[test]
fn test_ipc_output_with_findings() {
let output = PluginIpcOutput {
success: true,
findings: vec![PluginIpcFinding {
title: "Test Issue".to_string(),
description: "A test finding".to_string(),
severity: "high".to_string(),
file: Some("Vault.sol".to_string()),
line: Some(42),
column: Some(8),
code_snippet: Some("msg.sender.call{value: amount}(\"\");".to_string()),
recommendation: Some("Use checks-effects-interactions.".to_string()),
category: Some("Logic".to_string()),
blocks_deployment: true,
references: vec![],
}],
error: None,
stats: PluginExecutionStats::default(),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"title\":\"Test Issue\""));
assert!(json.contains("\"severity\":\"high\""));
assert!(json.contains("\"blocks_deployment\":true"));
let deserialized: PluginIpcOutput = serde_json::from_str(&json).unwrap();
let finding: Finding = deserialized.findings.into_iter().next().unwrap().into();
assert_eq!(finding.title, "Test Issue");
assert_eq!(finding.severity, Severity::High);
assert_eq!(finding.file, Some("Vault.sol".to_string()));
assert_eq!(finding.line, Some(42));
assert!(finding.blocks_deployment);
}
#[test]
fn test_ipc_finding_critical_severity() {
let ipc_finding = PluginIpcFinding {
title: "Critical Bug".to_string(),
description: "test".to_string(),
severity: "CRITICAL".to_string(),
file: None,
line: None,
column: None,
code_snippet: None,
recommendation: None,
category: None,
blocks_deployment: true,
references: vec![],
};
let finding: Finding = ipc_finding.into();
assert_eq!(finding.severity, Severity::Critical);
assert!(finding.blocks_deployment);
}
#[test]
fn test_ipc_finding_informational_severity() {
let ipc_finding = PluginIpcFinding {
title: "Style".to_string(),
description: "test".to_string(),
severity: "info".to_string(),
file: None,
line: None,
column: None,
code_snippet: None,
recommendation: None,
category: None,
blocks_deployment: false,
references: vec![],
};
let finding: Finding = ipc_finding.into();
assert_eq!(finding.severity, Severity::Informational);
assert!(!finding.blocks_deployment);
}
#[test]
fn test_ipc_output_empty_findings_default() {
let output = PluginIpcOutput {
success: true,
findings: vec![],
error: None,
stats: PluginExecutionStats::default(),
};
let json = serde_json::to_string(&output).unwrap();
let deserialized: PluginIpcOutput = serde_json::from_str(&json).unwrap();
assert!(deserialized.findings.is_empty());
}
#[test]
fn test_plugin_context_new() {
let config = ProjectConfig::default();
let ctx = PluginContext::new(&config, vec![]);
assert!(ctx.metadata.is_empty());
assert!(ctx.source_files.is_empty());
}
#[test]
fn test_plugin_context_with_metadata() {
let config = ProjectConfig::default();
let ctx = PluginContext::new(&config, vec![])
.with_metadata("chain", "ethereum")
.with_metadata("verbose", "true");
assert_eq!(ctx.metadata.get("chain").unwrap(), "ethereum");
assert_eq!(ctx.metadata.get("verbose").unwrap(), "true");
assert_eq!(ctx.metadata.len(), 2);
}
#[test]
fn test_plugin_context_serialization() {
let config = ProjectConfig::default();
let ctx = PluginContext::new(&config, vec![PathBuf::from("contracts/Vault.sol")])
.with_metadata("chain", "base");
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("contracts/Vault.sol"));
assert!(json.contains("\"chain\":\"base\""));
let deserialized: PluginContext = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.source_files.len(), 1);
assert_eq!(deserialized.metadata.get("chain").unwrap(), "base");
}
}