using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Plugins.Emails.Controllers; using BTCPayServer.Plugins.Emails.Services; using BTCPayServer.Plugins.Emails.Views; using BTCPayServer.Services; using BTCPayServer.Tests.PMO; using BTCPayServer.Views.Server; using BTCPayServer.Views.Stores; using Microsoft.AspNetCore.Mvc; using Microsoft.Playwright; using MimeKit; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; namespace BTCPayServer.Tests; [Collection(nameof(NonParallelizableCollectionDefinition))] public class EmailsTests(ITestOutputHelper helper) : UnitTestBase(helper) { [Fact] [Trait("FastTest", "FastTest")] public void CanParseEmailDestination() { var vm = new StoreEmailRuleViewModel(); var actual = vm.AsArray("\"Nicolas, The, Great\" ,{SomeTemplate} ,\"Madd,Test\" "); string[] expected = ["\"Nicolas, The, Great\" ", "{SomeTemplate}", "\"Madd,Test\" "]; Assert.Equal(expected, actual); } [Fact(Timeout = TestUtils.LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task EmailSenderTests() { using var tester = CreateServerTester(newDb: true); await tester.StartAsync(); var acc = tester.NewAccount(); await acc.GrantAccessAsync(true); var settings = tester.PayTester.GetService(); var emailSenderFactory = tester.PayTester.GetService(); Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()); Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = false }); await settings.UpdateSetting(new EmailSettings() { From = "admin@admin.com", Login = "admin@admin.com", Password = "admin@admin.com", Port = 1234, Server = "admin.com", }); async Task AssertIsLogin(string storeId, string expectedLogin) where TEmailSender: IEmailSender { var sender = storeId is not null ? Assert.IsType(await emailSenderFactory.GetEmailSender(storeId)) : Assert.IsType(await emailSenderFactory.GetEmailSender()); var emailSettings = await sender.GetEmailSettings(); if (emailSettings is null) { Assert.Null(emailSettings); } else { Assert.NotNull(emailSettings); Assert.Equal(expectedLogin, emailSettings.Login); } } await AssertIsLogin(null, "admin@admin.com"); await AssertIsLogin(acc.StoreId, "admin@admin.com"); await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = true }); await AssertIsLogin(null, "admin@admin.com"); await AssertIsLogin(acc.StoreId, null); Assert.IsType(await acc.GetController().StoreEmailSettings(acc.StoreId, new(new() { From = "store@store.com", Login = "store@store.com", Password = "store@store.com", Port = tester.MailPitSettings.SmtpPort, Server = tester.MailPitSettings.Hostname }) { IsCustomSMTP = true }, "")); await AssertIsLogin(acc.StoreId, "store@store.com"); var message = await tester.AssertHasEmail(async () => { var sender = await emailSenderFactory.GetEmailSender(acc.StoreId); sender.SendEmail(MailboxAddress.Parse("destination@test.com"), "test", "hello world"); }); Assert.Equal("test", message.Subject); Assert.Equal("hello world", message.Text); // Configure at server level Assert.IsType(await acc.GetController().ServerEmailSettings(new(new() { From = "server@server.com", Login = "server@server.com", Password = "server@server.com", Port = tester.MailPitSettings.SmtpPort, Server = tester.MailPitSettings.Hostname }) { EnableStoresToUseServerEmailSettings = true }, "")); // The store should now use it Assert.IsType(await acc.GetController().StoreEmailSettings(acc.StoreId, new(new()) { IsCustomSMTP = false }, "")); await AssertIsLogin(acc.StoreId, "server@server.com"); } [Fact] [Trait("Integration", "Integration")] public async Task ServerEmailTests() { using var tester = CreateServerTester(); await tester.StartAsync(); var admin = tester.NewAccount(); await admin.GrantAccessAsync(true); var adminClient = await admin.CreateClient(Policies.Unrestricted); // validate that clear email settings will not throw an error await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData()); var data = new ServerEmailSettingsData { From = "admin@admin.com", Login = "admin@admin.com", Password = "admin@admin.com", Port = 1234, Server = "admin.com", EnableStoresToUseServerEmailSettings = false }; var actualUpdated = await adminClient.UpdateServerEmailSettings(data); var finalEmailSettings = await adminClient.GetServerEmailSettings(); // email password is masked and not returned from the server once set data.Password = null; data.PasswordSet = true; Assert.Equal(JsonConvert.SerializeObject(finalEmailSettings), JsonConvert.SerializeObject(data)); Assert.Equal(JsonConvert.SerializeObject(finalEmailSettings), JsonConvert.SerializeObject(actualUpdated)); // check that email validation works await AssertEx.AssertValidationError(new[] { nameof(EmailSettingsData.From) }, async () => await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData { From = "invalid" })); // NOTE: This email test fails silently in EmailSender.cs#31, can't test, but leaving for the future as reminder //await adminClient.SendEmail(admin.StoreId, // new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" }); // check that clear server email settings works await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData()); var clearedSettings = await adminClient.GetServerEmailSettings(); Assert.Equal(JsonConvert.SerializeObject(new ServerEmailSettingsData { PasswordSet = false }), JsonConvert.SerializeObject(clearedSettings)); } [Fact] [Trait("Integration", "Integration")] public async Task StoreEmailTests() { using var tester = CreateServerTester(); await tester.StartAsync(); var admin = tester.NewAccount(); await admin.GrantAccessAsync(true); var adminClient = await admin.CreateClient(Policies.Unrestricted); // validate that clear email settings will not throw an error await adminClient.UpdateStoreEmailSettings(admin.StoreId, new EmailSettingsData()); var data = new EmailSettingsData { From = "admin@admin.com", Login = "admin@admin.com", Password = "admin@admin.com", Port = 1234, Server = "admin.com", }; await adminClient.UpdateStoreEmailSettings(admin.StoreId, data); var s = await adminClient.GetStoreEmailSettings(admin.StoreId); // email password is masked and not returned from the server once set data.Password = null; data.PasswordSet = true; Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data)); await AssertEx.AssertValidationError(new[] { nameof(EmailSettingsData.From) }, async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId, new EmailSettingsData { From = "invalid" })); // send test email await adminClient.SendEmail(admin.StoreId, new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" }); // clear store email settings await adminClient.UpdateStoreEmailSettings(admin.StoreId, new EmailSettingsData()); var clearedSettings = await adminClient.GetStoreEmailSettings(admin.StoreId); Assert.Equal(JsonConvert.SerializeObject(new EmailSettingsData { PasswordSet = false }), JsonConvert.SerializeObject(clearedSettings)); } [Fact] [Trait("Playwright", "Playwright")] public async Task CanSetupEmailRules() { await using var s = CreatePlaywrightTester(newDb: true); await s.StartAsync(); await s.RegisterNewUser(true); var (storeName, _) = await s.CreateNewStore(); await s.GoToStore(StoreNavPages.Emails); await s.Page.ClickAsync("#ConfigureEmailRules"); Assert.Contains("There are no rules yet.", await s.Page.ContentAsync()); Assert.Contains("You need to configure email settings before this feature works", await s.Page.ContentAsync()); await s.Page.ClickAsync(".configure-email"); var mailPMO = new ConfigureEmailPMO(s); await mailPMO.FillMailPit(new() { From = "store@store.com", Login = "store@store.com", Password = "password" }); await s.GoToStore(StoreNavPages.Emails); await s.Page.ClickAsync("#ConfigureEmailRules"); var pmo = new EmailRulePMO(s); await s.Page.ClickAsync("#CreateEmailRule"); await pmo.Fill(new() { Trigger = "WH-InvoiceCreated", To = "invoicecreated@gmail.com", Subject = "Invoice Created in {Invoice.Currency}!", Body = "Invoice has been created in {Invoice.Currency} for {Invoice.Price}!", CustomerEmail = true }); await s.FindAlertMessage(); var page = await s.Page.ContentAsync(); Assert.DoesNotContain("There are no rules yet.", page); Assert.Contains("invoicecreated@gmail.com", page); Assert.Contains("Invoice Created in {Invoice.Currency}!", page); Assert.Contains("Yes", page); await s.Page.ClickAsync("#CreateEmailRule"); await pmo.Fill(new() { Trigger = "WH-PaymentRequestStatusChanged", To = "statuschanged@gmail.com", Subject = "Status changed!", Body = "Your Payment Request Status is Changed" }); await s.FindAlertMessage(); Assert.Contains("statuschanged@gmail.com", await s.Page.ContentAsync()); Assert.Contains("Status changed!", await s.Page.ContentAsync()); var editButtons = s.Page.GetByRole(AriaRole.Link, new() { Name = "Edit" }); Assert.True(await editButtons.CountAsync() >= 2); await editButtons.Nth(1).ClickAsync(); await pmo.Fill(new() { To = "changedagain@gmail.com" }); await s.FindAlertMessage(); Assert.Contains("changedagain@gmail.com", await s.Page.ContentAsync()); Assert.DoesNotContain("statuschanged@gmail.com", await s.Page.ContentAsync()); var rulesUrl = s.Page.Url; await s.AddDerivationScheme(); await s.GoToInvoices(); var message = await s.Server.AssertHasEmail(() => s.CreateInvoice(amount: 10m, currency: "USD")); Assert.Equal("Invoice has been created in USD for 10!", message.Text); await s.GoToUrl(rulesUrl); var deleteLinks = s.Page.GetByRole(AriaRole.Link, new() { Name = "Remove" }); Assert.Equal(2, await deleteLinks.CountAsync()); await deleteLinks.First.ClickAsync(); await s.ConfirmDeleteModal(); await s.FindAlertMessage(); deleteLinks = s.Page.GetByRole(AriaRole.Link, new() { Name = "Remove" }); Assert.Equal(1, await deleteLinks.CountAsync()); await deleteLinks.First.ClickAsync(); await s.ConfirmDeleteModal(); await s.FindAlertMessage(); Assert.Contains("There are no rules yet.", await s.Page.ContentAsync()); await s.Page.ClickAsync("#CreateEmailRule"); await pmo.Fill(new() { Trigger = "WH-InvoiceCreated", To = "invoicecreated@gmail.com", Subject = "Invoice Created in {Invoice.Currency} for {Store.Name}!", Body = "Invoice has been created in {Invoice.Currency} for {Invoice.Price}!", CustomerEmail = true, Condition = "$ ?(@.Invoice.Metadata.buyerEmail == \"john@test.com\")" }); await s.GoToInvoices(); message = await s.Server.AssertHasEmail(() => s.CreateInvoice(amount: 10m, currency: "USD", refundEmail: "john@test.com")); Assert.Equal("Invoice Created in USD for " + storeName + "!", message.Subject); Assert.Equal("Invoice has been created in USD for 10!", message.Text); Assert.Equal("john@test.com", message.To[0].Address); await s.GoToServer(ServerNavPages.Emails); await mailPMO.FillMailPit(); var rules = await mailPMO.ConfigureEmailRules(); await rules.EditRule("SRV-PasswordReset"); await pmo.Fill(new() { Trigger = "SRV-PasswordReset", HtmlBody = true, Body = "

Hello, click here to reset the password

" }); await s.Logout(); await s.Page.GetByRole(AriaRole.Link, new() { Name = "Forgot password?" }).ClickAsync(); await s.Page.FillAsync("#Email", s.CreatedUser); message = await s.Server.AssertHasEmail(() => s.ClickPagePrimary()); Assert.Contains("

Hello, "); await s.ClickPagePrimary(); await s.FindAlertMessage(partialText: "Email settings saved"); Assert.Contains("Configured", await s.Page.ContentAsync()); await s.Page.Locator("#Settings_Login").ClearAsync(); await s.Page.FillAsync("#Settings_Login", "test_fix@gmail.com"); await s.ClickPagePrimary(); await s.FindAlertMessage(partialText: "Email settings saved"); Assert.Contains("Configured", await s.Page.ContentAsync()); Assert.Contains("test_fix", await s.Page.ContentAsync()); await s.Page.Locator("#ResetPassword").PressAsync("Enter"); await s.FindAlertMessage(partialText: "Email server password reset"); Assert.DoesNotContain("Configured", await s.Page.ContentAsync()); Assert.Contains("test_fix", await s.Page.ContentAsync()); } }