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:
171
crates/picloud-cli/tests/invoke.rs
Normal file
171
crates/picloud-cli/tests/invoke.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
//! `pic scripts invoke` — body sources (inline, `@file`, `@-`), header
|
||||
//! propagation, exit-code semantics for non-2xx responses, and 404
|
||||
//! handling for unknown ids.
|
||||
|
||||
use predicates::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::common;
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_with_inline_json_body_echoes() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "invoke-inline", "echo.rhai");
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id, "--body", r#"{"x":1}"#])
|
||||
.output()
|
||||
.expect("invoke");
|
||||
assert!(out.status.success(), "invoke failed: {out:?}");
|
||||
let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
|
||||
assert_eq!(parsed["body"]["x"], 1, "echoed body: {parsed}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_with_file_body() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "invoke-file", "echo.rhai");
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
|
||||
std::fs::write(tmp.path(), r#"{"src":"file"}"#).unwrap();
|
||||
let body_arg = format!("@{}", tmp.path().display());
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id, "--body", &body_arg])
|
||||
.output()
|
||||
.expect("invoke");
|
||||
assert!(out.status.success(), "invoke failed: {out:?}");
|
||||
let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
|
||||
assert_eq!(parsed["body"]["src"], "file", "echoed body: {parsed}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_with_stdin_body() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "invoke-stdin", "echo.rhai");
|
||||
|
||||
let assert = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id, "--body", "@-"])
|
||||
.write_stdin(r#"{"src":"stdin"}"#)
|
||||
.assert()
|
||||
.success();
|
||||
let out = assert.get_output();
|
||||
let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
|
||||
assert_eq!(parsed["body"]["src"], "stdin", "echoed body: {parsed}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_propagates_headers() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "invoke-hdr", "echo.rhai");
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args([
|
||||
"scripts",
|
||||
"invoke",
|
||||
&id,
|
||||
"-H",
|
||||
"X-Foo: bar",
|
||||
"-H",
|
||||
"X-Baz=qux",
|
||||
])
|
||||
.output()
|
||||
.expect("invoke");
|
||||
assert!(out.status.success(), "invoke failed: {out:?}");
|
||||
let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON");
|
||||
// HTTP normalises header names to lowercase.
|
||||
assert_eq!(parsed["headers"]["x-foo"], "bar", "echoed: {parsed}");
|
||||
assert_eq!(parsed["headers"]["x-baz"], "qux", "echoed: {parsed}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_unknown_script_id_errors() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
|
||||
// Any well-formed UUID that doesn't exist server-side. The
|
||||
// orchestrator's `/execute/{id}` handler returns 404 specifically
|
||||
// for unknown ids — tighten the predicate so a regressed 401
|
||||
// wouldn't sneak through.
|
||||
let bogus = "00000000-0000-0000-0000-000000000000";
|
||||
common::pic_as(&env)
|
||||
.args(["scripts", "invoke", bogus])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("HTTP 404"));
|
||||
}
|
||||
|
||||
/// `pic invoke <id>` (top-level alias) and `pic scripts invoke <id>`
|
||||
/// must hit the same handler and produce identical-shape stdout.
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn top_level_invoke_alias_works() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "inv-alias", "hello.rhai");
|
||||
|
||||
let nested = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.output()
|
||||
.expect("scripts invoke");
|
||||
assert!(nested.status.success());
|
||||
let nested_body: Value = serde_json::from_slice(&nested.stdout).unwrap();
|
||||
|
||||
let aliased = common::pic_as(&env)
|
||||
.args(["invoke", &id])
|
||||
.output()
|
||||
.expect("invoke (top-level)");
|
||||
assert!(aliased.status.success());
|
||||
let aliased_body: Value = serde_json::from_slice(&aliased.stdout).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
nested_body, aliased_body,
|
||||
"top-level alias should produce identical body to scripts invoke"
|
||||
);
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn invoke_non_2xx_exits_nonzero_but_prints_body() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "invoke-500", "boom.rhai");
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.output()
|
||||
.expect("invoke");
|
||||
assert!(!out.status.success(), "expected non-zero exit: {out:?}");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("<- HTTP 500"),
|
||||
"stderr should report HTTP 500: {stderr}"
|
||||
);
|
||||
let parsed: Value = serde_json::from_slice(&out.stdout)
|
||||
.unwrap_or_else(|e| panic!("stdout was not JSON ({e}): {:?}", out.stdout));
|
||||
assert_eq!(parsed["ok"], false, "boom body: {parsed}");
|
||||
assert_eq!(parsed["why"], "intentional", "boom body: {parsed}");
|
||||
}
|
||||
Reference in New Issue
Block a user