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:
97
crates/picloud-cli/tests/common/member.rs
Normal file
97
crates/picloud-cli/tests/common/member.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Helpers for non-admin (`instance_role: Member`) user lifecycle plus
|
||||
//! direct API calls for granting / updating app memberships.
|
||||
//!
|
||||
//! These talk to the manager HTTP surface directly instead of going
|
||||
//! through the CLI, so role-gated tests can stage state without
|
||||
//! requiring `pic` to grow new commands.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::cleanup::UserGuard;
|
||||
use super::Fixture;
|
||||
|
||||
pub const MEMBER_PASSWORD: &str = "pic-cli-test-pw-12345678";
|
||||
|
||||
pub struct MemberUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub _guard: UserGuard,
|
||||
}
|
||||
|
||||
/// Mint a fresh `instance_role: Member` user, log them in for a bearer
|
||||
/// token, and register a `UserGuard` for teardown.
|
||||
pub fn member_user(fx: &Fixture, username: &str) -> MemberUser {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let create = client
|
||||
.post(format!("{}/api/v1/admin/admins", fx.url))
|
||||
.bearer_auth(&fx.admin_token)
|
||||
.json(&json!({
|
||||
"username": username,
|
||||
"password": MEMBER_PASSWORD,
|
||||
"instance_role": "Member",
|
||||
}))
|
||||
.send()
|
||||
.expect("create member user");
|
||||
assert!(
|
||||
create.status().is_success(),
|
||||
"create member user failed: {} {}",
|
||||
create.status(),
|
||||
create.text().unwrap_or_default(),
|
||||
);
|
||||
let body: Value = create.json().expect("admin create json");
|
||||
let id = body["id"]
|
||||
.as_str()
|
||||
.expect("admin create returns id")
|
||||
.to_string();
|
||||
|
||||
// Register cleanup before we attempt anything else that could fail.
|
||||
let guard = UserGuard::new(&fx.url, &fx.admin_token, &id);
|
||||
|
||||
let token = super::server::login_for_bearer_token(&fx.url, username, MEMBER_PASSWORD);
|
||||
|
||||
MemberUser {
|
||||
id,
|
||||
username: username.to_string(),
|
||||
token,
|
||||
_guard: guard,
|
||||
}
|
||||
}
|
||||
|
||||
/// `POST /api/v1/admin/apps/{slug}/members` — grant `role` to `user_id`.
|
||||
pub fn grant_membership(fx: &Fixture, app_slug: &str, user_id: &str, role: &str) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let resp = client
|
||||
.post(format!("{}/api/v1/admin/apps/{}/members", fx.url, app_slug))
|
||||
.bearer_auth(&fx.admin_token)
|
||||
.json(&json!({ "user_id": user_id, "role": role }))
|
||||
.send()
|
||||
.expect("grant membership");
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"grant membership failed: {} {}",
|
||||
resp.status(),
|
||||
resp.text().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// `PATCH /api/v1/admin/apps/{slug}/members/{user_id}` — promote/demote.
|
||||
pub fn update_membership(fx: &Fixture, app_slug: &str, user_id: &str, role: &str) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let resp = client
|
||||
.patch(format!(
|
||||
"{}/api/v1/admin/apps/{}/members/{}",
|
||||
fx.url, app_slug, user_id
|
||||
))
|
||||
.bearer_auth(&fx.admin_token)
|
||||
.json(&json!({ "role": role }))
|
||||
.send()
|
||||
.expect("update membership");
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"update membership failed: {} {}",
|
||||
resp.status(),
|
||||
resp.text().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user