use compliance_core::models::TrackerType; use serde::{Deserialize, Serialize}; use compliance_core::models::ScanRun; #[derive(Deserialize)] pub struct PaginationParams { #[serde(default = "default_page")] pub page: u64, #[serde(default = "default_limit")] pub limit: i64, } pub(crate) fn default_page() -> u64 { 1 } pub(crate) fn default_limit() -> i64 { 50 } #[derive(Deserialize)] pub struct FindingsFilter { #[serde(default)] pub repo_id: Option, #[serde(default)] pub severity: Option, #[serde(default)] pub scan_type: Option, #[serde(default)] pub status: Option, #[serde(default)] pub q: Option, #[serde(default)] pub sort_by: Option, #[serde(default)] pub sort_order: Option, #[serde(default = "default_page")] pub page: u64, #[serde(default = "default_limit")] pub limit: i64, } #[derive(Serialize)] pub struct ApiResponse { pub data: T, #[serde(skip_serializing_if = "Option::is_none")] pub total: Option, #[serde(skip_serializing_if = "Option::is_none")] pub page: Option, } #[derive(Serialize)] pub struct OverviewStats { pub total_repositories: u64, pub total_findings: u64, pub critical_findings: u64, pub high_findings: u64, pub medium_findings: u64, pub low_findings: u64, pub total_sbom_entries: u64, pub total_cve_alerts: u64, pub total_issues: u64, pub recent_scans: Vec, } #[derive(Deserialize)] pub struct AddRepositoryRequest { pub name: String, pub git_url: String, #[serde(default = "default_branch")] pub default_branch: String, pub auth_token: Option, pub auth_username: Option, pub tracker_type: Option, pub tracker_owner: Option, pub tracker_repo: Option, pub tracker_token: Option, pub scan_schedule: Option, } #[derive(Deserialize)] pub struct UpdateRepositoryRequest { pub name: Option, pub default_branch: Option, pub auth_token: Option, pub auth_username: Option, pub tracker_type: Option, pub tracker_owner: Option, pub tracker_repo: Option, pub tracker_token: Option, pub scan_schedule: Option, } fn default_branch() -> String { "main".to_string() } #[derive(Deserialize)] pub struct UpdateStatusRequest { pub status: String, } #[derive(Deserialize)] pub struct BulkUpdateStatusRequest { pub ids: Vec, pub status: String, } #[derive(Deserialize)] pub struct UpdateFeedbackRequest { pub feedback: String, } #[derive(Deserialize)] pub struct SbomFilter { #[serde(default)] pub repo_id: Option, #[serde(default)] pub package_manager: Option, #[serde(default)] pub q: Option, #[serde(default)] pub has_vulns: Option, #[serde(default)] pub license: Option, #[serde(default = "default_page")] pub page: u64, #[serde(default = "default_limit")] pub limit: i64, } #[derive(Deserialize)] pub struct SbomExportParams { pub repo_id: String, #[serde(default = "default_export_format")] pub format: String, } fn default_export_format() -> String { "cyclonedx".to_string() } #[derive(Deserialize)] pub struct SbomDiffParams { pub repo_a: String, pub repo_b: String, } #[derive(Serialize)] pub struct LicenseSummary { pub license: String, pub count: u64, pub is_copyleft: bool, pub packages: Vec, } #[derive(Serialize)] pub struct SbomDiffResult { pub only_in_a: Vec, pub only_in_b: Vec, pub version_changed: Vec, pub common_count: u64, } #[derive(Serialize)] pub struct SbomDiffEntry { pub name: String, pub version: String, pub package_manager: String, } #[derive(Serialize)] pub struct SbomVersionDiff { pub name: String, pub package_manager: String, pub version_a: String, pub version_b: String, } pub(crate) type AgentExt = axum::extract::Extension>; pub(crate) type ApiResult = Result>, axum::http::StatusCode>; pub(crate) async fn collect_cursor_async( mut cursor: mongodb::Cursor, ) -> Vec { use futures_util::StreamExt; let mut items = Vec::new(); while let Some(result) = cursor.next().await { match result { Ok(item) => items.push(item), Err(e) => tracing::warn!("Failed to deserialize document: {e}"), } } items } #[cfg(test)] mod tests { use super::*; use serde_json::json; // ── PaginationParams ───────────────────────────────────────── #[test] fn pagination_params_defaults() { let p: PaginationParams = serde_json::from_str("{}").unwrap(); assert_eq!(p.page, 1); assert_eq!(p.limit, 50); } #[test] fn pagination_params_custom_values() { let p: PaginationParams = serde_json::from_str(r#"{"page":3,"limit":10}"#).unwrap(); assert_eq!(p.page, 3); assert_eq!(p.limit, 10); } #[test] fn pagination_params_partial_override() { let p: PaginationParams = serde_json::from_str(r#"{"page":5}"#).unwrap(); assert_eq!(p.page, 5); assert_eq!(p.limit, 50); } #[test] fn pagination_params_zero_page() { let p: PaginationParams = serde_json::from_str(r#"{"page":0}"#).unwrap(); assert_eq!(p.page, 0); } // ── FindingsFilter ─────────────────────────────────────────── #[test] fn findings_filter_all_defaults() { let f: FindingsFilter = serde_json::from_str("{}").unwrap(); assert!(f.repo_id.is_none()); assert!(f.severity.is_none()); assert!(f.scan_type.is_none()); assert!(f.status.is_none()); assert!(f.q.is_none()); assert!(f.sort_by.is_none()); assert!(f.sort_order.is_none()); assert_eq!(f.page, 1); assert_eq!(f.limit, 50); } #[test] fn findings_filter_with_all_fields() { let f: FindingsFilter = serde_json::from_str( r#"{ "repo_id": "abc", "severity": "high", "scan_type": "sast", "status": "open", "q": "sql injection", "sort_by": "severity", "sort_order": "desc", "page": 2, "limit": 25 }"#, ) .unwrap(); assert_eq!(f.repo_id.as_deref(), Some("abc")); assert_eq!(f.severity.as_deref(), Some("high")); assert_eq!(f.scan_type.as_deref(), Some("sast")); assert_eq!(f.status.as_deref(), Some("open")); assert_eq!(f.q.as_deref(), Some("sql injection")); assert_eq!(f.sort_by.as_deref(), Some("severity")); assert_eq!(f.sort_order.as_deref(), Some("desc")); assert_eq!(f.page, 2); assert_eq!(f.limit, 25); } #[test] fn findings_filter_empty_string_fields() { let f: FindingsFilter = serde_json::from_str(r#"{"repo_id":"","severity":""}"#).unwrap(); assert_eq!(f.repo_id.as_deref(), Some("")); assert_eq!(f.severity.as_deref(), Some("")); } // ── ApiResponse ────────────────────────────────────────────── #[test] fn api_response_serializes_with_all_fields() { let resp = ApiResponse { data: vec!["a", "b"], total: Some(100), page: Some(1), }; let v = serde_json::to_value(&resp).unwrap(); assert_eq!(v["data"], json!(["a", "b"])); assert_eq!(v["total"], 100); assert_eq!(v["page"], 1); } #[test] fn api_response_skips_none_fields() { let resp = ApiResponse { data: "hello", total: None, page: None, }; let v = serde_json::to_value(&resp).unwrap(); assert_eq!(v["data"], "hello"); assert!(v.get("total").is_none()); assert!(v.get("page").is_none()); } #[test] fn api_response_with_nested_struct() { #[derive(Serialize)] struct Item { id: u32, } let resp = ApiResponse { data: Item { id: 42 }, total: Some(1), page: None, }; let v = serde_json::to_value(&resp).unwrap(); assert_eq!(v["data"]["id"], 42); assert_eq!(v["total"], 1); assert!(v.get("page").is_none()); } #[test] fn api_response_empty_vec() { let resp: ApiResponse> = ApiResponse { data: vec![], total: Some(0), page: Some(1), }; let v = serde_json::to_value(&resp).unwrap(); assert!(v["data"].as_array().unwrap().is_empty()); } // ── SbomFilter ─────────────────────────────────────────────── #[test] fn sbom_filter_defaults() { let f: SbomFilter = serde_json::from_str("{}").unwrap(); assert!(f.repo_id.is_none()); assert!(f.package_manager.is_none()); assert!(f.q.is_none()); assert!(f.has_vulns.is_none()); assert!(f.license.is_none()); assert_eq!(f.page, 1); assert_eq!(f.limit, 50); } #[test] fn sbom_filter_has_vulns_bool() { let f: SbomFilter = serde_json::from_str(r#"{"has_vulns": true}"#).unwrap(); assert_eq!(f.has_vulns, Some(true)); } // ── SbomExportParams ───────────────────────────────────────── #[test] fn sbom_export_params_default_format() { let p: SbomExportParams = serde_json::from_str(r#"{"repo_id":"r1"}"#).unwrap(); assert_eq!(p.repo_id, "r1"); assert_eq!(p.format, "cyclonedx"); } #[test] fn sbom_export_params_custom_format() { let p: SbomExportParams = serde_json::from_str(r#"{"repo_id":"r1","format":"spdx"}"#).unwrap(); assert_eq!(p.format, "spdx"); } // ── AddRepositoryRequest ───────────────────────────────────── #[test] fn add_repository_request_defaults() { let r: AddRepositoryRequest = serde_json::from_str( r#"{ "name": "my-repo", "git_url": "https://github.com/x/y.git" }"#, ) .unwrap(); assert_eq!(r.name, "my-repo"); assert_eq!(r.git_url, "https://github.com/x/y.git"); assert_eq!(r.default_branch, "main"); assert!(r.auth_token.is_none()); assert!(r.tracker_type.is_none()); assert!(r.scan_schedule.is_none()); } #[test] fn add_repository_request_custom_branch() { let r: AddRepositoryRequest = serde_json::from_str( r#"{ "name": "repo", "git_url": "url", "default_branch": "develop" }"#, ) .unwrap(); assert_eq!(r.default_branch, "develop"); } // ── UpdateStatusRequest / BulkUpdateStatusRequest ──────────── #[test] fn update_status_request() { let r: UpdateStatusRequest = serde_json::from_str(r#"{"status":"resolved"}"#).unwrap(); assert_eq!(r.status, "resolved"); } #[test] fn bulk_update_status_request() { let r: BulkUpdateStatusRequest = serde_json::from_str(r#"{"ids":["a","b"],"status":"dismissed"}"#).unwrap(); assert_eq!(r.ids, vec!["a", "b"]); assert_eq!(r.status, "dismissed"); } #[test] fn bulk_update_status_empty_ids() { let r: BulkUpdateStatusRequest = serde_json::from_str(r#"{"ids":[],"status":"x"}"#).unwrap(); assert!(r.ids.is_empty()); } // ── SbomDiffResult serialization ───────────────────────────── #[test] fn sbom_diff_result_serializes() { let r = SbomDiffResult { only_in_a: vec![SbomDiffEntry { name: "pkg-a".to_string(), version: "1.0".to_string(), package_manager: "npm".to_string(), }], only_in_b: vec![], version_changed: vec![SbomVersionDiff { name: "shared".to_string(), package_manager: "cargo".to_string(), version_a: "0.1".to_string(), version_b: "0.2".to_string(), }], common_count: 10, }; let v = serde_json::to_value(&r).unwrap(); assert_eq!(v["only_in_a"].as_array().unwrap().len(), 1); assert_eq!(v["only_in_b"].as_array().unwrap().len(), 0); assert_eq!(v["version_changed"][0]["version_a"], "0.1"); assert_eq!(v["common_count"], 10); } // ── LicenseSummary ─────────────────────────────────────────── #[test] fn license_summary_serializes() { let ls = LicenseSummary { license: "MIT".to_string(), count: 42, is_copyleft: false, packages: vec!["serde".to_string()], }; let v = serde_json::to_value(&ls).unwrap(); assert_eq!(v["license"], "MIT"); assert_eq!(v["is_copyleft"], false); assert_eq!(v["count"], 42); } // ── Default helper functions ───────────────────────────────── #[test] fn default_page_returns_1() { assert_eq!(default_page(), 1); } #[test] fn default_limit_returns_50() { assert_eq!(default_limit(), 50); } }