Files
Gmail-Catchall-Labeler/tests/GmailCatchallLabeler.Tests/GmailOrganizerTests.cs
T
arne 48488b72b5
Build and Push Docker Image / build-and-push (push) Successful in 2m21s
feat: Add Gmail Catchall Labeler service with configuration and functionality
- Introduced a new Docker Compose configuration for the Gmail Catchall Labeler service.
- Implemented GmailOptions class for managing configuration settings.
- Developed GmailApiClient to interact with the Gmail API, including methods for fetching messages and managing labels.
- Created models for Gmail messages, labels, and headers.
- Added IGmailClient interface to define the contract for Gmail client implementations.
- Implemented GmailOrganizer to handle the organization of emails based on recipient addresses.
- Developed LabelNameResolver for generating label names based on local parts of email addresses.
- Created RecipientExtractor to extract local parts from email headers.
- Implemented state management with OrganizerState and StateStore for persisting state information.
- Added Worker class to manage the background processing of email organization.
- Created unit tests for GmailOrganizer, LabelNameResolver, and RecipientExtractor to ensure functionality.
- Added necessary project files and configurations for building and running the application.
2026-09-26 20:05:21 +02:00

198 lines
7.5 KiB
C#

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<GmailOrganizer>.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<GmailOrganizer>.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<string, GmailMessage> Messages { get; } = [];
public Dictionary<string, GmailLabel> Labels { get; } = new(StringComparer.OrdinalIgnoreCase);
public List<string> LabeledMessageIds { get; } = [];
public List<Modification> Modifications { get; } = [];
public List<string> DeletedLabelIds { get; } = [];
public Task<ulong> GetCurrentHistoryIdAsync(CancellationToken cancellationToken) =>
Task.FromResult<ulong>(42);
public Task<GmailHistory> GetHistoryAsync(ulong startHistoryId, CancellationToken cancellationToken) =>
Task.FromResult(new GmailHistory([], startHistoryId));
public async IAsyncEnumerable<string> ListInboxMessageIdsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
foreach (var messageId in Messages.Keys)
{
yield return messageId;
}
}
public async IAsyncEnumerable<string> ListMessageIdsByLabelAsync(
string labelId,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
foreach (var messageId in LabeledMessageIds)
{
yield return messageId;
}
}
public Task<GmailMessage> GetMessageAsync(string messageId, CancellationToken cancellationToken) =>
Task.FromResult(Messages[messageId]);
public Task<GmailLabel?> FindLabelAsync(string name, CancellationToken cancellationToken) =>
Task.FromResult(Labels.GetValueOrDefault(name));
public Task<GmailLabel> 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<string> addLabelIds,
IReadOnlyCollection<string> 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<string> AddedLabelIds,
IReadOnlyCollection<string> RemovedLabelIds);
}