CalDav synchronizer, new IMAP setup/edit page.

This commit is contained in:
Burak Kaan Köse
2026-02-15 02:20:18 +01:00
parent 64b9bfc392
commit acf0f649e8
58 changed files with 3993 additions and 1732 deletions
@@ -0,0 +1,98 @@
using FluentAssertions;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Calendar;
using Wino.Services;
using Xunit;
using Xunit.Abstractions;
namespace Wino.Core.Tests.Synchronizers;
public sealed class ICloudCalDavLiveTests
{
private const string ManualSkipMessage = "Manual live iCloud CalDAV test. Fill credentials/constants in this file and remove Skip to run.";
// Inline credentials/configuration (manual test by design).
// For iCloud, ServiceUri is typically https://caldav.icloud.com/
private const string ServiceUri = "https://caldav.icloud.com/";
private static readonly CustomServerInformation ServerInformation = new()
{
IncomingServerUsername = "",
IncomingServerPassword = "",
Address = ""
};
// Fixed UTC range for deterministic fetch checks.
private static readonly DateTimeOffset PeriodStartUtc = new(2026, 01, 01, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset PeriodEndUtc = new(2026, 12, 31, 23, 59, 59, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public ICloudCalDavLiveTests(ITestOutputHelper output)
{
_output = output;
}
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task FetchesEventsForFixedUtcRange_AllCalendars()
{
var client = new CalDavClient();
var settings = GetConnectionSettings();
var calendars = await client.DiscoverCalendarsAsync(settings);
calendars.Should().NotBeNull();
calendars.Should().NotBeEmpty();
foreach (var calendar in calendars)
{
var events = await client.GetCalendarEventsAsync(settings, calendar, PeriodStartUtc, PeriodEndUtc);
_output.WriteLine($"Calendar: {calendar.Name} ({calendar.RemoteCalendarId}) => {events.Count} events");
}
}
[Fact(Skip = ManualSkipMessage)]
[Trait("Category", "Live")]
public async Task ParsesRequiredIcsFields_ForFetchedEvents()
{
var client = new CalDavClient();
var settings = GetConnectionSettings();
var calendars = await client.DiscoverCalendarsAsync(settings);
calendars.Should().NotBeNull();
calendars.Should().NotBeEmpty();
foreach (var calendar in calendars)
{
var events = await client.GetCalendarEventsAsync(settings, calendar, PeriodStartUtc, PeriodEndUtc);
foreach (var item in events)
{
item.Uid.Should().NotBeNullOrWhiteSpace();
item.Start.Should().NotBe(default(DateTimeOffset));
item.Title.Should().NotBeNull();
}
}
}
[Fact]
public void BuildsConnectionSettings_FromCustomServerInformation()
{
var settings = GetConnectionSettings();
settings.ServiceUri.Should().Be(new Uri(ServiceUri));
settings.Username.Should().Be(string.IsNullOrWhiteSpace(ServerInformation.IncomingServerUsername)
? ServerInformation.Address
: ServerInformation.IncomingServerUsername);
settings.Password.Should().Be(ServerInformation.IncomingServerPassword);
}
private static CalDavConnectionSettings GetConnectionSettings()
=> new()
{
ServiceUri = new Uri(ServiceUri),
Username = string.IsNullOrWhiteSpace(ServerInformation.IncomingServerUsername)
? ServerInformation.Address
: ServerInformation.IncomingServerUsername,
Password = ServerInformation.IncomingServerPassword
};
}
@@ -1,8 +1,4 @@
using FluentAssertions;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Models.Connectivity;
using Wino.Core.Integration;
using Xunit;
@@ -34,50 +30,4 @@ public class ImapClientPoolTests
{
ImapClientPool.CalculateTargetMinimumConnections(maxConnections: 5, useConservativeConnections: false).Should().Be(2);
}
[Fact]
public async Task RentAsync_ShouldThrowImapClientPoolException_WhenAcquireTimesOut()
{
var serverInformation = new CustomServerInformation
{
Id = Guid.NewGuid(),
IncomingServer = "127.0.0.1",
IncomingServerPort = "1",
IncomingServerUsername = "user",
IncomingServerPassword = "password",
IncomingServerSocketOption = ImapConnectionSecurity.None,
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
MaxConcurrentClients = 2
};
using var pool = new ImapClientPool(ImapClientPoolOptions.CreateTestPool(serverInformation, protocolLog: null));
var act = async () => await pool.RentAsync(TimeSpan.FromMilliseconds(400));
var exception = await act.Should().ThrowAsync<ImapClientPoolException>();
exception.Which.CustomServerInformation.Should().NotBeNull();
}
[Fact]
public async Task InitializeAsync_ShouldBeSafe_WhenCalledConcurrently()
{
var serverInformation = new CustomServerInformation
{
Id = Guid.NewGuid(),
IncomingServer = "127.0.0.1",
IncomingServerPort = "1",
IncomingServerUsername = "user",
IncomingServerPassword = "password",
IncomingServerSocketOption = ImapConnectionSecurity.None,
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
MaxConcurrentClients = 2
};
using var pool = new ImapClientPool(ImapClientPoolOptions.CreateTestPool(serverInformation, protocolLog: null));
var init1 = pool.InitializeAsync();
var init2 = pool.InitializeAsync();
await Task.WhenAll(init1, init2);
}
}
@@ -0,0 +1,172 @@
using System.Reflection;
using FluentAssertions;
using Moq;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Integration.Processors;
using Wino.Core.Synchronizers.ImapSync;
using Wino.Core.Synchronizers.Mail;
using Xunit;
namespace Wino.Core.Tests.Synchronizers;
public class ImapSynchronizerCalDavConfigurationTests
{
[Fact]
public async Task ResolveCalDavServiceUriAsync_UsesExplicitConfigurationBeforeAutoDiscovery()
{
var tempDirectory = CreateTempDirectory();
var autoDiscovery = new Mock<IAutoDiscoveryService>(MockBehavior.Strict);
var serverInformation = CreateServerInformation();
serverInformation.CalDavServiceUrl = "https://caldav.explicit.example.com/";
var synchronizer = CreateSynchronizer(tempDirectory, serverInformation, autoDiscovery.Object);
try
{
var resolvedUri = await InvokePrivateAsync<Uri>(synchronizer, "ResolveCalDavServiceUriAsync", CancellationToken.None);
resolvedUri.Should().Be(new Uri("https://caldav.explicit.example.com/"));
autoDiscovery.Verify(a => a.DiscoverCalDavServiceUriAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
finally
{
await synchronizer.KillSynchronizerAsync();
DeleteDirectory(tempDirectory);
}
}
[Fact]
public async Task ResolveCalDavPassword_PrefersExplicitCalDavPassword()
{
var tempDirectory = CreateTempDirectory();
var serverInformation = CreateServerInformation();
serverInformation.IncomingServerPassword = "incoming-password";
serverInformation.OutgoingServerPassword = "outgoing-password";
serverInformation.CalDavPassword = "caldav-password";
var synchronizer = CreateSynchronizer(tempDirectory, serverInformation);
try
{
var password = InvokePrivate<string>(synchronizer, "ResolveCalDavPassword");
password.Should().Be("caldav-password");
}
finally
{
await synchronizer.KillSynchronizerAsync();
DeleteDirectory(tempDirectory);
}
}
[Fact]
public async Task ResolveCalDavUsername_PrefersExplicitCalDavUsername()
{
var tempDirectory = CreateTempDirectory();
var serverInformation = CreateServerInformation();
serverInformation.Address = "fallback@example.com";
serverInformation.CalDavUsername = "calendar-user@example.com";
var synchronizer = CreateSynchronizer(tempDirectory, serverInformation);
try
{
var username = InvokePrivate<string>(synchronizer, "ResolveCalDavUsername");
username.Should().Be("calendar-user@example.com");
}
finally
{
await synchronizer.KillSynchronizerAsync();
DeleteDirectory(tempDirectory);
}
}
private static ImapSynchronizer CreateSynchronizer(string appDataFolder,
CustomServerInformation serverInformation,
IAutoDiscoveryService autoDiscoveryService = null)
{
var account = new MailAccount
{
Id = Guid.NewGuid(),
Name = "IMAP Test",
Address = "test@example.com",
ProviderType = MailProviderType.IMAP4,
IsCalendarAccessGranted = true,
ServerInformation = serverInformation
};
var applicationConfiguration = new Mock<IApplicationConfiguration>();
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<IFolderService>(),
Mock.Of<IMailService>(),
Mock.Of<IImapSynchronizerErrorHandlerFactory>());
return new ImapSynchronizer(
account,
Mock.Of<IImapChangeProcessor>(),
applicationConfiguration.Object,
unifiedSynchronizer,
Mock.Of<IImapSynchronizerErrorHandlerFactory>(),
Mock.Of<ICalDavClient>(),
autoDiscoveryService ?? Mock.Of<IAutoDiscoveryService>());
}
private static CustomServerInformation CreateServerInformation()
=> new()
{
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
};
private static string CreateTempDirectory()
{
var path = Path.Combine(Path.GetTempPath(), "wino-imap-caldav-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 T InvokePrivate<T>(object instance, string methodName)
{
var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException($"Method '{methodName}' not found.");
return (T)method.Invoke(instance, null)!;
}
private static async Task<T> InvokePrivateAsync<T>(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<T>)method.Invoke(instance, parameters)!;
return await task.ConfigureAwait(false);
}
}
@@ -74,6 +74,8 @@ public class ImapSynchronizerIdleTests
Mock.Of<IImapChangeProcessor>(),
applicationConfiguration.Object,
unifiedSynchronizer,
Mock.Of<IImapSynchronizerErrorHandlerFactory>());
Mock.Of<IImapSynchronizerErrorHandlerFactory>(),
Mock.Of<ICalDavClient>(),
Mock.Of<IAutoDiscoveryService>());
}
}