diff --git a/crates/picloud/tests/authz.rs b/crates/picloud/tests/authz.rs index 282cbd2..4b0f1a6 100644 --- a/crates/picloud/tests/authz.rs +++ b/crates/picloud/tests/authz.rs @@ -1042,6 +1042,7 @@ async fn member_app_admin_can_manage_members(pool: PgPool) { #[sqlx::test(migrations = "../manager-core/migrations")] async fn membership_makes_app_appear_in_members_app_list(pool: PgPool) { let s = boot(pool).await; + let owner_token = login_token(&s.server, "owner", "owner-pw").await; let bob = seed_user(&s.pool, "bob", "bob-pw", InstanceRole::Member).await; let bob_token = login_token(&s.server, "bob", "bob-pw").await; @@ -1058,8 +1059,12 @@ async fn membership_makes_app_appear_in_members_app_list(pool: PgPool) { "bob has no memberships → empty apps list" ); + // Grant via the public POST endpoint — exercises the full + // round-trip the dashboard goes through, not just the repo seam. + let r = add_member(&s.server, &owner_token, "default", bob, AppRole::Viewer).await; + r.assert_status(axum::http::StatusCode::CREATED); + // After grant: bob sees the default app. - grant_membership(&s.pool, bob, s.default_app, AppRole::Viewer).await; let r = s .server .get("/api/v1/admin/apps") @@ -1070,3 +1075,25 @@ async fn membership_makes_app_appear_in_members_app_list(pool: PgPool) { assert_eq!(apps.len(), 1); assert_eq!(apps[0]["slug"], "default"); } + +#[ignore = "needs DATABASE_URL pointing at a running Postgres"] +#[sqlx::test(migrations = "../manager-core/migrations")] +async fn add_member_with_missing_user_id_is_rejected(pool: PgPool) { + let s = boot(pool).await; + let owner_token = login_token(&s.server, "owner", "owner-pw").await; + + // Body missing `user_id` — Axum's Json extractor produces a 4xx + // before our handler runs. Pinning the status to keep the contract + // honest if anyone ever swaps the extractor. + let r = s + .server + .post("/api/v1/admin/apps/default/members") + .add_header("authorization", format!("Bearer {owner_token}")) + .json(&json!({ "role": "viewer" })) + .await; + let status = r.status_code().as_u16(); + assert!( + (400..500).contains(&status), + "malformed body should produce a 4xx, got {status}" + ); +}