//! 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, // InstanceRole / AppRole serialize via `rename_all = // "snake_case"` — wire forms are always lowercase. "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(), ); }