using GmailCatchallLabeler.Configuration; using GmailCatchallLabeler.Gmail; using GmailCatchallLabeler.Labels; using GmailCatchallLabeler.State; using Microsoft.Extensions.Logging.Abstractions; namespace GmailCatchallLabeler.Tests; public sealed class GmailOrganizerTests { [Fact] public async Task Initialize_LabelsOnlyInboxMessagesAndSkipsExistingLabel() { var statePath = Path.Combine(Path.GetTempPath(), $"gmail-labeler-{Guid.NewGuid():N}.json"); try { var options = new GmailOptions { CatchallDomain = "arnemoerman.be", LabelPrefix = "ArneM.be", StatePath = statePath, ProcessExisting = true }; var gmailClient = new FakeGmailClient { Labels = { ["ArneM.be/Tom Scott"] = new GmailLabel( "label-tom-scott", "ArneM.be/Tom Scott", false) }, Messages = { ["new"] = new GmailMessage( "new", ["INBOX"], [new GmailHeader("Delivered-To", "tomscott@arnemoerman.be")]), ["already-labeled"] = new GmailMessage( "already-labeled", ["INBOX", "label-tom-scott"], [new GmailHeader("To", "tomscott@arnemoerman.be")]), ["sent"] = new GmailMessage( "sent", ["SENT"], [new GmailHeader("To", "tomscott@arnemoerman.be")]) } }; var organizer = new GmailOrganizer( gmailClient, new RecipientExtractor(options), new LabelNameResolver("ArneM.be", "{\"tomscott\":\"Tom Scott\"}"), new StateStore(options), options, NullLogger.Instance); await organizer.InitializeAsync(CancellationToken.None); var modification = Assert.Single(gmailClient.Modifications); Assert.Equal("new", modification.MessageId); Assert.Equal(["label-tom-scott"], modification.AddedLabelIds); } finally { File.Delete(statePath); } } [Fact] public async Task Initialize_MigratesChangedOverrideAcrossAllMail() { var statePath = Path.Combine(Path.GetTempPath(), $"gmail-labeler-{Guid.NewGuid():N}.json"); try { var options = new GmailOptions { CatchallDomain = "arnemoerman.be", LabelPrefix = "ArneM.be", StatePath = statePath, MigrateChangedLabels = true }; var stateStore = new StateStore(options); await stateStore.SaveAsync( new OrganizerState { HistoryId = 42, LabelMappings = { ["tomscott"] = "ArneM.be/Tomscott" }, CreatedLabels = { "ArneM.be/Tomscott" } }, CancellationToken.None); var gmailClient = new FakeGmailClient { Labels = { ["ArneM.be/Tomscott"] = new GmailLabel("old-label", "ArneM.be/Tomscott", false), ["ArneM.be/Tom Scott"] = new GmailLabel("new-label", "ArneM.be/Tom Scott", false) }, LabeledMessageIds = { "archived-message" } }; var organizer = new GmailOrganizer( gmailClient, new RecipientExtractor(options), new LabelNameResolver("ArneM.be", "{\"tomscott\":\"Tom Scott\"}"), stateStore, options, NullLogger.Instance); await organizer.InitializeAsync(CancellationToken.None); var modification = Assert.Single(gmailClient.Modifications); Assert.Equal("archived-message", modification.MessageId); Assert.Equal(["new-label"], modification.AddedLabelIds); Assert.Equal(["old-label"], modification.RemovedLabelIds); Assert.Equal(["old-label"], gmailClient.DeletedLabelIds); var persistedState = await stateStore.LoadAsync(CancellationToken.None); Assert.Equal("ArneM.be/Tom Scott", persistedState.LabelMappings["tomscott"]); } finally { File.Delete(statePath); } } private sealed class FakeGmailClient : IGmailClient { public Dictionary Messages { get; } = []; public Dictionary Labels { get; } = new(StringComparer.OrdinalIgnoreCase); public List LabeledMessageIds { get; } = []; public List Modifications { get; } = []; public List DeletedLabelIds { get; } = []; public Task GetCurrentHistoryIdAsync(CancellationToken cancellationToken) => Task.FromResult(42); public Task GetHistoryAsync(ulong startHistoryId, CancellationToken cancellationToken) => Task.FromResult(new GmailHistory([], startHistoryId)); public async IAsyncEnumerable ListInboxMessageIdsAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.Yield(); foreach (var messageId in Messages.Keys) { yield return messageId; } } public async IAsyncEnumerable ListMessageIdsByLabelAsync( string labelId, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.Yield(); foreach (var messageId in LabeledMessageIds) { yield return messageId; } } public Task GetMessageAsync(string messageId, CancellationToken cancellationToken) => Task.FromResult(Messages[messageId]); public Task FindLabelAsync(string name, CancellationToken cancellationToken) => Task.FromResult(Labels.GetValueOrDefault(name)); public Task EnsureLabelAsync(string name, CancellationToken cancellationToken) { if (Labels.TryGetValue(name, out var label)) { return Task.FromResult(label); } label = new GmailLabel($"label-{Labels.Count + 1}", name, true); Labels[name] = label; return Task.FromResult(label); } public Task ModifyLabelsAsync( string messageId, IReadOnlyCollection addLabelIds, IReadOnlyCollection removeLabelIds, CancellationToken cancellationToken) { Modifications.Add(new Modification(messageId, addLabelIds, removeLabelIds)); return Task.CompletedTask; } public Task DeleteLabelAsync(string labelId, CancellationToken cancellationToken) { DeletedLabelIds.Add(labelId); return Task.CompletedTask; } } private sealed record Modification( string MessageId, IReadOnlyCollection AddedLabelIds, IReadOnlyCollection RemovedLabelIds); }