93 lines
3.3 KiB
Rust
93 lines
3.3 KiB
Rust
//! §3 M3 — per-env approval gating (`[project.environments]`).
|
|
//!
|
|
//! A `[project.environments]` block marks some envs confirm-required. Applying
|
|
//! to such an env with `pic apply --env <e>` is refused unless it is explicitly
|
|
//! `--approve <e>`d — and a blanket `--yes` does NOT cover it (§4.2, "CI must
|
|
//! opt in per environment"). An unlisted or `confirm = false` env applies
|
|
//! freely. The gate is client-side, so a refused apply never reaches the server.
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
use crate::common;
|
|
use crate::common::cleanup::GroupGuard;
|
|
|
|
/// A repo whose `[project]` gates `production` (confirm-required) but not
|
|
/// `staging`, managing one pre-existing `[group]`. Minimal per-env overlay files
|
|
/// exist so `--env` can load them.
|
|
fn gated_repo(dir: &Path, project: &str, group: &str) {
|
|
fs::write(
|
|
dir.join("picloud.toml"),
|
|
format!(
|
|
"[project]\nslug = \"{project}\"\n\n\
|
|
[project.environments]\nproduction = {{ confirm = true }}\n\
|
|
staging = {{ confirm = false }}\n\n\
|
|
[group]\nslug = \"{group}\"\nname = \"Env Gated\"\n"
|
|
),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("picloud.production.toml"),
|
|
"# production overlay\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(dir.join("picloud.staging.toml"), "# staging overlay\n").unwrap();
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn confirm_required_env_needs_explicit_approve() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let group = common::unique_slug("ea-grp");
|
|
let project = common::unique_slug("ea-proj");
|
|
let _g = GroupGuard::new(&env.url, &env.token, &group);
|
|
common::pic_as(&env)
|
|
.args(["groups", "create", &group])
|
|
.assert()
|
|
.success();
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
gated_repo(dir.path(), &project, &group);
|
|
let manifest = dir.path().join("picloud.toml");
|
|
let apply = |args: &[&str]| -> std::process::Output {
|
|
let mut c = common::pic_as(&env);
|
|
c.args(["apply", "--file"]).arg(&manifest).args(args);
|
|
c.output().expect("apply")
|
|
};
|
|
|
|
// production is confirm-required → a bare `--env production` is refused,
|
|
// and the message names `--approve`. (Client-side: never hits the server.)
|
|
let out = apply(&["--env", "production"]);
|
|
assert!(!out.status.success(), "production must require approval");
|
|
let err = String::from_utf8_lossy(&out.stderr).to_lowercase();
|
|
assert!(
|
|
err.contains("approve") && err.contains("production"),
|
|
"the refusal must point at --approve production:\n{err}"
|
|
);
|
|
|
|
// A blanket `--yes` does NOT cover a gated env.
|
|
assert!(
|
|
!apply(&["--env", "production", "--yes"]).status.success(),
|
|
"--yes must not bypass a confirm-required env"
|
|
);
|
|
|
|
// `--approve production` lets it through.
|
|
assert!(
|
|
apply(&["--env", "production", "--approve", "production"])
|
|
.status
|
|
.success(),
|
|
"an explicit --approve production must apply"
|
|
);
|
|
|
|
// staging is `confirm = false` → applies freely, no approval needed.
|
|
assert!(
|
|
apply(&["--env", "staging"]).status.success(),
|
|
"an un-gated env must apply without --approve"
|
|
);
|
|
}
|