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>
180 lines
5.5 KiB
Rust
180 lines
5.5 KiB
Rust
//! `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}"
|
|
);
|
|
}
|