mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
test: cover deletion lifecycle edge cases
This commit is contained in:
@@ -420,6 +420,41 @@ mod tests {
|
||||
assert!(main_has(&service, &request).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_repairs_orphan_metadata_idempotently_after_partial_removal() {
|
||||
let service = service(false);
|
||||
let request = deletion();
|
||||
service
|
||||
.ctx
|
||||
.tombstones()
|
||||
.record_request(
|
||||
&request,
|
||||
Timestamp::from_secs(0),
|
||||
RequestClassification::LocallyActionable,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
service
|
||||
.ctx
|
||||
.tombstones()
|
||||
.remove_request_payload_without_metadata(request.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = service
|
||||
.cleanup_expired_requests(Timestamp::from_secs(15))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(first.canonical_records_examined, 0);
|
||||
assert_eq!(first.metadata_rows_removed, 1);
|
||||
let rerun = service
|
||||
.cleanup_expired_requests(Timestamp::from_secs(15))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rerun.metadata_rows_removed, 0);
|
||||
assert_eq!(rerun.failures, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_reconciliation_and_periodic_cleanup_share_serving_boundary() {
|
||||
let startup = service(false);
|
||||
|
||||
@@ -615,6 +615,44 @@ mod tests {
|
||||
assert_eq!(record.last_used_at, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nip09_failed_archive_preserves_target_and_leaves_request_unused() {
|
||||
let mut ctx = make_context();
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
ctx.git_data_path = directory.path().to_path_buf();
|
||||
let keys = Keys::generate();
|
||||
let target = make_announcement_event(&keys, "archive-failure");
|
||||
let owner = owner_directory_component(&keys.public_key());
|
||||
std::fs::create_dir_all(directory.path().join(&owner).join("archive-failure.git")).unwrap();
|
||||
// A file where the archive directory belongs makes the archival attempt
|
||||
// fail, so the fail-safe path must not award deletion-use credit.
|
||||
std::fs::write(directory.path().join(".archive"), "not a directory").unwrap();
|
||||
ctx.database.save_event(&target).await.unwrap();
|
||||
let request = EventBuilder::new(Kind::EventDeletion, "")
|
||||
.tags(vec![Tag::event(target.id)])
|
||||
.finalize(&keys)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
DeletionPolicy::new(ctx.clone()).handle(&request).await,
|
||||
WritePolicyResult::Accept
|
||||
));
|
||||
assert!(ctx
|
||||
.database
|
||||
.event_by_id(&target.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
ctx.tombstones
|
||||
.lifecycle_for_request(&request.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.last_used_at,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn coordinate_deletion_requests_with_different_cutoffs_coexist() {
|
||||
let ctx = make_context();
|
||||
|
||||
@@ -1327,4 +1327,40 @@ mod tests {
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn coordinate_request_with_insufficient_cutoff_never_receives_use_credit() {
|
||||
let ctx = context(crate::config::Config::for_testing());
|
||||
let service = DeletionService::new(ctx.clone());
|
||||
let keys = Keys::generate();
|
||||
let coordinate = format!("30617:{}:repo", keys.public_key().to_hex());
|
||||
let request = EventBuilder::new(Kind::EventDeletion, "")
|
||||
.tags(vec![Tag::custom("a", vec![coordinate])])
|
||||
.custom_created_at(Timestamp::from_secs(100))
|
||||
.finalize(&keys)
|
||||
.unwrap();
|
||||
ctx.tombstones
|
||||
.record_request(
|
||||
&request,
|
||||
Timestamp::from_secs(10),
|
||||
RequestClassification::LocallyActionable,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let incoming = EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
||||
.tags(vec![Tag::identifier("repo")])
|
||||
.custom_created_at(Timestamp::from_secs(101))
|
||||
.finalize(&keys)
|
||||
.unwrap();
|
||||
|
||||
assert!(service.gate(&incoming).await.is_none());
|
||||
assert_eq!(
|
||||
ctx.tombstones
|
||||
.lifecycle_for_request(&request.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.last_used_at,
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,4 +837,67 @@ mod tests {
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mode_round_trip_reclassifies_without_changing_lifecycle_timestamps() {
|
||||
let normal = service(false);
|
||||
let request = deletion(&Keys::generate());
|
||||
normal
|
||||
.ctx
|
||||
.tombstones()
|
||||
.record_request(
|
||||
&request,
|
||||
Timestamp::from_secs(10),
|
||||
RequestClassification::LocallyActionable,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
normal
|
||||
.ctx
|
||||
.tombstones()
|
||||
.mark_request_used(&request.id, Timestamp::from_secs(20))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut disrespector_context = normal.ctx.clone();
|
||||
disrespector_context.config.deletion_request_disrespector = true;
|
||||
let disrespector = DeletionService::new(disrespector_context);
|
||||
let entering = disrespector
|
||||
.run_request_lifecycle_startup_reconciliation(Timestamp::from_secs(30))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(entering.requests_reclassified, 1);
|
||||
let entering_record = disrespector
|
||||
.ctx
|
||||
.tombstones()
|
||||
.lifecycle_for_request_result(&request.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(entering_record.first_seen_at, Timestamp::from_secs(10));
|
||||
assert_eq!(entering_record.last_used_at, Some(Timestamp::from_secs(20)));
|
||||
assert_eq!(
|
||||
entering_record.classification,
|
||||
RequestClassification::Disrespector
|
||||
);
|
||||
|
||||
let leaving = normal
|
||||
.run_request_lifecycle_startup_reconciliation(Timestamp::from_secs(40))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(leaving.requests_reclassified, 1);
|
||||
let leaving_record = normal
|
||||
.ctx
|
||||
.tombstones()
|
||||
.lifecycle_for_request_result(&request.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(leaving_record.first_seen_at, Timestamp::from_secs(10));
|
||||
assert_eq!(leaving_record.last_used_at, Some(Timestamp::from_secs(20)));
|
||||
assert_eq!(
|
||||
leaving_record.classification,
|
||||
RequestClassification::LocallyActionable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,17 @@ impl Tombstones {
|
||||
.map_err(|error| anyhow::anyhow!("Failed to save test request payload: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn remove_request_payload_without_metadata(
|
||||
&self,
|
||||
request_id: EventId,
|
||||
) -> anyhow::Result<()> {
|
||||
self.db
|
||||
.delete(Filter::new().ids(vec![request_id]))
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("Failed to remove test request payload: {error}"))
|
||||
}
|
||||
|
||||
/// Persist a signed deletion/vanish request and relay-generated lifecycle
|
||||
/// metadata. Exact replays retain their original `first_seen_at`.
|
||||
pub async fn record_request(
|
||||
|
||||
Reference in New Issue
Block a user