Files
PiCloud/crates/picloud-cli/tests/scripts.rs
MechaCat02 c73e3c80c0 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>
2026-05-29 23:34:03 +02:00

241 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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}"
);
}