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>
171 lines
5.0 KiB
Rust
171 lines
5.0 KiB
Rust
//! `pic api-keys` — mint / ls / rm journeys.
|
|
//!
|
|
//! Server semantics asserted here:
|
|
//! * `mint` emits the `raw_token` *exactly once* and never on `ls`.
|
|
//! * A minted key is a valid bearer for `/auth/me`.
|
|
//! * After `rm`, the same token is rejected (401).
|
|
|
|
use predicates::prelude::*;
|
|
use serde_json::Value;
|
|
|
|
use crate::common;
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn mint_prints_raw_token_once_and_ls_omits_it() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let name = format!("pic-cli-mint-{}", common::unique_slug("k"));
|
|
|
|
let out = common::pic_as(&env)
|
|
.args([
|
|
"--output",
|
|
"json",
|
|
"api-keys",
|
|
"mint",
|
|
&name,
|
|
"--scope",
|
|
"script:read",
|
|
])
|
|
.output()
|
|
.expect("api-keys mint");
|
|
assert!(out.status.success(), "mint failed: {out:?}");
|
|
let body: Value = serde_json::from_slice(&out.stdout).expect("JSON");
|
|
let token = body["token"]
|
|
.as_str()
|
|
.expect("mint should expose `token`")
|
|
.to_string();
|
|
let key_id = body["id"]
|
|
.as_str()
|
|
.expect("mint should expose `id`")
|
|
.to_string();
|
|
assert!(
|
|
token.starts_with("pic_"),
|
|
"tokens are pic_-prefixed: {token}"
|
|
);
|
|
|
|
// `ls` must NEVER carry the raw token. The key row should appear,
|
|
// identified by name, but `token` is mint-only.
|
|
let ls = common::pic_as(&env)
|
|
.args(["--output", "json", "api-keys", "ls"])
|
|
.output()
|
|
.expect("api-keys ls");
|
|
assert!(ls.status.success(), "ls failed: {ls:?}");
|
|
let ls_body: Value = serde_json::from_slice(&ls.stdout).expect("JSON");
|
|
let arr = ls_body.as_array().expect("array");
|
|
let row = arr
|
|
.iter()
|
|
.find(|r| r.get("id").and_then(Value::as_str) == Some(key_id.as_str()))
|
|
.expect("our key in ls");
|
|
assert!(
|
|
row.get("token").is_none(),
|
|
"ls must not expose raw_token: {row}"
|
|
);
|
|
|
|
// Cleanup so we don't leak keys across runs.
|
|
common::pic_as(&env)
|
|
.args(["api-keys", "rm", &key_id])
|
|
.assert()
|
|
.success();
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn minted_key_works_as_bearer() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let name = format!("pic-cli-bearer-{}", common::unique_slug("k"));
|
|
|
|
let mint = common::pic_as(&env)
|
|
.args([
|
|
"--output",
|
|
"json",
|
|
"api-keys",
|
|
"mint",
|
|
&name,
|
|
"--scope",
|
|
"script:read",
|
|
])
|
|
.output()
|
|
.expect("mint");
|
|
assert!(mint.status.success());
|
|
let body: Value = serde_json::from_slice(&mint.stdout).unwrap();
|
|
let token = body["token"].as_str().unwrap().to_string();
|
|
let id = body["id"].as_str().unwrap().to_string();
|
|
|
|
// Drive whoami with the minted token — proves the bearer string we
|
|
// captured really is what the server stamped.
|
|
let key_env = common::custom_env(&fx.url, &token);
|
|
common::seed_credentials(&key_env, &fx.admin_username);
|
|
common::pic_as(&key_env)
|
|
.args(["whoami"])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains(fx.admin_username.as_str()));
|
|
|
|
common::pic_as(&env)
|
|
.args(["api-keys", "rm", &id])
|
|
.assert()
|
|
.success();
|
|
}
|
|
|
|
/// After `rm`, the bearer token is dead server-side: a follow-up
|
|
/// `whoami` driven by it must 401, not 500.
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn rm_revokes_the_token() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let name = format!("pic-cli-rm-{}", common::unique_slug("k"));
|
|
|
|
let mint = common::pic_as(&env)
|
|
.args([
|
|
"--output",
|
|
"json",
|
|
"api-keys",
|
|
"mint",
|
|
&name,
|
|
"--scope",
|
|
"script:read",
|
|
])
|
|
.output()
|
|
.expect("mint");
|
|
let body: Value = serde_json::from_slice(&mint.stdout).unwrap();
|
|
let token = body["token"].as_str().unwrap().to_string();
|
|
let id = body["id"].as_str().unwrap().to_string();
|
|
|
|
common::pic_as(&env)
|
|
.args(["api-keys", "rm", &id])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains(format!("Revoked api-key {id}")));
|
|
|
|
let dead = common::custom_env(&fx.url, &token);
|
|
common::pic_as(&dead)
|
|
.args(["whoami"])
|
|
.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("HTTP 401"));
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn mint_with_unknown_scope_is_rejected_client_side() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
|
|
common::pic_as(&env)
|
|
.args(["api-keys", "mint", "doomed", "--scope", "script:nope"])
|
|
.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("unknown scope"));
|
|
}
|