mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Migrate CanManageWallet test to Playwright (#6797)
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Playwright;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace BTCPayServer.Tests
|
||||
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
|
||||
{
|
||||
Headless = Server.PayTester.InContainer,
|
||||
SlowMo = Server.PayTester.InContainer ? 0 : 50, // Add slight delay, nicer during dev
|
||||
SlowMo = 0 // 50 if you want to slow down
|
||||
});
|
||||
var context = await Browser.NewContextAsync();
|
||||
Page = await context.NewPageAsync();
|
||||
@@ -230,8 +230,9 @@ namespace BTCPayServer.Tests
|
||||
var isImport = !string.IsNullOrEmpty(seed);
|
||||
await GoToWalletSettings(cryptoCode);
|
||||
// Replace previous wallet case
|
||||
if (await Page.Locator("#ChangeWalletLink").IsVisibleAsync())
|
||||
if (await Page.Locator("#ActionsDropdownToggle").IsVisibleAsync())
|
||||
{
|
||||
TestLogs.LogInformation($"Replacing the wallet");
|
||||
await Page.ClickAsync("#ActionsDropdownToggle");
|
||||
await Page.ClickAsync("#ChangeWalletLink");
|
||||
await Page.FillAsync("#ConfirmInput", "REPLACE");
|
||||
@@ -624,24 +625,25 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await GoToWallet(walletId, navPages: WalletsNavPages.Transactions);
|
||||
await Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync(new() { State = WaitForSelectorState.Visible });
|
||||
return new WalletTransactionsPMO(Page);
|
||||
return new WalletTransactionsPMO(this);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public class WalletTransactionsPMO(IPage page)
|
||||
public class WalletTransactionsPMO(PlaywrightTester tester)
|
||||
{
|
||||
public Task SelectAll() => page.SetCheckedAsync(".mass-action-select-all", true);
|
||||
private IPage Page => tester.Page;
|
||||
public Task SelectAll() => Page.SetCheckedAsync(".mass-action-select-all", true);
|
||||
public async Task Select(params uint256[] txs)
|
||||
{
|
||||
foreach (var txId in txs)
|
||||
{
|
||||
await page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true);
|
||||
await Page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task BumpFeeSelected() => page.ClickAsync("#BumpFee");
|
||||
public Task BumpFeeSelected() => Page.ClickAsync("#BumpFee");
|
||||
|
||||
public Task BumpFee(uint256? txId = null) => page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn");
|
||||
public Task BumpFee(uint256? txId = null) => Page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn");
|
||||
static string TxRowSelector(uint256? txId = null) => txId is null ? ".transaction-row:first-of-type" : $".transaction-row[data-value=\"{txId}\"]";
|
||||
|
||||
public Task AssertHasLabels(string label) => AssertHasLabels(null, label);
|
||||
@@ -653,23 +655,32 @@ namespace BTCPayServer.Tests
|
||||
retry:
|
||||
await WaitTransactionsLoaded();
|
||||
var selector = $"{TxRowSelector(txId)} .transaction-label[data-value=\"{label}\"]";
|
||||
if (await page.Locator(selector).IsVisibleAsync())
|
||||
if (await Page.Locator(selector).IsVisibleAsync())
|
||||
return;
|
||||
if (tried > 5)
|
||||
{
|
||||
await page.Locator(selector).WaitForAsync();
|
||||
try
|
||||
{
|
||||
await Page.Locator(selector).WaitForAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tester.TakeScreenshot("AssertHasLabels.png");
|
||||
throw;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
tried++;
|
||||
await page.ReloadAsync();
|
||||
await Page.ReloadAsync();
|
||||
goto retry;
|
||||
}
|
||||
|
||||
public Task WaitTransactionsLoaded() => page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync();
|
||||
public Task WaitTransactionsLoaded() => Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync();
|
||||
|
||||
public async Task AssertNotFound(uint256 txId)
|
||||
{
|
||||
Assert.False(await page.Locator(TxRowSelector(txId)).IsVisibleAsync());
|
||||
Assert.False(await Page.Locator(TxRowSelector(txId)).IsVisibleAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,6 +701,8 @@ namespace BTCPayServer.Tests
|
||||
public Task Sign() => page.ClickAsync("#SignTransaction");
|
||||
|
||||
public Task SetFeeRate(decimal val) => page.FillAsync("[name=\"FeeSatoshiPerByte\"]", val.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
public Task FillAmount(decimal amount) => page.FillAsync("[name='Outputs[0].Amount']", amount.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public async Task MarkAsSettled()
|
||||
@@ -698,5 +711,22 @@ namespace BTCPayServer.Tests
|
||||
var client = await this.AsTestAccount().CreateClient();
|
||||
await client.MarkInvoiceStatus(StoreId, txId, new() { Status = InvoiceStatus.Settled });
|
||||
}
|
||||
|
||||
public WalletTransactionsPMO InWalletTransactions() => new WalletTransactionsPMO(this);
|
||||
|
||||
public WalletBroadcastPMO InBroadcast() => new WalletBroadcastPMO(Page);
|
||||
|
||||
public class WalletBroadcastPMO(IPage page)
|
||||
{
|
||||
public async Task AssertSending(BitcoinAddress destination, decimal amount)
|
||||
{
|
||||
await page.WaitForSelectorAsync($"td:text('{destination}')");
|
||||
var amt = await page.WaitForSelectorAsync($"td:text('{destination}') >> .. >> :nth-child(3)");
|
||||
var actual = (await amt!.TextContentAsync()).NormalizeWhitespaces();
|
||||
var expected = ("-" + Money.Coins(amount).ToString() + " " + "BTC").NormalizeWhitespaces();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
public async Task Broadcast() => await page.ClickAsync("#BroadcastTransaction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Playwright;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@@ -55,10 +62,10 @@ namespace BTCPayServer.Tests
|
||||
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");
|
||||
var popOutPage = await s.Page.Context.WaitForPageAsync();
|
||||
string invoiceId;
|
||||
await using (var o = await s.SwitchPage(popOutPage))
|
||||
await using (_ = await s.SwitchPage(opening))
|
||||
{
|
||||
await s.Page.Locator("button[type='submit']").First.ClickAsync();
|
||||
await s.Page.FillAsync("[name='buyerEmail']", "aa@aa.com");
|
||||
@@ -79,8 +86,9 @@ namespace BTCPayServer.Tests
|
||||
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");
|
||||
popOutPage = await s.Page.Context.WaitForPageAsync();
|
||||
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");
|
||||
@@ -581,5 +589,265 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
|
||||
}
|
||||
|
||||
[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.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}");
|
||||
await s.Page.ClickAsync("#WalletNav-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.Page.ClickAsync("#WalletNav-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);
|
||||
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);
|
||||
|
||||
await s.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}");
|
||||
await s.Page.ClickAsync("#WalletNav-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.Page.ClickAsync("#WalletNav-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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1493,238 +1493,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanManageWallet()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
(_, string storeId) = 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
|
||||
s.GenerateWallet(cryptoCode, "", true);
|
||||
|
||||
//let's test quickly the wallet send page
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
|
||||
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
Assert.Contains("Destination Address field is required", s.Driver.PageSource);
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
|
||||
|
||||
//generate a receiving address
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
|
||||
// Can add a label?
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
|
||||
});
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
|
||||
});
|
||||
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("test-label", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
// Remove a label
|
||||
s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click();
|
||||
await Task.Delay(500);
|
||||
s.Driver.ExecuteJavaScript("var 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.Driver.Navigate().RefreshAsync();
|
||||
Assert.DoesNotContain("test-label", s.Driver.PageSource);
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
|
||||
//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);
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
|
||||
// Check the label is applied to the tx
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
// Sometimes this fails in local, but not CI
|
||||
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
|
||||
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
s.GenerateWallet(cryptoCode, "", true);
|
||||
s.GoToWallet(null, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
|
||||
var invoiceId = 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);
|
||||
s.GoToStore(storeId);
|
||||
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
|
||||
|
||||
//lets import and save private keys
|
||||
invoiceId = 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 = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Coins(3.0m));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
s.GoToStore(storeId);
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.ClickOnAllSectionLinks();
|
||||
|
||||
// Make sure wallet info is correct
|
||||
s.GoToWalletSettings(cryptoCode);
|
||||
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
|
||||
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
|
||||
Assert.Contains("m/84'/1'/0'",
|
||||
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
|
||||
|
||||
// Make sure we can rescan, because we are admin!
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("Rescan")).Click();
|
||||
Assert.Contains("The batch size make sure", s.Driver.PageSource);
|
||||
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']"));
|
||||
|
||||
var walletTransactionUri = new Uri(s.Driver.Url);
|
||||
|
||||
// Send to bob
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, bob, 1);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
// Back button should lead back to the previous page inside the send wizard
|
||||
var backUrl = s.Driver.FindElement(By.Id("GoBack")).GetAttribute("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 = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
|
||||
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
|
||||
|
||||
// Broadcast
|
||||
Assert.Contains(bob.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("1.00000000", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
|
||||
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, jack, 0.01m);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
|
||||
Assert.Contains(jack.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("0.01000000", s.Driver.PageSource);
|
||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
|
||||
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);
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
|
||||
Assert.Equal(parsedBip21.Amount.ToString(false),
|
||||
s.Driver.FindElement(By.Id("Outputs_0__Amount")).GetAttribute("value"));
|
||||
Assert.Equal(parsedBip21.Address.ToString(),
|
||||
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value"));
|
||||
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
s.GoToWalletSettings(cryptoCode);
|
||||
var settingsUri = new Uri(s.Driver.Url);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewSeed")).Click();
|
||||
|
||||
// Seed backup page
|
||||
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First()
|
||||
.GetAttribute("data-mnemonic");
|
||||
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
|
||||
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.",
|
||||
s.Driver.PageSource);
|
||||
|
||||
// No confirmation, just a link to return to the wallet
|
||||
Assert.Empty(s.Driver.FindElements(By.Id("confirm")));
|
||||
s.Driver.FindElement(By.Id("proceed")).Click();
|
||||
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
|
||||
|
||||
// Once more, test the cancel link of the wallet send page leads back to the previous page
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
|
||||
Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl);
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
|
||||
|
||||
// Transactions list contains export, ensure functions are present.
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id("BumpFee"));
|
||||
|
||||
// JSON export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportJSON")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
|
||||
Assert.EndsWith("export?format=json", s.Driver.Url);
|
||||
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// CSV export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportCSV")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// BIP-329 export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BIP/@EntryIndexedValue">BIP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BTC/@EntryIndexedValue">BTC</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CPFP/@EntryIndexedValue">CPFP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HWI/@EntryIndexedValue">HWI</s:String>
|
||||
|
||||
Reference in New Issue
Block a user