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>
132 lines
3.9 KiB
Rust
132 lines
3.9 KiB
Rust
//! 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}"
|
|
);
|
|
}
|