From 2baa87daeb1bc00ecb5a11e30f862c8a90ce0f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sun, 15 Feb 2026 18:40:32 +0100 Subject: [PATCH] Add IMAP local calendar operation tests using in-memory DB (#807) * Add IMAP local calendar operation handler tests * Fix tests. * Fix calendar item show as not updating. * Create one default calendar for local calendar accounts. --- .../Services/AccountServiceTests.cs | 113 ++++++++++++++++++ ...mapSynchronizerCalDavConfigurationTests.cs | 3 +- .../ImapSynchronizerIdleTests.cs | 3 +- .../AccountManagementViewModel.cs | 2 +- .../ImapCalDavSettingsPageViewModel.cs | 4 +- .../Calendar/CalendarItemControl.xaml | 2 +- Wino.Services/AccountService.cs | 76 ++++++++---- 7 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 Wino.Core.Tests/Services/AccountServiceTests.cs diff --git a/Wino.Core.Tests/Services/AccountServiceTests.cs b/Wino.Core.Tests/Services/AccountServiceTests.cs new file mode 100644 index 00000000..cc38d90a --- /dev/null +++ b/Wino.Core.Tests/Services/AccountServiceTests.cs @@ -0,0 +1,113 @@ +using FluentAssertions; +using Moq; +using Wino.Core.Domain; +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.Tests.Helpers; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class AccountServiceTests : IAsyncLifetime +{ + private InMemoryDatabaseService _databaseService = null!; + private AccountService _accountService = null!; + + public async Task InitializeAsync() + { + _databaseService = new InMemoryDatabaseService(); + await _databaseService.InitializeAsync(); + _accountService = CreateService(_databaseService); + } + + public async Task DisposeAsync() + { + await _databaseService.DisposeAsync(); + } + + [Fact] + public async Task CreateAccountAsync_ImapLocalOnly_CreatesSinglePrimaryDefaultCalendar() + { + var accountId = Guid.NewGuid(); + var account = CreateImapAccount(accountId); + var server = new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = accountId, + CalendarSupportMode = ImapCalendarSupportMode.LocalOnly + }; + + await _accountService.CreateAccountAsync(account, server); + + var calendars = await _databaseService.Connection.Table() + .Where(a => a.AccountId == accountId) + .ToListAsync(); + + calendars.Should().HaveCount(1); + calendars[0].IsPrimary.Should().BeTrue(); + calendars[0].Name.Should().Be(Translator.AccountDetailsPage_TabCalendar); + } + + [Fact] + public async Task CreateAccountAsync_ImapCalDav_DoesNotCreateDefaultLocalCalendar() + { + var accountId = Guid.NewGuid(); + var account = CreateImapAccount(accountId); + var server = new CustomServerInformation + { + Id = Guid.NewGuid(), + AccountId = accountId, + CalendarSupportMode = ImapCalendarSupportMode.CalDav + }; + + await _accountService.CreateAccountAsync(account, server); + + var calendars = await _databaseService.Connection.Table() + .Where(a => a.AccountId == accountId) + .ToListAsync(); + + calendars.Should().BeEmpty(); + } + + private static MailAccount CreateImapAccount(Guid accountId) + { + return new MailAccount + { + Id = accountId, + Name = "IMAP Test Account", + Address = "imap@test.local", + SenderName = "IMAP Test", + ProviderType = MailProviderType.IMAP4 + }; + } + + private static AccountService CreateService(InMemoryDatabaseService databaseService) + { + var signatureService = new Mock(); + signatureService + .Setup(a => a.CreateDefaultSignatureAsync(It.IsAny())) + .ReturnsAsync((Guid accountId) => new AccountSignature + { + Id = Guid.NewGuid(), + MailAccountId = accountId, + Name = "Default", + HtmlBody = string.Empty + }); + + var authenticationProvider = new Mock(); + var mimeFileService = new Mock(); + + var preferencesService = new Mock(); + preferencesService.SetupProperty(a => a.StartupEntityId); + + return new AccountService( + databaseService, + signatureService.Object, + authenticationProvider.Object, + mimeFileService.Object, + preferencesService.Object); + } +} diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs index 3d60a841..2f103309 100644 --- a/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerCalDavConfigurationTests.cs @@ -119,7 +119,8 @@ public class ImapSynchronizerCalDavConfigurationTests unifiedSynchronizer, Mock.Of(), Mock.Of(), - autoDiscoveryService ?? Mock.Of()); + autoDiscoveryService ?? Mock.Of(), + Mock.Of()); } private static CustomServerInformation CreateServerInformation() diff --git a/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs b/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs index 5158e560..d14afe37 100644 --- a/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs +++ b/Wino.Core.Tests/Synchronizers/ImapSynchronizerIdleTests.cs @@ -76,6 +76,7 @@ public class ImapSynchronizerIdleTests unifiedSynchronizer, Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); } } diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index 469b769f..ee4930a0 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -129,7 +129,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel createdAccount.Address = accountCreationDialogResult.SpecialImapProviderDetails.Address; createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; - createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav; + createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; createdAccount.ServerInformation = customServerInformation; await ValidateSpecialImapConnectivityAsync(customServerInformation).ConfigureAwait(false); diff --git a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs index 0e44d6da..97d52026 100644 --- a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs +++ b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs @@ -713,7 +713,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel { DisplayName = DisplayName.Trim(), EmailAddress = EmailAddress.Trim(), - IsCalendarAccessGranted = serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav, + IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled, ServerInformation = serverInformation }); @@ -735,7 +735,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel account.SenderName = DisplayName.Trim(); account.Address = EmailAddress.Trim(); - account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav; + account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid(); serverInformation.AccountId = account.Id; diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml index fb7d9711..0c73897d 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml @@ -60,7 +60,7 @@ HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Canvas.ZIndex="10000" - Content="{x:Bind CalendarItem}" + Content="{x:Bind CalendarItem, Mode=OneWay}" ContentTemplateSelector="{StaticResource ShowAsStripeSelector}" IsTabStop="False" /> diff --git a/Wino.Services/AccountService.cs b/Wino.Services/AccountService.cs index 1a66c6e5..50e65296 100644 --- a/Wino.Services/AccountService.cs +++ b/Wino.Services/AccountService.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.Messaging; using Serilog; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; @@ -17,6 +19,22 @@ namespace Wino.Services; public class AccountService : BaseDatabaseService, IAccountService { + private static readonly string[] DefaultCalendarFlatColors = + [ + "#B91C1C", + "#15803D", + "#0E7490", + "#1D4ED8", + "#7C3AED", + "#C026D3", + "#EC4899", + "#F97316", + "#EAB308", + "#22C55E", + "#06B6D4", + "#60A5FA" + ]; + public IAuthenticator ExternalAuthenticationAuthenticator { get; set; } private readonly ISignatureService _signatureService; @@ -528,35 +546,49 @@ public class AccountService : BaseDatabaseService, IAccountService if (customServerInformation != null) await Connection.InsertAsync(customServerInformation, typeof(CustomServerInformation)); + + if (account.ProviderType == MailProviderType.IMAP4 && + customServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.LocalOnly) + { + await EnsureDefaultLocalCalendarForImapAsync(account.Id).ConfigureAwait(false); + } } - //public async Task UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier) - //{ - // var account = await GetAccountAsync(accountId); + private async Task EnsureDefaultLocalCalendarForImapAsync(Guid accountId) + { + var existingCalendarCount = await Connection.Table() + .Where(a => a.AccountId == accountId) + .CountAsync() + .ConfigureAwait(false); - // if (account == null) - // { - // _logger.Error("Could not find account with id {AccountId}", accountId); - // return string.Empty; - // } + if (existingCalendarCount > 0) + return; - // var currentIdentifier = account.SynchronizationDeltaIdentifier; + var localCalendar = new AccountCalendar + { + Id = Guid.NewGuid(), + AccountId = accountId, + Name = Translator.AccountDetailsPage_TabCalendar, + IsPrimary = true, + IsSynchronizationEnabled = true, + IsExtended = true, + RemoteCalendarId = string.Empty, + TimeZone = string.Empty, + BackgroundColorHex = GetDefaultCalendarFlatColor(accountId), + TextColorHex = "#FFFFFF" + }; - // bool shouldUpdateIdentifier = account.ProviderType == MailProviderType.Gmail ? - // string.IsNullOrEmpty(currentIdentifier) ? true : !string.IsNullOrEmpty(currentIdentifier) - // && ulong.TryParse(currentIdentifier, out ulong currentIdentifierValue) - // && ulong.TryParse(newIdentifier, out ulong newIdentifierValue) - // && newIdentifierValue > currentIdentifierValue : true; + await Connection.InsertAsync(localCalendar, typeof(AccountCalendar)).ConfigureAwait(false); + } - // if (shouldUpdateIdentifier) - // { - // account.SynchronizationDeltaIdentifier = newIdentifier; + private static string GetDefaultCalendarFlatColor(Guid accountId) + { + var bytes = accountId.ToByteArray(); + var hash = BitConverter.ToUInt32(bytes, 0); + var index = (int)(hash % (uint)DefaultCalendarFlatColors.Length); - // await UpdateAccountAsync(account); - // } - - // return account.SynchronizationDeltaIdentifier; - //} + return DefaultCalendarFlatColors[index]; + } public async Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair) {