Import functionality for wino accounts, calendar sync UI, bunch of shell improvements
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Tests.Helpers;
|
||||
using Wino.Mail.Api.Contracts.Users;
|
||||
using Wino.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests.Services;
|
||||
|
||||
public sealed class WinoAccountDataSyncServiceTests : IAsyncLifetime
|
||||
{
|
||||
private InMemoryDatabaseService _databaseService = null!;
|
||||
private Mock<IWinoAccountProfileService> _profileService = null!;
|
||||
private Mock<IPreferencesService> _preferencesService = null!;
|
||||
private AccountService _accountService = null!;
|
||||
private WinoAccountDataSyncService _service = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_databaseService = new InMemoryDatabaseService();
|
||||
await _databaseService.InitializeAsync();
|
||||
|
||||
_profileService = new Mock<IWinoAccountProfileService>(MockBehavior.Strict);
|
||||
_preferencesService = new Mock<IPreferencesService>();
|
||||
_preferencesService.SetupProperty(a => a.StartupEntityId);
|
||||
|
||||
_accountService = CreateAccountService(_databaseService, _preferencesService.Object);
|
||||
_service = new WinoAccountDataSyncService(_profileService.Object, _preferencesService.Object, _accountService);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _databaseService.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_ImapMailbox_MapsSanitizedPayload()
|
||||
{
|
||||
var accountId = Guid.NewGuid();
|
||||
|
||||
await _accountService.CreateAccountAsync(
|
||||
new MailAccount
|
||||
{
|
||||
Id = accountId,
|
||||
Name = "Custom IMAP",
|
||||
SenderName = "Custom IMAP Sender",
|
||||
Address = "imap@example.com",
|
||||
ProviderType = MailProviderType.IMAP4,
|
||||
SpecialImapProvider = SpecialImapProvider.iCloud,
|
||||
AccountColorHex = "#123456",
|
||||
IsCalendarAccessGranted = true,
|
||||
SynchronizationDeltaIdentifier = "delta-token",
|
||||
CalendarSynchronizationDeltaIdentifier = "calendar-delta",
|
||||
Base64ProfilePictureData = "profile"
|
||||
},
|
||||
new CustomServerInformation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = accountId,
|
||||
Address = "imap@example.com",
|
||||
IncomingServer = "imap.example.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerUsername = "imap-user",
|
||||
IncomingServerPassword = "secret-incoming",
|
||||
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword,
|
||||
OutgoingServer = "smtp.example.com",
|
||||
OutgoingServerPort = "465",
|
||||
OutgoingServerUsername = "smtp-user",
|
||||
OutgoingServerPassword = "secret-outgoing",
|
||||
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword,
|
||||
CalendarSupportMode = ImapCalendarSupportMode.CalDav,
|
||||
CalDavServiceUrl = "https://dav.example.com",
|
||||
CalDavUsername = "dav-user",
|
||||
CalDavPassword = "secret-caldav",
|
||||
ProxyServer = "proxy.example.com",
|
||||
ProxyServerPort = "8080",
|
||||
MaxConcurrentClients = 7
|
||||
});
|
||||
|
||||
ReplaceUserMailboxesRequestDto? capturedRequest = null;
|
||||
_profileService
|
||||
.Setup(a => a.ReplaceMailboxesAsync(It.IsAny<ReplaceUserMailboxesRequestDto>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ReplaceUserMailboxesRequestDto, CancellationToken>((request, _) => capturedRequest = request)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var result = await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||
|
||||
result.ExportedMailboxCount.Should().Be(1);
|
||||
capturedRequest.Should().NotBeNull();
|
||||
capturedRequest!.Mailboxes.Should().ContainSingle();
|
||||
|
||||
var exportedMailbox = capturedRequest.Mailboxes[0];
|
||||
exportedMailbox.Address.Should().Be("imap@example.com");
|
||||
exportedMailbox.ProviderType.Should().Be((int)MailProviderType.IMAP4);
|
||||
exportedMailbox.SpecialImapProvider.Should().Be((int)SpecialImapProvider.iCloud);
|
||||
exportedMailbox.AccountName.Should().Be("Custom IMAP");
|
||||
exportedMailbox.SenderName.Should().Be("Custom IMAP Sender");
|
||||
exportedMailbox.AccountColorHex.Should().Be("#123456");
|
||||
exportedMailbox.IsCalendarAccessGranted.Should().BeTrue();
|
||||
exportedMailbox.IncomingServer.Should().Be("imap.example.com");
|
||||
exportedMailbox.IncomingServerUsername.Should().Be("imap-user");
|
||||
exportedMailbox.OutgoingServer.Should().Be("smtp.example.com");
|
||||
exportedMailbox.OutgoingServerUsername.Should().Be("smtp-user");
|
||||
exportedMailbox.CalDavServiceUrl.Should().Be("https://dav.example.com");
|
||||
exportedMailbox.CalDavUsername.Should().Be("dav-user");
|
||||
exportedMailbox.ProxyServer.Should().Be("proxy.example.com");
|
||||
exportedMailbox.ProxyServerPort.Should().Be("8080");
|
||||
exportedMailbox.MaxConcurrentClients.Should().Be(7);
|
||||
|
||||
_profileService.Verify(a => a.SaveSettingsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_GmailMailbox_DoesNotIncludeCustomServerSettings()
|
||||
{
|
||||
await _accountService.CreateAccountAsync(
|
||||
new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Gmail",
|
||||
SenderName = "Gmail Sender",
|
||||
Address = "gmail@example.com",
|
||||
ProviderType = MailProviderType.Gmail
|
||||
},
|
||||
null!);
|
||||
|
||||
ReplaceUserMailboxesRequestDto? capturedRequest = null;
|
||||
_profileService
|
||||
.Setup(a => a.ReplaceMailboxesAsync(It.IsAny<ReplaceUserMailboxesRequestDto>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ReplaceUserMailboxesRequestDto, CancellationToken>((request, _) => capturedRequest = request)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||
|
||||
var exportedMailbox = capturedRequest!.Mailboxes.Single();
|
||||
exportedMailbox.IncomingServer.Should().BeNull();
|
||||
exportedMailbox.OutgoingServer.Should().BeNull();
|
||||
exportedMailbox.CalDavServiceUrl.Should().BeNull();
|
||||
exportedMailbox.MaxConcurrentClients.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportAsync_SkipsDuplicateMailbox_ByAddressAndProviderCaseInsensitive()
|
||||
{
|
||||
await _accountService.CreateAccountAsync(
|
||||
new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Existing Gmail",
|
||||
SenderName = "Existing Gmail",
|
||||
Address = "User@Example.com",
|
||||
ProviderType = MailProviderType.Gmail
|
||||
},
|
||||
null!);
|
||||
|
||||
_profileService
|
||||
.Setup(a => a.GetMailboxesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new UserMailboxSyncListDto(
|
||||
[
|
||||
new UserMailboxSyncItemDto
|
||||
{
|
||||
Address = "user@example.com",
|
||||
ProviderType = (int)MailProviderType.Gmail,
|
||||
AccountName = "Duplicate Gmail"
|
||||
},
|
||||
new UserMailboxSyncItemDto
|
||||
{
|
||||
Address = "second@example.com",
|
||||
ProviderType = (int)MailProviderType.Outlook,
|
||||
AccountName = "New Outlook"
|
||||
}
|
||||
]));
|
||||
|
||||
var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||
|
||||
result.ImportedMailboxCount.Should().Be(1);
|
||||
result.SkippedDuplicateMailboxCount.Should().Be(1);
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
accounts.Should().HaveCount(2);
|
||||
accounts.Should().Contain(a => a.Address == "second@example.com" && a.ProviderType == MailProviderType.Outlook);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportAsync_ImapMailbox_CreatesRootAliasAndInvalidCredentialsAttentionWithoutPasswords()
|
||||
{
|
||||
_profileService
|
||||
.Setup(a => a.GetMailboxesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new UserMailboxSyncListDto(
|
||||
[
|
||||
new UserMailboxSyncItemDto
|
||||
{
|
||||
Address = "imap@example.com",
|
||||
ProviderType = (int)MailProviderType.IMAP4,
|
||||
SpecialImapProvider = (int)SpecialImapProvider.Yahoo,
|
||||
AccountName = "Imported IMAP",
|
||||
SenderName = "Imported Sender",
|
||||
CalendarSupportMode = (int)ImapCalendarSupportMode.CalDav,
|
||||
IncomingServer = "imap.example.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerUsername = "imap-user",
|
||||
IncomingServerSocketOption = (int)ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword,
|
||||
OutgoingServer = "smtp.example.com",
|
||||
OutgoingServerPort = "465",
|
||||
OutgoingServerUsername = "smtp-user",
|
||||
OutgoingServerSocketOption = (int)ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword,
|
||||
CalDavServiceUrl = "https://dav.example.com",
|
||||
CalDavUsername = "dav-user",
|
||||
MaxConcurrentClients = 9
|
||||
}
|
||||
]));
|
||||
|
||||
var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||
|
||||
result.ImportedMailboxCount.Should().Be(1);
|
||||
|
||||
var importedAccount = (await _accountService.GetAccountsAsync()).Single();
|
||||
importedAccount.AttentionReason.Should().Be(AccountAttentionReason.InvalidCredentials);
|
||||
importedAccount.SynchronizationDeltaIdentifier.Should().BeEmpty();
|
||||
importedAccount.CalendarSynchronizationDeltaIdentifier.Should().BeEmpty();
|
||||
|
||||
var importedAliases = await _accountService.GetAccountAliasesAsync(importedAccount.Id);
|
||||
importedAliases.Should().ContainSingle(a => a.IsRootAlias && a.IsPrimary && a.AliasAddress == "imap@example.com");
|
||||
|
||||
var serverInformation = await _accountService.GetAccountCustomServerInformationAsync(importedAccount.Id);
|
||||
serverInformation.Should().NotBeNull();
|
||||
serverInformation.IncomingServerPassword.Should().BeEmpty();
|
||||
serverInformation.OutgoingServerPassword.Should().BeEmpty();
|
||||
serverInformation.CalDavPassword.Should().BeEmpty();
|
||||
serverInformation.MaxConcurrentClients.Should().Be(9);
|
||||
serverInformation.CalDavServiceUrl.Should().Be("https://dav.example.com");
|
||||
}
|
||||
|
||||
private static AccountService CreateAccountService(InMemoryDatabaseService databaseService, IPreferencesService preferencesService)
|
||||
{
|
||||
var signatureService = new Mock<ISignatureService>();
|
||||
signatureService
|
||||
.Setup(a => a.CreateDefaultSignatureAsync(It.IsAny<Guid>()))
|
||||
.ReturnsAsync((Guid accountId) => new AccountSignature
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MailAccountId = accountId,
|
||||
Name = "Default",
|
||||
HtmlBody = string.Empty
|
||||
});
|
||||
|
||||
return new AccountService(
|
||||
databaseService,
|
||||
signatureService.Object,
|
||||
Mock.Of<IAuthenticationProvider>(),
|
||||
Mock.Of<IMimeFileService>(),
|
||||
preferencesService,
|
||||
Mock.Of<IContactPictureFileService>());
|
||||
}
|
||||
}
|
||||
@@ -232,11 +232,10 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
||||
.ReturnsAsync(WinoAccountApiResult<AuthResultDto>.Success(authResult));
|
||||
|
||||
_apiClient
|
||||
.Setup(x => x.SummarizeAsync("<p>Hello</p>", default))
|
||||
.Setup(x => x.SummarizeAsync("<p>Hello</p>", "en", default))
|
||||
.ReturnsAsync(ApiEnvelope<AiTextResultDto>.Success(
|
||||
new AiTextResultDto("<p>Summary</p>"),
|
||||
new QuotaInfoDto(
|
||||
true,
|
||||
"Active",
|
||||
DateTimeOffset.UtcNow.AddDays(-1),
|
||||
DateTimeOffset.UtcNow.AddDays(29),
|
||||
@@ -247,7 +246,7 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
||||
|
||||
await _service.LoginAsync("first@example.com", "pw");
|
||||
|
||||
var response = await _service.SummarizeAsync("<p>Hello</p>");
|
||||
var response = await _service.SummarizeAsync("<p>Hello</p>", "en");
|
||||
|
||||
response.IsSuccess.Should().BeTrue();
|
||||
response.Result?.Html.Should().Be("<p>Summary</p>");
|
||||
|
||||
Reference in New Issue
Block a user