using AIStudio.Tools.Databases.IndexStore; namespace AIStudio.Tests.Tools.Databases; /// /// Checks what the index keeps about a mailbox as a whole: its sync and a refused sign-in. /// /// /// Both protect the user from something which cannot be taken back. A removal from the index the /// user did not agree to costs them a new embedding of every mail which comes back, and a sign-in /// tried again with a refused password may lock their directory account. /// [TestFixture] public sealed class MailboxStateStoreTests { private const string DATA_SOURCE_ID = "0c3f9b52-7d4e-4a1b-9e6f-2b8c5d7a1e40"; private static readonly CancellationToken TOKEN = CancellationToken.None; private TemporaryIndexStore store = null!; private IndexStoreClient Client => this.store.Client; [SetUp] public async Task CreateMailboxAsync() { this.store = await TemporaryIndexStore.CreateAsync(); await this.Client.UpsertDataSourceAsync(DATA_SOURCE_ID, "MAILBOX", "b0a4c4d2-1f3e-4f0a-8c9d-5a6b7c8d9e01", "signature", string.Empty, 3, TOKEN); } [TearDown] public async Task DeleteMailboxAsync() => await this.store.DisposeAsync(); [Test] public async Task AMailboxWhichWasNeverSyncedKnowsNothingYet() { Assert.That(await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN), Is.EqualTo(new MailboxSyncState(null, null, null))); } [Test] public async Task ACompleteSyncEndsAHeldBackRemoval() { var completed = new DateTimeOffset(2026, 10, 1, 9, 16, 0, TimeSpan.Zero); await this.Client.HoldBackMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); await this.Client.ApprovePendingMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); await this.Client.CompleteMailboxSyncAsync(DATA_SOURCE_ID, completed, TOKEN); Assert.That( await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN), Is.EqualTo(new MailboxSyncState(completed, null, null)), "The approved removal took place in that sync, so the next large one has to be asked for again."); } [Test] public async Task AnApprovalCoversOnlyTheCountTheUserWasShown() { await this.Client.HoldBackMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); var approvedAnotherCount = await this.Client.ApprovePendingMailRemovalAsync(DATA_SOURCE_ID, 400, TOKEN); var stateAfterAnotherCount = await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN); var approvedTheCount = await this.Client.ApprovePendingMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); await this.Client.HoldBackMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); var stateAfterTheSameCountAgain = await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN); await this.Client.HoldBackMailRemovalAsync(DATA_SOURCE_ID, 800, TOKEN); var stateAfterMoreMails = await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN); Assert.Multiple(() => { Assert.That(approvedAnotherCount, Is.False, "A sync changed the count while the user looked at the old one."); Assert.That(stateAfterAnotherCount.PendingRemovalApprovedUtc, Is.Null); Assert.That(approvedTheCount, Is.True); Assert.That(stateAfterTheSameCountAgain.PendingRemovalApprovedUtc, Is.Not.Null, "The next sync finds the same count and may go ahead."); Assert.That(stateAfterMoreMails, Is.EqualTo(new MailboxSyncState(null, 800, null)), "The user agreed to 500 mails, not to whatever comes."); }); } [Test] public async Task NothingCanBeApprovedWhileNothingIsHeldBack() { var approved = await this.Client.ApprovePendingMailRemovalAsync(DATA_SOURCE_ID, 500, TOKEN); var state = await this.Client.GetMailboxSyncStateAsync(DATA_SOURCE_ID, TOKEN); Assert.Multiple(() => { Assert.That(approved, Is.False); Assert.That(state, Is.EqualTo(new MailboxSyncState(null, null, null))); }); } [Test] public void ARemovalOfNoMailIsNothingToHoldBack() { Assert.ThrowsAsync(() => this.Client.HoldBackMailRemovalAsync(DATA_SOURCE_ID, 0, TOKEN)); } [Test] public async Task ARefusedSignInStaysUntilItIsCleared() { var first = new MailboxAuthFailure(new DateTimeOffset(2026, 10, 1, 9, 0, 0, TimeSpan.Zero), "AUTHENTICATIONFAILED Invalid credentials"); var second = new MailboxAuthFailure(new DateTimeOffset(2026, 10, 2, 9, 0, 0, TimeSpan.Zero), string.Empty); await this.Client.UpsertMailboxAuthFailureAsync(DATA_SOURCE_ID, first, TOKEN); var afterFirst = await this.Client.GetMailboxAuthFailureAsync(DATA_SOURCE_ID, TOKEN); await this.Client.UpsertMailboxAuthFailureAsync(DATA_SOURCE_ID, second, TOKEN); var afterSecond = await this.Client.GetMailboxAuthFailureAsync(DATA_SOURCE_ID, TOKEN); await this.Client.ClearMailboxAuthFailureAsync(DATA_SOURCE_ID, TOKEN); var afterClearing = await this.Client.GetMailboxAuthFailureAsync(DATA_SOURCE_ID, TOKEN); Assert.Multiple(() => { Assert.That(afterFirst, Is.EqualTo(first)); Assert.That(afterSecond, Is.EqualTo(second), "The one try the user asked for failed again."); Assert.That(afterClearing, Is.Null); }); } [Test] public async Task ASignInCanBeRefusedBeforeAnythingWasIndexed() { const string NEVER_INDEXED = "5a1e9c37-2b4d-4e8f-a6c0-3d7b9e1f2a48"; var failure = new MailboxAuthFailure(DateTimeOffset.UtcNow, "AUTHENTICATIONFAILED Invalid credentials"); await this.Client.UpsertMailboxAuthFailureAsync(NEVER_INDEXED, failure, TOKEN); Assert.That(await this.Client.GetMailboxAuthFailureAsync(NEVER_INDEXED, TOKEN), Is.EqualTo(failure), "The very first sign-in of a mailbox can fail, long before the index holds anything of it."); } [Test] public async Task EveryMailboxTheIndexKeepsSomethingOfIsListedOnce() { const string NEVER_INDEXED = "5a1e9c37-2b4d-4e8f-a6c0-3d7b9e1f2a48"; const string FOLDER = "9d2b4f6a-1c3e-4a5b-8d7f-0e2c4a6b8d1f"; var failure = new MailboxAuthFailure(DateTimeOffset.UtcNow, "AUTHENTICATIONFAILED Invalid credentials"); await this.Client.UpsertDataSourceAsync(FOLDER, "LOCAL_DIRECTORY", "b0a4c4d2-1f3e-4f0a-8c9d-5a6b7c8d9e01", "signature", string.Empty, 3, TOKEN); await this.Client.UpsertMailboxAuthFailureAsync(DATA_SOURCE_ID, failure, TOKEN); await this.Client.UpsertMailboxAuthFailureAsync(NEVER_INDEXED, failure, TOKEN); Assert.That( await this.Client.GetStoredMailboxIdsAsync(TOKEN), Is.EquivalentTo(new[] { DATA_SOURCE_ID, NEVER_INDEXED }), "A refused sign-in outlives the index, so it is left over just as well. A folder is no mailbox, whatever happens to it."); } }