mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(db): allow naturally expired push lease replacement (#7653)
## Summary Allow a naturally expired push lease to be replaced by a newer lease for the same author and installation. ## Failure mode Expiry cleanup marked the old lease inactive but left active-only fields populated. The push_leases_check constraint requires those fields to be NULL when a lease is inactive. When the client later registered a replacement lease, the cleanup ran before the replacement and violated the constraint with SQLSTATE 23514. The transaction rolled back, so the replacement failed instead of taking over the expired lease. ## Fix Clear all active-only fields as part of natural-expiry cleanup, preserving the existing constraint and replacement flow. No schema change is required. ## Testing - Passed the focused Postgres regression test: cargo test -p buzz-db replacing_naturally_expired_lease_replaces_old_state -- --ignored - Passed scripts/test-postgres-test-discovery.sh - Ran just ci: formatting, lint, security, file-size, desktop, web, mobile, and 12 unit-test lanes passed. The overall run was red only because existing timing-sensitive buzz-acp tests failed. - The pre-push test-unit hook reproduced the same unrelated buzz-acp failures, so the already-run verification was documented and the branch was published with that hook bypassed. ## Related issue No matching open issue or pull request was found. ## Scope One file changed: crates/buzz-db/src/store/push.rs. The test uses the production accept_lease_event path and identifies the replacement by its cryptographic source event ID and generation. Signed-off-by: Jarrod Sibbison <jsibbison@squareup.com>
This commit is contained in:
@@ -328,7 +328,8 @@ pub async fn accept_lease_event(
|
||||
// uniqueness forever. The author lock makes this cleanup atomic with the
|
||||
// subsequent author-wide checks and replacement.
|
||||
sqlx::query(
|
||||
"UPDATE push_leases SET active=false, endpoint_enabled=false, updated_at=now() \
|
||||
"UPDATE push_leases SET active=false, endpoint_enabled=false, app_profile=NULL, \
|
||||
endpoint_hash=NULL, endpoint_grant=NULL, max_class=NULL, subscriptions=NULL, updated_at=now() \
|
||||
WHERE community_id=$1 AND author=$2 AND active \
|
||||
AND expires_at <= EXTRACT(EPOCH FROM now())::bigint",
|
||||
)
|
||||
@@ -1665,6 +1666,82 @@ mod postgres_tests {
|
||||
assert_eq!((event_count, lease_count), (0, 0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn replacing_naturally_expired_lease_replaces_old_state() {
|
||||
let pool = setup_pool().await;
|
||||
let community = make_community(&pool).await;
|
||||
let keys = nostr::Keys::generate();
|
||||
let endpoint = [42; 32];
|
||||
let subscriptions = serde_json::json!([]);
|
||||
|
||||
let expired_event = lease_event(&keys, "install", 100);
|
||||
assert_eq!(
|
||||
accept_lease_event(
|
||||
&pool,
|
||||
community,
|
||||
&expired_event,
|
||||
"install",
|
||||
LeaseVersion {
|
||||
source_event_id: expired_event.id.as_bytes(),
|
||||
source_created_at: 100,
|
||||
generation: 1,
|
||||
expires_at: 1,
|
||||
},
|
||||
Some(ActiveLease {
|
||||
app_profile: "ios-production",
|
||||
endpoint_hash: &endpoint,
|
||||
endpoint_grant: "opaque-grant",
|
||||
max_class: "default",
|
||||
subscriptions: &subscriptions,
|
||||
}),
|
||||
16,
|
||||
)
|
||||
.await
|
||||
.expect("insert expired lease"),
|
||||
AcceptLeaseOutcome::Accepted
|
||||
);
|
||||
|
||||
let replacement_event = lease_event(&keys, "install", 200);
|
||||
let outcome = accept_lease_event(
|
||||
&pool,
|
||||
community,
|
||||
&replacement_event,
|
||||
"install",
|
||||
LeaseVersion {
|
||||
source_event_id: replacement_event.id.as_bytes(),
|
||||
source_created_at: 200,
|
||||
generation: 2,
|
||||
expires_at: i64::MAX / 2,
|
||||
},
|
||||
Some(ActiveLease {
|
||||
app_profile: "ios-production",
|
||||
endpoint_hash: &[43; 32],
|
||||
endpoint_grant: "replacement-grant",
|
||||
max_class: "default",
|
||||
subscriptions: &subscriptions,
|
||||
}),
|
||||
16,
|
||||
)
|
||||
.await
|
||||
.expect("replace expired lease");
|
||||
assert_eq!(outcome, AcceptLeaseOutcome::Accepted);
|
||||
|
||||
let persisted: (bool, Vec<u8>, i64) = sqlx::query_as(
|
||||
"SELECT active, source_event_id, generation FROM push_leases \
|
||||
WHERE community_id=$1 AND author=$2 AND installation_id=$3",
|
||||
)
|
||||
.bind(community.as_uuid())
|
||||
.bind(expired_event.pubkey.as_bytes())
|
||||
.bind("install")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("read original lease");
|
||||
assert!(persisted.0, "replacement lease must be active");
|
||||
assert_eq!(persisted.1, replacement_event.id.as_bytes());
|
||||
assert_eq!(persisted.2, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn source_event_collision_is_protocol_outcome_without_event_insert() {
|
||||
|
||||
Reference in New Issue
Block a user