feat(cli): pic config --effective (masked secret resolution)
The §11 Phase-1 `config --effective` surface. Single-app today, "config" = the app's secrets, so it cross-references the manifest's declared secret names against what's set on the server and renders each masked: `<set>` (managed / on-server-not-in-manifest) or `<unset>` (declared but not pushed). Values are never fetched or shown (§4.6). Shaped to grow an `--explain` mode and inherited `vars` when groups/vars arrive (Phase 3). Tested: a journey verifying declared-but-unset → `<unset>`, post-`secret set` → `<set>`/managed, and the value never appears in output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ mod api_keys;
|
||||
mod apply;
|
||||
mod apps;
|
||||
mod auth;
|
||||
mod config;
|
||||
mod dead_letters;
|
||||
mod email_queue;
|
||||
mod enabled;
|
||||
|
||||
57
crates/picloud-cli/tests/config.rs
Normal file
57
crates/picloud-cli/tests/config.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! `pic config --effective` — masked secret resolution against the manifest.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::common;
|
||||
use crate::common::cleanup::AppGuard;
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn config_effective_masks_and_classifies_secrets() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let slug = common::unique_slug("config");
|
||||
common::pic_as(&env)
|
||||
.args(["apps", "create", &slug])
|
||||
.assert()
|
||||
.success();
|
||||
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let manifest_path = dir.path().join("picloud.toml");
|
||||
fs::write(
|
||||
&manifest_path,
|
||||
format!("[app]\nslug = \"{slug}\"\nname = \"Cfg\"\n\n[secrets]\nnames = [\"API_KEY\"]\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Declared but not pushed → masked + flagged unset; value never shown.
|
||||
common::pic_as(&env)
|
||||
.args(["config", "--effective", "--file"])
|
||||
.arg(&manifest_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("API_KEY"))
|
||||
.stdout(predicate::str::contains("<unset>"))
|
||||
.stdout(predicate::str::contains("not pushed"));
|
||||
|
||||
// Push it → now masked as <set> / managed, still never the value.
|
||||
common::pic_as(&env)
|
||||
.args(["secrets", "set", "--app", &slug, "API_KEY"])
|
||||
.write_stdin("super-secret-value")
|
||||
.assert()
|
||||
.success();
|
||||
common::pic_as(&env)
|
||||
.args(["config", "--effective", "--file"])
|
||||
.arg(&manifest_path)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("<set>"))
|
||||
.stdout(predicate::str::contains("managed"))
|
||||
.stdout(predicate::str::contains("super-secret-value").not());
|
||||
}
|
||||
Reference in New Issue
Block a user