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
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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.playwright-mcp/
|
||||||
|
report-preview-full.png
|
||||||
|
compliance-dashboard/attack-chain-final.html
|
||||||
|
|||||||
@@ -298,3 +298,186 @@ async fn cleanup_okta(
|
|||||||
Err(format!("Okta delete failed ({status}): {body}"))
|
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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -612,3 +612,95 @@ fn summarize_tool_output(data: &serde_json::Value) -> serde_json::Value {
|
|||||||
}
|
}
|
||||||
serde_json::Value::Object(summarized)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -367,3 +367,156 @@ fn build_code_correlation(
|
|||||||
sections.join("\n")
|
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("<script>alert(1)</script>"),
|
||||||
|
"payload should be HTML-escaped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -517,6 +517,69 @@ fn pentest_auth_config_default() {
|
|||||||
assert!(!auth.cleanup_test_user);
|
assert!(!auth.cleanup_test_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── TestUserRecord ───
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_record_default() {
|
||||||
|
let r = pentest::TestUserRecord::default();
|
||||||
|
assert!(r.username.is_none());
|
||||||
|
assert!(r.email.is_none());
|
||||||
|
assert!(r.provider_user_id.is_none());
|
||||||
|
assert!(r.provider.is_none());
|
||||||
|
assert!(!r.cleaned_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_record_serde_roundtrip() {
|
||||||
|
let r = pentest::TestUserRecord {
|
||||||
|
username: Some("pentestuser".into()),
|
||||||
|
email: Some("pentest+abc@scanner.example.com".into()),
|
||||||
|
provider_user_id: Some("kc-uuid-123".into()),
|
||||||
|
provider: Some(pentest::IdentityProvider::Keycloak),
|
||||||
|
cleaned_up: false,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&r).unwrap();
|
||||||
|
let back: pentest::TestUserRecord = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(back.username, Some("pentestuser".into()));
|
||||||
|
assert_eq!(back.provider, Some(pentest::IdentityProvider::Keycloak));
|
||||||
|
assert!(!back.cleaned_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_provider_serde_all_variants() {
|
||||||
|
for (variant, expected) in [
|
||||||
|
(pentest::IdentityProvider::Keycloak, "\"keycloak\""),
|
||||||
|
(pentest::IdentityProvider::Auth0, "\"auth0\""),
|
||||||
|
(pentest::IdentityProvider::Okta, "\"okta\""),
|
||||||
|
(pentest::IdentityProvider::Firebase, "\"firebase\""),
|
||||||
|
(pentest::IdentityProvider::Custom, "\"custom\""),
|
||||||
|
] {
|
||||||
|
let json = serde_json::to_string(&variant).unwrap();
|
||||||
|
assert_eq!(json, expected);
|
||||||
|
let back: pentest::IdentityProvider = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(back, variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pentest_session_with_test_user() {
|
||||||
|
let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
|
||||||
|
assert!(s.test_user.is_none());
|
||||||
|
s.test_user = Some(pentest::TestUserRecord {
|
||||||
|
username: Some("pentester".into()),
|
||||||
|
email: Some("pentest+123@example.com".into()),
|
||||||
|
provider_user_id: None,
|
||||||
|
provider: Some(pentest::IdentityProvider::Auth0),
|
||||||
|
cleaned_up: false,
|
||||||
|
});
|
||||||
|
let bson_doc = bson::to_document(&s).unwrap();
|
||||||
|
let back: pentest::PentestSession = bson::from_document(bson_doc).unwrap();
|
||||||
|
assert!(back.test_user.is_some());
|
||||||
|
let tu = back.test_user.as_ref().unwrap();
|
||||||
|
assert_eq!(tu.username, Some("pentester".into()));
|
||||||
|
assert_eq!(tu.provider, Some(pentest::IdentityProvider::Auth0));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Serde helpers (BSON datetime) ───
|
// ─── Serde helpers (BSON datetime) ───
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -142,3 +142,105 @@ impl ToolRegistry {
|
|||||||
self.tools.keys().cloned().collect()
|
self.tools.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_has_all_expected_tools() {
|
||||||
|
let registry = ToolRegistry::new();
|
||||||
|
let names = registry.list_names();
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
"recon",
|
||||||
|
"openapi_parser",
|
||||||
|
"dns_checker",
|
||||||
|
"dmarc_checker",
|
||||||
|
"tls_analyzer",
|
||||||
|
"security_headers",
|
||||||
|
"cookie_analyzer",
|
||||||
|
"csp_analyzer",
|
||||||
|
"cors_checker",
|
||||||
|
"rate_limit_tester",
|
||||||
|
"console_log_detector",
|
||||||
|
"sql_injection_scanner",
|
||||||
|
"xss_scanner",
|
||||||
|
"ssrf_scanner",
|
||||||
|
"auth_bypass_scanner",
|
||||||
|
"api_fuzzer",
|
||||||
|
"browser",
|
||||||
|
];
|
||||||
|
|
||||||
|
for name in &expected {
|
||||||
|
assert!(
|
||||||
|
names.contains(&name.to_string()),
|
||||||
|
"Missing tool: {name}. Registered: {names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_get_returns_tool() {
|
||||||
|
let registry = ToolRegistry::new();
|
||||||
|
assert!(registry.get("recon").is_some());
|
||||||
|
assert!(registry.get("browser").is_some());
|
||||||
|
assert!(registry.get("nonexistent").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_definitions_have_valid_schemas() {
|
||||||
|
let registry = ToolRegistry::new();
|
||||||
|
let defs = registry.all_definitions();
|
||||||
|
|
||||||
|
assert!(!defs.is_empty());
|
||||||
|
for def in &defs {
|
||||||
|
assert!(!def.name.is_empty(), "Tool has empty name");
|
||||||
|
assert!(
|
||||||
|
!def.description.is_empty(),
|
||||||
|
"Tool {} has empty description",
|
||||||
|
def.name
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
def.input_schema.is_object(),
|
||||||
|
"Tool {} schema is not an object",
|
||||||
|
def.name
|
||||||
|
);
|
||||||
|
// Every schema should have "type": "object"
|
||||||
|
assert_eq!(
|
||||||
|
def.input_schema.get("type").and_then(|v| v.as_str()),
|
||||||
|
Some("object"),
|
||||||
|
"Tool {} schema type is not 'object'",
|
||||||
|
def.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn browser_tool_schema_has_action_enum() {
|
||||||
|
let registry = ToolRegistry::new();
|
||||||
|
let browser = registry.get("browser");
|
||||||
|
assert!(browser.is_some());
|
||||||
|
let schema = browser.map(|t| t.input_schema()).unwrap_or_default();
|
||||||
|
let action_prop = schema.get("properties").and_then(|p| p.get("action"));
|
||||||
|
assert!(
|
||||||
|
action_prop.is_some(),
|
||||||
|
"Browser tool missing 'action' property"
|
||||||
|
);
|
||||||
|
let action_enum = action_prop
|
||||||
|
.and_then(|a| a.get("enum"))
|
||||||
|
.and_then(|e| e.as_array());
|
||||||
|
assert!(action_enum.is_some(), "Browser action missing enum");
|
||||||
|
let actions: Vec<&str> = action_enum
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.collect();
|
||||||
|
assert!(actions.contains(&"navigate"));
|
||||||
|
assert!(actions.contains(&"screenshot"));
|
||||||
|
assert!(actions.contains(&"click"));
|
||||||
|
assert!(actions.contains(&"fill"));
|
||||||
|
assert!(actions.contains(&"get_content"));
|
||||||
|
assert!(actions.contains(&"close"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user