mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
* refactor: resovled merge conflict Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * (Refactor): Removed Selenium Test for CanUseRoleManager Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * Refactor : removed spacing and extra alert message Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * (Test):Converted/Added Playwright Test for CanSigninWithLoginCode Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * (Refactor): Removed Selenium Test for CanSigninWithLoginCode Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * fix: updated UIServerController.Roles.cs to handle storeID Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * refactor : updated some minor nits Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * Fix: Preserve store context when deleting server-wide roles from store page Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> * refactor: fix auth mismatch in role Edit/Remove links for store context Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> --------- Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
2812 lines
135 KiB
C#
2812 lines
135 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using BTCPayServer.Abstractions.Models;
|
|
using BTCPayServer.Client;
|
|
using BTCPayServer.Client.Models;
|
|
using BTCPayServer.Data;
|
|
using BTCPayServer.Events;
|
|
using BTCPayServer.Lightning;
|
|
using BTCPayServer.Payments;
|
|
using BTCPayServer.Services;
|
|
using BTCPayServer.Services.Invoices;
|
|
using BTCPayServer.Services.Rates;
|
|
using BTCPayServer.Services.Wallets;
|
|
using BTCPayServer.Tests.PMO;
|
|
using BTCPayServer.Views.Manage;
|
|
using BTCPayServer.Views.Server;
|
|
using BTCPayServer.Views.Stores;
|
|
using BTCPayServer.Views.Wallets;
|
|
using Dapper;
|
|
using ExchangeSharp;
|
|
using LNURL;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Playwright;
|
|
using static Microsoft.Playwright.Assertions;
|
|
using NBitcoin;
|
|
using NBitcoin.DataEncoders;
|
|
using NBitcoin.Payment;
|
|
using NBXplorer;
|
|
using NBXplorer.Models;
|
|
using Newtonsoft.Json.Linq;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace BTCPayServer.Tests
|
|
{
|
|
[Trait("Playwright", "Playwright")]
|
|
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
|
public class PlaywrightTests(ITestOutputHelper helper) : UnitTestBase(helper)
|
|
{
|
|
private const int TestTimeout = TestUtils.TestTimeout;
|
|
[Fact]
|
|
public async Task CanNavigateServerSettings()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToServer();
|
|
await s.Page.AssertNoError();
|
|
await s.ClickOnAllSectionLinks("#mainNavSettings");
|
|
await s.GoToServer(ServerNavPages.Services);
|
|
s.TestLogs.LogInformation("Let's check if we can access the logs");
|
|
await s.Page.GetByRole(AriaRole.Link, new() { Name = "Logs" }).ClickAsync();
|
|
await s.Page.Locator("a:has-text('.log')").First.ClickAsync();
|
|
Assert.Contains("Starting listening NBXplorer", await s.Page.ContentAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseForms()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.InitializeBTCPayServer();
|
|
// Point Of Sale
|
|
var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}";
|
|
await s.CreateApp("PointOfSale", appName);
|
|
await s.Page.SelectOptionAsync("#FormId", "Email");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "App updated");
|
|
var opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("#ViewApp");
|
|
string invoiceId;
|
|
await using (_ = await s.SwitchPage(opening))
|
|
{
|
|
await s.Page.Locator("button[type='submit']").First.ClickAsync();
|
|
await s.Page.FillAsync("[name='buyerEmail']", "aa@aa.com");
|
|
await s.Page.ClickAsync("input[type='submit']");
|
|
await s.PayInvoice(true);
|
|
invoiceId = s.Page.Url[(s.Page.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
|
|
}
|
|
|
|
await s.Page.Context.Pages.First().BringToFrontAsync();
|
|
await s.GoToUrl($"/invoices/{invoiceId}/");
|
|
Assert.Contains("aa@aa.com", await s.Page.ContentAsync());
|
|
// Payment Request
|
|
await s.Page.ClickAsync("#menu-item-PaymentRequests");
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("#Title", "Pay123");
|
|
await s.Page.FillAsync("#Amount", "700");
|
|
await s.Page.SelectOptionAsync("#FormId", "Email");
|
|
await s.ClickPagePrimary();
|
|
await s.Page.Locator("a[id^='Edit-']").First.ClickAsync();
|
|
var editUrl = new Uri(s.Page.Url);
|
|
opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("#ViewPaymentRequest");
|
|
var popOutPage = await opening;
|
|
await popOutPage.ClickAsync("[data-test='form-button']");
|
|
Assert.Contains("Enter your email", await popOutPage.ContentAsync());
|
|
await popOutPage.FillAsync("input[name='buyerEmail']", "aa@aa.com");
|
|
await popOutPage.ClickAsync("#page-primary");
|
|
invoiceId = popOutPage.Url.Split('/').Last();
|
|
await popOutPage.CloseAsync();
|
|
await s.Page.Context.Pages.First().BringToFrontAsync();
|
|
await s.GoToUrl(editUrl.PathAndQuery);
|
|
|
|
await Expect(s.Page.Locator("#Email")).ToHaveValueAsync("aa@aa.com");
|
|
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
|
Assert.Equal("aa@aa.com", invoice.Metadata.BuyerEmail);
|
|
|
|
//Custom Forms
|
|
await s.GoToStore();
|
|
await s.GoToStore(StoreNavPages.Forms);
|
|
Assert.Contains("There are no forms yet.", await s.Page.ContentAsync());
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("[name='Name']", "Custom Form 1");
|
|
await s.Page.ClickAsync("#ApplyEmailTemplate");
|
|
await s.Page.ClickAsync("#CodeTabButton");
|
|
await s.Page.Locator("#CodeTabPane").WaitForAsync();
|
|
var config = await s.Page.Locator("[name='FormConfig']").InputValueAsync();
|
|
Assert.Contains("buyerEmail", config);
|
|
await s.Page.Locator("[name='FormConfig']").ClearAsync();
|
|
await s.Page.FillAsync("[name='FormConfig']", config.Replace("Enter your email", "CustomFormInputTest"));
|
|
await s.ClickPagePrimary();
|
|
await s.Page.ClickAsync("#ViewForm");
|
|
var formUrl = s.Page.Url;
|
|
Assert.Contains("CustomFormInputTest", await s.Page.ContentAsync());
|
|
await s.Page.FillAsync("[name='buyerEmail']", "aa@aa.com");
|
|
await s.Page.ClickAsync("input[type='submit']");
|
|
await s.PayInvoice(true, 0.001m);
|
|
var result = await s.Server.PayTester.HttpClient.GetAsync(formUrl);
|
|
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
|
|
await s.GoToHome();
|
|
await s.GoToStore();
|
|
await s.GoToStore(StoreNavPages.Forms);
|
|
await s.Page.WaitForLoadStateAsync();
|
|
Assert.Contains("Custom Form 1", await s.Page.ContentAsync());
|
|
await s.Page.GetByRole(AriaRole.Link, new() { Name = "Remove" }).ClickAsync();
|
|
await s.ConfirmDeleteModal();
|
|
await s.Page.WaitForLoadStateAsync();
|
|
Assert.DoesNotContain("Custom Form 1", await s.Page.ContentAsync());
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("[name='Name']", "Custom Form 2");
|
|
await s.Page.ClickAsync("#ApplyEmailTemplate");
|
|
await s.Page.ClickAsync("#CodeTabButton");
|
|
await s.Page.Locator("#CodeTabPane").WaitForAsync();
|
|
await s.Page.Locator("input[type='checkbox'][name='Public']").SetCheckedAsync(true);
|
|
await s.Page.Locator("[name='FormConfig']").ClearAsync();
|
|
await s.Page.FillAsync("[name='FormConfig']", config.Replace("Enter your email", "CustomFormInputTest2"));
|
|
await s.ClickPagePrimary();
|
|
await s.Page.ClickAsync("#ViewForm");
|
|
formUrl = s.Page.Url;
|
|
result = await s.Server.PayTester.HttpClient.GetAsync(formUrl);
|
|
Assert.NotEqual(HttpStatusCode.NotFound, result.StatusCode);
|
|
await s.GoToHome();
|
|
await s.GoToStore();
|
|
await s.GoToStore(StoreNavPages.Forms);
|
|
Assert.Contains("Custom Form 2", await s.Page.ContentAsync());
|
|
await s.Page.GetByRole(AriaRole.Link, new() { Name = "Custom Form 2" }).ClickAsync();
|
|
await s.Page.Locator("[name='Name']").ClearAsync();
|
|
await s.Page.FillAsync("[name='Name']", "Custom Form 3");
|
|
await s.ClickPagePrimary();
|
|
await s.GoToStore(StoreNavPages.Forms);
|
|
Assert.Contains("Custom Form 3", await s.Page.ContentAsync());
|
|
await s.Page.ClickAsync("#menu-item-PaymentRequests");
|
|
await s.ClickPagePrimary();
|
|
var selectOptions = await s.Page.Locator("#FormId >> option").CountAsync();
|
|
Assert.Equal(4, selectOptions);
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public async Task CanChangeUserMail()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
var tester = s.Server;
|
|
var u1 = tester.NewAccount();
|
|
await u1.GrantAccessAsync();
|
|
await u1.MakeAdmin(false);
|
|
var u2 = tester.NewAccount();
|
|
await u2.GrantAccessAsync();
|
|
await u2.MakeAdmin(false);
|
|
await s.GoToLogin();
|
|
await s.LogIn(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
|
|
await s.GoToProfile();
|
|
await s.Page.Locator("#Email").ClearAsync();
|
|
await s.Page.FillAsync("#Email", u2.RegisterDetails.Email);
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error, partialText: "The email address is already in use with an other account.");
|
|
await s.GoToProfile();
|
|
await s.Page.Locator("#Email").ClearAsync();
|
|
var changedEmail = Guid.NewGuid() + "@lol.com";
|
|
await s.Page.FillAsync("#Email", changedEmail);
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage();
|
|
using var scope = tester.PayTester.ServiceProvider.CreateScope();
|
|
var manager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
|
Assert.NotNull(await manager.FindByNameAsync(changedEmail));
|
|
Assert.NotNull(await manager.FindByEmailAsync(changedEmail));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanManageUsers()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser();
|
|
var user = s.AsTestAccount();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
|
|
|
|
// Manage user password reset
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
|
await s.Page.Locator("#SearchTerm").PressAsync("Enter");
|
|
var rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(user.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .reset-password");
|
|
await s.Page.FillAsync("#Password", "Password@1!");
|
|
await s.Page.FillAsync("#ConfirmPassword", "Password@1!");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "Password successfully set");
|
|
|
|
var userPage = await s.Browser.NewPageAsync();
|
|
await using (await s.SwitchPage(userPage, false))
|
|
{
|
|
await s.GoToLogin();
|
|
await s.LogIn(user.Email, user.Password);
|
|
}
|
|
// Manage user status (disable and enable)
|
|
// Disable user
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
|
await s.Page.Locator("#SearchTerm").PressAsync("Enter");
|
|
rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(user.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .disable-user");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.FindAlertMessage(partialText: "User disabled");
|
|
|
|
await using (await s.SwitchPage(userPage, false))
|
|
{
|
|
await s.Page.ReloadAsync();
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning, partialText: "Your user account is currently disabled");
|
|
}
|
|
|
|
//Enable user
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
|
await s.Page.Locator("#SearchTerm").PressAsync("Enter");
|
|
rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(user.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .enable-user");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.FindAlertMessage(partialText: "User enabled");
|
|
|
|
await using (await s.SwitchPage(userPage))
|
|
{
|
|
// Can log again
|
|
await s.LogIn(user.Email, "Password@1!");
|
|
await s.CreateNewStore();
|
|
await s.Logout();
|
|
}
|
|
|
|
// Manage user details (edit)
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
|
await s.Page.Locator("#SearchTerm").PressAsync("Enter");
|
|
rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(user.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
|
await s.Page.FillAsync("#Name", "Test User");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "User successfully updated");
|
|
|
|
// Manage user deletion
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
|
await s.Page.Locator("#SearchTerm").PressAsync("Enter");
|
|
rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(user.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .delete-user");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.FindAlertMessage(partialText: "User deleted");
|
|
await s.Page.AssertNoError();
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public async Task CanUseSSHService()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
var settings = s.Server.PayTester.GetService<SettingsRepository>();
|
|
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
|
policies.DisableSSHService = false;
|
|
await settings.UpdateSetting(policies);
|
|
await s.RegisterNewUser(isAdmin: true);
|
|
await s.GoToUrl("/server/services");
|
|
Assert.Contains("server/services/ssh", await s.Page.ContentAsync());
|
|
using (var client = await s.Server.PayTester.GetService<Configuration.BTCPayServerOptions>().SSHSettings
|
|
.ConnectAsync())
|
|
{
|
|
var result = await client.RunBash("echo hello");
|
|
Assert.Equal(string.Empty, result.Error);
|
|
Assert.Equal("hello\n", result.Output);
|
|
Assert.Equal(0, result.ExitStatus);
|
|
}
|
|
|
|
await s.GoToUrl("/server/services/ssh");
|
|
await s.Page.AssertNoError();
|
|
await s.Page.Locator("#SSHKeyFileContent").ClearAsync();
|
|
await s.Page.FillAsync("#SSHKeyFileContent", "tes't\r\ntest2");
|
|
await s.Page.ClickAsync("#submit");
|
|
await s.Page.AssertNoError();
|
|
|
|
var text = await s.Page.Locator("#SSHKeyFileContent").TextContentAsync();
|
|
// Browser replace \n to \r\n, so it is hard to compare exactly what we want
|
|
Assert.Contains("tes't", text);
|
|
Assert.Contains("test2", text);
|
|
Assert.True((await s.Page.ContentAsync()).Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase));
|
|
|
|
await s.Page.Locator("#SSHKeyFileContent").ClearAsync();
|
|
await s.Page.ClickAsync("#submit");
|
|
|
|
text = await s.Page.Locator("#SSHKeyFileContent").TextContentAsync();
|
|
Assert.DoesNotContain("test2", text);
|
|
|
|
// Let's try to disable it now
|
|
await s.Page.ClickAsync("#disable");
|
|
await s.Page.FillAsync("#ConfirmInput", "DISABLE");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.GoToUrl("/server/services/ssh");
|
|
Assert.True((await s.Page.ContentAsync()).Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
|
|
|
|
policies = await settings.GetSettingAsync<PoliciesSettings>();
|
|
Assert.NotNull(policies);
|
|
Assert.True(policies.DisableSSHService);
|
|
|
|
policies.DisableSSHService = false;
|
|
await settings.UpdateSetting(policies);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NewUserLogin()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
//Register & Log Out
|
|
var email = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.Page.AssertNoError();
|
|
Assert.Contains("/login", s.Page.Url);
|
|
|
|
await s.GoToUrl("/account");
|
|
Assert.Contains("ReturnUrl=%2Faccount", s.Page.Url);
|
|
|
|
// We should be redirected to login
|
|
//Same User Can Log Back In
|
|
await s.Page.FillAsync("#Email", email);
|
|
await s.Page.FillAsync("#Password", "123456");
|
|
await s.Page.ClickAsync("#LoginButton");
|
|
|
|
// We should be redirected to invoice
|
|
Assert.EndsWith("/account", s.Page.Url);
|
|
|
|
// Should not be able to reach server settings
|
|
await s.GoToUrl("/server/users");
|
|
Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Page.Url);
|
|
await s.GoToHome();
|
|
await s.GoToHome();
|
|
|
|
//Change Password & Log Out
|
|
var newPassword = "abc???";
|
|
await s.GoToProfile(ManageNavPages.ChangePassword);
|
|
await s.Page.FillAsync("#OldPassword", "123456");
|
|
await s.Page.FillAsync("#NewPassword", newPassword);
|
|
await s.Page.FillAsync("#ConfirmPassword", newPassword);
|
|
await s.ClickPagePrimary();
|
|
await s.Logout();
|
|
await s.Page.AssertNoError();
|
|
|
|
//Log In With New Password
|
|
await s.Page.FillAsync("#Email", email);
|
|
await s.Page.FillAsync("#Password", newPassword);
|
|
await s.Page.ClickAsync("#LoginButton");
|
|
|
|
await s.GoToHome();
|
|
await s.GoToProfile();
|
|
await s.ClickOnAllSectionLinks("#mainNavSettings");
|
|
|
|
//let's test invite link
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
await s.ClickPagePrimary();
|
|
|
|
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
|
await s.Page.FillAsync("#Email", usr);
|
|
await s.ClickPagePrimary();
|
|
var url = await s.Page.Locator("#InvitationUrl").GetAttributeAsync("data-text");
|
|
Assert.NotNull(url);
|
|
await s.Logout();
|
|
await s.GoToUrl(new Uri(url).AbsolutePath);
|
|
Assert.Equal("hidden", await s.Page.Locator("#Email").GetAttributeAsync("type"));
|
|
Assert.Equal(usr, await s.Page.Locator("#Email").GetAttributeAsync("value"));
|
|
Assert.Equal("Create Account", await s.Page.Locator("h4").TextContentAsync());
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info, partialText: "Invitation accepted. Please set your password.");
|
|
|
|
await s.Page.FillAsync("#Password", "123456");
|
|
await s.Page.FillAsync("#ConfirmPassword", "123456");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "Account successfully created.");
|
|
|
|
// We should be logged in now
|
|
await s.GoToHome();
|
|
await s.Page.Locator("#mainNav").WaitForAsync();
|
|
|
|
//let's test delete user quickly while we're at it
|
|
await s.GoToProfile();
|
|
await s.Page.ClickAsync("#delete-user");
|
|
await s.ConfirmDeleteModal();
|
|
Assert.Contains("/login", s.Page.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseStoreTemplate()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore(preferredExchange: "Kraken");
|
|
var client = await s.AsTestAccount().CreateClient();
|
|
await client.UpdateStore(s.StoreId, new UpdateStoreRequest()
|
|
{
|
|
Name = "Can Use Store?",
|
|
Website = "https://test.com/",
|
|
CelebratePayment = false,
|
|
DefaultLang = "fr-FR",
|
|
NetworkFeeMode = NetworkFeeMode.MultiplePaymentsOnly,
|
|
ShowStoreHeader = false
|
|
});
|
|
await s.GoToServer();
|
|
await s.Page.ClickAsync("#SetTemplate");
|
|
await s.FindAlertMessage();
|
|
|
|
var newStore = await client.CreateStore(new ());
|
|
Assert.Equal("Can Use Store?", newStore.Name);
|
|
Assert.Equal("https://test.com/", newStore.Website);
|
|
Assert.False(newStore.CelebratePayment);
|
|
Assert.Equal("fr-FR", newStore.DefaultLang);
|
|
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
|
Assert.False(newStore.ShowStoreHeader);
|
|
|
|
newStore = await client.CreateStore(new (){ Name = "Yes you can also customize"});
|
|
Assert.Equal("Yes you can also customize", newStore.Name);
|
|
Assert.Equal("https://test.com/", newStore.Website);
|
|
Assert.False(newStore.CelebratePayment);
|
|
Assert.Equal("fr-FR", newStore.DefaultLang);
|
|
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
|
Assert.False(newStore.ShowStoreHeader);
|
|
|
|
await s.GoToUrl("/stores/create");
|
|
Assert.Equal("Can Use Store?" ,await s.Page.InputValueAsync("#Name"));
|
|
await s.Page.FillAsync("#Name", "Just changed it!");
|
|
await s.Page.ClickAsync("#Create");
|
|
await s.GoToStore();
|
|
var newStoreId = await s.Page.InputValueAsync("#Id");
|
|
Assert.NotEqual(newStoreId, s.StoreId);
|
|
|
|
newStore = await client.GetStore(newStoreId);
|
|
Assert.Equal("Just changed it!", newStore.Name);
|
|
Assert.Equal("https://test.com/", newStore.Website);
|
|
Assert.False(newStore.CelebratePayment);
|
|
Assert.Equal("fr-FR", newStore.DefaultLang);
|
|
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
|
Assert.False(newStore.ShowStoreHeader);
|
|
|
|
await s.GoToServer();
|
|
await s.Page.ClickAsync("#ResetTemplate");
|
|
await s.FindAlertMessage(partialText: "Store template successfully unset");
|
|
|
|
await s.GoToUrl("/stores/create");
|
|
Assert.Equal("" ,await s.Page.InputValueAsync("#Name"));
|
|
|
|
newStore = await client.CreateStore(new (){ Name = "Test"});
|
|
Assert.Equal(TimeSpan.FromDays(30), newStore.RefundBOLT11Expiration);
|
|
Assert.Equal(TimeSpan.FromDays(1), newStore.MonitoringExpiration);
|
|
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
|
|
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
|
|
|
|
// What happens if the default template doesn't have all the fields?
|
|
var settings = s.Server.PayTester.GetService<SettingsRepository>();
|
|
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new();
|
|
policies.DefaultStoreTemplate = new JObject()
|
|
{
|
|
["blob"] = new JObject()
|
|
{
|
|
["defaultCurrency"] = "AAA",
|
|
["defaultLang"] = "de-DE"
|
|
}
|
|
};
|
|
await settings.UpdateSetting(policies);
|
|
newStore = await client.CreateStore(new() { Name = "Test2"});
|
|
Assert.Equal("AAA", newStore.DefaultCurrency);
|
|
Assert.Equal("de-DE", newStore.DefaultLang);
|
|
Assert.Equal(TimeSpan.FromDays(30), newStore.RefundBOLT11Expiration);
|
|
Assert.Equal(TimeSpan.FromDays(1), newStore.MonitoringExpiration);
|
|
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
|
|
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Altcoins", "Altcoins")]
|
|
public async Task CanExposeRates()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
s.Server.ActivateLTC();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
|
|
var btcDerivationScheme = new ExtKey().Neuter().GetWif(Network.RegTest).ToString() + "-[legacy]";
|
|
await s.AddDerivationScheme("BTC", btcDerivationScheme);
|
|
await s.AddDerivationScheme("LTC", new ExtKey().Neuter().GetWif(NBitcoin.Altcoins.Litecoin.Instance.Regtest).ToString() + "-[legacy]");
|
|
|
|
await s.GoToStore();
|
|
await s.Page.FillAsync("[name='DefaultCurrency']", "USD");
|
|
await s.Page.FillAsync("[name='AdditionalTrackedRates']", "CAD,JPY,EUR");
|
|
await s.ClickPagePrimary();
|
|
|
|
await s.GoToStore(StoreNavPages.Rates);
|
|
await s.Page.ClickAsync($"#PrimarySource_ShowScripting_submit");
|
|
await s.FindAlertMessage();
|
|
|
|
// BTC can solves USD,EUR,CAD
|
|
// LTC can solves and JPY and USD
|
|
await s.Page.FillAsync("[name='PrimarySource.Script']",
|
|
"""
|
|
BTC_JPY = bitflyer(BTC_JPY);
|
|
|
|
BTC_USD = coingecko(BTC_USD);
|
|
BTC_EUR = coingecko(BTC_EUR);
|
|
BTC_CAD = coingecko(BTC_CAD);
|
|
LTC_BTC = coingecko(LTC_BTC);
|
|
|
|
LTC_USD = coingecko(LTC_USD);
|
|
LTC_JPY = LTC_BTC * BTC_JPY;
|
|
""");
|
|
await s.ClickPagePrimary();
|
|
var expectedSolvablePairs = new[]
|
|
{
|
|
(Crypto: "BTC", Currency: "JPY"),
|
|
(Crypto: "BTC", Currency: "USD"),
|
|
(Crypto: "BTC", Currency: "CAD"),
|
|
(Crypto: "BTC", Currency: "EUR"),
|
|
(Crypto: "LTC", Currency: "JPY"),
|
|
(Crypto: "LTC", Currency: "USD"),
|
|
};
|
|
var expectedUnsolvablePairs = new[]
|
|
{
|
|
(Crypto: "LTC", Currency: "CAD"),
|
|
(Crypto: "LTC", Currency: "EUR"),
|
|
};
|
|
|
|
Dictionary<string, uint256> txIds = new();
|
|
foreach (var cryptoCode in new[] { "BTC", "LTC" })
|
|
{
|
|
await s.Server.GetExplorerNode(cryptoCode).EnsureGenerateAsync(1);
|
|
await s.GoToWallet(new(s.StoreId, cryptoCode), WalletsNavPages.Receive);
|
|
var address = await s.Page.GetAttributeAsync("#Address", "data-text");
|
|
var network = s.Server.GetNetwork(cryptoCode);
|
|
|
|
var txId = uint256.Zero;
|
|
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
|
{
|
|
txId = await s.Server.GetExplorerNode(cryptoCode)
|
|
.SendToAddressAsync(BitcoinAddress.Create(address!, network.NBitcoinNetwork), Money.Coins(1));
|
|
});
|
|
txIds.Add(cryptoCode, txId);
|
|
// The rates are fetched asynchronously... let's wait it's done.
|
|
await Task.Delay(500);
|
|
var pmo = await s.GoToWalletTransactions(new(s.StoreId, cryptoCode));
|
|
await pmo.WaitTransactionsLoaded();
|
|
if (cryptoCode == "BTC")
|
|
{
|
|
await pmo.AssertRowContains(txId, "4,500.00 CAD");
|
|
await pmo.AssertRowContains(txId, "700,000 JPY");
|
|
await pmo.AssertRowContains(txId, "4 000,00 EUR");
|
|
await pmo.AssertRowContains(txId, "5,000.00 USD");
|
|
}
|
|
else if (cryptoCode == "LTC")
|
|
{
|
|
await pmo.AssertRowContains(txId, "4,321 JPY");
|
|
await pmo.AssertRowContains(txId, "500.00 USD");
|
|
}
|
|
}
|
|
|
|
var fee = Money.Zero;
|
|
var feeRate = FeeRate.Zero;
|
|
// Quick check on some internal of wallet that isn't related to this test
|
|
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet("BTC");
|
|
var derivation = s.Server.GetNetwork("BTC").NBXplorerNetwork.DerivationStrategyFactory.Parse(btcDerivationScheme);
|
|
foreach (var forceHasFeeInfo in new bool?[]{ true, false, null})
|
|
foreach (var inefficient in new[] { true, false })
|
|
{
|
|
wallet.ForceInefficientPath = inefficient;
|
|
wallet.ForceHasFeeInformation = forceHasFeeInfo;
|
|
wallet.InvalidateCache(derivation);
|
|
var fetched = await wallet.FetchTransactionHistory(derivation);
|
|
var tx = fetched.First(f => f.TransactionId == txIds["BTC"]);
|
|
if (forceHasFeeInfo is true or null || inefficient)
|
|
{
|
|
Assert.NotNull(tx.Fee);
|
|
Assert.NotNull(tx.FeeRate);
|
|
fee = tx.Fee;
|
|
feeRate = tx.FeeRate;
|
|
}
|
|
else
|
|
{
|
|
Assert.Null(tx.Fee);
|
|
Assert.Null(tx.FeeRate);
|
|
}
|
|
}
|
|
wallet.InvalidateCache(derivation);
|
|
wallet.ForceHasFeeInformation = null;
|
|
wallet.ForceInefficientPath = false;
|
|
|
|
var pmo3 = await s.GoToWalletTransactions(new(s.StoreId, "BTC"));
|
|
await pmo3.AssertRowContains(txIds["BTC"], $"{fee} ({feeRate})");
|
|
|
|
await s.ClickViewReport();
|
|
var csvTxt = await s.DownloadReportCSV();
|
|
var csvTester = new CSVWalletsTester(csvTxt);
|
|
|
|
foreach (var cryptoCode in new[] { "BTC", "LTC" })
|
|
{
|
|
if (cryptoCode == "BTC")
|
|
{
|
|
csvTester
|
|
.ForTxId(txIds[cryptoCode].ToString())
|
|
.AssertValues(
|
|
("FeeRate", feeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)),
|
|
("Fee", fee.ToString()),
|
|
("Rate (USD)", "5000"),
|
|
("Rate (CAD)", "4500"),
|
|
("Rate (JPY)", "700000"),
|
|
("Rate (EUR)", "4000")
|
|
);
|
|
}
|
|
else
|
|
{
|
|
csvTester
|
|
.ForTxId(txIds[cryptoCode].ToString())
|
|
.AssertValues(
|
|
("Rate (USD)", "500"),
|
|
("Rate (CAD)", ""),
|
|
("Rate (JPY)", "4320.9876543209875"),
|
|
("Rate (EUR)", "")
|
|
);
|
|
}
|
|
}
|
|
|
|
// This shouldn't crash if NBX doesn't support fee fetching
|
|
wallet.ForceHasFeeInformation = false;
|
|
await s.Page.ReloadAsync();
|
|
csvTxt = await s.DownloadReportCSV();
|
|
csvTester = new CSVWalletsTester(csvTxt);
|
|
csvTester
|
|
.ForTxId(txIds["BTC"].ToString())
|
|
.AssertValues(
|
|
("FeeRate", ""),
|
|
("Fee", ""),
|
|
("Rate (USD)", "5000"),
|
|
("Rate (CAD)", "4500"),
|
|
("Rate (JPY)", "700000"),
|
|
("Rate (EUR)", "4000")
|
|
);
|
|
wallet.ForceHasFeeInformation = null;
|
|
|
|
var invId = await s.CreateInvoice(storeId: s.StoreId, amount: 10_000);
|
|
await s.GoToInvoiceCheckout(invId);
|
|
await s.PayInvoice();
|
|
await s.GoToInvoices(s.StoreId);
|
|
await s.ClickViewReport();
|
|
|
|
await s.Page.ReloadAsync();
|
|
csvTxt = await s.DownloadReportCSV();
|
|
var csvInvTester = new CSVInvoicesTester(csvTxt);
|
|
csvInvTester
|
|
.ForInvoice(invId)
|
|
.AssertValues(
|
|
("Rate (BTC_CAD)", "4500"),
|
|
("Rate (BTC_JPY)", "700000"),
|
|
("Rate (BTC_EUR)", "4000"),
|
|
("Rate (BTC_USD)", "5000"),
|
|
("Rate (LTC_USD)", "500"),
|
|
("Rate (LTC_JPY)", "4320.9876543209875"),
|
|
("Rate (LTC_CAD)", ""),
|
|
("Rate (LTC_EUR)", "")
|
|
);
|
|
|
|
var txId2 = new uint256(csvInvTester.GetPaymentId().Split("-")[0]);
|
|
var pmo2 = await s.GoToWalletTransactions(new(s.StoreId, "BTC"));
|
|
await pmo2.WaitTransactionsLoaded();
|
|
await pmo2.AssertRowContains(txId2, "5,000.00 USD");
|
|
|
|
// When removing the wallet rates, we should still have the rates from the invoice
|
|
var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
|
Assert.Equal(1, await ctx.Database
|
|
.GetDbConnection()
|
|
.ExecuteAsync("""
|
|
UPDATE "WalletObjects" SET "Data"='{}'::JSONB WHERE "Id"=@txId
|
|
""", new{ txId = txId2.ToString() }));
|
|
|
|
pmo2 = await s.GoToWalletTransactions(new(s.StoreId, "BTC"));
|
|
await pmo2.WaitTransactionsLoaded();
|
|
await pmo2.AssertRowContains(txId2, "5,000.00 USD");
|
|
}
|
|
|
|
class CSVWalletsTester(string text) : CSVTester(text)
|
|
{
|
|
string txId = "";
|
|
|
|
public CSVWalletsTester ForTxId(string txId)
|
|
{
|
|
this.txId = txId;
|
|
return this;
|
|
}
|
|
|
|
public CSVWalletsTester AssertValues(params (string, string)[] values)
|
|
{
|
|
var line = _lines
|
|
.First(l => l[_indexes["TransactionId"]] == txId);
|
|
foreach (var (key, value) in values)
|
|
{
|
|
Assert.Equal(value, line[_indexes[key]]);
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public async Task CanManageWallet()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
var (_, storeId) = await s.CreateNewStore();
|
|
const string cryptoCode = "BTC";
|
|
|
|
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0',
|
|
// then try to use the seed to sign the transaction
|
|
await s.GenerateWallet(cryptoCode, "", true);
|
|
|
|
//let's test quickly the wallet send page
|
|
await s.GoToWallet(navPages: WalletsNavPages.Send);
|
|
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
|
|
Assert.DoesNotContain("nbx-seed", await s.Page.ContentAsync());
|
|
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
|
|
await s.Page.ClickAsync("#SignTransaction");
|
|
await s.Page.WaitForSelectorAsync("text=Destination Address field is required");
|
|
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
|
|
await s.Page.ClickAsync("#CancelWizard");
|
|
await s.GoToWallet(navPages: WalletsNavPages.Receive);
|
|
|
|
//generate a receiving address
|
|
await s.Page.WaitForSelectorAsync("#address-tab .qr-container");
|
|
Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync());
|
|
// no previous page in the wizard, hence no back button
|
|
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
|
|
var receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
|
|
|
|
// Can add a label?
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
await s.Page.ClickAsync("div.label-manager input");
|
|
await Task.Delay(500);
|
|
await s.Page.FillAsync("div.label-manager input", "test-label");
|
|
await s.Page.Keyboard.PressAsync("Enter");
|
|
await Task.Delay(500);
|
|
await s.Page.FillAsync("div.label-manager input", "label2");
|
|
await s.Page.Keyboard.PressAsync("Enter");
|
|
await Task.Delay(500);
|
|
});
|
|
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
await s.Page.ReloadAsync();
|
|
await s.Page.WaitForSelectorAsync("[data-value='test-label']");
|
|
});
|
|
|
|
Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync());
|
|
Assert.Equal(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text"));
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var content = await s.Page.ContentAsync();
|
|
Assert.Contains("test-label", content);
|
|
});
|
|
|
|
// Remove a label
|
|
await s.Page.WaitForSelectorAsync("[data-value='test-label']");
|
|
await s.Page.ClickAsync("[data-value='test-label']");
|
|
await Task.Delay(500);
|
|
await s.Page.EvaluateAsync(@"() => {
|
|
const l = document.querySelector('[data-value=""test-label""]');
|
|
l.click();
|
|
l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8}));
|
|
}");
|
|
await Task.Delay(500);
|
|
await s.Page.ReloadAsync();
|
|
Assert.DoesNotContain("test-label", await s.Page.ContentAsync());
|
|
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
|
|
|
|
//send money to addr and ensure it changed
|
|
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
|
|
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr!, Network.RegTest),
|
|
Money.Parse("0.1"));
|
|
await sess.WaitNext<NewTransactionEvent>(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr);
|
|
await Task.Delay(200);
|
|
await s.Page.ReloadAsync();
|
|
await s.Page.ClickAsync("button[value=generate-new-address]");
|
|
Assert.NotEqual(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text"));
|
|
receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
|
|
await s.Page.ClickAsync("#CancelWizard");
|
|
|
|
// Check the label is applied to the tx
|
|
var wt = s.InWalletTransactions();
|
|
await wt.AssertHasLabels("label2");
|
|
|
|
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
|
await s.GenerateWallet(cryptoCode, "", true);
|
|
await s.GoToWallet(null, WalletsNavPages.Receive);
|
|
await s.Page.ClickAsync("button[value=generate-new-address]");
|
|
var newAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
|
|
Assert.NotEqual(receiveAddr, newAddr);
|
|
|
|
var invoiceId = await s.CreateInvoice(storeId);
|
|
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
|
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
|
|
var address = invoice.GetPaymentPrompt(btc)!.Destination;
|
|
|
|
//wallet should have been imported to bitcoin core wallet in watch only mode.
|
|
var result =
|
|
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
|
Assert.True(result.IsWatchOnly);
|
|
await s.GoToStore(storeId);
|
|
var mnemonic = await s.GenerateWallet(cryptoCode, "", true, true);
|
|
|
|
//lets import and save private keys
|
|
invoiceId = await s.CreateInvoice(storeId);
|
|
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
|
address = invoice.GetPaymentPrompt(btc)!.Destination;
|
|
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
|
|
BitcoinAddress.Create(address, Network.RegTest));
|
|
//spendable from bitcoin core wallet!
|
|
Assert.False(result.IsWatchOnly);
|
|
var tx = await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
|
Money.Coins(3.0m));
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
|
|
await s.GoToStore(storeId);
|
|
await s.GoToWalletSettings();
|
|
var url = s.Page.Url;
|
|
await s.ClickOnAllSectionLinks("#Nav-Wallets");
|
|
|
|
// Make sure wallet info is correct
|
|
await s.GoToUrl(url);
|
|
|
|
await s.Page.WaitForSelectorAsync("#AccountKeys_0__MasterFingerprint");
|
|
Assert.Equal(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
|
|
await s.Page.Locator("#AccountKeys_0__MasterFingerprint").GetAttributeAsync("value"));
|
|
Assert.Equal("m/84'/1'/0'",
|
|
await s.Page.Locator("#AccountKeys_0__AccountKeyPath").GetAttributeAsync("value"));
|
|
|
|
// Make sure we can rescan, because we are admin!
|
|
await s.Page.ClickAsync("#ActionsDropdownToggle");
|
|
await s.Page.ClickAsync("#Rescan");
|
|
await s.Page.GetByText("The batch size make sure").WaitForAsync();
|
|
//
|
|
// Check the tx sent earlier arrived
|
|
wt = await s.GoToWalletTransactions();
|
|
await wt.WaitTransactionsLoaded();
|
|
await s.Page.Locator($"[data-text='{tx}']").WaitForAsync();
|
|
|
|
var walletTransactionUri = new Uri(s.Page.Url);
|
|
|
|
// Send to bob
|
|
var ws = await s.GoToWalletSend();
|
|
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
|
await ws.FillAddress(bob);
|
|
await ws.FillAmount(1);
|
|
|
|
// Add labels to the transaction output
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
await s.Page.ClickAsync("div.label-manager input");
|
|
await s.Page.FillAsync("div.label-manager input", "tx-label");
|
|
await s.Page.Keyboard.PressAsync("Enter");
|
|
await s.Page.WaitForSelectorAsync("[data-value='tx-label']");
|
|
});
|
|
|
|
await ws.Sign();
|
|
// Back button should lead back to the previous page inside the send wizard
|
|
var backUrl = await s.Page.Locator("#GoBack").GetAttributeAsync("href");
|
|
Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl);
|
|
// Cancel button should lead to the page that referred to the send wizard
|
|
var cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href");
|
|
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
|
|
|
|
// Broadcast
|
|
var wb = s.InBroadcast();
|
|
await wb.AssertSending(bob, 1.0m);
|
|
await wb.Broadcast();
|
|
Assert.Equal(walletTransactionUri.ToString(), s.Page.Url);
|
|
// Assert that the added label is associated with the transaction
|
|
await wt.AssertHasLabels("tx-label");
|
|
|
|
await s.GoToWallet(navPages: WalletsNavPages.Send);
|
|
|
|
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
|
await ws.FillAddress(jack);
|
|
await ws.FillAmount(0.01m);
|
|
await ws.Sign();
|
|
|
|
await wb.AssertSending(jack, 0.01m);
|
|
Assert.EndsWith("psbt/ready", s.Page.Url);
|
|
await wb.Broadcast();
|
|
await s.FindAlertMessage();
|
|
|
|
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
|
|
//let's make bip21 more interesting
|
|
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
|
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
|
await s.GoToWalletSend();
|
|
|
|
// ReSharper disable once AsyncVoidMethod
|
|
async void PasteBIP21(object sender, IDialog e)
|
|
{
|
|
await e.AcceptAsync(bip21);
|
|
}
|
|
s.Page.Dialog += PasteBIP21;
|
|
await s.Page.ClickAsync("#bip21parse");
|
|
s.Page.Dialog -= PasteBIP21;
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
|
|
|
|
Assert.Equal(parsedBip21.Amount!.ToString(false),
|
|
await s.Page.Locator("#Outputs_0__Amount").GetAttributeAsync("value"));
|
|
Assert.Equal(parsedBip21.Address!.ToString(),
|
|
await s.Page.Locator("#Outputs_0__DestinationAddress").GetAttributeAsync("value"));
|
|
|
|
await s.Page.ClickAsync("#CancelWizard");
|
|
await s.GoToWalletSettings();
|
|
var settingsUri = new Uri(s.Page.Url);
|
|
await s.Page.ClickAsync("#ActionsDropdownToggle");
|
|
await s.Page.ClickAsync("#ViewSeed");
|
|
|
|
// Seed backup page
|
|
var recoveryPhrase = await s.Page.Locator("#RecoveryPhrase").First.GetAttributeAsync("data-mnemonic");
|
|
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
|
|
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.",
|
|
await s.Page.ContentAsync());
|
|
|
|
// No confirmation, just a link to return to the wallet
|
|
Assert.Equal(0, await s.Page.Locator("#confirm").CountAsync());
|
|
await s.Page.ClickAsync("#proceed");
|
|
Assert.Equal(settingsUri.ToString(), s.Page.Url);
|
|
|
|
// Once more, test the cancel link of the wallet send page leads back to the previous page
|
|
await s.GoToWallet(navPages: WalletsNavPages.Send);
|
|
cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href");
|
|
Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl);
|
|
// no previous page in the wizard, hence no back button
|
|
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
|
|
await s.Page.ClickAsync("#CancelWizard");
|
|
Assert.Equal(settingsUri.ToString(), s.Page.Url);
|
|
|
|
// Transactions list contains export, ensure functions are present.
|
|
await s.GoToWalletTransactions();
|
|
|
|
await s.Page.ClickAsync(".mass-action-select-all");
|
|
await s.Page.Locator("#BumpFee").WaitForAsync();
|
|
|
|
// JSON export
|
|
await s.Page.ClickAsync("#ExportDropdownToggle");
|
|
var opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("#ExportJSON");
|
|
await using (_ = await s.SwitchPage(opening))
|
|
{
|
|
await s.Page.WaitForLoadStateAsync();
|
|
Assert.Contains(s.WalletId.ToString(), s.Page.Url);
|
|
Assert.EndsWith("export?format=json", s.Page.Url);
|
|
Assert.Contains("\"Amount\": \"3.00000000\"", await s.Page.ContentAsync());
|
|
}
|
|
|
|
// CSV export
|
|
await s.Page.ClickAsync("#ExportDropdownToggle");
|
|
var download = await s.Page.RunAndWaitForDownloadAsync(async () =>
|
|
{
|
|
await s.Page.ClickAsync("#ExportCSV");
|
|
});
|
|
Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync()));
|
|
|
|
// BIP-329 export
|
|
await s.Page.ClickAsync("#ExportDropdownToggle");
|
|
download = await s.Page.RunAndWaitForDownloadAsync(async () =>
|
|
{
|
|
await s.Page.ClickAsync("#ExportBIP329");
|
|
});
|
|
Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync()));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseReservedAddressesView()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
var walletId = new WalletId(s.StoreId, "BTC");
|
|
s.WalletId = walletId;
|
|
await s.GenerateWallet();
|
|
|
|
await s.GoToWallet(walletId, WalletsNavPages.Receive);
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var currentAddress = await s.Page.GetAttributeAsync("#Address", "data-text");
|
|
await s.Page.ClickAsync("button[value=generate-new-address]");
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var newAddress = await s.Page.GetAttributeAsync("#Address[data-text]", "data-text");
|
|
Assert.False(string.IsNullOrEmpty(newAddress));
|
|
Assert.NotEqual(currentAddress, newAddress);
|
|
});
|
|
}
|
|
|
|
await s.Page.ClickAsync("#reserved-addresses-button");
|
|
await s.Page.WaitForSelectorAsync("#reserved-addresses");
|
|
|
|
const string labelInputSelector = "#reserved-addresses table tbody tr .ts-control input";
|
|
await s.Page.WaitForSelectorAsync(labelInputSelector);
|
|
|
|
// Test Label Manager
|
|
await s.Page.FillAsync(labelInputSelector, "test-label");
|
|
await s.Page.Keyboard.PressAsync("Enter");
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var text = await s.Page.InnerTextAsync("#reserved-addresses table tbody");
|
|
Assert.Contains("test-label", text);
|
|
});
|
|
|
|
//Test Pagination
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var rows = await s.Page.QuerySelectorAllAsync("#reserved-addresses table tbody tr");
|
|
var visible = await Task.WhenAll(rows.Select(async r => await r.IsVisibleAsync()));
|
|
Assert.Equal(10, visible.Count(v => v));
|
|
});
|
|
|
|
await s.Page.ClickAsync(".pagination li:last-child a");
|
|
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var rows = await s.Page.QuerySelectorAllAsync("#reserved-addresses table tbody tr");
|
|
var visible = await Task.WhenAll(rows.Select(async r => await r.IsVisibleAsync()));
|
|
Assert.Single(visible, v => v);
|
|
});
|
|
|
|
await s.Page.ClickAsync(".pagination li:first-child a");
|
|
await s.Page.WaitForSelectorAsync("#reserved-addresses");
|
|
|
|
// Test Filter
|
|
await s.Page.FillAsync("#filter-reserved-addresses", "test-label");
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var rows = await s.Page.QuerySelectorAllAsync("#reserved-addresses table tbody tr");
|
|
var visible = await Task.WhenAll(rows.Select(async r => await r.IsVisibleAsync()));
|
|
Assert.Single(visible, v => v);
|
|
});
|
|
|
|
//Test WalletLabels redirect with filter
|
|
await s.GoToWallet(walletId, WalletsNavPages.Settings);
|
|
await s.Page.ClickAsync("#manage-wallet-labels-button");
|
|
await s.Page.WaitForSelectorAsync("table");
|
|
await s.Page.ClickAsync("a:has-text('Addresses')");
|
|
|
|
await s.Page.WaitForSelectorAsync("#reserved-addresses");
|
|
var currentFilter = await s.Page.InputValueAsync("#filter-reserved-addresses");
|
|
Assert.Equal("test-label", currentFilter);
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var rows = await s.Page.QuerySelectorAllAsync("#reserved-addresses table tbody tr");
|
|
var visible = await Task.WhenAll(rows.Select(r => r.IsVisibleAsync()));
|
|
Assert.Single(visible, v => v);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanMarkPaymentRequestAsSettled()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
await s.GenerateWallet("BTC", "", true);
|
|
|
|
// Create a payment request
|
|
await s.GoToStore();
|
|
await s.Page.ClickAsync("#menu-item-PaymentRequests");
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("#Title", "Test Payment Request");
|
|
await s.Page.FillAsync("#Amount", "0.1");
|
|
await s.Page.FillAsync("#Currency", "BTC");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "Payment request");
|
|
|
|
var paymentRequestUrl = s.Page.Url;
|
|
var uri = new Uri(paymentRequestUrl);
|
|
var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);
|
|
var payReqId = queryParams["payReqId"];
|
|
Assert.NotNull(payReqId);
|
|
Assert.NotEmpty(payReqId);
|
|
var markAsSettledExists = await s.Page.Locator("button:has-text('Mark as settled')").CountAsync();
|
|
Assert.Equal(0, markAsSettledExists);
|
|
var opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("a:has-text('View')");
|
|
string invoiceId;
|
|
await using (_ = await s.SwitchPage(opening))
|
|
{
|
|
await s.Page.ClickAsync("button:has-text('Pay')");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
await s.Page.WaitForSelectorAsync("iframe[name='btcpay']", new() { Timeout = 10000 });
|
|
|
|
var iframe = s.Page.Frame("btcpay");
|
|
Assert.NotNull(iframe);
|
|
|
|
await iframe.FillAsync("#test-payment-amount", "0.05");
|
|
await iframe.ClickAsync("#FakePayment");
|
|
await iframe.WaitForSelectorAsync("#CheatSuccessMessage", new() { Timeout = 10000 });
|
|
|
|
invoiceId = s.Page.Url.Split('/').Last();
|
|
}
|
|
await s.GoToInvoices();
|
|
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:has-text('Mark as settled')");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
await s.GoToStore();
|
|
await s.Page.ClickAsync("#menu-item-PaymentRequests");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
var opening2 = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("a:has-text('View')");
|
|
await using (_ = await s.SwitchPage(opening2))
|
|
{
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
var markSettledExists = await s.Page.Locator("button:has-text('Mark as settled')").CountAsync();
|
|
Assert.True(markSettledExists > 0, "Mark as settled button should be visible on public page after invoice is settled");
|
|
await s.Page.ClickAsync("button:has-text('Mark as settled')");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
}
|
|
|
|
await s.GoToStore();
|
|
await s.Page.ClickAsync("#menu-item-PaymentRequests");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
var listContent = await s.Page.ContentAsync();
|
|
var isSettledInList = listContent.Contains("Settled");
|
|
var isPendingInList = listContent.Contains("Pending");
|
|
|
|
var settledBadgeExists = await s.Page.Locator(".badge:has-text('Settled')").CountAsync();
|
|
var pendingBadgeExists = await s.Page.Locator(".badge:has-text('Pending')").CountAsync();
|
|
|
|
Assert.True(isSettledInList || settledBadgeExists > 0, "Payment request should show as Settled in the list");
|
|
Assert.False(isPendingInList && pendingBadgeExists > 0, "Payment request should not show as Pending anymore");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanRequireApprovalForNewAccounts()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
await s.StartAsync();
|
|
|
|
var settings = s.Server.PayTester.GetService<SettingsRepository>();
|
|
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
|
Assert.True(policies.EnableRegistration);
|
|
Assert.False(policies.RequiresUserApproval);
|
|
|
|
await s.RegisterNewUser(true);
|
|
var admin = s.AsTestAccount();
|
|
await s.GoToHome();
|
|
await s.GoToServer(ServerNavPages.Policies);
|
|
|
|
Assert.True(await s.Page.Locator("#EnableRegistration").IsCheckedAsync());
|
|
Assert.False(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
|
|
|
await s.Page.Locator("#RequiresUserApproval").ClickAsync();
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "Policies updated successfully");
|
|
Assert.True(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
|
|
|
await Expect(s.Page.Locator("#NotificationsBadge")).Not.ToBeVisibleAsync();
|
|
await s.Logout();
|
|
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser();
|
|
await s.Page.AssertNoError();
|
|
await s.FindAlertMessage(partialText: "Account created. The new account requires approval by an admin before you can log in");
|
|
Assert.Contains("/login", s.Page.Url);
|
|
|
|
var unapproved = s.AsTestAccount();
|
|
await s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning, partialText: "Your user account requires approval by an admin before you can log in");
|
|
Assert.Contains("/login", s.Page.Url);
|
|
|
|
await s.GoToLogin();
|
|
await s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
|
|
await s.GoToHome();
|
|
|
|
await Expect(s.Page.Locator("#NotificationsBadge")).ToContainTextAsync("1");
|
|
await s.Page.ClickAsync("#NotificationsHandle");
|
|
await Expect(s.Page.Locator("#NotificationsList .notification")).ToContainTextAsync($"New user {unapproved.RegisterDetails.Email} requires approval");
|
|
await s.Page.ClickAsync("#NotificationsMarkAllAsSeen");
|
|
|
|
await s.GoToServer(ServerNavPages.Policies);
|
|
Assert.True(await s.Page.Locator("#EnableRegistration").IsCheckedAsync());
|
|
Assert.True(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
|
await s.Page.ClickAsync("#RequiresUserApproval");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "Policies updated successfully");
|
|
Assert.False(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
|
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
await s.ClickPagePrimary();
|
|
await Expect(s.Page.Locator("#Approved")).Not.ToBeVisibleAsync();
|
|
|
|
await s.Logout();
|
|
|
|
await s.GoToLogin();
|
|
await s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning, partialText: "Your user account requires approval by an admin before you can log in");
|
|
Assert.Contains("/login", s.Page.Url);
|
|
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser();
|
|
await s.Page.AssertNoError();
|
|
Assert.DoesNotContain("/login", s.Page.Url);
|
|
var autoApproved = s.AsTestAccount();
|
|
await s.CreateNewStore();
|
|
await s.Logout();
|
|
|
|
await s.GoToLogin();
|
|
await s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
|
|
await s.GoToHome();
|
|
await Expect(s.Page.Locator("#NotificationsBadge")).Not.ToBeVisibleAsync();
|
|
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
var rows = s.Page.Locator("#UsersList tr.user-overview-row");
|
|
Assert.True(await rows.CountAsync() >= 3);
|
|
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", autoApproved.RegisterDetails.Email);
|
|
await s.Page.PressAsync("#SearchTerm", "Enter");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(autoApproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
await Expect(s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-approved")).Not.ToBeVisibleAsync();
|
|
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
|
await Expect(s.Page.Locator("#Approved")).Not.ToBeVisibleAsync();
|
|
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
|
await s.Page.FillAsync("#SearchTerm", unapproved.RegisterDetails.Email);
|
|
await s.Page.PressAsync("#SearchTerm", "Enter");
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(unapproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
Assert.Contains("Pending Approval", await s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-status").TextContentAsync());
|
|
|
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
|
await s.Page.ClickAsync("#Approved");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "User successfully updated");
|
|
|
|
await s.GoToServer(ServerNavPages.Users);
|
|
Assert.Contains(unapproved.RegisterDetails.Email, await s.Page.GetAttributeAsync("#SearchTerm", "value"));
|
|
Assert.Equal(1, await rows.CountAsync());
|
|
Assert.Contains(unapproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
|
Assert.Contains("Active", await s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-status").TextContentAsync());
|
|
|
|
await s.Logout();
|
|
await s.GoToLogin();
|
|
await s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
|
await s.Page.AssertNoError();
|
|
Assert.DoesNotContain("/login", s.Page.Url);
|
|
await s.CreateNewStore();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseDynamicDns()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(isAdmin: true);
|
|
await s.GoToUrl("/server/services");
|
|
Assert.Contains("Dynamic DNS", await s.Page.ContentAsync());
|
|
|
|
await s.GoToUrl("/server/services/dynamic-dns");
|
|
await s.Page.AssertNoError();
|
|
if ((await s.Page.ContentAsync()).Contains("pouet.hello.com"))
|
|
{
|
|
await s.GoToUrl("/server/services/dynamic-dns/pouet.hello.com/delete");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
}
|
|
|
|
await s.ClickPagePrimary();
|
|
await s.Page.AssertNoError();
|
|
await s.Page.FillAsync("#ServiceUrl", s.Link("/"));
|
|
await s.Page.FillAsync("#Settings_Hostname", "pouet.hello.com");
|
|
await s.Page.FillAsync("#Settings_Login", "MyLog");
|
|
await s.Page.FillAsync("#Settings_Password", "MyLog");
|
|
await s.ClickPagePrimary();
|
|
await s.Page.AssertNoError();
|
|
Assert.Contains("The Dynamic DNS has been successfully queried", await s.Page.ContentAsync());
|
|
Assert.EndsWith("/server/services/dynamic-dns", s.Page.Url);
|
|
|
|
// Try to create the same hostname (should fail)
|
|
await s.ClickPagePrimary();
|
|
await s.Page.AssertNoError();
|
|
await s.Page.FillAsync("#ServiceUrl", s.Link("/"));
|
|
await s.Page.FillAsync("#Settings_Hostname", "pouet.hello.com");
|
|
await s.Page.FillAsync("#Settings_Login", "MyLog");
|
|
await s.Page.FillAsync("#Settings_Password", "MyLog");
|
|
await s.ClickPagePrimary();
|
|
await s.Page.AssertNoError();
|
|
Assert.Contains("This hostname already exists", await s.Page.ContentAsync());
|
|
|
|
// Delete the hostname
|
|
await s.GoToUrl("/server/services/dynamic-dns");
|
|
Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", await s.Page.ContentAsync());
|
|
await s.GoToUrl("/server/services/dynamic-dns/pouet.hello.com/delete");
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.Page.AssertNoError();
|
|
|
|
Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", await s.Page.ContentAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanCreateInvoiceInUI()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
await s.GoToInvoices();
|
|
|
|
await s.ClickPagePrimary();
|
|
|
|
await s.AddDerivationScheme();
|
|
await s.GoToInvoices();
|
|
await s.CreateInvoice();
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:first-child");
|
|
await TestUtils.EventuallyAsync(async () => Assert.Contains("Invalid (marked)", await s.Page.ContentAsync()));
|
|
await s.Page.ReloadAsync();
|
|
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:first-child");
|
|
await TestUtils.EventuallyAsync(async () => Assert.Contains("Settled (marked)", await s.Page.ContentAsync()));
|
|
|
|
await s.Page.ReloadAsync();
|
|
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:first-child");
|
|
await TestUtils.EventuallyAsync(async () => Assert.Contains("Invalid (marked)", await s.Page.ContentAsync()));
|
|
await s.Page.ReloadAsync();
|
|
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:first-child");
|
|
await TestUtils.EventuallyAsync(async () => Assert.Contains("Settled (marked)", await s.Page.ContentAsync()));
|
|
|
|
// Zero amount invoice should redirect to receipt
|
|
var zeroAmountId = await s.CreateInvoice(0);
|
|
await s.GoToUrl($"/i/{zeroAmountId}");
|
|
Assert.EndsWith("/receipt", s.Page.Url);
|
|
Assert.Contains("$0.00", await s.Page.ContentAsync());
|
|
await s.GoToInvoice(zeroAmountId);
|
|
Assert.Equal("Settled", (await s.Page.Locator("[data-invoice-state-badge]").TextContentAsync())?.Trim());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanImportMnemonic()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
foreach (var isHotwallet in new[] { false, true })
|
|
{
|
|
var cryptoCode = "BTC";
|
|
await s.CreateNewStore();
|
|
await s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", isHotWallet: isHotwallet);
|
|
await s.GoToWalletSettings(cryptoCode);
|
|
if (isHotwallet)
|
|
{
|
|
await s.Page.ClickAsync("#ActionsDropdownToggle");
|
|
Assert.True(await s.Page.Locator("#ViewSeed").IsVisibleAsync());
|
|
}
|
|
else
|
|
{
|
|
Assert.False(await s.Page.Locator("#ViewSeed").IsVisibleAsync());
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanSetupStoreViaGuide()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser();
|
|
await s.GoToUrl("/");
|
|
|
|
// verify redirected to create store page
|
|
Assert.EndsWith("/stores/create", s.Page.Url);
|
|
Assert.Contains("Create your first store", await s.Page.ContentAsync());
|
|
Assert.Contains("Create a store to begin accepting payments", await s.Page.ContentAsync());
|
|
Assert.Equal(0, await s.Page.Locator("#StoreSelectorDropdown").CountAsync());
|
|
|
|
(_, string storeId) = await s.CreateNewStore();
|
|
|
|
// should redirect to first store
|
|
await s.GoToUrl("/");
|
|
Assert.Contains($"/stores/{storeId}", s.Page.Url);
|
|
Assert.Equal(1, await s.Page.Locator("#StoreSelectorDropdown").CountAsync());
|
|
Assert.Equal(1, await s.Page.Locator("#SetupGuide").CountAsync());
|
|
|
|
await s.GoToUrl("/stores/create");
|
|
Assert.Contains("Create a new store", await s.Page.ContentAsync());
|
|
Assert.DoesNotContain("Create your first store", await s.Page.ContentAsync());
|
|
Assert.DoesNotContain("To start accepting payments, set up a store.", await s.Page.ContentAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanImportWallet()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
const string cryptoCode = "BTC";
|
|
var mnemonic = await s.GenerateWallet(cryptoCode, "click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel");
|
|
|
|
// Make sure wallet info is correct
|
|
await s.GoToWalletSettings(cryptoCode);
|
|
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
|
|
await s.Page.GetAttributeAsync("#AccountKeys_0__MasterFingerprint", "value"));
|
|
Assert.Contains("m/84'/1'/0'",
|
|
await s.Page.GetAttributeAsync("#AccountKeys_0__AccountKeyPath", "value"));
|
|
|
|
// Transactions list is empty
|
|
await s.GoToWallet();
|
|
await s.Page.WaitForSelectorAsync("#WalletTransactions[data-loaded='true']");
|
|
Assert.Contains("There are no transactions yet", await s.Page.Locator("#WalletTransactions").TextContentAsync());
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanUseLndSeedBackup()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
s.Server.ActivateLightning();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToServer(ServerNavPages.Services);
|
|
await s.Page.AssertNoError();
|
|
s.TestLogs.LogInformation("Let's see if we can access LND's seed");
|
|
Assert.Contains("server/services/lndseedbackup/BTC", await s.Page.ContentAsync());
|
|
await s.GoToUrl("/server/services/lndseedbackup/BTC");
|
|
await s.Page.ClickAsync("#details");
|
|
var seedEl = s.Page.Locator("#Seed");
|
|
await Expect(seedEl).ToBeVisibleAsync();
|
|
Assert.Contains("about over million", await seedEl.GetAttributeAsync("value"), StringComparison.OrdinalIgnoreCase);
|
|
var passEl = s.Page.Locator("#WalletPassword");
|
|
await Expect(passEl).ToBeVisibleAsync();
|
|
Assert.Contains(await passEl.TextContentAsync(), "hellorockstar", StringComparison.OrdinalIgnoreCase);
|
|
await s.Page.ClickAsync("#delete");
|
|
await s.Page.WaitForSelectorAsync("#ConfirmInput");
|
|
await s.ConfirmDeleteModal();
|
|
await s.FindAlertMessage();
|
|
seedEl = s.Page.Locator("#Seed");
|
|
Assert.Contains("Seed removed", await seedEl.TextContentAsync(), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseLNURLAuth()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
var user = await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToProfile(ManageNavPages.TwoFactorAuthentication);
|
|
await s.Page.FillAsync("[name='Name']", "ln wallet");
|
|
await s.Page.SelectOptionAsync("[name='type']", $"{(int)Fido2Credential.CredentialType.LNURLAuth}");
|
|
await s.Page.ClickAsync("#btn-add");
|
|
var linkElements = await s.Page.Locator(".tab-content a").AllAsync();
|
|
var links = new List<string>();
|
|
foreach (var element in linkElements)
|
|
{
|
|
var href = await element.GetAttributeAsync("href");
|
|
if (href != null) links.Add(href);
|
|
}
|
|
Assert.Equal(2, links.Count);
|
|
Uri prevEndpoint = null;
|
|
foreach (string link in links)
|
|
{
|
|
var endpoint = LNURL.LNURL.Parse(link, out var tag);
|
|
Assert.Equal("login", tag);
|
|
if (endpoint.Scheme != "https")
|
|
prevEndpoint = endpoint;
|
|
}
|
|
|
|
var linkingKey = new Key();
|
|
var request = Assert.IsType<LNAuthRequest>(await LNURL.LNURL.FetchInformation(prevEndpoint, null));
|
|
_ = await request.SendChallenge(linkingKey, new HttpClient());
|
|
await TestUtils.EventuallyAsync(async () => await s.FindAlertMessage());
|
|
|
|
await s.CreateNewStore(); // create a store to prevent redirect after login
|
|
await s.Logout();
|
|
await s.LogIn(user, "123456");
|
|
var section = s.Page.Locator("#lnurlauth-section");
|
|
linkElements = await section.Locator(".tab-content a").AllAsync();
|
|
links = new List<string>();
|
|
foreach (var element in linkElements)
|
|
{
|
|
var href = await element.GetAttributeAsync("href");
|
|
if (href != null) links.Add(href);
|
|
}
|
|
Assert.Equal(2, links.Count);
|
|
prevEndpoint = null;
|
|
foreach (string link in links)
|
|
{
|
|
var endpoint = LNURL.LNURL.Parse(link, out var tag);
|
|
Assert.Equal("login", tag);
|
|
if (endpoint.Scheme != "https")
|
|
prevEndpoint = endpoint;
|
|
}
|
|
request = Assert.IsType<LNAuthRequest>(await LNURL.LNURL.FetchInformation(prevEndpoint, null));
|
|
_ = await request.SendChallenge(linkingKey, new HttpClient());
|
|
await TestUtils.EventuallyAsync(() =>
|
|
{
|
|
Assert.StartsWith(s.ServerUri.ToString(), s.Page.Url);
|
|
return Task.CompletedTask;
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseCoinSelectionFilters()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
(_, string storeId) = await s.CreateNewStore();
|
|
await s.GenerateWallet("BTC", "", false, true);
|
|
var walletId = new WalletId(storeId, "BTC");
|
|
|
|
await s.GoToWallet(walletId, WalletsNavPages.Receive);
|
|
var addressStr = await s.Page.GetAttributeAsync("#Address", "data-text");
|
|
var address = BitcoinAddress.Create(addressStr,
|
|
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
|
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
|
|
const decimal AmountTiny = 0.001m;
|
|
const decimal AmountSmall = 0.005m;
|
|
const decimal AmountMedium = 0.009m;
|
|
const decimal AmountLarge = 0.02m;
|
|
|
|
List<uint256> txs =
|
|
[
|
|
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(AmountTiny)),
|
|
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(AmountSmall)),
|
|
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(AmountMedium)),
|
|
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(AmountLarge))
|
|
];
|
|
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
await s.GoToWallet(walletId, WalletsNavPages.Send);
|
|
await s.Page.ClickAsync("#toggleInputSelection");
|
|
|
|
var input = s.Page.Locator("input[placeholder^='Filter']");
|
|
await input.WaitForAsync();
|
|
Assert.NotNull(input);
|
|
|
|
// Test amountmin
|
|
await input.ClearAsync();
|
|
await input.FillAsync("amountmin:0.01");
|
|
await TestUtils.EventuallyAsync(async () => {
|
|
Assert.Single(await s.Page.Locator("li.list-group-item").AllAsync());
|
|
});
|
|
|
|
// Test amountmax
|
|
await input.ClearAsync();
|
|
await input.FillAsync("amountmax:0.002");
|
|
await TestUtils.EventuallyAsync(async () => {
|
|
Assert.Single(await s.Page.Locator("li.list-group-item").AllAsync());
|
|
});
|
|
|
|
// Test general text (txid)
|
|
await input.ClearAsync();
|
|
await input.FillAsync(txs[2].ToString()[..8]);
|
|
await TestUtils.EventuallyAsync(async () => {
|
|
Assert.Single(await s.Page.Locator("li.list-group-item").AllAsync());
|
|
});
|
|
|
|
// Test timestamp before/after
|
|
await input.ClearAsync();
|
|
await input.FillAsync("after:2099-01-01");
|
|
await TestUtils.EventuallyAsync(async () => {
|
|
Assert.Empty(await s.Page.Locator("li.list-group-item").AllAsync());
|
|
});
|
|
|
|
await input.ClearAsync();
|
|
await input.FillAsync("before:2099-01-01");
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
Assert.True((await s.Page.Locator("li.list-group-item").AllAsync()).Count >= 4);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanManageLightningNode()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
s.Server.ActivateLightning();
|
|
await s.StartAsync();
|
|
await s.Server.EnsureChannelsSetup();
|
|
await s.RegisterNewUser(true);
|
|
(string storeName, _) = await s.CreateNewStore();
|
|
|
|
// Check status in navigation
|
|
await s.Page.Locator("#menu-item-LightningSettings-BTC .btcpay-status--pending").WaitForAsync();
|
|
|
|
// Set up LN node
|
|
await s.AddLightningNode();
|
|
await s.Page.Locator("#menu-item-Lightning-BTC .btcpay-status--enabled").WaitForAsync();
|
|
|
|
// Check public node info for availability
|
|
var opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("#PublicNodeInfo");
|
|
var newPage = await opening;
|
|
await Expect(newPage.Locator(".store-name")).ToHaveTextAsync(storeName);
|
|
await Expect(newPage.Locator("#LightningNodeTitle")).ToHaveTextAsync("BTC Lightning Node");
|
|
await Expect(newPage.Locator("#LightningNodeStatus")).ToHaveTextAsync("Online");
|
|
await newPage.Locator(".btcpay-status--enabled").WaitForAsync();
|
|
await newPage.Locator("#LightningNodeUrlClearnet").WaitForAsync();
|
|
await newPage.CloseAsync();
|
|
|
|
// Set wrong node connection string to simulate offline node
|
|
await s.GoToLightningSettings();
|
|
await s.Page.ClickAsync("#SetupLightningNodeLink");
|
|
await s.Page.ClickAsync("label[for=\"LightningNodeType-Custom\"]");
|
|
await s.Page.Locator("#ConnectionString").WaitForAsync();
|
|
await s.Page.Locator("#ConnectionString").ClearAsync();
|
|
await s.Page.FillAsync("#ConnectionString", "type=lnd-rest;server=https://doesnotwork:8080/");
|
|
await s.Page.ClickAsync("#test");
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage(partialText: "BTC Lightning node updated.");
|
|
|
|
// Check offline state is communicated in nav item
|
|
await s.Page.Locator("#menu-item-Lightning-BTC .btcpay-status--disabled").WaitForAsync();
|
|
|
|
// Check public node info for availability
|
|
opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("#PublicNodeInfo");
|
|
newPage = await opening;
|
|
await Expect(newPage.Locator(".store-name")).ToHaveTextAsync(storeName);
|
|
await Expect(newPage.Locator("#LightningNodeTitle")).ToHaveTextAsync("BTC Lightning Node");
|
|
await Expect(newPage.Locator("#LightningNodeStatus")).ToHaveTextAsync("Unavailable");
|
|
await newPage.Locator(".btcpay-status--disabled").WaitForAsync();
|
|
await Expect(newPage.Locator("#LightningNodeUrlClearnet")).ToBeHiddenAsync();
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanEditPullPaymentUI()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
s.Server.ActivateLightning(LightningConnectionType.LndREST);
|
|
await s.StartAsync();
|
|
await s.Server.EnsureChannelsSetup();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
await s.GenerateWallet("BTC", "", true, true);
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
await s.FundStoreWallet(denomination: 50.0m);
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
|
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("#Name", "PP1");
|
|
await s.Page.Locator("#Amount").ClearAsync();
|
|
await s.Page.FillAsync("#Amount", "99.0");
|
|
await s.ClickPagePrimary();
|
|
|
|
var opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("text=View");
|
|
var newPage = await opening;
|
|
await Expect(newPage.Locator("body")).ToContainTextAsync("PP1");
|
|
await newPage.CloseAsync();
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
|
|
|
await s.Page.ClickAsync("text=PP1");
|
|
var name = s.Page.Locator("#Name");
|
|
await name.ClearAsync();
|
|
await name.FillAsync("PP1 Edited");
|
|
var description = s.Page.Locator(".card-block");
|
|
await description.FillAsync("Description Edit");
|
|
await s.ClickPagePrimary();
|
|
|
|
opening = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("text=View");
|
|
newPage = await opening;
|
|
await Expect(newPage.GetByTestId("description")).ToContainTextAsync("Description Edit");
|
|
await Expect(newPage.GetByTestId("title")).ToContainTextAsync("PP1 Edited");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CookieReflectProperPermissions()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
var alice = s.Server.NewAccount();
|
|
alice.Register(false);
|
|
await alice.CreateStoreAsync();
|
|
var bob = s.Server.NewAccount();
|
|
await bob.CreateStoreAsync();
|
|
await bob.AddGuest(alice.UserId);
|
|
|
|
await s.GoToLogin();
|
|
await s.LogIn(alice.Email, alice.Password);
|
|
await s.GoToUrl($"/cheat/permissions/stores/{bob.StoreId}");
|
|
var pageSource = await s.Page.ContentAsync();
|
|
AssertPermissions(pageSource, true,
|
|
new[]
|
|
{
|
|
Policies.CanViewInvoices,
|
|
Policies.CanModifyInvoices,
|
|
Policies.CanViewPaymentRequests,
|
|
Policies.CanViewPullPayments,
|
|
Policies.CanViewPayouts,
|
|
Policies.CanModifyStoreSettingsUnscoped,
|
|
Policies.CanDeleteUser
|
|
});
|
|
AssertPermissions(pageSource, false,
|
|
new[]
|
|
{
|
|
Policies.CanModifyStoreSettings,
|
|
Policies.CanCreateNonApprovedPullPayments,
|
|
Policies.CanCreatePullPayments,
|
|
Policies.CanManagePullPayments,
|
|
Policies.CanModifyServerSettings
|
|
});
|
|
|
|
await s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
|
pageSource = await s.Page.ContentAsync();
|
|
|
|
AssertPermissions(pageSource, true,
|
|
new[]
|
|
{
|
|
Policies.CanViewInvoices,
|
|
Policies.CanModifyInvoices,
|
|
Policies.CanViewPaymentRequests,
|
|
Policies.CanViewStoreSettings,
|
|
Policies.CanModifyStoreSettingsUnscoped,
|
|
Policies.CanDeleteUser,
|
|
Policies.CanModifyStoreSettings,
|
|
Policies.CanCreateNonApprovedPullPayments,
|
|
Policies.CanCreatePullPayments,
|
|
Policies.CanManagePullPayments,
|
|
Policies.CanArchivePullPayments,
|
|
});
|
|
AssertPermissions(pageSource, false,
|
|
new[]
|
|
{
|
|
Policies.CanModifyServerSettings
|
|
});
|
|
|
|
await s.GoToUrl("/logout");
|
|
await alice.MakeAdmin();
|
|
|
|
await s.GoToLogin();
|
|
await s.LogIn(alice.Email, alice.Password);
|
|
await s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
|
|
pageSource = await s.Page.ContentAsync();
|
|
|
|
AssertPermissions(pageSource, true,
|
|
new[]
|
|
{
|
|
Policies.CanViewInvoices,
|
|
Policies.CanModifyInvoices,
|
|
Policies.CanViewPaymentRequests,
|
|
Policies.CanViewStoreSettings,
|
|
Policies.CanModifyStoreSettingsUnscoped,
|
|
Policies.CanDeleteUser,
|
|
Policies.CanModifyStoreSettings,
|
|
Policies.CanCreateNonApprovedPullPayments,
|
|
Policies.CanCreatePullPayments,
|
|
Policies.CanManagePullPayments,
|
|
Policies.CanModifyServerSettings,
|
|
Policies.CanCreateUser,
|
|
Policies.CanManageUsers
|
|
});
|
|
}
|
|
|
|
void AssertPermissions(string source, bool expected, string[] permissions)
|
|
{
|
|
if (expected)
|
|
{
|
|
foreach (var p in permissions)
|
|
Assert.Contains(p + "<", source);
|
|
}
|
|
else
|
|
{
|
|
foreach (var p in permissions)
|
|
Assert.DoesNotContain(p + "<", source);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseAwaitProgressForInProgressPayout()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
await s.GenerateWallet(isHotWallet: true);
|
|
await s.FundStoreWallet(denomination: 50.0m);
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.PayoutProcessors);
|
|
await s.Page.ClickAsync("#Configure-BTC-CHAIN");
|
|
await s.Page.SetCheckedAsync("#ProcessNewPayoutsInstantly", true);
|
|
await s.ClickPagePrimary();
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("#Name", "PP1");
|
|
await s.Page.FillAsync("#Amount", "99.0");
|
|
await s.Page.SetCheckedAsync("#AutoApproveClaims", true);
|
|
await s.ClickPagePrimary();
|
|
|
|
var o = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync("text=View");
|
|
var newPage = await o;
|
|
|
|
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
|
await newPage.FillAsync("#Destination", address.ToString());
|
|
await newPage.PressAsync("#Destination", "Enter");
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
|
await s.Page.ClickAsync("#InProgress-view");
|
|
|
|
// Wait for the payment processor to process the payment
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
await s.Page.ReloadAsync();
|
|
var massActionSelect = s.Page.Locator(".mass-action-select-all[data-payout-state='InProgress']");
|
|
await Expect(massActionSelect).ToBeVisibleAsync();
|
|
});
|
|
|
|
await s.Page.ClickAsync(".mass-action-select-all[data-payout-state='InProgress']");
|
|
await s.Page.ClickAsync("#InProgress-mark-awaiting-payment");
|
|
await s.Page.ClickAsync("#AwaitingPayment-view");
|
|
|
|
var pageContent = await s.Page.ContentAsync();
|
|
Assert.Contains("PP1", pageContent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseWebhooks()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.CreateNewStore();
|
|
await s.GoToStore(StoreNavPages.Webhooks);
|
|
|
|
TestLogs.LogInformation("Let's create two webhooks");
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
await s.ClickPagePrimary();
|
|
await s.Page.FillAsync("[name='PayloadUrl']", $"http://127.0.0.1/callback{i}");
|
|
await s.Page.SelectOptionAsync("#Everything", "false");
|
|
await s.Page.ClickAsync("#InvoiceCreated");
|
|
await s.Page.ClickAsync("#InvoiceProcessing");
|
|
await s.ClickPagePrimary();
|
|
}
|
|
|
|
TestLogs.LogInformation("Let's delete one of them");
|
|
var deleteLinks = await s.Page.Locator("a:has-text('Delete')").AllAsync();
|
|
Assert.Equal(2, deleteLinks.Count);
|
|
await deleteLinks[0].ClickAsync();
|
|
await s.ConfirmDeleteModal();
|
|
deleteLinks = await s.Page.Locator("a:has-text('Delete')").AllAsync();
|
|
Assert.Single(deleteLinks);
|
|
await s.FindAlertMessage();
|
|
|
|
TestLogs.LogInformation("Let's try to update one of them");
|
|
await s.Page.ClickAsync("text=Modify");
|
|
|
|
using var server = new FakeServer();
|
|
await server.Start();
|
|
await s.Page.FillAsync("[name='PayloadUrl']", server.ServerUri.AbsoluteUri);
|
|
await s.Page.FillAsync("[name='Secret']", "HelloWorld");
|
|
await s.Page.ClickAsync("[name='update']");
|
|
await s.FindAlertMessage();
|
|
await s.Page.ClickAsync("text=Modify");
|
|
|
|
// Check which events are selected
|
|
var pageContent = await s.Page.ContentAsync();
|
|
Assert.Contains("value=\"InvoiceProcessing\" checked", pageContent);
|
|
Assert.Contains("value=\"InvoiceCreated\" checked", pageContent);
|
|
Assert.DoesNotContain("value=\"InvoiceReceivedPayment\" checked", pageContent);
|
|
|
|
await s.Page.ClickAsync("[name='update']");
|
|
await s.FindAlertMessage();
|
|
pageContent = await s.Page.ContentAsync();
|
|
Assert.Contains(server.ServerUri.AbsoluteUri, pageContent);
|
|
|
|
TestLogs.LogInformation("Let's see if we can generate an event");
|
|
await s.GoToStore();
|
|
await s.AddDerivationScheme();
|
|
await s.CreateInvoice();
|
|
var request = await server.GetNextRequest();
|
|
var headers = request.Request.Headers;
|
|
var actualSig = headers["BTCPay-Sig"].First();
|
|
var bytes = await request.Request.Body.ReadBytesAsync((int)headers.ContentLength.Value);
|
|
var expectedSig =
|
|
$"sha256={Encoders.Hex.EncodeData(NBitcoin.Crypto.Hashes.HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld"), bytes))}";
|
|
Assert.Equal(expectedSig, actualSig);
|
|
request.Response.StatusCode = 200;
|
|
server.Done();
|
|
|
|
TestLogs.LogInformation("Let's make a failed event");
|
|
var invoiceId = await s.CreateInvoice();
|
|
request = await server.GetNextRequest();
|
|
request.Response.StatusCode = 404;
|
|
server.Done();
|
|
|
|
// The delivery is done asynchronously, so small wait here
|
|
await Task.Delay(500);
|
|
await s.GoToStore();
|
|
await s.GoToStore(StoreNavPages.Webhooks);
|
|
await s.Page.ClickAsync("text=Modify");
|
|
var redeliverElements = await s.Page.Locator("button.redeliver").AllAsync();
|
|
|
|
// One worked, one failed
|
|
await s.Page.Locator(".icon-cross").WaitForAsync();
|
|
await s.Page.Locator(".icon-checkmark").WaitForAsync();
|
|
await redeliverElements[0].ClickAsync();
|
|
|
|
await s.FindAlertMessage();
|
|
request = await server.GetNextRequest();
|
|
request.Response.StatusCode = 404;
|
|
server.Done();
|
|
|
|
TestLogs.LogInformation("Can we browse the json content?");
|
|
await CanBrowseContentAsync(s);
|
|
|
|
await s.GoToInvoices();
|
|
await s.Page.ClickAsync($"text={invoiceId}");
|
|
await CanBrowseContentAsync(s);
|
|
var redeliverElement = s.Page.Locator("button.redeliver").First;
|
|
await redeliverElement.ClickAsync();
|
|
|
|
await s.FindAlertMessage();
|
|
request = await server.GetNextRequest();
|
|
request.Response.StatusCode = 404;
|
|
server.Done();
|
|
|
|
TestLogs.LogInformation("Let's see if we can delete store with some webhooks inside");
|
|
await s.GoToStore();
|
|
await s.Page.ClickAsync("#DeleteStore");
|
|
await s.ConfirmDeleteModal();
|
|
await s.FindAlertMessage();
|
|
}
|
|
|
|
private static async Task CanBrowseContentAsync(PlaywrightTester s)
|
|
{
|
|
var newPageDoing = s.Page.Context.WaitForPageAsync();
|
|
await s.Page.ClickAsync(".delivery-content");
|
|
var newPage = await newPageDoing;
|
|
var bodyText = await newPage.Locator("body").TextContentAsync();
|
|
JObject.Parse(bodyText);
|
|
await newPage.CloseAsync();
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanUsePredefinedRoles()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
s.Server.ActivateLightning();
|
|
await s.StartAsync();
|
|
await s.Server.EnsureChannelsSetup();
|
|
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors",
|
|
"payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails/rules", "email-settings", "forms"};
|
|
|
|
// Setup users
|
|
var manager = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
var employee = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
var guest = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
|
|
// Setup store, wallets and add users
|
|
await s.RegisterNewUser(true);
|
|
var (_, storeId) = await s.CreateNewStore();
|
|
await s.GoToStore();
|
|
await s.GenerateWallet(isHotWallet: true);
|
|
await s.AddLightningNode(LightningConnectionType.CLightning, false);
|
|
await s.AddUserToStore(storeId, manager, "Manager");
|
|
await s.AddUserToStore(storeId, employee, "Employee");
|
|
await s.AddUserToStore(storeId, guest, "Guest");
|
|
|
|
// Add apps
|
|
var (_, posId) = await s.CreateApp("PointOfSale");
|
|
var (_, crowdfundId) = await s.CreateApp("Crowdfund");
|
|
|
|
string GetStorePath(string subPath) => $"/stores/{storeId}" + (string.IsNullOrEmpty(subPath) ? "" : $"/{subPath}");
|
|
|
|
// Owner access
|
|
await s.AssertPageAccess(true, GetStorePath(""));
|
|
await s.AssertPageAccess(true, GetStorePath("reports"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
|
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
|
await s.AssertPageAccess(true, GetStorePath("payouts"));
|
|
await s.AssertPageAccess(true, GetStorePath("onchain/BTC"));
|
|
await s.AssertPageAccess(true, GetStorePath("onchain/BTC/settings"));
|
|
await s.AssertPageAccess(true, GetStorePath("lightning/BTC"));
|
|
await s.AssertPageAccess(true, GetStorePath("lightning/BTC/settings"));
|
|
await s.AssertPageAccess(true, GetStorePath("apps/create"));
|
|
await s.AssertPageAccess(true, $"/apps/{posId}/settings/pos");
|
|
await s.AssertPageAccess(true, $"/apps/{crowdfundId}/settings/crowdfund");
|
|
foreach (var path in storeSettingsPaths)
|
|
{ // should have manage access to settings, hence should see submit buttons or create links
|
|
s.TestLogs.LogInformation($"Checking access to store page {path} as owner");
|
|
await s.AssertPageAccess(true, $"/stores/{storeId}/{path}");
|
|
await s.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
|
|
if (path != "payout-processors")
|
|
{
|
|
var saveButton = s.Page.GetByRole(AriaRole.Button, new() { Name = "Save" });
|
|
if (await saveButton.CountAsync() > 0)
|
|
{
|
|
Assert.True(await saveButton.IsVisibleAsync());
|
|
}
|
|
}
|
|
}
|
|
await s.Logout();
|
|
|
|
// Manager access
|
|
await s.LogIn(manager);
|
|
await s.AssertPageAccess(false, GetStorePath(""));
|
|
await s.AssertPageAccess(true, GetStorePath("reports"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
|
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
|
await s.AssertPageAccess(true, GetStorePath("payouts"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("apps/create"));
|
|
await s.AssertPageAccess(true, $"/apps/{posId}/settings/pos");
|
|
await s.AssertPageAccess(true, $"/apps/{crowdfundId}/settings/crowdfund");
|
|
foreach (var path in storeSettingsPaths)
|
|
{ // should have view access to settings, but no submit buttons or create links
|
|
s.TestLogs.LogInformation($"Checking access to store page {path} as manager");
|
|
await s.AssertPageAccess(true, $"stores/{storeId}/{path}");
|
|
Assert.False(await s.Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).IsVisibleAsync());
|
|
}
|
|
await s.Logout();
|
|
|
|
// Employee access
|
|
await s.LogIn(employee);
|
|
await s.AssertPageAccess(false, GetStorePath(""));
|
|
await s.AssertPageAccess(false, GetStorePath("reports"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
|
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
|
await s.AssertPageAccess(true, GetStorePath("payouts"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("apps/create"));
|
|
await s.AssertPageAccess(false, $"/apps/{posId}/settings/pos");
|
|
await s.AssertPageAccess(false, $"/apps/{crowdfundId}/settings/crowdfund");
|
|
foreach (var path in storeSettingsPaths)
|
|
{ // should not have access to settings
|
|
s.TestLogs.LogInformation($"Checking access to store page {path} as employee");
|
|
await s.AssertPageAccess(false, $"/stores/{storeId}/{path}");
|
|
}
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
|
|
// Guest access
|
|
await s.LogIn(guest);
|
|
await s.AssertPageAccess(false, GetStorePath(""));
|
|
await s.AssertPageAccess(false, GetStorePath("reports"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
|
await s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
|
|
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
|
await s.AssertPageAccess(true, GetStorePath("payouts"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("apps/create"));
|
|
await s.AssertPageAccess(false, $"/apps/{posId}/settings/pos");
|
|
await s.AssertPageAccess(false, $"/apps/{crowdfundId}/settings/crowdfund");
|
|
foreach (var path in storeSettingsPaths)
|
|
{ // should not have access to settings
|
|
s.TestLogs.LogInformation($"Checking access to store page {path} as guest");
|
|
await s.AssertPageAccess(false, $"/stores/{storeId}/{path}");
|
|
}
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUsePairing()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.Page.GotoAsync(s.Link("/api-access-request"));
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser();
|
|
await s.CreateNewStore();
|
|
await s.AddDerivationScheme();
|
|
|
|
await s.GoToStore(s.StoreId, StoreNavPages.Tokens);
|
|
await s.Page.Locator("#CreateNewToken").ClickAsync();
|
|
await s.ClickPagePrimary();
|
|
var url = s.Page.Url;
|
|
var pairingCode = System.Text.RegularExpressions.Regex.Match(new Uri(url, UriKind.Absolute).Query, "pairingCode=([^&]*)").Groups[1].Value;
|
|
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage();
|
|
Assert.Contains(pairingCode, await s.Page.ContentAsync());
|
|
|
|
var client = new NBitpayClient.Bitpay(new NBitcoin.Key(), s.ServerUri);
|
|
await client.AuthorizeClient(new NBitpayClient.PairingCode(pairingCode));
|
|
await client.CreateInvoiceAsync(
|
|
new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true },
|
|
NBitpayClient.Facade.Merchant);
|
|
|
|
client = new NBitpayClient.Bitpay(new NBitcoin.Key(), s.ServerUri);
|
|
|
|
var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant);
|
|
await s.Page.GotoAsync(code.CreateLink(s.ServerUri).ToString());
|
|
await s.ClickPagePrimary();
|
|
|
|
await client.CreateInvoiceAsync(
|
|
new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true },
|
|
NBitpayClient.Facade.Merchant);
|
|
|
|
await s.Page.GotoAsync(s.Link("/api-tokens"));
|
|
await s.ClickPagePrimary(); // Request
|
|
await s.ClickPagePrimary(); // Approve
|
|
var url2 = s.Page.Url;
|
|
var pairingCode2 = System.Text.RegularExpressions.Regex.Match(new Uri(url2, UriKind.Absolute).Query, "pairingCode=([^&]*)").Groups[1].Value;
|
|
Assert.False(string.IsNullOrEmpty(pairingCode2));
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanCreateStores()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
s.Server.ActivateLightning();
|
|
await s.StartAsync();
|
|
var alice = await s.RegisterNewUser(true);
|
|
var (storeName, storeId) = await s.CreateNewStore();
|
|
var storeUrl = $"/stores/{storeId}";
|
|
|
|
await s.GoToStore(storeId);
|
|
Assert.Contains(storeName, await s.Page.ContentAsync());
|
|
Assert.DoesNotContain("id=\"Dashboard\"", await s.Page.ContentAsync());
|
|
|
|
// verify steps for wallet setup are displayed correctly
|
|
await s.GoToStore(storeId, StoreNavPages.Dashboard);
|
|
await s.Page.WaitForSelectorAsync("#SetupGuide-StoreDone");
|
|
await s.Page.WaitForSelectorAsync("#SetupGuide-Wallet");
|
|
await s.Page.WaitForSelectorAsync("#SetupGuide-Lightning");
|
|
|
|
// setup onchain wallet
|
|
await s.Page.Locator("#SetupGuide-Wallet").ClickAsync();
|
|
await s.AddDerivationScheme();
|
|
await s.Page.AssertNoError();
|
|
|
|
await s.GoToStore(storeId, StoreNavPages.Dashboard);
|
|
await s.Page.WaitForSelectorAsync("#Dashboard");
|
|
Assert.DoesNotContain("id=\"SetupGuide\"", await s.Page.ContentAsync());
|
|
|
|
// setup offchain wallet
|
|
await s.Page.Locator("#menu-item-LightningSettings-BTC").ClickAsync();
|
|
await s.AddLightningNode();
|
|
await s.Page.AssertNoError();
|
|
await s.FindAlertMessage(partialText: "BTC Lightning node updated.");
|
|
|
|
// Only click on section links if they exist
|
|
if (await s.Page.Locator("#SectionNav .nav-link").CountAsync() > 0)
|
|
{
|
|
await s.ClickOnAllSectionLinks();
|
|
}
|
|
|
|
await s.GoToInvoices(storeId);
|
|
Assert.Contains("There are no invoices matching your criteria.", await s.Page.ContentAsync());
|
|
var invoiceId = await s.CreateInvoice(storeId);
|
|
await s.FindAlertMessage();
|
|
|
|
var invoiceUrl = s.Page.Url;
|
|
|
|
//let's test archiving an invoice
|
|
Assert.DoesNotContain("Archived", await s.Page.Locator("#btn-archive-toggle").InnerTextAsync());
|
|
await s.Page.Locator("#btn-archive-toggle").ClickAsync();
|
|
Assert.Contains("Unarchive", await s.Page.Locator("#btn-archive-toggle").InnerTextAsync());
|
|
|
|
//check that it no longer appears in list
|
|
await s.GoToInvoices(storeId);
|
|
Assert.DoesNotContain(invoiceId, await s.Page.ContentAsync());
|
|
|
|
//ok, let's unarchive and see that it shows again
|
|
await s.Page.GotoAsync(invoiceUrl);
|
|
await s.Page.Locator("#btn-archive-toggle").ClickAsync();
|
|
await s.FindAlertMessage();
|
|
Assert.DoesNotContain("Unarchive", await s.Page.Locator("#btn-archive-toggle").InnerTextAsync());
|
|
await s.GoToInvoices(storeId);
|
|
await s.Page.WaitForSelectorAsync($"tr[id=invoice_{invoiceId}]");
|
|
Assert.Contains(invoiceId, await s.Page.ContentAsync());
|
|
|
|
// archive via list
|
|
await s.Page.ClickAsync($".mass-action-select[value=\"{invoiceId}\"]");
|
|
await s.Page.ClickAsync("#ArchiveSelected");
|
|
await s.FindAlertMessage(partialText: "1 invoice archived");
|
|
Assert.DoesNotContain(invoiceId, await s.Page.ContentAsync());
|
|
|
|
// unarchive via list
|
|
await s.Page.Locator("#StatusOptionsToggle").ClickAsync();
|
|
await s.Page.Locator("#StatusOptionsIncludeArchived").ClickAsync();
|
|
Assert.Contains(invoiceId, await s.Page.ContentAsync());
|
|
await s.Page.ClickAsync($".mass-action-select[value=\"{invoiceId}\"]");
|
|
await s.Page.ClickAsync("#UnarchiveSelected");
|
|
await s.FindAlertMessage(partialText: "1 invoice unarchived");
|
|
await s.Page.WaitForSelectorAsync($"tr[id=invoice_{invoiceId}]");
|
|
|
|
// When logout out we should not be able to access store and invoice details
|
|
await s.GoToUrl("/account");
|
|
await s.Logout();
|
|
await s.GoToUrl(storeUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
await s.Page.GotoAsync(invoiceUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
await s.GoToRegister();
|
|
|
|
// When logged in as different user we should not be able to access store and invoice details
|
|
var bob = await s.RegisterNewUser();
|
|
await s.GoToUrl(storeUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
await s.Page.GotoAsync(invoiceUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
// s.AssertAccessDenied(); // TODO: Playwright equivalent if needed
|
|
await s.GoToUrl("/account");
|
|
await s.Logout();
|
|
|
|
// Let's add Bob as an employee to alice's store
|
|
await s.LogIn(alice);
|
|
await s.AddUserToStore(storeId, bob, "Employee");
|
|
await s.Logout();
|
|
|
|
// Bob should not have access to store, but should have access to invoice
|
|
await s.LogIn(bob);
|
|
await s.GoToUrl(storeUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
await s.GoToUrl(invoiceUrl);
|
|
await s.Page.AssertNoError();
|
|
await s.GoToUrl("/account");
|
|
await s.Logout();
|
|
await s.LogIn(alice);
|
|
|
|
// Check if we can enable the payment button
|
|
await s.GoToStore(storeId, StoreNavPages.PayButton);
|
|
await s.Page.Locator("#enable-pay-button").ClickAsync();
|
|
await s.Page.Locator("#disable-pay-button").ClickAsync();
|
|
await s.FindAlertMessage();
|
|
await s.GoToStore(storeId);
|
|
Assert.False(await s.Page.Locator("#AnyoneCanCreateInvoice").IsCheckedAsync());
|
|
await s.Page.Locator("#AnyoneCanCreateInvoice").CheckAsync();
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage();
|
|
Assert.True(await s.Page.Locator("#AnyoneCanCreateInvoice").IsCheckedAsync());
|
|
|
|
// Store settings: Set and unset brand color
|
|
await s.GoToStore(storeId);
|
|
await s.Page.Locator("#BrandColor").FillAsync("#f7931a");
|
|
await s.ClickPagePrimary();
|
|
Assert.Contains("Store successfully updated", await (await s.FindAlertMessage()).InnerTextAsync());
|
|
Assert.Equal("#f7931a", await s.Page.Locator("#BrandColor").InputValueAsync());
|
|
await s.Page.Locator("#BrandColor").FillAsync("");
|
|
await s.ClickPagePrimary();
|
|
Assert.Contains("Store successfully updated", await (await s.FindAlertMessage()).InnerTextAsync());
|
|
Assert.Equal(string.Empty, await s.Page.Locator("#BrandColor").InputValueAsync());
|
|
|
|
// Alice should be able to delete the store
|
|
await s.GoToStore(storeId);
|
|
await s.Page.Locator("#DeleteStore").ClickAsync();
|
|
await s.Page.Locator("#ConfirmInput").FillAsync("DELETE");
|
|
await s.Page.Locator("#ConfirmContinue").ClickAsync();
|
|
await s.GoToUrl(storeUrl);
|
|
Assert.Contains("ReturnUrl", s.Page.Url);
|
|
|
|
// Archive store
|
|
(storeName, storeId) = await s.CreateNewStore();
|
|
|
|
await s.Page.Locator("#StoreSelectorToggle").ClickAsync();
|
|
Assert.Contains(storeName, await s.Page.Locator("#StoreSelectorMenu").InnerTextAsync());
|
|
await s.Page.Locator($"#StoreSelectorMenuItem-{storeId}").ClickAsync();
|
|
await s.GoToStore(storeId);
|
|
await s.Page.Locator("#btn-archive-toggle").ClickAsync();
|
|
Assert.Contains("The store has been archived and will no longer appear in the stores list by default.", await (await s.FindAlertMessage()).InnerTextAsync());
|
|
|
|
await s.Page.Locator("#StoreSelectorToggle").ClickAsync();
|
|
Assert.DoesNotContain(storeName, await s.Page.Locator("#StoreSelectorMenu").InnerTextAsync());
|
|
Assert.Contains("1 Archived Store", await s.Page.Locator("#StoreSelectorMenu").InnerTextAsync());
|
|
await s.Page.Locator("#StoreSelectorArchived").ClickAsync();
|
|
|
|
var storeLink = s.Page.Locator($"#Store-{storeId}");
|
|
Assert.Contains(storeName, await storeLink.InnerTextAsync());
|
|
await s.GoToStore(storeId);
|
|
await s.Page.Locator("#btn-archive-toggle").ClickAsync();
|
|
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", await (await s.FindAlertMessage()).InnerTextAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanUseCoinSelection()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
var (_, storeId) = await s.CreateNewStore();
|
|
await s.GenerateWallet("BTC", "", false, true);
|
|
var walletId = new WalletId(storeId, "BTC");
|
|
await s.GoToWallet(walletId, WalletsNavPages.Receive);
|
|
var addressStr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
|
|
var address = BitcoinAddress.Create(addressStr!, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
|
|
}
|
|
var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
|
|
var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
|
|
var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
|
|
var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
|
|
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
|
|
await TestUtils.EventuallyAsync(async () =>
|
|
{
|
|
var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
|
|
var x = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
|
|
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
|
|
wallet.InvalidateCache(x.AccountDerivation);
|
|
Assert.Contains(
|
|
await wallet.GetUnspentCoins(x.AccountDerivation),
|
|
coin => coin.OutPoint == spentOutpoint);
|
|
});
|
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
|
await s.GoToWallet(walletId, WalletsNavPages.Send);
|
|
await s.Page.Locator("#toggleInputSelection").ClickAsync();
|
|
await s.Page.Locator($"[id='{spentOutpoint}']").WaitForAsync();
|
|
Assert.Equal("true", (await s.Page.Locator("[name='InputSelection']").InputValueAsync()).ToLowerInvariant());
|
|
|
|
// Select All test
|
|
await s.Page.Locator("#select-all-checkbox").ClickAsync();
|
|
var selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
|
|
var listItems = await s.Page.Locator("li.list-group-item").AllAsync();
|
|
Assert.Equal(listItems.Count, selectedOptions.Count);
|
|
await s.Page.Locator("#select-all-checkbox").ClickAsync();
|
|
selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
|
|
Assert.Empty(selectedOptions);
|
|
|
|
await s.Page.Locator($"[id='{spentOutpoint}']").ClickAsync();
|
|
selectedOptions = await s.Page.Locator("[name='SelectedInputs'] option[selected]").AllAsync();
|
|
Assert.Single(selectedOptions);
|
|
|
|
var bob = new NBitcoin.Key().PubKey.Hash.GetAddress(NBitcoin.Network.RegTest);
|
|
await s.Page.Locator("[name='Outputs[0].DestinationAddress']").FillAsync(bob.ToString());
|
|
var amountInput = s.Page.Locator("[name='Outputs[0].Amount']");
|
|
await amountInput.FillAsync("0.3");
|
|
var checkboxElement = s.Page.Locator("input[type='checkbox'][name='Outputs[0].SubtractFeesFromOutput']");
|
|
if (!await checkboxElement.IsCheckedAsync())
|
|
{
|
|
await checkboxElement.ClickAsync();
|
|
}
|
|
await s.Page.Locator("#SignTransaction").ClickAsync();
|
|
await s.Page.Locator("button[value='broadcast']").ClickAsync();
|
|
var happyElement = await s.FindAlertMessage();
|
|
var happyText = await happyElement.InnerTextAsync();
|
|
var txid = System.Text.RegularExpressions.Regex.Match(happyText, @"\((.*)\)").Groups[1].Value;
|
|
|
|
tx = await s.Server.ExplorerNode.GetRawTransactionAsync(new uint256(txid));
|
|
Assert.Single(tx.Inputs);
|
|
Assert.Equal(spentOutpoint, tx.Inputs[0].PrevOut);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
[Trait("Lightning", "Lightning")]
|
|
public async Task CanAccessUserStoreAsAdmin()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
s.Server.ActivateLightning();
|
|
await s.StartAsync();
|
|
await s.Server.EnsureChannelsSetup();
|
|
|
|
// Setup user, store and wallets
|
|
await s.RegisterNewUser();
|
|
var (_, storeId) = await s.CreateNewStore();
|
|
await s.GoToStore();
|
|
await s.GenerateWallet(isHotWallet: true);
|
|
await s.AddLightningNode(LightningConnectionType.CLightning, false);
|
|
|
|
// Add apps
|
|
await s.CreateApp("PointOfSale");
|
|
await s.CreateApp("Crowdfund");
|
|
await s.Logout();
|
|
|
|
// Setup admin and check access
|
|
await s.GoToRegister();
|
|
await s.RegisterNewUser(true);
|
|
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
|
|
|
|
// Admin access
|
|
await s.AssertPageAccess(false, GetStorePath(""));
|
|
await s.AssertPageAccess(true, GetStorePath("reports"));
|
|
await s.AssertPageAccess(true, GetStorePath("invoices"));
|
|
await s.AssertPageAccess(false, GetStorePath("invoices/create"));
|
|
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
|
await s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
|
|
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
|
await s.AssertPageAccess(true, GetStorePath("payouts"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
|
await s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
|
await s.AssertPageAccess(false, GetStorePath("apps/create"));
|
|
|
|
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks",
|
|
"payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC",
|
|
"emails/rules", "email-settings", "forms"};
|
|
foreach (var path in storeSettingsPaths)
|
|
{ // should have view access to settings, but no submit buttons or create links
|
|
s.TestLogs.LogInformation($"Checking access to store page {path} as admin");
|
|
await s.AssertPageAccess(true, $"stores/{storeId}/{path}");
|
|
if (path != "payout-processors")
|
|
{
|
|
Assert.Equal(0, await s.Page.Locator("#mainContent .btn-primary").CountAsync());
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
public async Task CanChangeUserRoles()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
await s.StartAsync();
|
|
|
|
// Setup users and store
|
|
var employee = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.Logout();
|
|
await s.GoToRegister();
|
|
var owner = await s.RegisterNewUser(true);
|
|
var (_, storeId) = await s.CreateNewStore();
|
|
await s.GoToStore();
|
|
await s.AddUserToStore(storeId, employee, "Employee");
|
|
|
|
// Should successfully change the role
|
|
var userRows = await s.Page.Locator("#StoreUsersList tr").AllAsync();
|
|
Assert.Equal(2, userRows.Count);
|
|
ILocator employeeRow = null;
|
|
foreach (var row in userRows)
|
|
{
|
|
if ((await row.InnerTextAsync()).Contains(employee, StringComparison.InvariantCultureIgnoreCase)) employeeRow = row;
|
|
}
|
|
Assert.NotNull(employeeRow);
|
|
await employeeRow.Locator("a[data-bs-target='#EditModal']").ClickAsync();
|
|
Assert.Equal(employee, await s.Page.InnerTextAsync("#EditUserEmail"));
|
|
await s.Page.SelectOptionAsync("#EditUserRole", "Manager");
|
|
await s.Page.ClickAsync("#EditContinue");
|
|
await s.FindAlertMessage(partialText: $"The role of {employee} has been changed to Manager.");
|
|
|
|
// Should not see a message when not changing role
|
|
userRows = await s.Page.Locator("#StoreUsersList tr").AllAsync();
|
|
Assert.Equal(2, userRows.Count);
|
|
employeeRow = null;
|
|
foreach (var row in userRows)
|
|
{
|
|
if ((await row.InnerTextAsync()).Contains(employee, StringComparison.InvariantCultureIgnoreCase)) employeeRow = row;
|
|
}
|
|
Assert.NotNull(employeeRow);
|
|
await employeeRow.Locator("a[data-bs-target='#EditModal']").ClickAsync();
|
|
Assert.Equal(employee, await s.Page.InnerTextAsync("#EditUserEmail"));
|
|
await s.Page.ClickAsync("#EditContinue");
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error, "The user already has the role Manager.");
|
|
|
|
// Should not change last owner
|
|
userRows = await s.Page.Locator("#StoreUsersList tr").AllAsync();
|
|
Assert.Equal(2, userRows.Count);
|
|
ILocator ownerRow = null;
|
|
foreach (var row in userRows)
|
|
{
|
|
if ((await row.InnerTextAsync()).Contains(owner, StringComparison.InvariantCultureIgnoreCase)) ownerRow = row;
|
|
}
|
|
Assert.NotNull(ownerRow);
|
|
await ownerRow.Locator("a[data-bs-target='#EditModal']").ClickAsync();
|
|
Assert.Equal(owner, await s.Page.InnerTextAsync("#EditUserEmail"));
|
|
await s.Page.SelectOptionAsync("#EditUserRole", "Employee");
|
|
await s.Page.ClickAsync("#EditContinue");
|
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error, "The user is the last owner. Their role cannot be changed.");
|
|
}
|
|
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
public async Task CanUseRoleManager()
|
|
{
|
|
await using var s = CreatePlaywrightTester(newDb: true);
|
|
await s.StartAsync();
|
|
await s.RegisterNewUser(true);
|
|
await s.GoToHome();
|
|
await s.GoToServer(ServerNavPages.Roles);
|
|
var existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
Assert.Equal(5, existingServerRoles.Count);
|
|
ILocator ownerRow = null;
|
|
ILocator managerRow = null;
|
|
ILocator employeeRow = null;
|
|
ILocator guestRow = null;
|
|
foreach (var roleItem in existingServerRoles)
|
|
{
|
|
var text = await roleItem.TextContentAsync();
|
|
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
ownerRow = roleItem;
|
|
}
|
|
else if (text.Contains("manager", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
managerRow = roleItem;
|
|
}
|
|
else if (text.Contains("employee", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
employeeRow = roleItem;
|
|
}
|
|
else if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
guestRow = roleItem;
|
|
}
|
|
}
|
|
|
|
Assert.NotNull(ownerRow);
|
|
Assert.NotNull(managerRow);
|
|
Assert.NotNull(employeeRow);
|
|
Assert.NotNull(guestRow);
|
|
|
|
var ownerBadges = await ownerRow.Locator(".badge").AllAsync();
|
|
var ownerBadgeTexts = await Task.WhenAll(ownerBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.Contains(ownerBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
Assert.Contains(ownerBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
var managerBadges = await managerRow.Locator(".badge").AllAsync();
|
|
var managerBadgeTexts = await Task.WhenAll(managerBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(managerBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
Assert.Contains(managerBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
var employeeBadges = await employeeRow.Locator(".badge").AllAsync();
|
|
var employeeBadgeTexts = await Task.WhenAll(employeeBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(employeeBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
Assert.Contains(employeeBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
var guestBadges = await guestRow.Locator(".badge").AllAsync();
|
|
var guestBadgeTexts = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(guestBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
Assert.Contains(guestBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
|
await guestRow.Locator("#SetDefault").ClickAsync();
|
|
await s.FindAlertMessage(partialText: "Role set default");
|
|
|
|
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
foreach (var roleItem in existingServerRoles)
|
|
{
|
|
var text = await roleItem.TextContentAsync();
|
|
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
ownerRow = roleItem;
|
|
}
|
|
else if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
guestRow = roleItem;
|
|
}
|
|
}
|
|
guestBadges = await guestRow.Locator(".badge").AllAsync();
|
|
var guestBadgeTexts2 = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.Contains(guestBadgeTexts2, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
ownerBadges = await ownerRow.Locator(".badge").AllAsync();
|
|
var ownerBadgeTexts2 = await Task.WhenAll(ownerBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(ownerBadgeTexts2, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
|
await ownerRow.Locator("#SetDefault").ClickAsync();
|
|
|
|
await s.FindAlertMessage(partialText: "Role set default");
|
|
|
|
await s.CreateNewStore();
|
|
await s.GoToStore(StoreNavPages.Roles);
|
|
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
Assert.Equal(5, existingServerRoles.Count);
|
|
var serverRoleTexts = await Task.WhenAll(existingServerRoles.Select(async element => await element.TextContentAsync()));
|
|
Assert.Equal(4, serverRoleTexts.Count(text => text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
|
|
|
foreach (var roleItem in existingServerRoles)
|
|
{
|
|
var text = await roleItem.TextContentAsync();
|
|
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
ownerRow = roleItem;
|
|
break;
|
|
}
|
|
}
|
|
|
|
await ownerRow.Locator("text=Remove").ClickAsync();
|
|
await s.Page.WaitForLoadStateAsync();
|
|
Assert.DoesNotContain("ConfirmContinue", await s.Page.ContentAsync());
|
|
await s.Page.GoBackAsync();
|
|
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
foreach (var roleItem in existingServerRoles)
|
|
{
|
|
var text = await roleItem.TextContentAsync();
|
|
if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
guestRow = roleItem;
|
|
break;
|
|
}
|
|
}
|
|
|
|
await guestRow.Locator("text=Remove").ClickAsync();
|
|
await s.Page.ClickAsync("#ConfirmContinue");
|
|
await s.FindAlertMessage();
|
|
|
|
await s.GoToStore(StoreNavPages.Roles);
|
|
await s.ClickPagePrimary();
|
|
|
|
Assert.Contains("Create role", await s.Page.ContentAsync());
|
|
await s.ClickPagePrimary();
|
|
await s.Page.Locator("#Role").FillAsync("store role");
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage();
|
|
|
|
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
foreach (var roleItem in existingServerRoles)
|
|
{
|
|
var text = await roleItem.TextContentAsync();
|
|
if (text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
guestRow = roleItem;
|
|
break;
|
|
}
|
|
}
|
|
|
|
guestBadges = await guestRow.Locator(".badge").AllAsync();
|
|
var guestBadgeTexts3 = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(guestBadgeTexts3, text => text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
|
|
await s.GoToStore(StoreNavPages.Users);
|
|
var options = await s.Page.Locator("#Role option").AllAsync();
|
|
Assert.Equal(4, options.Count);
|
|
var optionTexts = await Task.WhenAll(options.Select(async element => await element.TextContentAsync()));
|
|
Assert.Contains(optionTexts, text => text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
|
await s.CreateNewStore();
|
|
await s.GoToStore(StoreNavPages.Roles);
|
|
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
|
|
Assert.Equal(4, existingServerRoles.Count);
|
|
var serverRoleTexts2 = await Task.WhenAll(existingServerRoles.Select(async element => await element.TextContentAsync()));
|
|
Assert.Equal(3, serverRoleTexts2.Count(text => text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
|
Assert.Equal(0, serverRoleTexts2.Count(text => text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
|
|
await s.GoToStore(StoreNavPages.Users);
|
|
options = await s.Page.Locator("#Role option").AllAsync();
|
|
Assert.Equal(3, options.Count);
|
|
var optionTexts2 = await Task.WhenAll(options.Select(async element => await element.TextContentAsync()));
|
|
Assert.DoesNotContain(optionTexts2, text => text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
await s.Page.Locator("#Email").FillAsync(s.AsTestAccount().Email);
|
|
await s.Page.Locator("#Role").SelectOptionAsync("Owner");
|
|
await s.Page.ClickAsync("#AddUser");
|
|
Assert.Contains("The user already has the role Owner.", await s.Page.Locator(".validation-summary-errors").TextContentAsync());
|
|
await s.Page.Locator("#Role").SelectOptionAsync("Manager");
|
|
await s.Page.ClickAsync("#AddUser");
|
|
Assert.Contains("The user is the last owner. Their role cannot be changed.", await s.Page.Locator(".validation-summary-errors").TextContentAsync());
|
|
|
|
await s.GoToStore(StoreNavPages.Roles);
|
|
await s.ClickPagePrimary();
|
|
await s.Page.Locator("#Role").FillAsync("Malice");
|
|
|
|
await s.Page.EvaluateAsync($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
|
|
|
|
await s.ClickPagePrimary();
|
|
await s.FindAlertMessage();
|
|
Assert.Contains("Malice", await s.Page.ContentAsync());
|
|
Assert.DoesNotContain(Policies.CanModifyServerSettings, await s.Page.ContentAsync());
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Playwright", "Playwright")]
|
|
public async Task CanSigninWithLoginCode()
|
|
{
|
|
await using var s = CreatePlaywrightTester();
|
|
await s.StartAsync();
|
|
var user = await s.RegisterNewUser();
|
|
await s.GoToHome();
|
|
await s.GoToProfile(ManageNavPages.LoginCodes);
|
|
|
|
string code = null;
|
|
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
|
|
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
|
|
string prevCode = code;
|
|
await s.Page.ReloadAsync();
|
|
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
|
|
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
|
|
Assert.NotEqual(prevCode, code);
|
|
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
|
|
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
|
|
await s.Logout();
|
|
await s.GoToLogin();
|
|
await s.Page.EvaluateAsync("document.getElementById('LoginCode').value = 'bad code'");
|
|
await s.Page.EvaluateAsync("document.getElementById('logincode-form').submit()");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
await s.GoToLogin();
|
|
await s.Page.EvaluateAsync($"document.getElementById('LoginCode').value = '{code}'");
|
|
await s.Page.EvaluateAsync("document.getElementById('logincode-form').submit()");
|
|
await s.Page.WaitForLoadStateAsync();
|
|
await s.Page.WaitForLoadStateAsync();
|
|
|
|
await s.CreateNewStore();
|
|
await s.GoToHome();
|
|
await s.Page.WaitForLoadStateAsync();
|
|
await s.Page.WaitForLoadStateAsync();
|
|
var content = await s.Page.ContentAsync();
|
|
Assert.Contains(user, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
|