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