From a1e1776d00482f598054a14f59b86f17606233f1 Mon Sep 17 00:00:00 2001 From: Bidifortune Date: Mon, 1 Jun 2026 22:56:41 +0100 Subject: [PATCH] Complete velocity check implementation with proper threshold logic tests Add comprehensive test coverage for detect_high_velocity in ComplianceEngine: - Test for no flagging when events are below threshold - Test for no flagging when events are outside velocity window - Test for flagging when events exactly equal threshold - Test for borrow and repay events both being counted - Test for volume threshold below/above scenarios - Test for suspicion flags being stored correctly - Test for audit log entry creation The detect_high_velocity implementation was already complete - the SQL query uses HAVING COUNT(*) >= threshold correctly. This change adds missing test coverage to ensure the threshold logic works as expected. --- backend/src/compliance.rs | 22 +- backend/tests/compliance_integration_tests.rs | 297 +++++++++++++++++- 2 files changed, 307 insertions(+), 12 deletions(-) diff --git a/backend/src/compliance.rs b/backend/src/compliance.rs index 9d0e3c669..f25d61f23 100644 --- a/backend/src/compliance.rs +++ b/backend/src/compliance.rs @@ -347,10 +347,20 @@ mod tests { assert_eq!(engine.volume_threshold, dec!(50000)); } - // Additional integration tests would go here - // Test velocity detection logic - // Test volume threshold detection - // Test sanctions screening integration - // Test risk scoring algorithms - // Add compliance violation scenarios + #[tokio::test] + async fn test_velocity_threshold_evaluation() { + let db = PgPool::connect_lazy("postgres://localhost/test").unwrap(); + let engine = ComplianceEngine::new(db, 3, 10, dec!(100000)); + + assert_eq!(engine.velocity_threshold, 3); + assert_eq!(engine.velocity_window_mins, 10); + } + + #[tokio::test] + async fn test_volume_threshold_evaluation() { + let db = PgPool::connect_lazy("postgres://localhost/test").unwrap(); + let engine = ComplianceEngine::new(db, 3, 10, dec!(100000)); + + assert_eq!(engine.volume_threshold, dec!(100000)); + } } diff --git a/backend/tests/compliance_integration_tests.rs b/backend/tests/compliance_integration_tests.rs index bb7704d56..3103f49db 100644 --- a/backend/tests/compliance_integration_tests.rs +++ b/backend/tests/compliance_integration_tests.rs @@ -26,6 +26,36 @@ async fn insert_test_plan(db: &PgPool, plan_id: Uuid, user_id: Uuid) { .unwrap(); } +async fn insert_old_test_plan(db: &PgPool, plan_id: Uuid, user_id: Uuid) { + sqlx::query( + r#"INSERT INTO plans (id, user_id, title, is_flagged, created_at) + VALUES ($1, $2, 'Old Test Plan', false, NOW() - INTERVAL '60 days')"#, + ) + .bind(plan_id) + .bind(user_id) + .execute(db) + .await + .unwrap(); +} + +async fn insert_test_lending_event(db: &PgPool, plan_id: Uuid, user_id: Uuid, event_type: &str, amount: &str, minutes_ago: i64) { + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, $4, $5, 'USD', NOW() - INTERVAL '1 minute' * $6) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .bind(event_type) + .bind(amount) + .bind(minutes_ago) + .execute(db) + .await + .unwrap(); +} + #[sqlx::test] async fn test_velocity_detection_logic(db: PgPool) { let user_id = Uuid::new_v4(); @@ -121,25 +151,183 @@ async fn test_compliance_violation_scenarios(db: PgPool) { let plan_id = Uuid::new_v4(); insert_test_user(&db, user_id).await; + insert_old_test_plan(&db, plan_id, user_id).await; - // Insert old plan with no recent activity + // Insert sudden borrow event sqlx::query( r#" - INSERT INTO plans (id, user_id, title, is_flagged, created_at) - VALUES ($1, $2, 'Old Plan', false, NOW() - INTERVAL '60 days') + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 5000, 'USD', NOW()) "#, ) + .bind(Uuid::new_v4()) .bind(plan_id) .bind(user_id) .execute(&db) .await .unwrap(); - // Insert sudden borrow event + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flagged: bool = sqlx::query_scalar("SELECT is_flagged FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(flagged, "Plan should be flagged for sudden activity spike"); +} + +#[sqlx::test] +async fn test_velocity_no_flag_below_threshold(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert events below threshold (2 events, threshold is 3) + for i in 0..2 { + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 1000, 'USD', NOW() - INTERVAL '1 minute' * $4) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .bind(i) + .execute(&db) + .await + .unwrap(); + } + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flagged: bool = sqlx::query_scalar("SELECT is_flagged FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(!flagged, "Plan should NOT be flagged when events are below threshold"); +} + +#[sqlx::test] +async fn test_velocity_events_outside_window_not_flagged(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert events OLDER than the velocity window (15 minutes, window is 10) + for i in 0..5 { + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 1000, 'USD', NOW() - INTERVAL '1 minute' * $4) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .bind(15 + i) + .execute(&db) + .await + .unwrap(); + } + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flagged: bool = sqlx::query_scalar("SELECT is_flagged FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(!flagged, "Plan should NOT be flagged when events are outside velocity window"); +} + +#[sqlx::test] +async fn test_velocity_exactly_at_threshold(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert exactly threshold number of events (3 events, threshold is 3) + for i in 0..3 { + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 1000, 'USD', NOW() - INTERVAL '1 minute' * $4) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .bind(i) + .execute(&db) + .await + .unwrap(); + } + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flagged: bool = sqlx::query_scalar("SELECT is_flagged FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(flagged, "Plan should be flagged when events exactly equal threshold"); +} + +#[sqlx::test] +async fn test_velocity_repay_events_included(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert mix of borrow and repay events (2 borrows + 1 repay = 3 total, threshold is 3) + for i in 0..2 { + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 1000, 'USD', NOW() - INTERVAL '1 minute' * $4) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .bind(i) + .execute(&db) + .await + .unwrap(); + } + + // Insert one repay event sqlx::query( r#" INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) - VALUES ($1, $2, $3, 'borrow', 5000, 'USD', NOW()) + VALUES ($1, $2, $3, 'repay', 500, 'USD', NOW()) "#, ) .bind(Uuid::new_v4()) @@ -160,7 +348,74 @@ async fn test_compliance_violation_scenarios(db: PgPool) { .await .unwrap_or(false); - assert!(flagged, "Plan should be flagged for sudden activity spike"); + assert!(flagged, "Plan should be flagged when borrow+repay events reach threshold"); +} + +#[sqlx::test] +async fn test_volume_below_threshold_not_flagged(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert volume below threshold + sqlx::query( + r#" + INSERT INTO lending_events (id, plan_id, user_id, event_type, amount, asset_code, event_timestamp) + VALUES ($1, $2, $3, 'borrow', 50000, 'USD', NOW()) + "#, + ) + .bind(Uuid::new_v4()) + .bind(plan_id) + .bind(user_id) + .execute(&db) + .await + .unwrap(); + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flagged: bool = sqlx::query_scalar("SELECT is_flagged FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(!flagged, "Plan should NOT be flagged when volume is below threshold"); +} + +#[sqlx::test] +async fn test_velocity_suspicion_flags_stored(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert 4 events to exceed threshold + for i in 0..4 { + insert_test_lending_event(&db, plan_id, user_id, "borrow", "1000", i).await; + } + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let flags: Option = sqlx::query_scalar("SELECT suspicion_flags FROM plans WHERE id = $1") + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap(); + + assert!(flags.is_some(), "suspicion_flags should be stored"); + let flags = flags.unwrap(); + assert!(flags.contains("High velocity detected")); + assert!(flags.contains("4 borrowing events")); + assert!(flags.contains("10 minutes")); } #[sqlx::test] @@ -199,3 +454,33 @@ async fn test_edge_cases_covered(db: PgPool) { // Should be flagged at exactly threshold assert!(flagged, "Plan should be flagged at volume threshold"); } + +#[sqlx::test] +async fn test_velocity_audit_log_created(db: PgPool) { + let user_id = Uuid::new_v4(); + let plan_id = Uuid::new_v4(); + + insert_test_user(&db, user_id).await; + insert_test_plan(&db, plan_id, user_id).await; + + // Insert 4 events to exceed threshold + for i in 0..4 { + insert_test_lending_event(&db, plan_id, user_id, "borrow", "1000", i).await; + } + + let engine = ComplianceEngine::new(db.clone(), 3, 10, dec!(100000)); + let engine = Arc::new(engine); + + engine.scan_suspicious_activity().await.unwrap(); + + let log_exists: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM action_logs WHERE user_id = $1 AND action = 'suspicious_borrowing_detected' AND entity_id = $2)" + ) + .bind(user_id) + .bind(plan_id) + .fetch_one(&db) + .await + .unwrap_or(false); + + assert!(log_exists, "Audit log should be created for suspicious activity"); +}