test(relay): expose invitation restrictions to PostgreSQL discovery

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Signed-off-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz>
This commit is contained in:
Brad Seiler
2026-10-09 20:19:18 -04:00
committed by seiler
parent 19ecad9ad2
commit 34795a3675

View File

@@ -1,254 +1,256 @@
use super::*;
mod postgres_tests {
use super::super::*;
async fn fixture() -> (Arc<AppState>, String, buzz_core::CommunityId, Keys) {
let host = format!("restricted-invite-{}.example", Uuid::new_v4().simple());
let state = invite_test_state(&host)
.await
.expect("isolated PostgreSQL state");
let community = state
.db
.ensure_configured_community(&host)
.await
.unwrap()
.id;
let owner = Keys::generate();
state
.db
.add_relay_member(community, &owner.public_key().to_hex(), "owner", None)
.await
.unwrap();
(state, host, community, owner)
}
async fn fixture() -> (Arc<AppState>, String, buzz_core::CommunityId, Keys) {
let host = format!("restricted-invite-{}.example", Uuid::new_v4().simple());
let state = invite_test_state(&host)
.await
.expect("isolated PostgreSQL state");
let community = state
.db
.ensure_configured_community(&host)
.await
.unwrap()
.id;
let owner = Keys::generate();
state
.db
.add_relay_member(community, &owner.public_key().to_hex(), "owner", None)
.await
.unwrap();
(state, host, community, owner)
}
async fn post_with_owner_tag(
state: Arc<AppState>,
host: &str,
path: &str,
agent: &Keys,
body: String,
auth_tag: &str,
) -> axum::response::Response {
let auth = nip98_auth_header(agent, &format!("https://{host}{path}"), body.as_bytes());
build_router(state)
.oneshot(
Request::builder()
.method("POST")
.uri(path)
.header(header::HOST, host)
.header(header::AUTHORIZATION, auth)
.header("x-auth-tag", auth_tag)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap(),
async fn post_with_owner_tag(
state: Arc<AppState>,
host: &str,
path: &str,
agent: &Keys,
body: String,
auth_tag: &str,
) -> axum::response::Response {
let auth = nip98_auth_header(agent, &format!("https://{host}{path}"), body.as_bytes());
build_router(state)
.oneshot(
Request::builder()
.method("POST")
.uri(path)
.header(header::HOST, host)
.header(header::AUTHORIZATION, auth)
.header("x-auth-tag", auth_tag)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap()
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn signed_nonmember_claims_reject_restrictions_without_consuming_invite() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(
state.clone(),
&host,
&issuer,
serde_json::json!({"max_uses":1}),
)
.await
.unwrap()
}
.await;
let claimant = Keys::generate();
state
.db
.ban_community_member(
community,
claimant.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
)
.await
.unwrap();
let key = derive_invite_key(&state.relay_keypair);
let (legacy, _) = crate::invite_token::mint_invite(&key, community, 3600);
for bearer in [&code, &legacy] {
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&claimant,
serde_json::json!({"code":bearer}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_restricted");
}
assert!(!state
.db
.is_relay_member(community, &claimant.public_key().to_hex())
.await
.unwrap());
let unrestricted = Keys::generate();
assert_eq!(
post_json(
state,
&host,
"/api/invites/claim",
&unrestricted,
serde_json::json!({"code":code}).to_string()
)
.await
.status(),
StatusCode::OK
);
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn signed_nonmember_claims_reject_restrictions_without_consuming_invite() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(
state.clone(),
&host,
&issuer,
serde_json::json!({"max_uses":1}),
)
.await;
let claimant = Keys::generate();
state
.db
.ban_community_member(
community,
claimant.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
#[tokio::test]
#[ignore = "requires Postgres"]
async fn verified_owner_blocks_first_agent_claim_but_forged_owner_does_not_bind() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
let owner = Keys::generate();
state
.db
.ban_community_member(
community,
owner.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
)
.await
.unwrap();
let agent = Keys::generate();
let tag = buzz_sdk::nip_oa::compute_auth_tag(&owner, &agent.public_key(), "").unwrap();
let response = post_with_owner_tag(
state.clone(),
&host,
"/api/invites/claim",
&agent,
serde_json::json!({"code":code}).to_string(),
&tag,
)
.await
.unwrap();
let key = derive_invite_key(&state.relay_keypair);
let (legacy, _) = crate::invite_token::mint_invite(&key, community, 3600);
for bearer in [&code, &legacy] {
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_restricted");
assert!(!state
.db
.is_relay_member(community, &agent.public_key().to_hex())
.await
.unwrap());
let other_agent = Keys::generate();
// A credential signed for another key cannot bind this claimant's owner.
let response = post_with_owner_tag(
state.clone(),
&host,
"/api/invites/claim",
&other_agent,
serde_json::json!({"code":code}).to_string(),
&tag,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert!(!state
.db
.is_agent_owner(
community,
other_agent.public_key().as_bytes(),
owner.public_key().as_bytes()
)
.await
.unwrap());
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn committed_issuer_ban_survives_unban_through_http() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
state
.db
.ban_community_member(
community,
issuer.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
)
.await
.unwrap();
state
.db
.unban_community_member(
community,
issuer.public_key().as_bytes(),
issuer.public_key().as_bytes(),
)
.await
.unwrap();
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":code}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_invalid");
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn cutoff_rejects_only_v1_and_restriction_lookup_failure_never_admits() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
let key = derive_invite_key(&state.relay_keypair);
let (legacy, _) = crate::invite_token::mint_invite(&key, community, 3600);
let Ok(mut state) = Arc::try_unwrap(state) else {
panic!("exclusive fixture state");
};
Arc::make_mut(&mut state.config).invite_v1_invalid_after = Some(0);
let state = Arc::new(state);
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":legacy}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_expired");
assert_eq!(
post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":code}).to_string()
)
.await
.status(),
StatusCode::OK
);
// This test owns its per-process database; never alters a shared fixture.
sqlx::query("ALTER TABLE community_bans RENAME TO unavailable_restrictions")
.execute(state.db.pool())
.await
.unwrap();
let claimant = Keys::generate();
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&claimant,
serde_json::json!({"code":bearer}).to_string(),
serde_json::json!({"code":code}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_restricted");
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(!state
.db
.is_relay_member(community, &claimant.public_key().to_hex())
.await
.unwrap());
}
assert!(!state
.db
.is_relay_member(community, &claimant.public_key().to_hex())
.await
.unwrap());
let unrestricted = Keys::generate();
assert_eq!(
post_json(
state,
&host,
"/api/invites/claim",
&unrestricted,
serde_json::json!({"code":code}).to_string()
)
.await
.status(),
StatusCode::OK
);
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn verified_owner_blocks_first_agent_claim_but_forged_owner_does_not_bind() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
let owner = Keys::generate();
state
.db
.ban_community_member(
community,
owner.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
)
.await
.unwrap();
let agent = Keys::generate();
let tag = buzz_sdk::nip_oa::compute_auth_tag(&owner, &agent.public_key(), "").unwrap();
let response = post_with_owner_tag(
state.clone(),
&host,
"/api/invites/claim",
&agent,
serde_json::json!({"code":code}).to_string(),
&tag,
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_restricted");
assert!(!state
.db
.is_relay_member(community, &agent.public_key().to_hex())
.await
.unwrap());
let other_agent = Keys::generate();
// A credential signed for another key cannot bind this claimant's owner.
let response = post_with_owner_tag(
state.clone(),
&host,
"/api/invites/claim",
&other_agent,
serde_json::json!({"code":code}).to_string(),
&tag,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert!(!state
.db
.is_agent_owner(
community,
other_agent.public_key().as_bytes(),
owner.public_key().as_bytes()
)
.await
.unwrap());
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn committed_issuer_ban_survives_unban_through_http() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
state
.db
.ban_community_member(
community,
issuer.public_key().as_bytes(),
issuer.public_key().as_bytes(),
None,
None,
)
.await
.unwrap();
state
.db
.unban_community_member(
community,
issuer.public_key().as_bytes(),
issuer.public_key().as_bytes(),
)
.await
.unwrap();
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":code}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_invalid");
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn cutoff_rejects_only_v1_and_restriction_lookup_failure_never_admits() {
let (state, host, community, issuer) = fixture().await;
let code = mint_code(state.clone(), &host, &issuer, serde_json::json!({})).await;
let key = derive_invite_key(&state.relay_keypair);
let (legacy, _) = crate::invite_token::mint_invite(&key, community, 3600);
let Ok(mut state) = Arc::try_unwrap(state) else {
panic!("exclusive fixture state");
};
Arc::make_mut(&mut state.config).invite_v1_invalid_after = Some(0);
let state = Arc::new(state);
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":legacy}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(read_json(response).await["error"], "invite_expired");
assert_eq!(
post_json(
state.clone(),
&host,
"/api/invites/claim",
&Keys::generate(),
serde_json::json!({"code":code}).to_string()
)
.await
.status(),
StatusCode::OK
);
// This test owns its per-process database; never alters a shared fixture.
sqlx::query("ALTER TABLE community_bans RENAME TO unavailable_restrictions")
.execute(state.db.pool())
.await
.unwrap();
let claimant = Keys::generate();
let response = post_json(
state.clone(),
&host,
"/api/invites/claim",
&claimant,
serde_json::json!({"code":code}).to_string(),
)
.await;
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(!state
.db
.is_relay_member(community, &claimant.public_key().to_hex())
.await
.unwrap());
}