test(cli): focused journey suite + cover new commands + tighten asserts
Replace the single bare-metal `integration.rs` test with focused
modules driven by the shared `LazyLock<Fixture>` server. Each module
owns one journey:
* `auth.rs` — login (both bearer and username+password paths),
logout (local file + server-side session invalidation), env-vars
overriding the on-disk credentials file, role-label rendering.
* `apps.rs` — create / ls / show / delete (with and without
`--force`), invalid-slug rejection, conflict on duplicate slug.
* `scripts.rs` — deploy (create + update), name override, version
bumping, `ls` (with and without `--app`), delete.
* `invoke.rs` — body sources (inline, `@file`, `@-`), header
propagation, non-2xx exit semantics, top-level `pic invoke` alias.
* `logs.rs` — emptiness, status labels, `--limit`, summary truncation.
* `roles.rs` — Member RBAC: app-list filtering, viewer-vs-editor on
deploy, member can hit the unguarded data plane, non-member 403
on logs.
* `output.rs` — TSV column headers, stdout/stderr separation, RFC3339
shape, and the `--output json` invariants for apps / scripts /
logs / whoami.
* `api_keys.rs` — mint emits `raw_token` once, `ls` omits it, the
minted token works as a real bearer, `rm` invalidates server-side.
Bug-bug-fix-bug-fix:
* The 5× retry loop in `ls_without_app_walks_every_accessible_app`
was masking the abort-on-first-404 walk in the CLI. Now that the
CLI uses a single server call, the retry is gone — the test runs
one `pic scripts ls` and asserts.
* Six `predicate::str::contains("HTTP 4")` assertions tightened to
the specific status code: 422 for invalid-slug, 404 for unknown
app/script/log id, 403 for role denials. Loose `HTTP 4` would
have silently matched a regressed 401 from broken auth.
* `tests/integration.rs` deleted — every step it covered is in one
of the focused modules above.
* Members module exposes `MEMBER_PASSWORD` so auth tests can drive
the real username+password flow over stdin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,9 @@ pub fn member_user(fx: &Fixture, username: &str) -> MemberUser {
|
||||
.json(&json!({
|
||||
"username": username,
|
||||
"password": MEMBER_PASSWORD,
|
||||
"instance_role": "Member",
|
||||
// InstanceRole / AppRole serialize via `rename_all =
|
||||
// "snake_case"` — wire forms are always lowercase.
|
||||
"instance_role": "member",
|
||||
}))
|
||||
.send()
|
||||
.expect("create member user");
|
||||
|
||||
@@ -89,20 +89,27 @@ pub struct TestEnv {
|
||||
pub home: TempDir,
|
||||
}
|
||||
|
||||
/// Per-test env pre-loaded with the admin token. Mirrors what the seed
|
||||
/// test built inline.
|
||||
/// Per-test env pre-loaded with the admin token, and a credentials
|
||||
/// file already on disk so non-login commands ("pic apps create", …)
|
||||
/// can run without first calling `pic login`. As of the env-var
|
||||
/// consistency fix, `PICLOUD_URL`/`PICLOUD_TOKEN` (set by `pic_as`)
|
||||
/// also work for *every* command, not just `login` — `config::resolve`
|
||||
/// reads them first and falls back to the on-disk file.
|
||||
pub fn admin_env(fx: &Fixture) -> TestEnv {
|
||||
TestEnv {
|
||||
let env = TestEnv {
|
||||
url: fx.url.clone(),
|
||||
token: fx.admin_token.clone(),
|
||||
config_dir: TempDir::new().expect("config tempdir"),
|
||||
home: TempDir::new().expect("home tempdir"),
|
||||
}
|
||||
};
|
||||
seed_credentials(&env, &fx.admin_username);
|
||||
env
|
||||
}
|
||||
|
||||
/// Per-test env pre-loaded with a specific (URL, token) pair. Used by
|
||||
/// tests that want a non-admin token, a bogus token, or an unreachable
|
||||
/// URL.
|
||||
/// URL. Does **not** seed a credentials file — call `seed_credentials`
|
||||
/// explicitly when the test needs to run non-login commands.
|
||||
pub fn custom_env(url: &str, token: &str) -> TestEnv {
|
||||
TestEnv {
|
||||
url: url.to_string(),
|
||||
@@ -112,6 +119,19 @@ pub fn custom_env(url: &str, token: &str) -> TestEnv {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a valid credentials TOML into `env.config_dir` so subsequent
|
||||
/// `pic_as(&env)` invocations can issue non-login subcommands. Mirrors
|
||||
/// the file shape `pic login` produces (url/token/username). Tests that
|
||||
/// exercise the "no credentials" / "stale token" error paths construct
|
||||
/// `TestEnv` directly to keep the config dir empty.
|
||||
pub fn seed_credentials(env: &TestEnv, username: &str) {
|
||||
let body = format!(
|
||||
"url = \"{}\"\ntoken = \"{}\"\nusername = \"{}\"\n",
|
||||
env.url, env.token, username,
|
||||
);
|
||||
std::fs::write(env.config_dir.path().join("credentials"), body).expect("seed credentials file");
|
||||
}
|
||||
|
||||
/// `pic` invocation with the env wired up — credentials dir, HOME, and
|
||||
/// the `PICLOUD_URL`/`PICLOUD_TOKEN` shortcut env vars.
|
||||
pub fn pic_as(env: &TestEnv) -> AssertCommand {
|
||||
@@ -183,6 +203,53 @@ pub fn fixture_path(name: &str) -> PathBuf {
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// Create a fresh app and deploy a `tests/fixtures/<fixture_name>` into
|
||||
/// it. Returns the new script id plus the `AppGuard` that cleans the
|
||||
/// app (and its scripts via `force=true`) on Drop. Used by invoke /
|
||||
/// logs / output journeys that all need "deploy something, then drive
|
||||
/// `pic` against it".
|
||||
pub fn deploy_fixture(
|
||||
env: &TestEnv,
|
||||
app_label: &str,
|
||||
fixture_name: &str,
|
||||
) -> (String, cleanup::AppGuard) {
|
||||
let slug = unique_slug(app_label);
|
||||
pic_as(env)
|
||||
.args(["apps", "create", &slug])
|
||||
.assert()
|
||||
.success();
|
||||
let guard = cleanup::AppGuard::new(&env.url, &env.token, &slug);
|
||||
|
||||
let fixture = fixture_path(fixture_name);
|
||||
pic_as(env)
|
||||
.args([
|
||||
"scripts",
|
||||
"deploy",
|
||||
fixture.to_str().unwrap(),
|
||||
"--app",
|
||||
&slug,
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let out = pic_as(env)
|
||||
.args(["scripts", "ls", "--app", &slug])
|
||||
.output()
|
||||
.expect("scripts ls");
|
||||
let id = parse_first_id(std::str::from_utf8(&out.stdout).unwrap())
|
||||
.expect("scripts ls should produce one row");
|
||||
(id, guard)
|
||||
}
|
||||
|
||||
/// Split a row from `pic apps ls` / `pic scripts ls` into trimmed
|
||||
/// cells. The output writer space-pads each cell to its column's max
|
||||
/// width before the tab, so raw `split('\t')` leaves trailing spaces;
|
||||
/// this helper hides that detail from tests that only care about the
|
||||
/// logical values.
|
||||
pub fn cells(row: &str) -> Vec<&str> {
|
||||
row.split('\t').map(str::trim).collect()
|
||||
}
|
||||
|
||||
/// First data row's first tab-delimited cell, used to extract IDs from
|
||||
/// `pic scripts ls` output. The header is expected to start with "id".
|
||||
pub fn parse_first_id(table: &str) -> Option<String> {
|
||||
|
||||
Reference in New Issue
Block a user