using System.Reflection; using System.IO; using System.Xml.Linq; using FluentAssertions; using Moq; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Integration.Processors; using Wino.Core.Misc; using Wino.Core.Synchronizers.ImapSync; using Wino.Core.Synchronizers.Mail; using Wino.Services; using Xunit; namespace Wino.Core.Tests.Synchronizers; public class CalDavCalendarMetadataTests { [Fact] public void ParseCalendarCollection_MapsCollectionMetadataAndSkipsNonEventCalendars() { var xml = XDocument.Parse( """ /calendars/work/ HTTP/1.1 200 OK Work Team calendar "ctag-1" sync-1 #5b617aff 2 /calendars/tasks/ HTTP/1.1 200 OK Tasks """); var calendars = ParseCalendars(xml, new Uri("https://calendar.example.com/")); calendars.Should().ContainSingle(); var calendar = calendars[0]; calendar.RemoteCalendarId.Should().Be("https://calendar.example.com/calendars/work"); calendar.Name.Should().Be("Work"); calendar.Description.Should().Be("Team calendar"); calendar.CTag.Should().Be("\"ctag-1\""); calendar.SyncToken.Should().Be("sync-1"); calendar.TimeZone.Should().Be("Europe/Warsaw"); calendar.BackgroundColorHex.Should().Be("#5B617A"); calendar.IsReadOnly.Should().BeTrue(); calendar.SupportsEvents.Should().BeTrue(); calendar.DefaultShowAs.Should().Be(CalendarItemShowAs.Free); calendar.Order.Should().Be(2d); } [Fact] public async Task SynchronizeCalendarMetadataAsync_UpdatesServerBackedSettingsAndPreservesUserColorOverride() { var tempDirectory = CreateTempDirectory(); var serverInformation = new CustomServerInformation { Id = Guid.NewGuid(), IncomingServer = "imap.example.com", IncomingServerPort = "993", IncomingServerUsername = "user@example.com", IncomingServerPassword = "password", OutgoingServer = "smtp.example.com", OutgoingServerPort = "587", OutgoingServerUsername = "user@example.com", OutgoingServerPassword = "password", MaxConcurrentClients = 5, CalendarSupportMode = ImapCalendarSupportMode.CalDav }; var account = new MailAccount { Id = Guid.NewGuid(), Name = "IMAP Test", Address = "test@example.com", ProviderType = MailProviderType.IMAP4, IsCalendarAccessGranted = true, ServerInformation = serverInformation }; var localCalendar = new AccountCalendar { Id = Guid.NewGuid(), AccountId = account.Id, RemoteCalendarId = "https://calendar.example.com/calendars/work", Name = "Local", BackgroundColorHex = "#123456", TextColorHex = "#FFFFFF", IsBackgroundColorUserOverridden = true, TimeZone = "UTC", IsReadOnly = false, DefaultShowAs = CalendarItemShowAs.Busy }; var changeProcessor = new Mock(); changeProcessor .Setup(x => x.GetAccountCalendarsAsync(account.Id)) .ReturnsAsync(new List { localCalendar }); changeProcessor .Setup(x => x.UpdateAccountCalendarAsync(It.IsAny())) .Returns(Task.CompletedTask); changeProcessor .Setup(x => x.DeleteCalendarIcsForCalendarAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); changeProcessor .Setup(x => x.DeleteAccountCalendarAsync(It.IsAny())) .Returns(Task.CompletedTask); changeProcessor .Setup(x => x.InsertAccountCalendarAsync(It.IsAny())) .Returns(Task.CompletedTask); var synchronizer = CreateSynchronizer(tempDirectory, account, changeProcessor.Object); try { await InvokePrivateAsync( synchronizer, "SynchronizeCalendarMetadataAsync", new List { new() { RemoteCalendarId = localCalendar.RemoteCalendarId, Name = "Remote", TimeZone = "Europe/Warsaw", BackgroundColorHex = "#ABCDEF", IsReadOnly = true, DefaultShowAs = CalendarItemShowAs.Free, Order = 0 } }); localCalendar.Name.Should().Be("Remote"); localCalendar.TimeZone.Should().Be("Europe/Warsaw"); localCalendar.IsReadOnly.Should().BeTrue(); localCalendar.DefaultShowAs.Should().Be(CalendarItemShowAs.Free); localCalendar.IsPrimary.Should().BeTrue(); localCalendar.BackgroundColorHex.Should().Be("#123456"); localCalendar.TextColorHex.Should().Be(ColorHelpers.GetReadableTextColorHex("#123456")); changeProcessor.Verify(x => x.UpdateAccountCalendarAsync(localCalendar), Times.Once); changeProcessor.Verify(x => x.InsertAccountCalendarAsync(It.IsAny()), Times.Never); changeProcessor.Verify(x => x.DeleteAccountCalendarAsync(It.IsAny()), Times.Never); } finally { await synchronizer.KillSynchronizerAsync(); DeleteDirectory(tempDirectory); } } private static List ParseCalendars(XDocument xml, Uri baseUri) { var parseMethod = typeof(CalDavClient).GetMethod( "ParseCalendarCollection", BindingFlags.NonPublic | BindingFlags.Static); parseMethod.Should().NotBeNull(); var result = parseMethod!.Invoke(null, [xml, baseUri]); return result.Should().BeOfType>().Subject; } private static ImapSynchronizer CreateSynchronizer(string appDataFolder, MailAccount account, IImapChangeProcessor changeProcessor) { var applicationConfiguration = new Mock(); applicationConfiguration.SetupProperty(x => x.ApplicationDataFolderPath, appDataFolder); applicationConfiguration.SetupProperty(x => x.PublisherSharedFolderPath, appDataFolder); applicationConfiguration.SetupProperty(x => x.ApplicationTempFolderPath, appDataFolder); applicationConfiguration.SetupGet(x => x.SentryDNS).Returns(string.Empty); var unifiedSynchronizer = new UnifiedImapSynchronizer( Mock.Of(), Mock.Of(), Mock.Of()); return new ImapSynchronizer( account, changeProcessor, applicationConfiguration.Object, unifiedSynchronizer, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); } private static string CreateTempDirectory() { var path = Path.Combine(Path.GetTempPath(), "wino-caldav-calendar-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } private static void DeleteDirectory(string path) { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } private static async Task InvokePrivateAsync(object instance, string methodName, params object[] parameters) { var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new InvalidOperationException($"Method '{methodName}' not found."); var task = (Task)method.Invoke(instance, parameters)!; await task.ConfigureAwait(false); } }