222 lines
8.4 KiB
C#
222 lines
8.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|