From 9d3f0bddde396a24e385346a0d731b05a9df7a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 21 Feb 2026 11:47:16 +0100 Subject: [PATCH] Add manual live ImapSynchronizer coverage tests (#818) * Add manual live IMAP synchronizer tests * Fixing build errors and testing. --- .../ImapSynchronizerLiveTests.cs | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs new file mode 100644 index 00000000..0f508b58 --- /dev/null +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerLiveTests.cs @@ -0,0 +1,290 @@ +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 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 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 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 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(); + + var folderService = new Mock(); + folderService.Setup(x => x.GetKnownUidsForFolderAsync(inboxFolder.Id)) + .ReturnsAsync(() => storedMails.Values.Select(m => MailkitClientExtensions.ResolveUid(m.Id)).ToList()); + folderService.Setup(x => x.UpdateFolderAsync(It.IsAny())).Returns(Task.CompletedTask); + folderService.Setup(x => x.UpdateFolderHighestModeSeqAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + folderService.Setup(x => x.DeleteFolderAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + var mailService = new Mock(); + mailService.Setup(x => x.GetMailsByFolderIdAsync(inboxFolder.Id)).ReturnsAsync(() => storedMails.Values.ToList()); + mailService.Setup(x => x.GetExistingMailsAsync(inboxFolder.Id, It.IsAny>())) + .ReturnsAsync((Guid _, IEnumerable 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())) + .ReturnsAsync((Guid _, NewMailItemPackage package) => + { + storedMails[package.Copy.Id] = package.Copy; + return true; + }); + mailService.Setup(x => x.ChangeReadStatusAsync(It.IsAny(), It.IsAny())) + .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(), It.IsAny())).Returns(Task.CompletedTask); + mailService.Setup(x => x.DeleteMailAsync(It.IsAny(), It.IsAny())) + .Returns((Guid _, string mailCopyId) => + { + storedMails.Remove(mailCopyId); + return Task.CompletedTask; + }); + + var changeProcessor = new Mock(); + changeProcessor.Setup(x => x.GetSynchronizationFoldersAsync(It.IsAny())) + .ReturnsAsync([inboxFolder]); + changeProcessor.Setup(x => x.GetRecentMailIdsForFolderAsync(inboxFolder.Id, It.IsAny())) + .ReturnsAsync((Guid _, int count) => storedMails.Keys.Take(count).ToList()); + changeProcessor.Setup(x => x.GetDownloadedUnreadMailsAsync(account.Id, It.IsAny>())) + .ReturnsAsync(new List()); + + var appConfiguration = new Mock(); + 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()); + + var synchronizer = new ImapSynchronizer( + account, + changeProcessor.Object, + appConfiguration.Object, + unifiedSynchronizer, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + 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); + } + } + } +}