CalDav synchronizer, new IMAP setup/edit page.
This commit is contained in:
@@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user