fix(account): close deletion cache gap
Require authenticated sidecar cache purge before a deletion can complete and keep failures retryable. Mount tombstones outside restored application data while leaving deletion disabled by default.
This commit is contained in:
@@ -152,6 +152,35 @@ public sealed class AccountDeletionTests
|
||||
Assert.Equal(1, await fixture.Service.ProcessPendingAsync(CancellationToken.None));
|
||||
Assert.False(await fixture.Db.Users.AsNoTracking().AnyAsync(item => item.Id == owner.Id));
|
||||
Assert.Equal(2, (await fixture.Tombstones.ReadAsync(CancellationToken.None)).Count);
|
||||
fixture.CachePurger.Verify(item => item.PurgeAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sidecar_cache_failure_keeps_deleted_account_retryable_until_purge_succeeds()
|
||||
{
|
||||
await InFixtureAsync(enabled: true, async fixture =>
|
||||
{
|
||||
fixture.Db.Users.Add(User("owner"));
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
fixture.CachePurger.Setup(item => item.PurgeAsync(It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new HttpRequestException("synthetic sidecar outage"));
|
||||
|
||||
var accepted = await fixture.Service.RequestAsync("owner", "owner", CancellationToken.None);
|
||||
Assert.NotNull(accepted);
|
||||
Assert.False(await fixture.Service.ProcessAsync(accepted!.RequestId, CancellationToken.None));
|
||||
|
||||
fixture.Db.ChangeTracker.Clear();
|
||||
Assert.False(await fixture.Db.Users.AsNoTracking().AnyAsync(item => item.Id == "owner"));
|
||||
var retry = await fixture.Db.AccountDeletionRequests.SingleAsync(item => item.Id == accepted.RequestId);
|
||||
Assert.Equal(AccountDeletionRequestStatuses.RetryRequired, retry.Status);
|
||||
Assert.Equal(AccountDeletionStages.PurgingFiles, retry.Stage);
|
||||
Assert.Empty(await fixture.Tombstones.ReadAsync(CancellationToken.None));
|
||||
|
||||
fixture.CachePurger.Setup(item => item.PurgeAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
Assert.True(await fixture.Service.ProcessAsync(accepted.RequestId, CancellationToken.None));
|
||||
Assert.Single(await fixture.Tombstones.ReadAsync(CancellationToken.None));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,8 +299,10 @@ public sealed class AccountDeletionTests
|
||||
using var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var inventory = new AccountOwnedFileInventory(db, paths, new AttachmentStorage(paths));
|
||||
var tombstones = new AccountDeletionTombstoneStore(paths);
|
||||
var service = new AccountDeletionService(db, inventory, tombstones, configuration, cache, TimeProvider.System, NullLogger<AccountDeletionService>.Instance);
|
||||
await test(new Fixture(db, paths, tombstones, service));
|
||||
var cachePurger = new Mock<IAiSidecarCachePurger>();
|
||||
cachePurger.Setup(item => item.PurgeAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
|
||||
var service = new AccountDeletionService(db, inventory, tombstones, cachePurger.Object, configuration, cache, TimeProvider.System, NullLogger<AccountDeletionService>.Instance);
|
||||
await test(new Fixture(db, paths, tombstones, cachePurger, service));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -280,5 +311,10 @@ public sealed class AccountDeletionTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record Fixture(JobTrackerContext Db, AppPaths Paths, AccountDeletionTombstoneStore Tombstones, AccountDeletionService Service);
|
||||
private sealed record Fixture(
|
||||
JobTrackerContext Db,
|
||||
AppPaths Paths,
|
||||
AccountDeletionTombstoneStore Tombstones,
|
||||
Mock<IAiSidecarCachePurger> CachePurger,
|
||||
AccountDeletionService Service);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user