Files
Wino-Mail/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs
Burak Kaan Köse 9d3f0bddde Add manual live ImapSynchronizer coverage tests (#818)
* Add manual live IMAP synchronizer tests

* Fixing build errors and testing.
2026-02-21 11:47:16 +01:00

291 lines
12 KiB
C#

using FluentAssertions;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using Moq;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration.Processors;
using Wino.Core.Requests.Mail;
using Wino.Core.Synchronizers.ImapSync;
using Wino.Core.Synchronizers.Mail;
using Wino.Services.Extensions;
using Xunit;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Core.Tests.Synchronizers;
public sealed class ImapSynchronizerLiveTests
{
private const string ManualSkipMessage = "Manual live IMAP test. Fill Server/Port/Username/Password placeholders and remove Skip to run.";
// Replace placeholders with your own credentials when running these live tests.
private const string Server = "imap.example.com";
private const int Port = 993;
private const string Username = "REPLACE_WITH_USERNAME";
private const string Password = "REPLACE_WITH_PASSWORD";
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task InitialSynchronization_DownloadsInboxMetadata()
{
using var context = await CreateContextAsync();
var result = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id));
result.CompletedState.Should().Be(SynchronizationCompletedState.Success);
result.FolderResults.Should().ContainSingle();
result.FolderResults[0].Success.Should().BeTrue();
result.FolderResults[0].DownloadedCount.Should().BeGreaterThan(0);
}
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task DeltaSynchronization_SecondRunDownloadsNoAdditionalMessages()
{
using var context = await CreateContextAsync();
var initialResult = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id));
initialResult.CompletedState.Should().Be(SynchronizationCompletedState.Success);
var deltaResult = await context.Synchronizer.SynchronizeMailsAsync(CreateCustomFolderSyncOptions(context.Account.Id, context.InboxFolder.Id));
deltaResult.CompletedState.Should().Be(SynchronizationCompletedState.Success);
deltaResult.FolderResults.Should().ContainSingle();
deltaResult.FolderResults[0].DownloadedCount.Should().Be(0);
}
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task MarkFirstInboxMailUnreadThenRead_ValidatesReadFlagChanges()
{
using var context = await CreateContextAsync();
var firstUid = await GetFirstInboxMessageUidAsync(context.Account);
var mailCopy = CreateInboxMailCopy(context.InboxFolder, firstUid);
await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: false);
(await GetIsSeenAsync(context.Account, firstUid)).Should().BeFalse();
await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: true);
(await GetIsSeenAsync(context.Account, firstUid)).Should().BeTrue();
}
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task MarkFirstInboxMailReadThenUnread_ValidatesUnreadFlagChanges()
{
using var context = await CreateContextAsync();
var firstUid = await GetFirstInboxMessageUidAsync(context.Account);
var mailCopy = CreateInboxMailCopy(context.InboxFolder, firstUid);
await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: true);
(await GetIsSeenAsync(context.Account, firstUid)).Should().BeTrue();
await ExecuteMarkReadAsync(context.Synchronizer, mailCopy, isRead: false);
(await GetIsSeenAsync(context.Account, firstUid)).Should().BeFalse();
}
private static MailSynchronizationOptions CreateCustomFolderSyncOptions(Guid accountId, Guid folderId)
=> new()
{
AccountId = accountId,
Type = MailSynchronizationType.CustomFolders,
SynchronizationFolderIds = [folderId]
};
private static async Task ExecuteMarkReadAsync(ImapSynchronizer synchronizer, MailCopy mailCopy, bool isRead)
{
var requests = synchronizer.MarkRead(new BatchMarkReadRequest([new MarkReadRequest(mailCopy, isRead)]));
await synchronizer.ExecuteNativeRequestsAsync(requests);
}
private static MailCopy CreateInboxMailCopy(MailItemFolder folder, UniqueId uid)
=> new()
{
Id = MailkitClientExtensions.CreateUid(folder.Id, uid.Id),
AssignedFolder = folder,
IsRead = false,
Subject = "Live test placeholder"
};
private static async Task<UniqueId> GetFirstInboxMessageUidAsync(MailAccount account)
{
using var client = await CreateConnectedClientAsync(account);
var inbox = client.Inbox;
await inbox.OpenAsync(FolderAccess.ReadWrite);
var allUids = await inbox.SearchAsync(SearchQuery.All);
allUids.Should().NotBeEmpty("Inbox must contain at least one message for mark-read live tests.");
var firstUid = allUids.First();
await inbox.CloseAsync();
await client.DisconnectAsync(true);
return firstUid;
}
private static async Task<bool> GetIsSeenAsync(MailAccount account, UniqueId uid)
{
using var client = await CreateConnectedClientAsync(account);
var inbox = client.Inbox;
await inbox.OpenAsync(FolderAccess.ReadOnly);
var summary = await inbox.FetchAsync([uid], MessageSummaryItems.Flags);
await inbox.CloseAsync();
await client.DisconnectAsync(true);
summary.Should().ContainSingle();
var flags = summary[0].Flags;
flags.Should().NotBeNull();
return flags!.Value.HasFlag(MessageFlags.Seen);
}
private static async Task<ImapClient> CreateConnectedClientAsync(MailAccount account)
{
var client = new ImapClient();
await client.ConnectAsync(account.ServerInformation.IncomingServer, int.Parse(account.ServerInformation.IncomingServerPort), MailKit.Security.SecureSocketOptions.Auto);
await client.AuthenticateAsync(account.ServerInformation.IncomingServerUsername, account.ServerInformation.IncomingServerPassword);
return client;
}
private static async Task<LiveTestContext> CreateContextAsync()
{
var tempDirectory = Path.Combine(Path.GetTempPath(), "wino-imap-live-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDirectory);
var account = new MailAccount
{
Id = Guid.NewGuid(),
Name = "IMAP Live Test",
Address = Username,
ProviderType = MailProviderType.IMAP4,
ServerInformation = new CustomServerInformation
{
Id = Guid.NewGuid(),
IncomingServer = Server,
IncomingServerPort = Port.ToString(),
IncomingServerUsername = Username,
IncomingServerPassword = Password,
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
MaxConcurrentClients = 2
}
};
var inboxFolder = new MailItemFolder
{
Id = Guid.NewGuid(),
MailAccountId = account.Id,
FolderName = "Inbox",
RemoteFolderId = "INBOX",
SpecialFolderType = SpecialFolderType.Inbox,
IsSynchronizationEnabled = true,
ShowUnreadCount = true,
UidValidity = 0,
HighestModeSeq = 0,
HighestKnownUid = 0
};
var storedMails = new Dictionary<string, MailCopy>();
var folderService = new Mock<IFolderService>();
folderService.Setup(x => x.GetKnownUidsForFolderAsync(inboxFolder.Id))
.ReturnsAsync(() => storedMails.Values.Select(m => MailkitClientExtensions.ResolveUid(m.Id)).ToList());
folderService.Setup(x => x.UpdateFolderAsync(It.IsAny<MailItemFolder>())).Returns(Task.CompletedTask);
folderService.Setup(x => x.UpdateFolderHighestModeSeqAsync(It.IsAny<Guid>(), It.IsAny<long>())).Returns(Task.CompletedTask);
folderService.Setup(x => x.DeleteFolderAsync(It.IsAny<Guid>(), It.IsAny<string>())).Returns(Task.CompletedTask);
var mailService = new Mock<IMailService>();
mailService.Setup(x => x.GetMailsByFolderIdAsync(inboxFolder.Id)).ReturnsAsync(() => storedMails.Values.ToList());
mailService.Setup(x => x.GetExistingMailsAsync(inboxFolder.Id, It.IsAny<IEnumerable<UniqueId>>()))
.ReturnsAsync((Guid _, IEnumerable<UniqueId> ids) =>
ids.Select(uid => MailkitClientExtensions.CreateUid(inboxFolder.Id, uid.Id))
.Where(storedMails.ContainsKey)
.Select(id => storedMails[id])
.ToList());
mailService.Setup(x => x.CreateMailAsync(account.Id, It.IsAny<NewMailItemPackage>()))
.ReturnsAsync((Guid _, NewMailItemPackage package) =>
{
storedMails[package.Copy.Id] = package.Copy;
return true;
});
mailService.Setup(x => x.ChangeReadStatusAsync(It.IsAny<string>(), It.IsAny<bool>()))
.Returns((string mailCopyId, bool isRead) =>
{
if (storedMails.TryGetValue(mailCopyId, out var copy))
{
copy.IsRead = isRead;
}
return Task.CompletedTask;
});
mailService.Setup(x => x.ChangeFlagStatusAsync(It.IsAny<string>(), It.IsAny<bool>())).Returns(Task.CompletedTask);
mailService.Setup(x => x.DeleteMailAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns((Guid _, string mailCopyId) =>
{
storedMails.Remove(mailCopyId);
return Task.CompletedTask;
});
var changeProcessor = new Mock<IImapChangeProcessor>();
changeProcessor.Setup(x => x.GetSynchronizationFoldersAsync(It.IsAny<MailSynchronizationOptions>()))
.ReturnsAsync([inboxFolder]);
changeProcessor.Setup(x => x.GetRecentMailIdsForFolderAsync(inboxFolder.Id, It.IsAny<int>()))
.ReturnsAsync((Guid _, int count) => storedMails.Keys.Take(count).ToList());
changeProcessor.Setup(x => x.GetDownloadedUnreadMailsAsync(account.Id, It.IsAny<IEnumerable<string>>()))
.ReturnsAsync(new List<MailCopy>());
var appConfiguration = new Mock<IApplicationConfiguration>();
appConfiguration.SetupProperty(x => x.ApplicationDataFolderPath, tempDirectory);
appConfiguration.SetupProperty(x => x.PublisherSharedFolderPath, tempDirectory);
appConfiguration.SetupProperty(x => x.ApplicationTempFolderPath, tempDirectory);
appConfiguration.SetupGet(x => x.SentryDNS).Returns(string.Empty);
var unifiedSynchronizer = new UnifiedImapSynchronizer(
folderService.Object,
mailService.Object,
Mock.Of<IImapSynchronizerErrorHandlerFactory>());
var synchronizer = new ImapSynchronizer(
account,
changeProcessor.Object,
appConfiguration.Object,
unifiedSynchronizer,
Mock.Of<IImapSynchronizerErrorHandlerFactory>(),
Mock.Of<ICalDavClient>(),
Mock.Of<IAutoDiscoveryService>(),
Mock.Of<ICalendarService>());
return await Task.FromResult(new LiveTestContext(account, inboxFolder, synchronizer, tempDirectory));
}
private sealed class LiveTestContext(MailAccount account, MailItemFolder inboxFolder, ImapSynchronizer synchronizer, string tempDirectory) : IDisposable
{
public MailAccount Account { get; } = account;
public MailItemFolder InboxFolder { get; } = inboxFolder;
public ImapSynchronizer Synchronizer { get; } = synchronizer;
public void Dispose()
{
Synchronizer.KillSynchronizerAsync().GetAwaiter().GetResult();
if (Directory.Exists(tempDirectory))
{
Directory.Delete(tempDirectory, recursive: true);
}
}
}
}