using FluentAssertions; using MailKit; using MailKit.Net.Imap; using Moq; using System.Reflection; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Synchronizers.ImapSync; using Xunit; using IMailService = Wino.Core.Domain.Interfaces.IMailService; namespace Wino.Core.Tests.Synchronizers; public class UnifiedImapSynchronizerTests { private static UnifiedImapSynchronizer CreateSut() { return new UnifiedImapSynchronizer( Mock.Of(), Mock.Of(), Mock.Of()); } [Fact] public void DetermineSyncStrategy_ShouldPrioritizeQResync_WhenEnabledAndSupported() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.QuickResync | ImapCapabilities.CondStore, isQResyncEnabled: true, serverHost: "imap.example.com"); strategy.Should().Be(ImapSyncStrategy.QResync); } [Fact] public void DetermineSyncStrategy_ShouldFallbackToCondstore_WhenQResyncNotEnabled() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.QuickResync | ImapCapabilities.CondStore, isQResyncEnabled: false, serverHost: "imap.example.com"); strategy.Should().Be(ImapSyncStrategy.Condstore); } [Fact] public void DetermineSyncStrategy_ShouldUseUidFallback_WhenNoAdvancedCapability() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.None, isQResyncEnabled: false, serverHost: "imap.example.com"); strategy.Should().Be(ImapSyncStrategy.UidBased); } [Fact] public void DetermineSyncStrategy_ShouldRespectQuirkOverride_ForStrictProviders() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.QuickResync | ImapCapabilities.CondStore, isQResyncEnabled: true, serverHost: "imap.qq.com"); strategy.Should().Be(ImapSyncStrategy.Condstore); } [Fact] public void DetermineSyncStrategy_ShouldFallbackToUid_WhenCondstoreIsUnavailable() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.QuickResync, isQResyncEnabled: false, serverHost: "imap.example.com"); strategy.Should().Be(ImapSyncStrategy.UidBased); } [Fact] public void DetermineSyncStrategy_ShouldFallbackToUid_WhenQuirkDisablesQresyncAndNoCondstore() { var sut = CreateSut(); var strategy = sut.DetermineSyncStrategy( ImapCapabilities.QuickResync, isQResyncEnabled: true, serverHost: "imap.163.com"); strategy.Should().Be(ImapSyncStrategy.UidBased); } [Fact] public void CalculateHighestKnownUid_ShouldUseMaxOfCurrentObservedAndUidNext() { var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( currentHighestKnownUid: 100, uidNext: new MailKit.UniqueId(151), observedUids: new uint[] { 120, 140, 130 }); result.Should().Be(150); } [Fact] public void CalculateHighestKnownUid_ShouldNotRegress_WhenObservedUidsAreLower() { var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( currentHighestKnownUid: 500, uidNext: null, observedUids: new uint[] { 110, 120, 130 }); result.Should().Be(500); } [Fact] public void CalculateHighestKnownUid_ShouldUseUidNextMinusOne_WhenNoObservedUids() { var result = UnifiedImapSynchronizer.CalculateHighestKnownUid( currentHighestKnownUid: 0, uidNext: new MailKit.UniqueId(901), observedUids: null); result.Should().Be(900); } [Fact] public void ShouldRunUidReconcile_ShouldReturnTrue_WhenNeverReconciled() { var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( lastUidReconcileUtc: null, utcNow: DateTime.UtcNow, reconcileInterval: TimeSpan.FromHours(12)); shouldRun.Should().BeTrue(); } [Fact] public void ShouldRunUidReconcile_ShouldReturnFalse_WhenWithinInterval() { var now = DateTime.UtcNow; var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( lastUidReconcileUtc: now.AddHours(-1), utcNow: now, reconcileInterval: TimeSpan.FromHours(12)); shouldRun.Should().BeFalse(); } [Fact] public void ShouldRunUidReconcile_ShouldReturnTrue_WhenIntervalElapsed() { var now = DateTime.UtcNow; var shouldRun = UnifiedImapSynchronizer.ShouldRunUidReconcile( lastUidReconcileUtc: now.AddHours(-13), utcNow: now, reconcileInterval: TimeSpan.FromHours(12)); shouldRun.Should().BeTrue(); } [Fact] public async Task ProcessSummariesAsync_ShouldUseMetadataOnlyPackage() { var localFolder = new MailItemFolder { Id = Guid.NewGuid(), MailAccountId = Guid.NewGuid(), FolderName = "Inbox", RemoteFolderId = "INBOX" }; var summaryMock = new Mock(); summaryMock.SetupGet(x => x.UniqueId).Returns(new UniqueId(42)); summaryMock.SetupGet(x => x.Flags).Returns(MessageFlags.None); var mailServiceMock = new Mock(); mailServiceMock .Setup(x => x.GetExistingMailsAsync(localFolder.Id, It.IsAny>())) .ReturnsAsync(new List()); mailServiceMock .Setup(x => x.CreateMailAsync(localFolder.MailAccountId, It.IsAny())) .ReturnsAsync(true); var sut = new UnifiedImapSynchronizer( Mock.Of(), mailServiceMock.Object, Mock.Of()); ImapMessageCreationPackage? capturedPackage = null; var imapSynchronizerMock = new Mock(); imapSynchronizerMock .Setup(x => x.CreateNewMailPackagesAsync(It.IsAny(), localFolder, It.IsAny())) .Callback((package, _, _) => capturedPackage = package) .ReturnsAsync(new List { new(new MailCopy { Id = "mail-id" }, null, localFolder.RemoteFolderId, Array.Empty()) }); var processMethod = typeof(UnifiedImapSynchronizer).GetMethod("ProcessSummariesAsync", BindingFlags.Instance | BindingFlags.NonPublic); processMethod.Should().NotBeNull(); var task = (Task>)processMethod!.Invoke( sut, [imapSynchronizerMock.Object, localFolder, new List { summaryMock.Object }, CancellationToken.None])!; var result = await task; result.Should().ContainSingle().Which.Should().Be("mail-id"); capturedPackage.Should().NotBeNull(); capturedPackage!.MimeMessage.Should().BeNull(); } }