feat(migrate): onboarding backfill (repositories + dast_targets -> onboarded_targets) (#141)
This commit was merged in pull request #141.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// 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 (`<prefix>_<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::<Document>("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::<Document>("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::<Document>("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;
|
||||
}
|
||||
@@ -7,3 +7,4 @@
|
||||
// Or nightly: (via CI with MongoDB service container)
|
||||
|
||||
mod api;
|
||||
mod migration;
|
||||
|
||||
Reference in New Issue
Block a user