test(cli): extract shared Fixture into tests/common

The single bare-metal integration test now reuses a `LazyLock<Fixture>`
that spawns picloud once on a private port and shares it across every
test in the binary. Sets the stage for per-surface journey modules
(auth, apps, scripts, invoke, logs, roles, output) without each one
paying for its own server spawn — same trick the dashboard Playwright
suite uses with global-setup.

Notes:
- `tests/cli.rs` becomes a tiny module list; the seed flow moved to
  `tests/integration.rs`. The seed slug now goes through
  `common::unique_slug` so parallel/serial reruns can't collide.
- `autotests = false` + an explicit `[[test]] name = "cli"` keeps Cargo
  from auto-promoting future `tests/*.rs` files into their own binaries
  (which would each respawn picloud).
- Subprocess cleanup uses `libc::atexit` to SIGTERM picloud when the
  test binary exits. PR_SET_PDEATHSIG was tried and rejected: it fires
  when the *thread* that forked dies, and cargo's per-test worker
  threads exit between tests, which killed the fixture mid-suite.
- New helpers: AppGuard/UserGuard (RAII teardown), member_user /
  grant_membership / update_membership (direct API for role tests),
  unique_slug / unique_username, pic_as / pic_no_env.
- Two `fixture_url_is_shared_*` tests prove the LazyLock is actually
  shared, not respawned per test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-28 21:21:12 +02:00
parent 5d08974876
commit e4851b3deb
8 changed files with 720 additions and 363 deletions

View File

@@ -0,0 +1,131 @@
//! End-to-end smoke test: login → whoami → apps create → apps ls →
//! scripts deploy (create + update) → scripts ls → scripts invoke →
//! logs. The original seed test, refactored to run against the shared
//! fixture so subsequent journey modules don't each pay for a server
//! spawn.
#![allow(clippy::too_many_lines)]
use predicates::prelude::*;
use serde_json::Value;
use crate::common;
use crate::common::cleanup::AppGuard;
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn end_to_end_login_deploy_invoke_logs() {
let Some(fx) = common::fixture_or_skip() else {
return;
};
let env = common::admin_env(fx);
let slug = common::unique_slug("e2e");
let username = &fx.admin_username;
// 1) login
common::pic_as(&env)
.args(["login"])
.assert()
.success()
.stdout(predicate::str::contains(format!("Logged in as {username}")));
let creds_path = env.config_dir.path().join("credentials");
assert!(
creds_path.exists(),
"credentials file should exist after login"
);
let body = std::fs::read_to_string(&creds_path).unwrap();
assert!(body.contains(&env.url), "creds should contain url: {body}");
assert!(
body.contains(username.as_str()),
"creds should contain username: {body}"
);
// 2) whoami
common::pic_as(&env)
.args(["whoami"])
.assert()
.success()
.stdout(predicate::str::contains(username.clone()));
// 3) apps create
common::pic_as(&env)
.args(["apps", "create", &slug])
.assert()
.success()
.stdout(predicate::str::contains(format!("Created app {slug}")));
// Cleanup no matter what subsequent assertions do.
let _guard = AppGuard::new(&env.url, &env.token, &slug);
// 4) apps ls
common::pic_as(&env)
.args(["apps", "ls"])
.assert()
.success()
.stdout(predicate::str::contains(slug.as_str()));
// 5) scripts deploy (create then update)
let fixture = common::fixture_path("hello.rhai");
common::pic_as(&env)
.args([
"scripts",
"deploy",
fixture.to_str().unwrap(),
"--app",
&slug,
])
.assert()
.success()
.stdout(predicate::str::contains("Created hello v1"));
common::pic_as(&env)
.args([
"scripts",
"deploy",
fixture.to_str().unwrap(),
"--app",
&slug,
])
.assert()
.success()
.stdout(predicate::str::contains("Updated hello v2"));
// 6) scripts ls and capture the id
let ls_out = common::pic_as(&env)
.args(["scripts", "ls", "--app", &slug])
.output()
.expect("scripts ls");
assert!(ls_out.status.success(), "scripts ls failed: {ls_out:?}");
let id = common::parse_first_id(std::str::from_utf8(&ls_out.stdout).unwrap())
.expect("scripts ls should print at least one row");
// 7) invoke
let invoke_out = common::pic_as(&env)
.args(["scripts", "invoke", &id])
.output()
.expect("scripts invoke");
assert!(
invoke_out.status.success(),
"invoke failed: {}",
String::from_utf8_lossy(&invoke_out.stderr)
);
let parsed: Value =
serde_json::from_slice(&invoke_out.stdout).expect("invoke stdout should be JSON");
assert_eq!(
parsed["ok"], true,
"expected hello.rhai response, got {parsed}"
);
// 8) logs (the invoke above should have produced exactly one row)
let logs_out = common::pic_as(&env)
.args(["logs", &id])
.output()
.expect("pic logs");
assert!(logs_out.status.success(), "logs failed: {logs_out:?}");
let stdout = String::from_utf8_lossy(&logs_out.stdout);
assert!(
stdout.lines().any(|l| !l.trim().is_empty()),
"logs should have at least one row, got: {stdout}"
);
}