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>
241 lines
7.1 KiB
Rust
241 lines
7.1 KiB
Rust
//! `pic scripts deploy` / `pic scripts ls` edge cases beyond the
|
||
//! smoke test: unknown app, name override, version bumping, missing
|
||
//! file, and the no-`--app` walk across every accessible app.
|
||
|
||
use predicates::prelude::*;
|
||
|
||
use crate::common;
|
||
use crate::common::cleanup::AppGuard;
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn deploy_against_unknown_app_errors() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let fixture = common::fixture_path("hello.rhai");
|
||
let bogus_slug = common::unique_slug("nope");
|
||
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
fixture.to_str().unwrap(),
|
||
"--app",
|
||
&bogus_slug,
|
||
])
|
||
.assert()
|
||
.failure()
|
||
// Specifically 404 — `apps_get` short-circuits before the deploy
|
||
// request even starts. Loose `"HTTP 4"` would have matched a
|
||
// regressed 401 from broken auth.
|
||
.stderr(predicate::str::contains("HTTP 404"));
|
||
}
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn deploy_with_name_override() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let slug = common::unique_slug("scripts-named");
|
||
common::pic_as(&env)
|
||
.args(["apps", "create", &slug])
|
||
.assert()
|
||
.success();
|
||
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
||
|
||
let fixture = common::fixture_path("hello.rhai");
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
fixture.to_str().unwrap(),
|
||
"--app",
|
||
&slug,
|
||
"--name",
|
||
"custom-name",
|
||
])
|
||
.assert()
|
||
.success()
|
||
.stdout(predicate::str::contains("Created custom-name v1"));
|
||
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
fixture.to_str().unwrap(),
|
||
"--app",
|
||
&slug,
|
||
"--name",
|
||
"custom-name",
|
||
])
|
||
.assert()
|
||
.success()
|
||
.stdout(predicate::str::contains("Updated custom-name v2"));
|
||
|
||
let out = common::pic_as(&env)
|
||
.args(["scripts", "ls", "--app", &slug])
|
||
.output()
|
||
.expect("scripts ls");
|
||
assert!(out.status.success());
|
||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||
assert!(
|
||
stdout
|
||
.lines()
|
||
.map(common::cells)
|
||
.any(|c| c.get(2).copied() == Some("custom-name") && c.get(3).copied() == Some("2")),
|
||
"expected custom-name v2 row, got: {stdout}",
|
||
);
|
||
}
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn deploy_bumps_version_each_redeploy() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let slug = common::unique_slug("scripts-bump");
|
||
common::pic_as(&env)
|
||
.args(["apps", "create", &slug])
|
||
.assert()
|
||
.success();
|
||
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
||
|
||
let fixture = common::fixture_path("hello.rhai");
|
||
for expected in ["Created hello v1", "Updated hello v2", "Updated hello v3"] {
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
fixture.to_str().unwrap(),
|
||
"--app",
|
||
&slug,
|
||
])
|
||
.assert()
|
||
.success()
|
||
.stdout(predicate::str::contains(expected));
|
||
}
|
||
}
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn deploy_missing_file_errors() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let slug = common::unique_slug("scripts-missing");
|
||
common::pic_as(&env)
|
||
.args(["apps", "create", &slug])
|
||
.assert()
|
||
.success();
|
||
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
||
|
||
let missing = std::env::temp_dir().join(common::unique_slug("ghost") + ".rhai");
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
missing.to_str().unwrap(),
|
||
"--app",
|
||
&slug,
|
||
])
|
||
.assert()
|
||
.failure()
|
||
.stderr(predicate::str::contains("reading"));
|
||
}
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn ls_without_app_walks_every_accessible_app() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let slug_a = common::unique_slug("scripts-walk-a");
|
||
let slug_b = common::unique_slug("scripts-walk-b");
|
||
|
||
common::pic_as(&env)
|
||
.args(["apps", "create", &slug_a])
|
||
.assert()
|
||
.success();
|
||
let _guard_a = AppGuard::new(&env.url, &env.token, &slug_a);
|
||
common::pic_as(&env)
|
||
.args(["apps", "create", &slug_b])
|
||
.assert()
|
||
.success();
|
||
let _guard_b = AppGuard::new(&env.url, &env.token, &slug_b);
|
||
|
||
let fixture = common::fixture_path("hello.rhai");
|
||
for slug in [&slug_a, &slug_b] {
|
||
common::pic_as(&env)
|
||
.args([
|
||
"scripts",
|
||
"deploy",
|
||
fixture.to_str().unwrap(),
|
||
"--app",
|
||
slug,
|
||
])
|
||
.assert()
|
||
.success();
|
||
}
|
||
|
||
// `pic scripts ls` (no `--app`) issues a single `GET /admin/scripts`
|
||
// against the server now — there's nothing per-app to race against
|
||
// a concurrent AppGuard drop. The previous implementation walked
|
||
// `apps_list` followed by per-app `scripts_list_by_app` calls and
|
||
// aborted on the first 404, which forced this test to retry 5× to
|
||
// paper over the bug. Both the walk and the retry are gone.
|
||
let out = common::pic_as(&env)
|
||
.args(["scripts", "ls"])
|
||
.output()
|
||
.expect("scripts ls");
|
||
assert!(out.status.success(), "scripts ls failed: {out:?}");
|
||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||
|
||
let slugs: std::collections::HashSet<&str> = stdout
|
||
.lines()
|
||
.map(common::cells)
|
||
.filter_map(|c| c.get(1).copied())
|
||
.collect();
|
||
assert!(
|
||
slugs.contains(slug_a.as_str()),
|
||
"missing app A in: {stdout}"
|
||
);
|
||
assert!(
|
||
slugs.contains(slug_b.as_str()),
|
||
"missing app B in: {stdout}"
|
||
);
|
||
}
|
||
|
||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||
#[test]
|
||
fn delete_removes_script_from_ls() {
|
||
let Some(fx) = common::fixture_or_skip() else {
|
||
return;
|
||
};
|
||
let env = common::admin_env(fx);
|
||
let (id, _guard) = common::deploy_fixture(&env, "scripts-del", "hello.rhai");
|
||
|
||
common::pic_as(&env)
|
||
.args(["scripts", "delete", &id])
|
||
.assert()
|
||
.success()
|
||
.stdout(predicate::str::contains(format!("Deleted script {id}")));
|
||
|
||
let out = common::pic_as(&env)
|
||
.args(["scripts", "ls"])
|
||
.output()
|
||
.expect("scripts ls");
|
||
assert!(out.status.success());
|
||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||
assert!(
|
||
!stdout.contains(&id),
|
||
"deleted script id should not appear in ls: {stdout}"
|
||
);
|
||
}
|