use gatekpr_opencode::{FileContext, OpenCodeClient, OpenCodeConfig, RawFinding, Severity};
use std::path::PathBuf;
fn cli_available() -> bool {
OpenCodeConfig::new().is_ok()
}
macro_rules! skip_if_no_cli {
() => {
if !cli_available() {
eprintln!("Skipping test: OpenCode CLI not installed");
eprintln!("Install with: curl -fsSL https://opencode.ai/install | bash");
return;
}
};
}
fn fixtures_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent() .unwrap()
.parent() .unwrap()
.join("tests")
.join("fixtures")
.join("apps")
}
#[test]
fn test_config_with_cli_path() {
let config = OpenCodeConfig::with_cli_path(PathBuf::from("/usr/bin/opencode"));
assert_eq!(config.cli_path, PathBuf::from("/usr/bin/opencode"));
}
#[test]
fn test_config_with_model() {
let config = OpenCodeConfig::with_cli_path(PathBuf::from("/usr/bin/opencode"))
.with_model("custom/model");
assert_eq!(config.model, "custom/model");
}
#[test]
fn test_config_with_timeout() {
use std::time::Duration;
let config = OpenCodeConfig::with_cli_path(PathBuf::from("/usr/bin/opencode"))
.with_timeout(Duration::from_secs(300));
assert_eq!(config.timeout, Duration::from_secs(300));
}
#[test]
fn test_config_default_model() {
let config = OpenCodeConfig::with_cli_path(PathBuf::from("/usr/bin/opencode"));
assert!(config.model.contains("zai-coding-plan"));
}
#[test]
fn test_config_auto_detection() {
let result = OpenCodeConfig::new();
if let Ok(config) = result {
assert!(config.cli_path.exists());
}
}
#[test]
fn test_raw_finding_creation() {
let finding = RawFinding::new(
"WH001",
Severity::Critical,
"webhooks",
"src/webhooks.ts",
"Missing GDPR webhook handler",
)
.with_line(42)
.with_column(10)
.with_match("// TODO: add webhook");
assert_eq!(finding.rule_id, "WH001");
assert_eq!(finding.severity, Severity::Critical);
assert_eq!(finding.category, "webhooks");
assert_eq!(finding.file_path, "src/webhooks.ts");
assert_eq!(finding.line, Some(42));
assert_eq!(finding.column, Some(10));
assert_eq!(finding.raw_match, "// TODO: add webhook");
}
#[test]
fn test_raw_finding_location() {
let finding = RawFinding::new(
"WH001",
Severity::Critical,
"webhooks",
"src/app.ts",
"Test",
)
.with_line(42)
.with_column(5);
assert_eq!(finding.location(), "src/app.ts:42:5");
}
#[test]
fn test_severity_priority() {
assert!(Severity::Critical.priority() < Severity::Warning.priority());
assert!(Severity::Warning.priority() < Severity::Info.priority());
}
#[test]
fn test_severity_is_blocking() {
assert!(Severity::Critical.is_blocking());
assert!(!Severity::Warning.is_blocking());
assert!(!Severity::Info.is_blocking());
}
#[test]
fn test_file_context_creation() {
let content = "const app = express();\napp.listen(3000);";
let context = FileContext::new("src/app.ts", content);
assert_eq!(context.path, "src/app.ts");
assert_eq!(context.language, "typescript");
assert_eq!(context.line_count, 2);
}
#[test]
fn test_file_context_snippet() {
let content = "line1\nline2\nline3\nline4\nline5\nline6\nline7";
let context = FileContext::new("test.ts", content);
let snippet = context.snippet(4, 1);
assert!(snippet.contains("line3"));
assert!(snippet.contains("line4"));
assert!(snippet.contains("line5"));
}
#[test]
fn test_file_context_language_detection() {
assert_eq!(FileContext::new("app.ts", "").language, "typescript");
assert_eq!(FileContext::new("app.tsx", "").language, "typescript");
assert_eq!(FileContext::new("app.js", "").language, "javascript");
assert_eq!(FileContext::new("app.jsx", "").language, "javascript");
assert_eq!(FileContext::new("app.rb", "").language, "ruby");
assert_eq!(FileContext::new("app.py", "").language, "python");
assert_eq!(FileContext::new("app.go", "").language, "go");
assert_eq!(FileContext::new("app.rs", "").language, "rust");
}
#[test]
fn test_client_creation_with_valid_path() {
skip_if_no_cli!();
let config = OpenCodeConfig::new().expect("CLI should be found");
let client = OpenCodeClient::new(config);
assert!(client.is_ok());
}
#[test]
fn test_client_auto_creation() {
skip_if_no_cli!();
let client = OpenCodeClient::auto();
assert!(client.is_ok());
let client = client.unwrap();
assert!(client.cli_path().exists());
assert!(client.model().contains("zai-coding-plan"));
}
#[test]
fn test_client_invalid_path() {
let config = OpenCodeConfig::with_cli_path(PathBuf::from("/nonexistent/opencode"));
let client = OpenCodeClient::new(config);
assert!(client.is_err());
}
#[tokio::test]
async fn test_enrich_single_finding() {
skip_if_no_cli!();
if std::env::var("CI").is_ok() {
eprintln!("Skipping: CI environment detected");
return;
}
let client = match OpenCodeClient::auto() {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
let finding = RawFinding::new(
"WH001",
Severity::Critical,
"webhooks",
"src/webhooks.ts",
"Missing GDPR customers/redact webhook handler",
)
.with_line(1);
let context = FileContext::new(
"src/webhooks.ts",
r#"
import express from 'express';
const app = express();
// Webhook handlers
app.post('/webhooks/orders/create', (req, res) => {
console.log('Order created');
res.sendStatus(200);
});
// Missing: customers/redact
// Missing: customers/data_request
// Missing: shop/redact
"#,
);
match client.enrich_finding(&finding, &context).await {
Ok(enriched) => {
assert_eq!(enriched.rule_id, "WH001");
assert_eq!(enriched.severity, Severity::Critical);
assert!(!enriched.issue.title.is_empty());
println!("Enriched: {:#?}", enriched);
}
Err(e) => {
let msg = e.to_string();
if msg.contains("auth") || msg.contains("credential") {
eprintln!("Skipping: Not authenticated");
} else {
eprintln!("Error: {}", e);
}
}
}
}
#[tokio::test]
async fn test_enrich_findings_batch() {
skip_if_no_cli!();
if std::env::var("CI").is_ok() {
eprintln!("Skipping: CI environment detected");
return;
}
let app_path = fixtures_path().join("passing-app");
if !app_path.exists() {
eprintln!("Skipping: fixtures not found at {}", app_path.display());
return;
}
let client = match OpenCodeClient::auto() {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
let findings = vec![
RawFinding::new(
"WH001",
Severity::Critical,
"webhooks",
"src/webhooks/gdpr.ts",
"Missing webhook",
),
RawFinding::new(
"API001",
Severity::Warning,
"api",
"src/api/products.ts",
"Using REST API",
),
];
match client.enrich_findings(findings, &app_path).await {
Ok(enriched) => {
assert_eq!(enriched.len(), 2);
for finding in &enriched {
println!("{}: {}", finding.rule_id, finding.issue.title);
}
}
Err(e) => {
let msg = e.to_string();
if msg.contains("auth") || msg.contains("credential") || msg.contains("Failed to read")
{
eprintln!("Skipping: {}", e);
} else {
eprintln!("Error: {}", e);
}
}
}
}
#[tokio::test]
async fn test_analyze_file() {
skip_if_no_cli!();
if std::env::var("CI").is_ok() {
eprintln!("Skipping: CI environment detected");
return;
}
let app_path = fixtures_path().join("failing-app");
if !app_path.exists() {
eprintln!("Skipping: fixtures not found");
return;
}
let client = match OpenCodeClient::auto() {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
let file_path = app_path.join("src").join("index.ts");
if !file_path.exists() {
eprintln!("Skipping: No TypeScript file found in fixtures");
return;
}
match client
.analyze_file(&file_path, &["webhooks", "security"])
.await
{
Ok(findings) => {
println!("Found {} issues", findings.len());
for finding in &findings {
println!(" {}: {}", finding.rule_id, finding.message);
}
}
Err(e) => {
let msg = e.to_string();
if msg.contains("auth") || msg.contains("credential") {
eprintln!("Skipping: Not authenticated");
} else {
eprintln!("Error: {}", e);
}
}
}
}
#[test]
fn test_validation_result_summary() {
use gatekpr_opencode::{EnrichedFinding, ValidationResult, ValidationStatus};
let mut result = ValidationResult::new("/app");
result
.findings
.push(EnrichedFinding::from_raw(&RawFinding::new(
"WH001",
Severity::Critical,
"webhooks",
"src/app.ts",
"Missing webhook",
)));
result
.findings
.push(EnrichedFinding::from_raw(&RawFinding::new(
"SEC001",
Severity::Warning,
"security",
"src/utils.ts",
"Eval usage",
)));
result
.findings
.push(EnrichedFinding::from_raw(&RawFinding::new(
"PERF001",
Severity::Info,
"performance",
"src/api.ts",
"Large bundle",
)));
result.calculate_summary();
assert_eq!(result.summary.status, ValidationStatus::NotReady);
assert_eq!(result.summary.critical_count, 1);
assert_eq!(result.summary.warning_count, 1);
assert_eq!(result.summary.info_count, 1);
assert!(result.summary.score < 100);
}
#[test]
fn test_validation_result_ready() {
use gatekpr_opencode::{EnrichedFinding, ValidationResult, ValidationStatus};
let mut result = ValidationResult::new("/app");
result
.findings
.push(EnrichedFinding::from_raw(&RawFinding::new(
"PERF001",
Severity::Info,
"performance",
"src/api.ts",
"Consider caching",
)));
result.calculate_summary();
assert_eq!(result.summary.status, ValidationStatus::Ready);
assert_eq!(result.summary.score, 100);
}
#[test]
fn test_validation_result_needs_review() {
use gatekpr_opencode::{EnrichedFinding, ValidationResult, ValidationStatus};
let mut result = ValidationResult::new("/app");
result
.findings
.push(EnrichedFinding::from_raw(&RawFinding::new(
"SEC001",
Severity::Warning,
"security",
"src/utils.ts",
"Eval usage",
)));
result.calculate_summary();
assert_eq!(result.summary.status, ValidationStatus::NeedsReview);
assert!(result.summary.score < 100);
assert!(result.summary.score > 0);
}