From e49debdeccf458792bc48bb7392226e98abccb2e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 26 Apr 2025 12:59:31 +0900 Subject: [PATCH] Move CanPlayWithPSBT to Playwright --- BTCPayServer.Tests/PSBTTests.cs | 111 ++++++++++++------------- BTCPayServer.Tests/PlaywrightTester.cs | 54 ++++++++---- 2 files changed, 93 insertions(+), 72 deletions(-) diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index da0243ac6..6c9d20054 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -17,52 +17,48 @@ using Xunit.Abstractions; namespace BTCPayServer.Tests { - public class PSBTTests : UnitTestBase + public class PSBTTests(ITestOutputHelper helper) : UnitTestBase(helper) { - public PSBTTests(ITestOutputHelper helper) : base(helper) - { - } - [Fact] - [Trait("Selenium", "Selenium")] + [Trait("Playwright", "Playwright")] public async Task CanPlayWithPSBT() { - using var s = CreateSeleniumTester(newDb: true); + using var s = CreatePlaywrightTester(newDb: true); await s.StartAsync(); - s.RegisterNewUser(true); - var hot = s.CreateNewStore(); - var seed = s.GenerateWallet(isHotWallet: true); - var cold = s.CreateNewStore(); - s.GenerateWallet(isHotWallet: false, seed: seed.ToString()); + await s.RegisterNewUser(true); + var hot = await s.CreateNewStore(); + var seed = await s.GenerateWallet(isHotWallet: true); + var cold = await s.CreateNewStore(); + await s.GenerateWallet(isHotWallet: false, seed: seed.ToString()); // Scenario 1: one user has two stores sharing same seed // one store is hot wallet, the other not. // Here, the cold wallet create a PSBT, then we switch to hot wallet to sign // the PSBT and broadcast - s.GoToStore(cold.storeId); + await s.GoToStore(cold.storeId); var address = await s.FundStoreWallet(); - Thread.Sleep(1000); - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); - SendAllTo(s, address); - s.Driver.FindElement(By.Id("SignWithPSBT")).Click(); + await Task.Delay(1000); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); + await SendAllTo(s, address); + await s.Page.ClickAsync("#SignWithPSBT"); - var psbt = ExtractPSBT(s); + var psbt = await ExtractPSBT(s); - s.GoToStore(hot.storeId); - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); - s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt); - s.Driver.FindElement(By.Id("Decode")).Click(); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - s.FindAlertMessage(); + await s.GoToStore(hot.storeId); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + await s.Page.Locator("[name='PSBT']").FillAsync(psbt); + await s.Page.ClickAsync("#Decode"); + await s.Page.ClickAsync("#SignTransaction"); + await s.Page.ClickAsync("#BroadcastTransaction"); + await s.FindAlertMessage(); // Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually // entering the seed on the cold wallet. - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); - SendAllTo(s, address); - psbt = ExtractPSBT(s); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send); + await SendAllTo(s, address); + psbt = await ExtractPSBT(s); // Let's check it has been signed, then remove the signature. // Also remove the hdkeys so we can test the update later @@ -75,53 +71,52 @@ namespace BTCPayServer.Tests psbtParsed.Inputs[0].HDKeyPaths.Clear(); var skeletonPSBT = psbtParsed; - s.GoToStore(cold.storeId); - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); - s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); - s.Driver.FindElement(By.Id("Decode")).Click(); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - s.Driver.FindElement(By.Id("SignWithSeed")).Click(); - s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString()); - s.Driver.FindElement(By.Id("Submit")).Click(); - s.Driver.FindElement(By.Id("BroadcastTransaction")).Click(); - s.FindAlertMessage(); + await s.GoToStore(cold.storeId); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + await s.Page.Locator("[name='PSBT']").FillAsync(skeletonPSBT.ToBase64()); + await s.Page.ClickAsync("#Decode"); + await s.Page.ClickAsync("#SignTransaction"); + await s.Page.ClickAsync("#SignWithSeed"); + await s.Page.Locator("[name='SeedOrKey']").FillAsync(seed.ToString()); + await s.Page.ClickAsync("#Submit"); + await s.Page.ClickAsync("#BroadcastTransaction"); + await s.FindAlertMessage(); // Let's check if the update feature works - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); - s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64()); - s.Driver.FindElement(By.Id("Decode")).Click(); - s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); - s.Driver.WaitForElement(By.Id("update-psbt")).Click(); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + await s.Page.Locator("[name='PSBT']").FillAsync(skeletonPSBT.ToBase64()); + await s.Page.ClickAsync("#Decode"); + await s.Page.ClickAsync("#PSBTOptionsAdvancedHeader"); + await s.Page.ClickAsync("#update-psbt"); - psbt = ExtractPSBT(s); + psbt = await ExtractPSBT(s); psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); Assert.Empty(psbtParsed.Inputs[0].PartialSigs); // Let's if we can combine the updated psbt (which has hdkeys, but no sig) // with the signed psbt (which has sig, but no hdkeys) - s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); - s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64()); - s.Driver.FindElement(By.Id("Decode")).Click(); - s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click(); - s.Driver.WaitForElement(By.Id("combine-psbt")).Click(); + await s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT); + await s.Page.Locator("[name='PSBT']").FillAsync(psbtParsed.ToBase64()); + await s.Page.ClickAsync("#Decode"); + await s.Page.ClickAsync("#PSBTOptionsAdvancedHeader"); + await s.Page.ClickAsync("#combine-psbt"); signedPSBT.Inputs[0].HDKeyPaths.Clear(); - s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64()); - s.Driver.WaitForElement(By.Id("Submit")).Click(); - - psbt = ExtractPSBT(s); + await s.Page.Locator("[name='PSBT']").FillAsync(signedPSBT.ToBase64()); + await s.Page.ClickAsync("#Submit"); + psbt = await ExtractPSBT(s); psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork); Assert.Single(psbtParsed.Inputs[0].HDKeyPaths); Assert.Single(psbtParsed.Inputs[0].PartialSigs); } - private static void SendAllTo(SeleniumTester s, string address) + private static async Task SendAllTo(PlaywrightTester s, string address) { - s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address); - s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click(); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); + await s.Page.Locator("[name='Outputs[0].DestinationAddress']").FillAsync(address); + await s.Page.ClickAsync(".crypto-balance-link"); + await s.Page.ClickAsync("#SignTransaction"); } - private string ExtractPSBT(SeleniumTester s) => s.Driver.FindElement(By.Id("psbt-base64")).GetAttribute("innerText"); + private Task ExtractPSBT(PlaywrightTester s) => s.Page.Locator("#psbt-base64").TextContentAsync(); } } diff --git a/BTCPayServer.Tests/PlaywrightTester.cs b/BTCPayServer.Tests/PlaywrightTester.cs index d33342407..19961b07c 100644 --- a/BTCPayServer.Tests/PlaywrightTester.cs +++ b/BTCPayServer.Tests/PlaywrightTester.cs @@ -13,6 +13,7 @@ using BTCPayServer.Views.Wallets; using Microsoft.Extensions.Configuration; using Microsoft.Playwright; using NBitcoin; +using NBitcoin.RPC; using OpenQA.Selenium; using Xunit; @@ -253,17 +254,17 @@ namespace BTCPayServer.Tests // Replace previous wallet case if (await Page.Locator("#ChangeWalletLink").IsVisibleAsync()) { - await Page.Locator("#ActionsDropdownToggle").ClickAsync(); - await Page.Locator("#ChangeWalletLink").ClickAsync(); + await Page.ClickAsync("#ActionsDropdownToggle"); + await Page.ClickAsync("#ChangeWalletLink"); await Page.Locator("#ConfirmInput").FillAsync("REPLACE"); - await Page.Locator("#ConfirmContinue").ClickAsync(); + await Page.ClickAsync("#ConfirmContinue"); } if (isImport) { TestLogs.LogInformation("Progressing with existing seed"); - await Page.Locator("#ImportWalletOptionsLink").ClickAsync(); - await Page.Locator("#ImportSeedLink").ClickAsync(); + await Page.ClickAsync("#ImportWalletOptionsLink"); + await Page.ClickAsync("#ImportSeedLink"); await Page.Locator("#ExistingMnemonic").FillAsync(seed); await Page.Locator("#SavePrivateKeys").SetCheckedAsync(isHotWallet); } @@ -271,21 +272,20 @@ namespace BTCPayServer.Tests { var option = isHotWallet ? "Hotwallet" : "Watchonly"; TestLogs.LogInformation($"Generating new seed ({option})"); - await Page.Locator("#GenerateWalletLink").ClickAsync(); - await Page.Locator($"#Generate{option}Link").ClickAsync(); + await Page.ClickAsync("#GenerateWalletLink"); + await Page.ClickAsync($"#Generate{option}Link"); } - await Page.Locator("#ScriptPubKeyType").ClickAsync(); - await Page.Locator($"#ScriptPubKeyType option[value={format}]").ClickAsync(); - await Page.Locator("[data-toggle='collapse'][href='#AdvancedSettings']").ClickAsync(); + await Page.SelectOptionAsync("#ScriptPubKeyType", new SelectOptionValue { Value = format.ToString() }); + await Page.ClickAsync("#AdvancedSettingsButton"); if (importkeys is bool v) await Page.Locator("#ImportKeysToRPC").SetCheckedAsync(v); - await Page.Locator("#Continue").ClickAsync(); + await Page.ClickAsync("#Continue"); if (isImport) { // Confirm addresses - await Page.Locator("#Confirm").ClickAsync(); + await Page.ClickAsync("#Confirm"); } else { @@ -297,8 +297,8 @@ namespace BTCPayServer.Tests } // Confirm seed backup - await Page.Locator("#confirm").ClickAsync(); - await Page.Locator("#submit").ClickAsync(); + await Page.ClickAsync("#confirm"); + await Page.ClickAsync("#submit"); } WalletId = new WalletId(StoreId, cryptoCode); @@ -451,5 +451,31 @@ namespace BTCPayServer.Tests }); Server?.Dispose(); } + + public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m) + { + walletId ??= WalletId; + await GoToWallet(walletId, WalletsNavPages.Receive); + var addressStr = await Page.Locator("#Address").GetAttributeAsync("data-text"); + var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); + for (var i = 0; i < coins; i++) + { + bool mined = false; + retry: + try + { + await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination)); + } + catch (RPCException) when (!mined) + { + mined = true; + await Server.ExplorerNode.GenerateAsync(1); + goto retry; + } + } + await Page.ReloadAsync(); + await Page.Locator("#CancelWizard").ClickAsync(); + return addressStr; + } } }