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:
Jarrod Sibbison
2026-09-16 12:58:30 +10:00
committed by GitHub
parent b8aa0233f9
commit c507a4d488

View File

@@ -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() {