mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
test(relay): pin the kind:9000 refusal decided under the membership lock
A trigger removes the actor inside the write transaction, so validation passes and the locked decision refuses. The test pins the invalid: rejection for a single add and a batch, and that nothing is stored. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luke Tornquist <tornquist@squareup.com>
This commit is contained in:
@@ -670,3 +670,80 @@ async fn failed_put_user_stores_nothing() {
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// A refusal decided under the membership lock, after validation passed,
|
||||
/// rejects the event with `invalid:` and stores nothing, for a single add and
|
||||
/// for a batch. A trigger removes the actor inside the write transaction, so
|
||||
/// validation sees an owner and the locked decision sees a non-member of a
|
||||
/// private channel.
|
||||
/// Mutations: map the under-lock `AccessDenied` to `error:` → RED; commit the
|
||||
/// refused event → RED.
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn put_user_refused_under_the_lock_stores_nothing() {
|
||||
let state = state().await;
|
||||
let tenant = community(&state, "put-user-locked-refusal").await;
|
||||
let (owner, first, second) = (Keys::generate(), Keys::generate(), Keys::generate());
|
||||
let channel_id = create_channel(&state, &tenant, &owner, &[&["visibility", "private"]]).await;
|
||||
let (first_hex, second_hex) = (first.public_key().to_hex(), second.public_key().to_hex());
|
||||
|
||||
// Each test runs in its own database, so this trigger affects only it. The
|
||||
// removal rolls back with the refused transaction.
|
||||
for statement in [
|
||||
"CREATE FUNCTION put_user_revoke_actor() RETURNS trigger LANGUAGE plpgsql AS \
|
||||
$$ BEGIN UPDATE channel_members SET removed_at = NOW() \
|
||||
WHERE community_id = NEW.community_id AND channel_id = NEW.channel_id \
|
||||
AND pubkey = NEW.pubkey AND removed_at IS NULL; RETURN NULL; END $$",
|
||||
"CREATE TRIGGER put_user_revoke_actor AFTER INSERT ON events \
|
||||
FOR EACH ROW WHEN (NEW.kind = 9000) EXECUTE FUNCTION put_user_revoke_actor()",
|
||||
] {
|
||||
sqlx::query(statement)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.expect("install revoke trigger");
|
||||
}
|
||||
|
||||
let reason = "private channel requires an invite from an active member";
|
||||
let single = put_user(&owner, channel_id, &[&[&first_hex]], &[]);
|
||||
let batch = put_user(&owner, channel_id, &[&[&first_hex], &[&second_hex]], &[]);
|
||||
for event in [single, batch.clone()] {
|
||||
let message = match ingest(&state, &tenant, event, &owner).await {
|
||||
Err(IngestError::Rejected(message)) => message,
|
||||
Err(error) => panic!("expected an invalid: rejection, got {error:?}"),
|
||||
Ok(result) => panic!("expected a rejection, got OK {}", result.message),
|
||||
};
|
||||
let body = message.strip_prefix("invalid: ").expect("invalid prefix");
|
||||
if body.starts_with("members: ") {
|
||||
assert_eq!(
|
||||
report(body),
|
||||
serde_json::json!({
|
||||
"rejected": [{ "reason": reason, "pubkeys": [first_hex, second_hex] }],
|
||||
})
|
||||
);
|
||||
} else {
|
||||
assert_eq!(body, reason);
|
||||
}
|
||||
assert_eq!(put_user_events(&state, &tenant, channel_id).await, 0);
|
||||
for keys in [&first, &second] {
|
||||
assert_eq!(role_of(&state, &tenant, channel_id, keys).await, None);
|
||||
assert_eq!(joined_notices(&state, &tenant, channel_id, keys).await, 0);
|
||||
}
|
||||
assert_eq!(
|
||||
role_of(&state, &tenant, channel_id, &owner)
|
||||
.await
|
||||
.as_deref(),
|
||||
Some("owner"),
|
||||
"the actor's removal rolls back with the refusal"
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing was stored, so the same signed event applies once the actor
|
||||
// keeps their role.
|
||||
sqlx::query("DROP TRIGGER put_user_revoke_actor ON events")
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.expect("drop revoke trigger");
|
||||
let retry = ingest(&state, &tenant, batch, &owner).await.expect("retry");
|
||||
assert!(retry.accepted, "{}", retry.message);
|
||||
assert_eq!(put_user_events(&state, &tenant, channel_id).await, 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user