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
@@ -1,4 +1,5 @@
using SQLite;
using System.IO;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
@@ -12,12 +13,14 @@ namespace Wino.Core.Tests.Helpers;
/// </summary>
public class InMemoryDatabaseService : IDatabaseService
{
private readonly string _databasePath;
public SQLiteAsyncConnection Connection { get; private set; }
public InMemoryDatabaseService()
{
// Use :memory: for a truly in-memory database or a temporary file
Connection = new SQLiteAsyncConnection(":memory:");
// Use a unique temporary file per test instance for stable async access.
_databasePath = Path.Combine(Path.GetTempPath(), $"wino-tests-{Guid.NewGuid():N}.db");
Connection = new SQLiteAsyncConnection(_databasePath);
}
public async Task InitializeAsync()
@@ -27,23 +30,24 @@ public class InMemoryDatabaseService : IDatabaseService
private async Task CreateTablesAsync()
{
await Task.WhenAll(
Connection.CreateTableAsync<MailCopy>(),
Connection.CreateTableAsync<MailItemFolder>(),
Connection.CreateTableAsync<MailAccount>(),
Connection.CreateTableAsync<AccountContact>(),
Connection.CreateTableAsync<CustomServerInformation>(),
Connection.CreateTableAsync<AccountSignature>(),
Connection.CreateTableAsync<MergedInbox>(),
Connection.CreateTableAsync<MailAccountPreferences>(),
Connection.CreateTableAsync<MailAccountAlias>(),
Connection.CreateTableAsync<Thumbnail>(),
Connection.CreateTableAsync<KeyboardShortcut>(),
Connection.CreateTableAsync<AccountCalendar>(),
Connection.CreateTableAsync<CalendarEventAttendee>(),
Connection.CreateTableAsync<CalendarItem>(),
Connection.CreateTableAsync<Reminder>()
);
// Keep table creation sequential for in-memory SQLite to avoid connection contention.
await Connection.CreateTableAsync<MailCopy>();
await Connection.CreateTableAsync<MailItemFolder>();
await Connection.CreateTableAsync<MailAccount>();
await Connection.CreateTableAsync<AccountContact>();
await Connection.CreateTableAsync<CustomServerInformation>();
await Connection.CreateTableAsync<AccountSignature>();
await Connection.CreateTableAsync<MergedInbox>();
await Connection.CreateTableAsync<MailAccountPreferences>();
await Connection.CreateTableAsync<MailAccountAlias>();
await Connection.CreateTableAsync<Thumbnail>();
await Connection.CreateTableAsync<KeyboardShortcut>();
await Connection.CreateTableAsync<AccountCalendar>();
await Connection.CreateTableAsync<CalendarEventAttendee>();
await Connection.CreateTableAsync<CalendarItem>();
await Connection.CreateTableAsync<CalendarAttachment>();
await Connection.CreateTableAsync<Reminder>();
await Connection.CreateTableAsync<MailInvitationCalendarMapping>();
}
public async ValueTask DisposeAsync()
@@ -53,5 +57,10 @@ public class InMemoryDatabaseService : IDatabaseService
await Connection.CloseAsync();
Connection = null!;
}
if (File.Exists(_databasePath))
{
File.Delete(_databasePath);
}
}
}
@@ -0,0 +1,221 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using FluentAssertions;
using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Services;
using Xunit;
namespace Wino.Core.Tests.Services;
public class AutoDiscoveryServiceTests
{
[Fact]
public async Task GetAutoDiscoverySettings_UsesThunderbirdAutoconfig_WhenAvailable()
{
var handler = new StubHttpMessageHandler(request =>
{
var uri = request.RequestUri!.ToString();
if (uri.StartsWith("https://autoconfig.example.com/mail/config-v1.1.xml", StringComparison.OrdinalIgnoreCase))
{
return CreateXmlResponse("""
<clientConfig version="1.1">
<emailProvider id="example.com">
<incomingServer type="imap">
<hostname>imap.example.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILLOCALPART%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>smtp.example.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>
""", request);
}
return CreateStatusResponse(HttpStatusCode.NotFound, request);
});
using var client = new HttpClient(handler);
var sut = new AutoDiscoveryService(client);
var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings
{
Email = "user@example.com",
DisplayName = "User",
Password = "secret"
});
settings.Should().NotBeNull();
settings!.Domain.Should().Be("example.com");
settings.GetImapSettings()!.Address.Should().Be("imap.example.com");
settings.GetImapSettings()!.Username.Should().Be("user");
settings.GetSmptpSettings()!.Address.Should().Be("smtp.example.com");
settings.GetSmptpSettings()!.Username.Should().Be("user@example.com");
handler.RequestedUris.Should().NotContain(uri => uri.Contains("emailsettings.firetrust.com", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task GetAutoDiscoverySettings_FallsBackToFiretrust_WhenThunderbirdMethodsFail()
{
var handler = new StubHttpMessageHandler(request =>
{
var uri = request.RequestUri!.ToString();
if (uri.StartsWith("https://emailsettings.firetrust.com/settings?q=", StringComparison.OrdinalIgnoreCase))
{
return CreateJsonResponse("""
{
"domain": "example.com",
"settings": [
{
"protocol": "IMAP",
"address": "imap.firetrust.example.com",
"port": 993,
"secure": "SSL",
"username": "user@example.com"
},
{
"protocol": "SMTP",
"address": "smtp.firetrust.example.com",
"port": 587,
"secure": "STARTTLS",
"username": "user@example.com"
}
]
}
""", request);
}
if (uri.StartsWith("https://dns.google/resolve", StringComparison.OrdinalIgnoreCase))
{
return CreateJsonResponse("{\"Status\":0}", request);
}
return CreateStatusResponse(HttpStatusCode.NotFound, request);
});
using var client = new HttpClient(handler);
var sut = new AutoDiscoveryService(client);
var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings
{
Email = "user@example.com"
});
settings.Should().NotBeNull();
settings!.GetImapSettings()!.Address.Should().Be("imap.firetrust.example.com");
settings.GetSmptpSettings()!.Address.Should().Be("smtp.firetrust.example.com");
handler.RequestedUris.Should().Contain(uri => uri.Contains("emailsettings.firetrust.com", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task DiscoverCalDavServiceUriAsync_ReturnsKnownYahooEndpoint()
{
var sut = new AutoDiscoveryService(new HttpClient(new StubHttpMessageHandler(_ => throw new InvalidOperationException("No network call expected"))));
var uri = await sut.DiscoverCalDavServiceUriAsync("user@yahoo.com");
uri.Should().Be(new Uri("https://caldav.calendar.yahoo.com/"));
}
[Fact]
public async Task DiscoverCalDavServiceUriAsync_ResolvesWellKnownRedirect()
{
var handler = new StubHttpMessageHandler(request =>
{
var uri = request.RequestUri!.ToString();
if (uri.Equals("https://calendar.example.com/.well-known/caldav", StringComparison.OrdinalIgnoreCase))
{
var response = CreateStatusResponse(HttpStatusCode.Found, request);
response.Headers.Location = new Uri("https://dav.example.net/caldav/");
return response;
}
return CreateStatusResponse(HttpStatusCode.NotFound, request);
});
using var client = new HttpClient(handler);
var sut = new AutoDiscoveryService(client);
var uri = await sut.DiscoverCalDavServiceUriAsync("user@calendar.example.com");
uri.Should().Be(new Uri("https://dav.example.net/caldav/"));
}
private static HttpResponseMessage CreateXmlResponse(string xml, HttpRequestMessage request)
=> new(HttpStatusCode.OK)
{
RequestMessage = request,
Content = new StringContent(xml, Encoding.UTF8, "application/xml")
};
private static HttpResponseMessage CreateJsonResponse(string json, HttpRequestMessage request)
=> new(HttpStatusCode.OK)
{
RequestMessage = request,
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
private static HttpResponseMessage CreateStatusResponse(HttpStatusCode statusCode, HttpRequestMessage request)
=> new(statusCode)
{
RequestMessage = request
};
private sealed class StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory) : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage> _responseFactory = responseFactory;
public List<string> RequestedUris { get; } = [];
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
RequestedUris.Add(request.RequestUri?.ToString() ?? string.Empty);
var response = _responseFactory(request);
// Ensure probing logic sees a request URI even if response factory forgot it.
response.RequestMessage ??= request;
if (response.Headers.Date == null)
{
response.Headers.Date = DateTimeOffset.UtcNow;
}
if (!response.Headers.Contains("DAV") &&
response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
response.Headers.Add("DAV", "1, calendar-access");
}
var hasAllowHeader = response.Headers.TryGetValues("Allow", out var allowValues) &&
allowValues.Any();
if (!hasAllowHeader &&
response.StatusCode == HttpStatusCode.OK &&
request.Method == HttpMethod.Options)
{
response.Headers.Add("Allow", "PROPFIND");
}
if (response.Content == null)
{
response.Content = new StringContent(string.Empty, Encoding.UTF8, "text/plain");
}
if (response.Content.Headers.ContentType == null)
{
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
}
return Task.FromResult(response);
}
}
}
@@ -117,79 +117,6 @@ public class CalendarServiceTests : IAsyncLifetime
result.Should().BeEmpty();
}
[Fact]
public async Task GetCalendarEventsAsync_WithRecurringEventInstances_ReturnsAllInstancesInPeriod()
{
// Arrange - Simulate synced recurring event instances (as they would come from server)
var parentId = Guid.NewGuid();
var startDate1 = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc);
var startDate2 = new DateTime(2025, 1, 16, 10, 0, 0, DateTimeKind.Utc);
var startDate3 = new DateTime(2025, 1, 17, 10, 0, 0, DateTimeKind.Utc);
// Parent series master (typically hidden or outside display range)
var parentEvent = new CalendarItem
{
Id = parentId,
Title = "Daily Standup",
Description = "Daily team sync",
StartDate = startDate1,
DurationInSeconds = 1800, // 30 minutes
CalendarId = _testCalendar.Id,
IsHidden = false,
Recurrence = "RRULE:FREQ=DAILY;COUNT=3"
};
// Individual occurrence instances (as synced from server)
var instance1 = new CalendarItem
{
Id = Guid.NewGuid(),
Title = "Daily Standup",
StartDate = startDate1,
DurationInSeconds = 1800,
CalendarId = _testCalendar.Id,
RecurringCalendarItemId = parentId,
IsHidden = false
};
var instance2 = new CalendarItem
{
Id = Guid.NewGuid(),
Title = "Daily Standup",
StartDate = startDate2,
DurationInSeconds = 1800,
CalendarId = _testCalendar.Id,
RecurringCalendarItemId = parentId,
IsHidden = false
};
var instance3 = new CalendarItem
{
Id = Guid.NewGuid(),
Title = "Daily Standup",
StartDate = startDate3,
DurationInSeconds = 1800,
CalendarId = _testCalendar.Id,
RecurringCalendarItemId = parentId,
IsHidden = false
};
await _calendarService.CreateNewCalendarItemAsync(parentEvent, null);
await _calendarService.CreateNewCalendarItemAsync(instance1, null);
await _calendarService.CreateNewCalendarItemAsync(instance2, null);
await _calendarService.CreateNewCalendarItemAsync(instance3, null);
var period = new TimeRange(
new DateTime(2025, 1, 15, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2025, 1, 18, 0, 0, 0, DateTimeKind.Utc));
// Act
var result = await _calendarService.GetCalendarEventsAsync(_testCalendar, period);
// Assert - Should return parent + 3 instances = 4 total
result.Should().HaveCount(4, "parent event plus 3 instances");
result.Where(e => e.Title == "Daily Standup").Should().HaveCount(4);
}
[Fact]
public async Task GetCalendarEventsAsync_WithHiddenEvent_ExcludesFromResults()
{
@@ -267,7 +194,7 @@ public class CalendarServiceTests : IAsyncLifetime
// Add events to both calendars
var startDate = new DateTime(2025, 1, 15, 10, 0, 0, DateTimeKind.Utc);
var event1 = new CalendarItem
{
Id = Guid.NewGuid(),
@@ -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>());
}
}