test: add 29 new tests for cleanup, orchestrator, findings, tool registry, models
Some checks failed
CI / Check (pull_request) Failing after 6m3s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped

- cleanup.rs: 8 tests — routing logic, skip conditions, missing config errors
- orchestrator.rs: 7 tests — summarize_tool_output (screenshot strip, truncation, recursion)
- findings.rs: 6 tests — empty state, severity grouping, SAST correlation, evidence table
- tools/mod.rs: 4 tests — registry completeness, schema validation, browser action enum
- models.rs: 4 tests — TestUserRecord serde, IdentityProvider variants, BSON roundtrip

Total: 326 tests (was 297)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-17 21:13:18 +01:00
parent 37690ce734
commit 0236cad536
6 changed files with 596 additions and 0 deletions

View File

@@ -298,3 +298,186 @@ async fn cleanup_okta(
Err(format!("Okta delete failed ({status}): {body}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use compliance_core::models::pentest::{IdentityProvider, TestUserRecord};
use secrecy::SecretString;
fn make_config_no_keycloak() -> AgentConfig {
AgentConfig {
mongodb_uri: String::new(),
mongodb_database: String::new(),
litellm_url: String::new(),
litellm_api_key: SecretString::from(String::new()),
litellm_model: String::new(),
litellm_embed_model: String::new(),
github_token: None,
github_webhook_secret: None,
gitlab_url: None,
gitlab_token: None,
gitlab_webhook_secret: None,
jira_url: None,
jira_email: None,
jira_api_token: None,
jira_project_key: None,
searxng_url: None,
nvd_api_key: None,
agent_port: 3001,
scan_schedule: String::new(),
cve_monitor_schedule: String::new(),
git_clone_base_path: String::new(),
ssh_key_path: String::new(),
keycloak_url: None,
keycloak_realm: None,
keycloak_admin_username: None,
keycloak_admin_password: None,
pentest_verification_email: None,
pentest_imap_host: None,
pentest_imap_port: None,
pentest_imap_username: None,
pentest_imap_password: None,
}
}
#[tokio::test]
async fn already_cleaned_up_returns_false() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: Some(IdentityProvider::Keycloak),
cleaned_up: true,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert_eq!(result, Ok(false));
}
#[tokio::test]
async fn firebase_returns_false_not_implemented() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: Some(IdentityProvider::Firebase),
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert_eq!(result, Ok(false));
}
#[tokio::test]
async fn no_provider_no_keycloak_skips() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: None,
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert_eq!(result, Ok(false));
}
#[tokio::test]
async fn custom_provider_no_keycloak_skips() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: Some(IdentityProvider::Custom),
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert_eq!(result, Ok(false));
}
#[tokio::test]
async fn keycloak_missing_config_returns_error() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: Some(IdentityProvider::Keycloak),
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert!(result.is_err());
assert!(result
.as_ref()
.err()
.is_some_and(|e| e.contains("KEYCLOAK_URL")));
}
#[tokio::test]
async fn keycloak_missing_username_returns_error() {
let user = TestUserRecord {
username: None,
email: Some("test@example.com".into()),
provider_user_id: None,
provider: Some(IdentityProvider::Keycloak),
cleaned_up: false,
};
let mut config = make_config_no_keycloak();
config.keycloak_url = Some("http://localhost:8080".into());
config.keycloak_realm = Some("test".into());
config.keycloak_admin_username = Some("admin".into());
config.keycloak_admin_password = Some(SecretString::from("pass".to_string()));
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert!(result.is_err());
assert!(result
.as_ref()
.err()
.is_some_and(|e| e.contains("username")));
}
#[tokio::test]
async fn auth0_missing_env_returns_error() {
let user = TestUserRecord {
username: None,
email: Some("test@example.com".into()),
provider_user_id: None,
provider: Some(IdentityProvider::Auth0),
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert!(result.is_err());
assert!(result
.as_ref()
.err()
.is_some_and(|e| e.contains("AUTH0_DOMAIN")));
}
#[tokio::test]
async fn okta_missing_env_returns_error() {
let user = TestUserRecord {
username: Some("test".into()),
email: None,
provider_user_id: None,
provider: Some(IdentityProvider::Okta),
cleaned_up: false,
};
let config = make_config_no_keycloak();
let http = reqwest::Client::new();
let result = cleanup_test_user(&user, &config, &http).await;
assert!(result.is_err());
assert!(result
.as_ref()
.err()
.is_some_and(|e| e.contains("OKTA_DOMAIN")));
}
}

View File

@@ -612,3 +612,95 @@ fn summarize_tool_output(data: &serde_json::Value) -> serde_json::Value {
}
serde_json::Value::Object(summarized)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_summarize_strips_screenshot() {
let input = json!({
"screenshot_base64": "iVBOR...",
"url": "https://example.com"
});
let result = summarize_tool_output(&input);
assert_eq!(
result["screenshot_base64"],
"[screenshot captured and saved to report]"
);
assert_eq!(result["url"], "https://example.com");
}
#[test]
fn test_summarize_truncates_html() {
let long_html = "x".repeat(3000);
let input = json!({ "html": long_html });
let result = summarize_tool_output(&input);
let s = result["html"].as_str().unwrap_or_default();
assert!(s.contains("[truncated, 3000 chars total]"));
assert!(s.starts_with(&"x".repeat(2000)));
assert!(s.len() < 3000);
}
#[test]
fn test_summarize_truncates_text() {
let long_text = "a".repeat(2000);
let input = json!({ "text": long_text });
let result = summarize_tool_output(&input);
let s = result["text"].as_str().unwrap_or_default();
assert!(s.contains("[truncated]"));
assert!(s.starts_with(&"a".repeat(1500)));
assert!(s.len() < 2000);
}
#[test]
fn test_summarize_trims_large_arrays() {
let elements: Vec<serde_json::Value> = (0..20).map(|i| json!(format!("el-{i}"))).collect();
let input = json!({ "elements": elements });
let result = summarize_tool_output(&input);
let arr = result["elements"].as_array();
assert!(arr.is_some());
if let Some(arr) = arr {
// 15 kept + 1 summary entry
assert_eq!(arr.len(), 16);
assert_eq!(arr[15], json!("... and 5 more"));
}
}
#[test]
fn test_summarize_preserves_small_data() {
let input = json!({
"url": "https://example.com",
"status": 200,
"title": "Example"
});
let result = summarize_tool_output(&input);
assert_eq!(result, input);
}
#[test]
fn test_summarize_recursive() {
let input = json!({
"page": {
"screenshot_base64": "iVBORw0KGgoAAAA...",
"url": "https://example.com"
}
});
let result = summarize_tool_output(&input);
assert_eq!(
result["page"]["screenshot_base64"],
"[screenshot captured and saved to report]"
);
assert_eq!(result["page"]["url"], "https://example.com");
}
#[test]
fn test_summarize_non_object() {
let string_val = json!("just a string");
assert_eq!(summarize_tool_output(&string_val), string_val);
let num_val = json!(42);
assert_eq!(summarize_tool_output(&num_val), num_val);
}
}

View File

@@ -367,3 +367,156 @@ fn build_code_correlation(
sections.join("\n")
)
}
#[cfg(test)]
mod tests {
use super::*;
use compliance_core::models::dast::{DastEvidence, DastVulnType};
use compliance_core::models::finding::Severity;
use compliance_core::models::scan::ScanType;
/// Helper: create a minimal `DastFinding`.
fn make_dast(title: &str, severity: Severity, endpoint: &str) -> DastFinding {
DastFinding::new(
"run1".into(),
"target1".into(),
DastVulnType::Xss,
title.into(),
"desc".into(),
severity,
endpoint.into(),
"GET".into(),
)
}
/// Helper: create a minimal SAST `Finding` with an ObjectId.
fn make_sast(title: &str) -> Finding {
let mut f = Finding::new(
"repo1".into(),
"fp1".into(),
"semgrep".into(),
ScanType::Sast,
title.into(),
"sast desc".into(),
Severity::High,
);
f.id = Some(mongodb::bson::oid::ObjectId::new());
f
}
#[test]
fn test_findings_empty() {
let result = findings(&[], &[], &[], &[]);
assert!(
result.contains("No vulnerabilities were identified"),
"Empty findings should contain the no-vulns message"
);
}
#[test]
fn test_findings_grouped_by_severity() {
let f_high = make_dast("High vuln", Severity::High, "/a");
let f_low = make_dast("Low vuln", Severity::Low, "/b");
let f_critical = make_dast("Crit vuln", Severity::Critical, "/c");
let result = findings(&[f_high, f_low, f_critical], &[], &[], &[]);
// All severity group headers should appear
assert!(
result.contains("Critical (1)"),
"should have Critical header"
);
assert!(result.contains("High (1)"), "should have High header");
assert!(result.contains("Low (1)"), "should have Low header");
// Critical should appear before High, High before Low
let crit_pos = result.find("Critical (1)");
let high_pos = result.find("High (1)");
let low_pos = result.find("Low (1)");
assert!(crit_pos < high_pos, "Critical should come before High");
assert!(high_pos < low_pos, "High should come before Low");
}
#[test]
fn test_code_correlation_sast_link() {
let mut sast = make_sast("SQL Injection in query");
sast.file_path = Some("src/db/query.rs".into());
sast.line_number = Some(42);
sast.code_snippet =
Some("let q = format!(\"SELECT * FROM {} WHERE id={}\", table, id);".into());
let sast_id = sast.id.as_ref().map(|oid| oid.to_hex()).unwrap_or_default();
let mut dast = make_dast("SQLi on /api/users", Severity::High, "/api/users");
dast.linked_sast_finding_id = Some(sast_id);
let result = findings(&[dast], &[sast], &[], &[]);
assert!(
result.contains("SAST Correlation"),
"should render SAST Correlation badge"
);
assert!(
result.contains("src/db/query.rs"),
"should contain the file path"
);
assert!(result.contains(":42"), "should contain the line number");
assert!(
result.contains("Vulnerable Code"),
"should render code snippet block"
);
}
#[test]
fn test_code_correlation_no_match() {
let dast = make_dast("XSS in search", Severity::Medium, "/search");
// No linked_sast_finding_id, no code context, no sbom
let result = findings(&[dast], &[], &[], &[]);
assert!(
!result.contains("code-correlation"),
"should not contain any code-correlation div"
);
}
#[test]
fn test_evidence_html_empty() {
let f = make_dast("No evidence", Severity::Low, "/x");
let result = build_evidence_html(&f);
assert!(result.is_empty(), "no evidence should yield empty string");
}
#[test]
fn test_evidence_html_with_entries() {
let mut f = make_dast("Has evidence", Severity::High, "/y");
f.evidence.push(DastEvidence {
request_method: "POST".into(),
request_url: "https://example.com/login".into(),
request_headers: None,
request_body: None,
response_status: 200,
response_headers: None,
response_snippet: Some("OK".into()),
screenshot_path: None,
payload: Some("<script>alert(1)</script>".into()),
response_time_ms: None,
});
let result = build_evidence_html(&f);
assert!(
result.contains("evidence-table"),
"should render the evidence table"
);
assert!(result.contains("POST"), "should contain request method");
assert!(
result.contains("https://example.com/login"),
"should contain request URL"
);
assert!(result.contains("200"), "should contain response status");
assert!(
result.contains("&lt;script&gt;alert(1)&lt;/script&gt;"),
"payload should be HTML-escaped"
);
}
}