use pgevolve::api::build_cluster_plan;
use pgevolve::cluster_config::{Bootstrap, ClusterConfig, ClusterConnection, ClusterProject};
use pgevolve_core::plan::raw_step::StepKind;
use pgevolve_testkit::ephemeral_pg::{EphemeralPostgres, default_pg_version, docker_available};
fn write_roles_dir(dir: &std::path::Path, sql: &str) {
let roles_dir = dir.join("roles");
std::fs::create_dir_all(&roles_dir).unwrap();
std::fs::write(roles_dir.join("roles.sql"), sql).unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn build_cluster_plan_empty_roles_dir() {
if !docker_available() {
eprintln!("skipping: docker not available");
return;
}
let pg = EphemeralPostgres::start(default_pg_version())
.await
.expect("start ephemeral postgres");
let dsn = pg.dsn().to_string();
let tmp = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join("roles")).unwrap();
let cfg = ClusterConfig {
project: ClusterProject {
name: "test".into(),
},
connection: ClusterConnection { dsn },
bootstrap: Bootstrap {
roles: vec!["postgres".into(), "pgevolve".into()],
},
};
let plan = build_cluster_plan(tmp.path(), &cfg)
.await
.expect("build_cluster_plan should succeed");
assert!(
plan.steps.is_empty(),
"expected no steps for empty roles dir, got {:?}",
plan.steps
);
assert!(
plan.advisory_findings.is_empty(),
"expected no advisory findings, got {:?}",
plan.advisory_findings
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn build_cluster_plan_surfaces_role_loses_superuser_finding() {
if !docker_available() {
eprintln!("skipping: docker not available");
return;
}
let pg = EphemeralPostgres::start(default_pg_version())
.await
.expect("start ephemeral postgres");
pg.exec_sql("CREATE ROLE admin WITH SUPERUSER LOGIN;")
.await
.expect("seed superuser role");
let dsn = pg.dsn().to_string();
let tmp = tempfile::TempDir::new().unwrap();
write_roles_dir(tmp.path(), "CREATE ROLE admin WITH LOGIN;\n");
let cfg = ClusterConfig {
project: ClusterProject {
name: "test".into(),
},
connection: ClusterConnection { dsn },
bootstrap: Bootstrap {
roles: vec!["postgres".into(), "pgevolve".into()],
},
};
let plan = build_cluster_plan(tmp.path(), &cfg)
.await
.expect("build_cluster_plan should succeed");
let has_superuser_finding = plan
.advisory_findings
.iter()
.any(|f| f.rule == "role-loses-superuser");
assert!(
has_superuser_finding,
"expected at least one role-loses-superuser finding — check_cluster_changeset \
is not being called from build_cluster_plan. Got {} total findings: {:#?}",
plan.advisory_findings.len(),
plan.advisory_findings
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn build_cluster_plan_creates_new_role() {
if !docker_available() {
eprintln!("skipping: docker not available");
return;
}
let pg = EphemeralPostgres::start(default_pg_version())
.await
.expect("start ephemeral postgres");
let dsn = pg.dsn().to_string();
let tmp = tempfile::TempDir::new().unwrap();
write_roles_dir(tmp.path(), "CREATE ROLE app_user WITH LOGIN;\n");
let cfg = ClusterConfig {
project: ClusterProject {
name: "test".into(),
},
connection: ClusterConnection { dsn },
bootstrap: Bootstrap {
roles: vec!["postgres".into(), "pgevolve".into()],
},
};
let plan = build_cluster_plan(tmp.path(), &cfg)
.await
.expect("build_cluster_plan should succeed");
let has_create_role_step = plan
.steps
.iter()
.any(|s| matches!(s.kind, StepKind::CreateRole));
assert!(
has_create_role_step,
"expected at least one CreateRole step for a role that exists in source \
but not in the live cluster. Got {} steps: {:#?}",
plan.steps.len(),
plan.steps
);
}