// Integration tests for the onboarding backfill migration. // // Requires MongoDB (set TEST_MONGODB_URI if not at the default). // Not run in CI (which is `--lib` only) — run locally: // cargo test -p compliance-agent --test e2e migration use compliance_agent::database::{Database, DatabasePool}; use compliance_agent::migrate::onboarding; use compliance_core::models::{ ArtifactKind, DastTarget, DastTargetType, TargetType, TrackedRepository, }; use mongodb::bson::{doc, Document}; async fn fresh_db() -> (DatabasePool, String, Database) { let uri = std::env::var("TEST_MONGODB_URI") .unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into()); // Prefix must fit the pool's 30-char cap (`_<32 hex>` <= 63). let prefix = format!("t_{}", &uuid::Uuid::new_v4().simple().to_string()[..16]); let pool = DatabasePool::connect(&uri, &prefix) .await .expect("connect mongo"); let db = pool.for_tenant_id("t1").await.expect("tenant db"); (pool, prefix, db) } async fn cleanup(pool: &DatabasePool, prefix: &str) { if let Ok(names) = pool.client().list_database_names().await { for n in names { if n.starts_with(prefix) { pool.client().database(&n).drop().await.ok(); } } } } #[tokio::test] async fn backfill_folds_relinks_is_idempotent_and_reversible() { let (pool, prefix, db) = fresh_db().await; // Seed a repo. let repo = TrackedRepository::new("acme".into(), "https://git/acme.git".into()); let repo_id = db .repositories() .insert_one(repo) .await .expect("insert repo") .inserted_id .as_object_id() .expect("repo oid"); // A DAST target linked to the repo (folds + promotes to WebApp + relinks). let mut linked = DastTarget::new( "acme-web".into(), "https://acme.example.com".into(), DastTargetType::WebApp, ); linked.repo_id = Some(repo_id.to_hex()); let linked_id = db .dast_targets() .insert_one(linked) .await .expect("insert linked dast") .inserted_id .as_object_id() .expect("linked oid"); // A repo-less DAST target (standalone). let standalone = DastTarget::new( "acme-api".into(), "https://api.acme.com".into(), DastTargetType::RestApi, ); let standalone_id = db .dast_targets() .insert_one(standalone) .await .expect("insert standalone dast") .inserted_id .as_object_id() .expect("standalone oid"); // A DAST scan run pointing at the linked target — should be relinked to the repo. db.collection_named::("dast_scan_runs") .insert_one(doc! { "target_id": linked_id.to_hex(), "status": "completed" }) .await .expect("insert dast run"); // --- Backfill --- assert!(!onboarding::already_applied(&db).await.unwrap()); let report = onboarding::backfill_onboarded_targets(&db, false) .await .expect("backfill"); assert_eq!(report.repos_migrated, 1); assert_eq!(report.dast_targets_folded, 1); assert_eq!(report.dast_targets_standalone, 1); assert!(onboarding::already_applied(&db).await.unwrap()); // Repo target: preserved _id, has git + folded live-url, promoted to WebApp. let repo_target = db .onboarded_targets() .find_one(doc! { "_id": repo_id }) .await .unwrap() .expect("repo target"); assert!(repo_target.has(ArtifactKind::GitRepo)); assert!(repo_target.has(ArtifactKind::LiveUrl)); assert_eq!(repo_target.target_type, TargetType::WebApp); // Standalone target: preserved _id, live-url, backend service. let standalone_target = db .onboarded_targets() .find_one(doc! { "_id": standalone_id }) .await .unwrap() .expect("standalone target"); assert!(standalone_target.has(ArtifactKind::LiveUrl)); assert_eq!(standalone_target.target_type, TargetType::BackendService); // The DAST run was relinked from the old dast id to the repo (unified) id. let run = db .collection_named::("dast_scan_runs") .find_one(doc! {}) .await .unwrap() .expect("run"); assert_eq!(run.get_str("target_id").unwrap(), repo_id.to_hex()); // --- Idempotent: re-run migrates nothing new --- let again = onboarding::backfill_onboarded_targets(&db, false) .await .expect("backfill again"); assert_eq!(again.repos_migrated, 0); assert_eq!(again.dast_targets_folded, 0); assert_eq!(again.dast_targets_standalone, 0); assert!(again.skipped_existing >= 2); // --- Revert: onboarded targets gone, relink undone, marker cleared --- onboarding::revert(&db).await.expect("revert"); assert_eq!( db.onboarded_targets() .count_documents(doc! {}) .await .unwrap(), 0 ); let run_after = db .collection_named::("dast_scan_runs") .find_one(doc! {}) .await .unwrap() .expect("run"); assert_eq!(run_after.get_str("target_id").unwrap(), linked_id.to_hex()); assert!(!onboarding::already_applied(&db).await.unwrap()); cleanup(&pool, &prefix).await; }