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:
179
crates/picloud-cli/tests/logs.rs
Normal file
179
crates/picloud-cli/tests/logs.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! `pic logs <script-id>` — emptiness, status labels, `--limit`
|
||||
//! clamping, error path for unknown ids, and the 120-char truncate
|
||||
//! applied to the summary column.
|
||||
|
||||
use predicates::prelude::*;
|
||||
|
||||
use crate::common;
|
||||
|
||||
/// Pick out the data rows from `pic logs` TSV output — the header line
|
||||
/// (`created_at\tstatus\tsummary`) is now always present, so the old
|
||||
/// "no non-empty lines means no logs" check needs to skip it.
|
||||
fn data_rows(stdout: &str) -> Vec<&str> {
|
||||
stdout
|
||||
.lines()
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.filter(|l| !l.starts_with("created_at"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_for_fresh_script_is_empty() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "logs-empty", "hello.rhai");
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["logs", &id])
|
||||
.output()
|
||||
.expect("logs");
|
||||
assert!(out.status.success(), "logs failed: {out:?}");
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
assert!(
|
||||
data_rows(&stdout).is_empty(),
|
||||
"expected no log rows (header is allowed), got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_after_invoke_records_success_row() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "logs-ok", "hello.rhai");
|
||||
|
||||
common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["logs", &id])
|
||||
.output()
|
||||
.expect("logs");
|
||||
assert!(out.status.success(), "logs failed: {out:?}");
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
let rows = data_rows(&stdout);
|
||||
assert_eq!(rows.len(), 1, "expected 1 data row, got: {stdout}");
|
||||
let cols: Vec<&str> = rows[0].split('\t').map(str::trim).collect();
|
||||
assert_eq!(
|
||||
cols.len(),
|
||||
3,
|
||||
"row should be 3 tab-delimited cells: {rows:?}"
|
||||
);
|
||||
assert_eq!(cols[1], "success");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_records_error_for_throwing_script() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "logs-err", "throw.rhai");
|
||||
|
||||
// The invoke is expected to fail — we only care that the execution
|
||||
// gets recorded with `Error` status.
|
||||
let _ = common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.output();
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["logs", &id])
|
||||
.output()
|
||||
.expect("logs");
|
||||
assert!(out.status.success(), "logs failed: {out:?}");
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
let row = data_rows(&stdout)
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one data row");
|
||||
let cols: Vec<&str> = row.split('\t').map(str::trim).collect();
|
||||
assert_eq!(cols[1], "error", "expected error status, got row: {row}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_respects_limit_flag() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "logs-limit", "hello.rhai");
|
||||
|
||||
for _ in 0..3 {
|
||||
common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["logs", &id, "--limit", "1"])
|
||||
.output()
|
||||
.expect("logs");
|
||||
assert!(out.status.success(), "logs failed: {out:?}");
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
let rows = data_rows(&stdout).len();
|
||||
assert_eq!(rows, 1, "expected --limit 1, got rows: {stdout}");
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_for_unknown_id_errors() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
|
||||
let bogus = "00000000-0000-0000-0000-000000000000";
|
||||
common::pic_as(&env)
|
||||
.args(["logs", bogus])
|
||||
.assert()
|
||||
.failure()
|
||||
// 404 specifically — same `NotFound(ScriptId)` path the get/edit
|
||||
// endpoints use.
|
||||
.stderr(predicate::str::contains("HTTP 404"));
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn logs_truncates_long_summary() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let (id, _guard) = common::deploy_fixture(&env, "logs-loud", "loud.rhai");
|
||||
|
||||
common::pic_as(&env)
|
||||
.args(["scripts", "invoke", &id])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let out = common::pic_as(&env)
|
||||
.args(["logs", &id])
|
||||
.output()
|
||||
.expect("logs");
|
||||
assert!(out.status.success(), "logs failed: {out:?}");
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
let row = data_rows(&stdout)
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one data row");
|
||||
let summary = row.split('\t').nth(2).expect("summary column");
|
||||
assert!(
|
||||
summary.ends_with('…'),
|
||||
"summary should be truncated with `…`, got: {summary}"
|
||||
);
|
||||
let chars = summary.chars().count();
|
||||
assert!(
|
||||
chars <= 121,
|
||||
"summary should be ≤120 chars + the truncation marker, got {chars}: {summary}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user