From 50d4b55f730b495727a3ca5aefb0bf6466716094 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 14 Jan 2022 17:50:29 +0900 Subject: [PATCH] Warning if not using 'simple using' --- .editorconfig | 1 + BTCPayServer.Common/ZipUtils.cs | 10 +- BTCPayServer.Tests/ApiKeysTests.cs | 288 +- BTCPayServer.Tests/BTCPayServerTester.cs | 8 +- BTCPayServer.Tests/CheckoutUITests.cs | 344 +- BTCPayServer.Tests/CrowdfundTests.cs | 440 +- BTCPayServer.Tests/CustomerHttpServer.cs | 22 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 1455 ++++--- BTCPayServer.Tests/LanguageServiceTests.cs | 46 +- BTCPayServer.Tests/POSTests.cs | 68 +- BTCPayServer.Tests/PSBTTests.cs | 182 +- BTCPayServer.Tests/PayJoinTests.cs | 939 +++-- BTCPayServer.Tests/PaymentRequestTests.cs | 338 +- BTCPayServer.Tests/SeleniumTests.cs | 1598 ++++--- BTCPayServer.Tests/ThirdPartyTests.cs | 82 +- BTCPayServer.Tests/UnitTest1.cs | 3693 ++++++++--------- BTCPayServer.Tests/Utils.cs | 26 +- .../Controllers/UIServerController.cs | 20 +- .../Controllers/UIStoresController.Onchain.cs | 6 +- BTCPayServer/Extensions.cs | 8 +- .../CheckConfigurationHostedService.cs | 10 +- .../HostedServices/DynamicDnsHostedService.cs | 12 +- BTCPayServer/Hosting/MigrationStartupTask.cs | 226 +- .../Hosting/ResourceBundleProvider.cs | 20 +- BTCPayServer/Models/GetTokensResponse.cs | 6 +- .../Lightning/LightningLikePaymentHandler.cs | 70 +- .../Payments/Lightning/LightningListener.cs | 52 +- .../Security/Bitpay/TokenRepository.cs | 138 +- .../Security/GreenField/APIKeyRepository.cs | 24 +- BTCPayServer/Services/Apps/AppService.cs | 152 +- BTCPayServer/Services/Cheater.cs | 14 +- .../Services/DelayedTransactionBroadcaster.cs | 26 +- .../Services/Invoices/InvoiceRepository.cs | 283 +- BTCPayServer/Services/LanguageService.cs | 8 +- BTCPayServer/Services/Mails/EmailSender.cs | 10 +- .../PaymentRequestRepository.cs | 154 +- .../Services/Stores/StoreRepository.cs | 176 +- BTCPayServer/Services/WalletRepository.cs | 86 +- .../Storage/Services/StoredFileRepository.cs | 36 +- BTCPayServer/WebSocketHelper.cs | 10 +- 40 files changed, 5355 insertions(+), 5732 deletions(-) diff --git a/.editorconfig b/.editorconfig index b2d120d60..cce17d713 100644 --- a/.editorconfig +++ b/.editorconfig @@ -122,6 +122,7 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false csharp_style_prefer_null_check_over_type_check = true:warning +csharp_prefer_simple_using_statement = true:warning # C++ Files diff --git a/BTCPayServer.Common/ZipUtils.cs b/BTCPayServer.Common/ZipUtils.cs index bd41d7792..8b0dce5ac 100644 --- a/BTCPayServer.Common/ZipUtils.cs +++ b/BTCPayServer.Common/ZipUtils.cs @@ -21,12 +21,10 @@ namespace BTCPayServer public static string Unzip(byte[] bytes) { MemoryStream ms = new MemoryStream(bytes); - using (GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress)) - { - StreamReader reader = new StreamReader(gzip, Encoding.UTF8); - var unzipped = reader.ReadToEnd(); - return unzipped; - } + using GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress); + StreamReader reader = new StreamReader(gzip, Encoding.UTF8); + var unzipped = reader.ReadToEnd(); + return unzipped; } } } diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 5c52c2e12..c194b656f 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -35,166 +35,164 @@ namespace BTCPayServer.Tests //as a user through your profile //as an external application requesting an api key from a user - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + var tester = s.Server; + + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + await user.MakeAdmin(false); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + + //not an admin, so this permission should not show + Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource); + await user.MakeAdmin(); + s.Logout(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource); + + //server management should show now + s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true); + s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true); + s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + + //this api key has access to everything + await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile); + + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, + Policies.CanModifyServerSettings); + + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, + Policies.CanModifyStoreSettings); + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click(); + //there should be a store already by default in the dropdown + var src = s.Driver.PageSource; + var getPermissionValueIndex = + s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']")) + .GetAttribute("name") + .Replace(".Permission", ".SpecificStores[0]"); + var dropdown = s.Driver.FindElement(By.Name(getPermissionValueIndex)); + var option = dropdown.FindElement(By.TagName("option")); + var storeId = option.GetAttribute("value"); + option.Click(); + s.Driver.FindElement(By.Id("Generate")).Click(); + var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, + Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString()); + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.FindElement(By.Id("Generate")).Click(); + var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user); + + await Assert.ThrowsAnyAsync(async () => { - await s.StartAsync(); - var tester = s.Server; + await TestApiAgainstAccessToken("incorrect key", $"{TestApiPath}/me/id", + tester.PayTester.HttpClient); + }); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - await user.MakeAdmin(false); - s.GoToLogin(); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.GoToProfile(ManageNavPages.APIKeys); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); + //let's test the authorized screen now + //options for authorize are: + //applicationName + //redirect + //permissions + //strict + //selectiveStores + //redirect + //appidentifier + var appidentifier = "testapp"; + var callbackUrl = s.ServerUri + "postredirect-callback-test"; + var authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString(); + s.Driver.Navigate().GoToUrl(authUrl); + Assert.Contains(appidentifier, s.Driver.PageSource); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); + Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); + s.Driver.FindElement(By.Id("consent-yes")).Click(); + Assert.Equal(callbackUrl, s.Driver.Url); - //not an admin, so this permission should not show - Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource); - await user.MakeAdmin(); - s.Logout(); - s.GoToLogin(); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.GoToProfile(ManageNavPages.APIKeys); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource); + var apiKeyRepo = s.Server.PayTester.GetService(); + var accessToken = GetAccessTokenFromCallbackResult(s.Driver); - //server management should show now - s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true); - s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true); - s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true); - s.Driver.FindElement(By.Id("Generate")).Click(); - var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(accessToken, tester, user, + (await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions); - //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile); + authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString(); + s.Driver.Navigate().GoToUrl(authUrl); + Assert.DoesNotContain("kukksappname", s.Driver.PageSource); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true); - s.Driver.FindElement(By.Id("Generate")).Click(); - var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; - await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, - Policies.CanModifyServerSettings); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); + s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false); + Assert.Contains("change-store-mode", s.Driver.PageSource); + s.Driver.FindElement(By.Id("consent-yes")).Click(); + Assert.Equal(callbackUrl, s.Driver.Url); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true); - s.Driver.FindElement(By.Id("Generate")).Click(); - var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; - await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, - Policies.CanModifyStoreSettings); + accessToken = GetAccessTokenFromCallbackResult(s.Driver); + await TestApiAgainstAccessToken(accessToken, tester, user, + (await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click(); - //there should be a store already by default in the dropdown - var src = s.Driver.PageSource; - var getPermissionValueIndex = - s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']")) - .GetAttribute("name") - .Replace(".Permission", ".SpecificStores[0]"); - var dropdown = s.Driver.FindElement(By.Name(getPermissionValueIndex)); - var option = dropdown.FindElement(By.TagName("option")); - var storeId = option.GetAttribute("value"); - option.Click(); - s.Driver.FindElement(By.Id("Generate")).Click(); - var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; - await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, - Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString()); + //let's test the app identifier system + authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString(); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.Driver.FindElement(By.Id("Generate")).Click(); - var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; - await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user); + //if it's the same, go to the confirm page + s.Driver.Navigate().GoToUrl(authUrl); + s.Driver.FindElement(By.Id("continue")).Click(); + Assert.Equal(callbackUrl, s.Driver.Url); - await Assert.ThrowsAnyAsync(async () => - { - await TestApiAgainstAccessToken("incorrect key", $"{TestApiPath}/me/id", - tester.PayTester.HttpClient); - }); + //same app but different redirect = nono + authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString(); - //let's test the authorized screen now - //options for authorize are: - //applicationName - //redirect - //permissions - //strict - //selectiveStores - //redirect - //appidentifier - var appidentifier = "testapp"; - var callbackUrl = s.ServerUri + "postredirect-callback-test"; - var authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, - new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString(); - s.Driver.Navigate().GoToUrl(authUrl); - Assert.Contains(appidentifier, s.Driver.PageSource); - Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); - Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); - Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); - s.Driver.FindElement(By.Id("consent-yes")).Click(); - Assert.Equal(callbackUrl, s.Driver.Url); + s.Driver.Navigate().GoToUrl(authUrl); + Assert.False(s.Driver.Url.StartsWith("https://international.com/callback")); - var apiKeyRepo = s.Server.PayTester.GetService(); - var accessToken = GetAccessTokenFromCallbackResult(s.Driver); - - await TestApiAgainstAccessToken(accessToken, tester, user, - (await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions); - - authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, - new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString(); - - s.Driver.Navigate().GoToUrl(authUrl); - Assert.DoesNotContain("kukksappname", s.Driver.PageSource); - - Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); - Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); - - s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false); - Assert.Contains("change-store-mode", s.Driver.PageSource); - s.Driver.FindElement(By.Id("consent-yes")).Click(); - Assert.Equal(callbackUrl, s.Driver.Url); - - accessToken = GetAccessTokenFromCallbackResult(s.Driver); - await TestApiAgainstAccessToken(accessToken, tester, user, - (await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions); - - //let's test the app identifier system - authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, - new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString(); - - //if it's the same, go to the confirm page - s.Driver.Navigate().GoToUrl(authUrl); - s.Driver.FindElement(By.Id("continue")).Click(); - Assert.Equal(callbackUrl, s.Driver.Url); - - //same app but different redirect = nono - authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri, - new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString(); - - s.Driver.Navigate().GoToUrl(authUrl); - Assert.False(s.Driver.Url.StartsWith("https://international.com/callback")); - - // Make sure we can check all permissions when not an admin - await user.MakeAdmin(false); - s.Logout(); - s.GoToLogin(); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.GoToProfile(ManageNavPages.APIKeys); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - int checkedPermissionCount = 0; - foreach (var checkbox in s.Driver.FindElements(By.ClassName("form-check-input"))) - { - checkedPermissionCount++; - checkbox.Click(); - } - s.Driver.FindElement(By.Id("Generate")).Click(); - var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; - var apikeydata = await TestApiAgainstAccessToken(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient); - Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length); + // Make sure we can check all permissions when not an admin + await user.MakeAdmin(false); + s.Logout(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + int checkedPermissionCount = 0; + foreach (var checkbox in s.Driver.FindElements(By.ClassName("form-check-input"))) + { + checkedPermissionCount++; + checkbox.Click(); } + s.Driver.FindElement(By.Id("Generate")).Click(); + var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; + var apikeydata = await TestApiAgainstAccessToken(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient); + Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length); } async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount, diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 4bf2b1082..8630546a1 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -243,11 +243,9 @@ namespace BTCPayServer.Tests private async Task WaitSiteIsOperational() { _ = HttpClient.GetAsync("/").ConfigureAwait(false); - using (var cts = new CancellationTokenSource(20_000)) - { - var synching = WaitIsFullySynched(cts.Token); - await Task.WhenAll(synching).ConfigureAwait(false); - } + using var cts = new CancellationTokenSource(20_000); + var synching = WaitIsFullySynched(cts.Token); + await Task.WhenAll(synching).ConfigureAwait(false); // Opportunistic call to wake up view compilation in debug mode, we don't need to await. } diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index dcd45685b..6bebeec01 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -23,227 +23,215 @@ namespace BTCPayServer.Tests public async Task CanHandleRefundEmailForm() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme("BTC"); + s.GoToStore(StoreNavPages.CheckoutAppearance); + s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click(); + s.Driver.FindElement(By.Name("command")).Click(); + + var emailAlreadyThereInvoiceId = s.CreateInvoice(100, "USD", "a@g.com"); + s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.GoToHome(); + s.CreateInvoice(); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + + Assert.True(s.Driver.FindElement(By.Id("emailAddressFormInput")).Displayed); + s.Driver.FindElement(By.Id("emailAddressFormInput")).SendKeys("xxx"); + s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")) + .Click(); + var formInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); + + Assert.True(formInput.Displayed); + Assert.Contains("form-input-invalid", formInput.GetAttribute("class")); + formInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); + formInput.SendKeys("@g.com"); + var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")); + actionButton.Click(); + try // Sometimes the click only take the focus, without actually really clicking on it... { - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme("BTC"); - s.GoToStore(StoreNavPages.CheckoutAppearance); - s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click(); - s.Driver.FindElement(By.Name("command")).Click(); - - var emailAlreadyThereInvoiceId = s.CreateInvoice(100, "USD", "a@g.com"); - s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - s.GoToHome(); - s.CreateInvoice(); - s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); - s.Driver.AssertNoError(); - s.Driver.Navigate().Back(); - s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); - Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); - - Assert.True(s.Driver.FindElement(By.Id("emailAddressFormInput")).Displayed); - s.Driver.FindElement(By.Id("emailAddressFormInput")).SendKeys("xxx"); - s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")) - .Click(); - var formInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); - - Assert.True(formInput.Displayed); - Assert.Contains("form-input-invalid", formInput.GetAttribute("class")); - formInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); - formInput.SendKeys("@g.com"); - var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")); actionButton.Click(); - try // Sometimes the click only take the focus, without actually really clicking on it... - { - actionButton.Click(); - } - catch { } - - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - s.Driver.Navigate().Refresh(); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); } + catch { } + + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); } [Fact(Timeout = TestTimeout)] public async Task CanHandleRefundEmailForm2() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + // Prepare user account and store + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme("BTC"); + + // Now create an invoice that requires a refund email + var invoice = s.CreateInvoice(100, "USD", "", null, true); + s.GoToInvoiceCheckout(invoice); + + var emailInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); + Assert.True(emailInput.Displayed); + + emailInput.SendKeys("a@g.com"); + + var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")); + actionButton.Click(); + try // Sometimes the click only take the focus, without actually really clicking on it... { - // Prepare user account and store - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme("BTC"); - - // Now create an invoice that requires a refund email - var invoice = s.CreateInvoice(100, "USD", "", null, true); - s.GoToInvoiceCheckout(invoice); - - var emailInput = s.Driver.FindElement(By.Id("emailAddressFormInput")); - Assert.True(emailInput.Displayed); - - emailInput.SendKeys("a@g.com"); - - var actionButton = s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button")); actionButton.Click(); - try // Sometimes the click only take the focus, without actually really clicking on it... - { - actionButton.Click(); - } - catch { } - - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - s.Driver.Navigate().Refresh(); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - - s.GoToHome(); - - // Now create an invoice that doesn't require a refund email - s.CreateInvoice(100, "USD", "", null, false); - s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); - s.Driver.AssertNoError(); - s.Driver.Navigate().Back(); - s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); - Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - s.Driver.Navigate().Refresh(); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - - s.GoToHome(); - - // Now create an invoice that requires refund email but already has one set, email input shouldn't show up - s.CreateInvoice(100, "USD", "a@g.com", null, true); - s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); - s.Driver.AssertNoError(); - s.Driver.Navigate().Back(); - s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); - Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); - s.Driver.Navigate().Refresh(); - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); } + catch { } + + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + + s.GoToHome(); + + // Now create an invoice that doesn't require a refund email + s.CreateInvoice(100, "USD", "", null, false); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + + s.GoToHome(); + + // Now create an invoice that requires refund email but already has one set, email input shouldn't show up + s.CreateInvoice(100, "USD", "a@g.com", null, true); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + s.Driver.AssertNoError(); + s.Driver.Navigate().Back(); + s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click(); + Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl"))); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); } [Fact(Timeout = TestTimeout)] public async Task CanUseLanguageDropdown() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme("BTC"); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme("BTC"); - var invoiceId = s.CreateInvoice(); - s.GoToInvoiceCheckout(invoiceId); - Assert.True(s.Driver.FindElement(By.Id("DefaultLang")).FindElements(By.TagName("option")).Count > 1); - var payWithTextEnglish = s.Driver.FindElement(By.Id("pay-with-text")).Text; + var invoiceId = s.CreateInvoice(); + s.GoToInvoiceCheckout(invoiceId); + Assert.True(s.Driver.FindElement(By.Id("DefaultLang")).FindElements(By.TagName("option")).Count > 1); + var payWithTextEnglish = s.Driver.FindElement(By.Id("pay-with-text")).Text; - var prettyDropdown = s.Driver.FindElement(By.Id("prettydropdown-DefaultLang")); - prettyDropdown.Click(); - await Task.Delay(200); - prettyDropdown.FindElement(By.CssSelector("[data-value=\"da-DK\"]")).Click(); - await Task.Delay(1000); - Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); - s.Driver.Navigate().GoToUrl(s.Driver.Url + "?lang=da-DK"); + var prettyDropdown = s.Driver.FindElement(By.Id("prettydropdown-DefaultLang")); + prettyDropdown.Click(); + await Task.Delay(200); + prettyDropdown.FindElement(By.CssSelector("[data-value=\"da-DK\"]")).Click(); + await Task.Delay(1000); + Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); + s.Driver.Navigate().GoToUrl(s.Driver.Url + "?lang=da-DK"); - Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); + Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); - s.Driver.Quit(); - } + s.Driver.Quit(); } [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanSetDefaultPaymentMethod() { - using (var s = CreateSeleniumTester()) - { - s.Server.ActivateLightning(); - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(true); - s.CreateNewStore(); - s.AddLightningNode(); - s.AddDerivationScheme("BTC"); + using var s = CreateSeleniumTester(); + s.Server.ActivateLightning(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(true); + s.CreateNewStore(); + s.AddLightningNode(); + s.AddDerivationScheme("BTC"); - var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike"); - s.GoToInvoiceCheckout(invoiceId); + var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike"); + s.GoToInvoiceCheckout(invoiceId); - Assert.Equal("Bitcoin (Lightning) (BTC)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text); - s.Driver.Quit(); - } + Assert.Equal("Bitcoin (Lightning) (BTC)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text); + s.Driver.Quit(); } [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanUseLightningSatsFeature() { - using (var s = CreateSeleniumTester()) - { - s.Server.ActivateLightning(); - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(true); - s.CreateNewStore(); - s.AddLightningNode(); - s.GoToStore(); - s.Driver.FindElement(By.Id("Modify-LightningBTC")).Click(); - s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); - s.Driver.FindElement(By.Id("save")).Click(); - Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text); + using var s = CreateSeleniumTester(); + s.Server.ActivateLightning(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(true); + s.CreateNewStore(); + s.AddLightningNode(); + s.GoToStore(); + s.Driver.FindElement(By.Id("Modify-LightningBTC")).Click(); + s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); + s.Driver.FindElement(By.Id("save")).Click(); + Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text); - var invoiceId = s.CreateInvoice(10, "USD", "a@g.com"); - s.GoToInvoiceCheckout(invoiceId); - Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text); - } + var invoiceId = s.CreateInvoice(10, "USD", "a@g.com"); + s.GoToInvoiceCheckout(invoiceId); + Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text); } [Fact(Timeout = TestTimeout)] public async Task CanUseJSModal() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.GoToStore(); + s.AddDerivationScheme(); + var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com"); + var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + s.Driver.Navigate() + .GoToUrl(new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}")); + TestUtils.Eventually(() => { - await s.StartAsync(); - s.GoToRegister(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.GoToStore(); - s.AddDerivationScheme(); - var invoiceId = s.CreateInvoice(0.001m, "BTC", "a@x.com"); - var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); - s.Driver.Navigate() - .GoToUrl(new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}")); - TestUtils.Eventually(() => - { - Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed); - }); - await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice - .GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)) - .GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest), - new Money(0.001m, MoneyUnit.BTC)); + Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed); + }); + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice + .GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)) + .GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest), + new Money(0.001m, MoneyUnit.BTC)); - IWebElement closebutton = null; - TestUtils.Eventually(() => - { - var frameElement = s.Driver.FindElement(By.Name("btcpay")); - var iframe = s.Driver.SwitchTo().Frame(frameElement); - closebutton = iframe.FindElement(By.ClassName("close-action")); - Assert.True(closebutton.Displayed); - }); - closebutton.Click(); - s.Driver.AssertElementNotFound(By.Name("btcpay")); - Assert.Equal(s.Driver.Url, - new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString()); - } + IWebElement closebutton = null; + TestUtils.Eventually(() => + { + var frameElement = s.Driver.FindElement(By.Name("btcpay")); + var iframe = s.Driver.SwitchTo().Frame(frameElement); + closebutton = iframe.FindElement(By.ClassName("close-action")); + Assert.True(closebutton.Displayed); + }); + closebutton.Click(); + s.Driver.AssertElementNotFound(By.Name("btcpay")); + Assert.Equal(s.Driver.Url, + new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString()); } } } diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index 0f6454579..7169055b5 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -26,256 +26,250 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanCreateAndDeleteCrowdfundApp() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - var user2 = tester.NewAccount(); - await user2.GrantAccessAsync(); - var apps = user.GetController(); - var apps2 = user2.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); - Assert.NotNull(vm.SelectedAppType); - Assert.Null(vm.AppName); - vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); - var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); - Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName); - var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); - var appList2 = - Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); - Assert.Single(appList.Apps); - Assert.Empty(appList2.Apps); - Assert.Equal("test", appList.Apps[0].AppName); - Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); - Assert.True(appList.Apps[0].IsOwner); - Assert.Equal(user.StoreId, appList.Apps[0].StoreId); - Assert.IsType(apps2.DeleteApp(appList.Apps[0].Id)); - Assert.IsType(apps.DeleteApp(appList.Apps[0].Id)); - redirectToAction = Assert.IsType(apps.DeleteAppPost(appList.Apps[0].Id).Result); - Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); - appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - Assert.Empty(appList.Apps); - } + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + var user2 = tester.NewAccount(); + await user2.GrantAccessAsync(); + var apps = user.GetController(); + var apps2 = user2.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + Assert.NotNull(vm.SelectedAppType); + Assert.Null(vm.AppName); + vm.AppName = "test"; + vm.SelectedAppType = AppType.Crowdfund.ToString(); + var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); + Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName); + var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + var app = appList.Apps[0]; + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + var appList2 = + Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); + Assert.Single(appList.Apps); + Assert.Empty(appList2.Apps); + Assert.Equal("test", appList.Apps[0].AppName); + Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); + Assert.True(appList.Apps[0].IsOwner); + Assert.Equal(user.StoreId, appList.Apps[0].StoreId); + Assert.IsType(apps2.DeleteApp(appList.Apps[0].Id)); + Assert.IsType(apps.DeleteApp(appList.Apps[0].Id)); + redirectToAction = Assert.IsType(apps.DeleteAppPost(appList.Apps[0].Id).Result); + Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); + appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + Assert.Empty(appList.Apps); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanContributeOnlyWhenAllowed() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + var apps = user.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + vm.AppName = "test"; + vm.SelectedAppType = AppType.Crowdfund.ToString(); + Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); + var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + var app = appList.Apps[0]; + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + + //Scenario 1: Not Enabled - Not Allowed + var crowdfundViewModel = Assert.IsType(Assert + .IsType(apps.UpdateCrowdfund(app.Id)).Model); + crowdfundViewModel.TargetCurrency = "BTC"; + crowdfundViewModel.Enabled = false; + crowdfundViewModel.EndDate = null; + + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + + var anonAppPubsController = tester.PayTester.GetController(); + var publicApps = user.GetController(); + + Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - var apps = user.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); - vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); - Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); - var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + Amount = new decimal(0.01) + }, default)); - //Scenario 1: Not Enabled - Not Allowed - var crowdfundViewModel = Assert.IsType(Assert - .IsType(apps.UpdateCrowdfund(app.Id)).Model); - crowdfundViewModel.TargetCurrency = "BTC"; - crowdfundViewModel.Enabled = false; - crowdfundViewModel.EndDate = null; + Assert.IsType(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + //Scenario 2: Not Enabled But Admin - Allowed + Assert.IsType(await publicApps.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() + { + RedirectToCheckout = false, + Amount = new decimal(0.01) + }, default)); + Assert.IsType(await publicApps.ViewCrowdfund(app.Id, string.Empty)); + Assert.IsType(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); - var anonAppPubsController = tester.PayTester.GetController(); - var publicApps = user.GetController(); + //Scenario 3: Enabled But Start Date > Now - Not Allowed + crowdfundViewModel.StartDate = DateTime.Today.AddDays(2); + crowdfundViewModel.Enabled = true; - Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - Amount = new decimal(0.01) - }, default)); + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() + { + Amount = new decimal(0.01) + }, default)); - Assert.IsType(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); + //Scenario 4: Enabled But End Date < Now - Not Allowed + crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); + crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1); + crowdfundViewModel.Enabled = true; - //Scenario 2: Not Enabled But Admin - Allowed - Assert.IsType(await publicApps.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - RedirectToCheckout = false, - Amount = new decimal(0.01) - }, default)); - Assert.IsType(await publicApps.ViewCrowdfund(app.Id, string.Empty)); - Assert.IsType(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() + { + Amount = new decimal(0.01) + }, default)); - //Scenario 3: Enabled But Start Date > Now - Not Allowed - crowdfundViewModel.StartDate = DateTime.Today.AddDays(2); - crowdfundViewModel.Enabled = true; + //Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed + crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); + crowdfundViewModel.EndDate = DateTime.Today.AddDays(2); + crowdfundViewModel.Enabled = true; + crowdfundViewModel.TargetAmount = 1; + crowdfundViewModel.TargetCurrency = "BTC"; + crowdfundViewModel.EnforceTargetAmount = true; + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() + { + Amount = new decimal(1.01) + }, default)); - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); - Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - Amount = new decimal(0.01) - }, default)); - - //Scenario 4: Enabled But End Date < Now - Not Allowed - crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); - crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1); - crowdfundViewModel.Enabled = true; - - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); - Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - Amount = new decimal(0.01) - }, default)); - - //Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed - crowdfundViewModel.StartDate = DateTime.Today.AddDays(-2); - crowdfundViewModel.EndDate = DateTime.Today.AddDays(2); - crowdfundViewModel.Enabled = true; - crowdfundViewModel.TargetAmount = 1; - crowdfundViewModel.TargetCurrency = "BTC"; - crowdfundViewModel.EnforceTargetAmount = true; - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); - Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - Amount = new decimal(1.01) - }, default)); - - //Scenario 6: Allowed - Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() - { - Amount = new decimal(0.05) - }, default)); - } + //Scenario 6: Allowed + Assert.IsType(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() + { + Amount = new decimal(0.05) + }, default)); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanComputeCrowdfundModel() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + await user.SetNetworkFeeMode(NetworkFeeMode.Never); + var apps = user.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + vm.AppName = "test"; + vm.SelectedAppType = AppType.Crowdfund.ToString(); + Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); + var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + var app = appList.Apps[0]; + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + + TestLogs.LogInformation("We create an invoice with a hardcap"); + var crowdfundViewModel = Assert.IsType(Assert + .IsType(apps.UpdateCrowdfund(app.Id)).Model); + crowdfundViewModel.Enabled = true; + crowdfundViewModel.EndDate = null; + crowdfundViewModel.TargetAmount = 100; + crowdfundViewModel.TargetCurrency = "BTC"; + crowdfundViewModel.UseAllStoreInvoices = true; + crowdfundViewModel.EnforceTargetAmount = true; + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + + var anonAppPubsController = tester.PayTester.GetController(); + var publicApps = user.GetController(); + + var model = Assert.IsType(Assert + .IsType(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model); + + Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount); + Assert.Equal(crowdfundViewModel.EndDate, model.EndDate); + Assert.Equal(crowdfundViewModel.StartDate, model.StartDate); + Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency); + Assert.Equal(0m, model.Info.CurrentAmount); + Assert.Equal(0m, model.Info.CurrentPendingAmount); + Assert.Equal(0m, model.Info.ProgressPercentage); + + TestLogs.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap"); + TestLogs.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution"); + var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - await user.SetNetworkFeeMode(NetworkFeeMode.Never); - var apps = user.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); - vm.AppName = "test"; - vm.SelectedAppType = AppType.Crowdfund.ToString(); - Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); - var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + Buyer = new Buyer() { email = "test@fwf.com" }, + Price = 1m, + Currency = "BTC", + PosData = "posData", + ItemDesc = "Some description", + TransactionSpeed = "high", + FullNotifications = true + }, Facade.Merchant); - TestLogs.LogInformation("We create an invoice with a hardcap"); - var crowdfundViewModel = Assert.IsType(Assert - .IsType(apps.UpdateCrowdfund(app.Id)).Model); - crowdfundViewModel.Enabled = true; - crowdfundViewModel.EndDate = null; - crowdfundViewModel.TargetAmount = 100; - crowdfundViewModel.TargetCurrency = "BTC"; - crowdfundViewModel.UseAllStoreInvoices = true; - crowdfundViewModel.EnforceTargetAmount = true; - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + model = Assert.IsType(Assert + .IsType(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model); - var anonAppPubsController = tester.PayTester.GetController(); - var publicApps = user.GetController(); - - var model = Assert.IsType(Assert - .IsType(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model); - - Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount); - Assert.Equal(crowdfundViewModel.EndDate, model.EndDate); - Assert.Equal(crowdfundViewModel.StartDate, model.StartDate); - Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency); - Assert.Equal(0m, model.Info.CurrentAmount); - Assert.Equal(0m, model.Info.CurrentPendingAmount); - Assert.Equal(0m, model.Info.ProgressPercentage); - - TestLogs.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap"); - TestLogs.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution"); - var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice - { - Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 1m, - Currency = "BTC", - PosData = "posData", - ItemDesc = "Some description", - TransactionSpeed = "high", - FullNotifications = true - }, Facade.Merchant); + Assert.Equal(0m, model.Info.CurrentAmount); + Assert.Equal(1m, model.Info.CurrentPendingAmount); + Assert.Equal(0m, model.Info.ProgressPercentage); + Assert.Equal(1m, model.Info.PendingProgressPercentage); + TestLogs.LogInformation("Let's check current amount change once payment is confirmed"); + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); + tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue); + tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block + TestUtils.Eventually(() => + { model = Assert.IsType(Assert - .IsType(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model); - - Assert.Equal(0m, model.Info.CurrentAmount); - Assert.Equal(1m, model.Info.CurrentPendingAmount); - Assert.Equal(0m, model.Info.ProgressPercentage); - Assert.Equal(1m, model.Info.PendingProgressPercentage); - - TestLogs.LogInformation("Let's check current amount change once payment is confirmed"); - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); - tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue); - tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block - TestUtils.Eventually(() => - { - model = Assert.IsType(Assert - .IsType(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model); - Assert.Equal(1m, model.Info.CurrentAmount); - Assert.Equal(0m, model.Info.CurrentPendingAmount); - }); - - TestLogs.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged"); - var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); - Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version); - Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); - - crowdfundViewModel.UseAllStoreInvoices = false; - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); - - TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged"); - invoice = await user.BitPay.CreateInvoiceAsync(new Invoice - { - Buyer = new Buyer { email = "test@fwf.com" }, - Price = 1m, - Currency = "BTC", - PosData = "posData", - ItemDesc = "Some description", - TransactionSpeed = "high", - FullNotifications = true - }, Facade.Merchant); - invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); - Assert.DoesNotContain(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); - - TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted"); - crowdfundViewModel.EnforceTargetAmount = false; - crowdfundViewModel.UseAllStoreInvoices = true; - Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); - invoice = await user.BitPay.CreateInvoiceAsync(new Invoice - { - Buyer = new Buyer { email = "test@fwf.com" }, - Price = 1m, - Currency = "BTC", - PosData = "posData", - ItemDesc = "Some description", - TransactionSpeed = "high", - FullNotifications = true - }, Facade.Merchant); + .IsType(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model); + Assert.Equal(1m, model.Info.CurrentAmount); Assert.Equal(0m, model.Info.CurrentPendingAmount); - invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); - await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.5m)); - await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.2m)); - TestUtils.Eventually(() => - { - model = Assert.IsType(Assert - .IsType(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model); - Assert.Equal(0.7m, model.Info.CurrentPendingAmount); - }); - } + }); + + TestLogs.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged"); + var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); + Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version); + Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); + + crowdfundViewModel.UseAllStoreInvoices = false; + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + + TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged"); + invoice = await user.BitPay.CreateInvoiceAsync(new Invoice + { + Buyer = new Buyer { email = "test@fwf.com" }, + Price = 1m, + Currency = "BTC", + PosData = "posData", + ItemDesc = "Some description", + TransactionSpeed = "high", + FullNotifications = true + }, Facade.Merchant); + invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult(); + Assert.DoesNotContain(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags); + + TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted"); + crowdfundViewModel.EnforceTargetAmount = false; + crowdfundViewModel.UseAllStoreInvoices = true; + Assert.IsType(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); + invoice = await user.BitPay.CreateInvoiceAsync(new Invoice + { + Buyer = new Buyer { email = "test@fwf.com" }, + Price = 1m, + Currency = "BTC", + PosData = "posData", + ItemDesc = "Some description", + TransactionSpeed = "high", + FullNotifications = true + }, Facade.Merchant); + Assert.Equal(0m, model.Info.CurrentPendingAmount); + invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); + await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.5m)); + await tester.ExplorerNode.SendToAddressAsync(invoiceAddress, Money.Coins(0.2m)); + TestUtils.Eventually(() => + { + model = Assert.IsType(Assert + .IsType(publicApps.ViewCrowdfund(app.Id, string.Empty).Result).Model); + Assert.Equal(0.7m, model.Info.CurrentPendingAmount); + }); } } } diff --git a/BTCPayServer.Tests/CustomerHttpServer.cs b/BTCPayServer.Tests/CustomerHttpServer.cs index 02867faff..c70a5e65e 100644 --- a/BTCPayServer.Tests/CustomerHttpServer.cs +++ b/BTCPayServer.Tests/CustomerHttpServer.cs @@ -42,22 +42,20 @@ namespace BTCPayServer.Tests public async Task GetNextRequest() { - using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000)) + using CancellationTokenSource cancellation = new CancellationTokenSource(2000000); + try { - try + JObject req = null; + while (!await _Requests.Reader.WaitToReadAsync(cancellation.Token) || + !_Requests.Reader.TryRead(out req)) { - JObject req = null; - while (!await _Requests.Reader.WaitToReadAsync(cancellation.Token) || - !_Requests.Reader.TryRead(out req)) - { - } - return req; - } - catch (TaskCanceledException) - { - throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet"); } + return req; + } + catch (TaskCanceledException) + { + throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet"); } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index a56381fbb..5bb0f2f7e 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -60,47 +60,43 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task MissingPermissionTest() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var clientWithWrongPermissions = await user.CreateClient(Policies.CanViewProfile); - var e = await AssertAPIError("missing-permission", () => clientWithWrongPermissions.CreateStore(new CreateStoreRequest() { Name = "mystore" })); - Assert.Equal("missing-permission", e.APIError.Code); - Assert.NotNull(e.APIError.Message); - GreenfieldPermissionAPIError permissionError = Assert.IsType(e.APIError); - Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings); - } + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var clientWithWrongPermissions = await user.CreateClient(Policies.CanViewProfile); + var e = await AssertAPIError("missing-permission", () => clientWithWrongPermissions.CreateStore(new CreateStoreRequest() { Name = "mystore" })); + Assert.Equal("missing-permission", e.APIError.Code); + Assert.NotNull(e.APIError.Message); + GreenfieldPermissionAPIError permissionError = Assert.IsType(e.APIError); + Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ApiKeysControllerTests() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - var client = await user.CreateClient(Policies.CanViewProfile); - var clientBasic = await user.CreateClient(); - //Get current api key - var apiKeyData = await client.GetCurrentAPIKeyInfo(); - Assert.NotNull(apiKeyData); - Assert.Equal(client.APIKey, apiKeyData.ApiKey); - Assert.Single(apiKeyData.Permissions); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + var client = await user.CreateClient(Policies.CanViewProfile); + var clientBasic = await user.CreateClient(); + //Get current api key + var apiKeyData = await client.GetCurrentAPIKeyInfo(); + Assert.NotNull(apiKeyData); + Assert.Equal(client.APIKey, apiKeyData.ApiKey); + Assert.Single(apiKeyData.Permissions); - //a client using Basic Auth has no business here - await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); + //a client using Basic Auth has no business here + await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); - //revoke current api key - await client.RevokeCurrentAPIKeyInfo(); - await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); - //a client using Basic Auth has no business here - await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); - } + //revoke current api key + await client.RevokeCurrentAPIKeyInfo(); + await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); + //a client using Basic Auth has no business here + await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); } [Fact(Timeout = TestTimeout)] @@ -129,57 +125,53 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task SpecificCanModifyStoreCantCreateNewStore() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - await acc.GrantAccessAsync(); - var unrestricted = await acc.CreateClient(); - var response = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "mystore" }); - var apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings", response.Id) } })).ApiKey; - var restricted = new BTCPayServerClient(unrestricted.Host, apiKey); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(); + var unrestricted = await acc.CreateClient(); + var response = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "mystore" }); + var apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings", response.Id) } })).ApiKey; + var restricted = new BTCPayServerClient(unrestricted.Host, apiKey); - // Unscoped permission should be required for create store - await this.AssertHttpError(403, async () => await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" })); - // Unrestricted should work fine - await unrestricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); - // Restricted but unscoped should work fine - apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings") } })).ApiKey; - restricted = new BTCPayServerClient(unrestricted.Host, apiKey); - await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); - } + // Unscoped permission should be required for create store + await this.AssertHttpError(403, async () => await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" })); + // Unrestricted should work fine + await unrestricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); + // Restricted but unscoped should work fine + apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings") } })).ApiKey; + restricted = new BTCPayServerClient(unrestricted.Host, apiKey); + await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanCreateAndDeleteAPIKeyViaAPI() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(); + var unrestricted = await acc.CreateClient(); + var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { - await tester.StartAsync(); - var acc = tester.NewAccount(); - await acc.GrantAccessAsync(); - var unrestricted = await acc.CreateClient(); - var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest() + Label = "Hello world", + Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } + }); + Assert.Equal("Hello world", apiKey.Label); + var p = Assert.Single(apiKey.Permissions); + Assert.Equal(Policies.CanViewProfile, p.Policy); + + var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey); + await AssertHttpError(403, + async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() { - Label = "Hello world", + Label = "Hello world2", Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } - }); - Assert.Equal("Hello world", apiKey.Label); - var p = Assert.Single(apiKey.Permissions); - Assert.Equal(Policies.CanViewProfile, p.Policy); + })); - var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey); - await AssertHttpError(403, - async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() - { - Label = "Hello world2", - Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } - })); - - await unrestricted.RevokeAPIKey(apiKey.ApiKey); - await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey)); - } + await unrestricted.RevokeAPIKey(apiKey.ApiKey); + await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey)); } [Fact(Timeout = TestTimeout)] @@ -225,376 +217,371 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanCreateUsersViaAPI() { - using (var tester = CreateServerTester(newDb: true)) + using var tester = CreateServerTester(newDb: true); + tester.PayTester.DisableRegistration = true; + await tester.StartAsync(); + var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); + await AssertValidationError(new[] { "Email", "Password" }, + async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); + await AssertValidationError(new[] { "Password" }, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test@gmail.com" })); + // Pass too simple + await AssertValidationError(new[] { "Password" }, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" })); + + // We have no admin, so it should work + var user1 = await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); + Assert.Empty(user1.Roles); + + // We have no admin, so it should work + var user2 = await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); + Assert.Empty(user2.Roles); + + // Duplicate email + await AssertValidationError(new[] { "Email" }, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" })); + + // Let's make an admin + var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { - tester.PayTester.DisableRegistration = true; - await tester.StartAsync(); - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await AssertValidationError(new[] { "Email", "Password" }, - async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); - await AssertValidationError(new[] { "Password" }, - async () => await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test@gmail.com" })); - // Pass too simple - await AssertValidationError(new[] { "Password" }, - async () => await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" })); + Email = "admin@gmail.com", + Password = "abceudhqw", + IsAdministrator = true + }); + Assert.Contains("ServerAdmin", admin.Roles); + Assert.NotNull(admin.Created); + Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10); - // We have no admin, so it should work - var user1 = await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); - Assert.Empty(user1.Roles); + // Creating a new user without proper creds is now impossible (unauthorized) + // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection + var ex = await AssertAPIError("unauthenticated", + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); + Assert.Equal("New user creation isn't authorized to users who are not admin", ex.APIError.Message); - // We have no admin, so it should work - var user2 = await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); - Assert.Empty(user2.Roles); + // But should be ok with subscriptions unlocked + var settings = tester.PayTester.GetService(); + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); + await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); - // Duplicate email - await AssertValidationError(new[] { "Email" }, - async () => await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" })); - - // Let's make an admin - var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() + // But it should be forbidden to create an admin without being authenticated + await AssertHttpError(401, + async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { - Email = "admin@gmail.com", - Password = "abceudhqw", - IsAdministrator = true - }); - Assert.Contains("ServerAdmin", admin.Roles); - Assert.NotNull(admin.Created); - Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10); - - // Creating a new user without proper creds is now impossible (unauthorized) - // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection - var ex = await AssertAPIError("unauthenticated", - async () => await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); - Assert.Equal("New user creation isn't authorized to users who are not admin", ex.APIError.Message); - - // But should be ok with subscriptions unlocked - var settings = tester.PayTester.GetService(); - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); - await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); - - // But it should be forbidden to create an admin without being authenticated - await AssertHttpError(401, - async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() - { - Email = "admin2@gmail.com", - Password = "afewfoiewiou", - IsAdministrator = true - })); - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = true }); - - var adminAcc = tester.NewAccount(); - adminAcc.UserId = admin.Id; - adminAcc.IsAdmin = true; - var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile); - - // We should be forbidden to create a new user without proper admin permissions - await AssertHttpError(403, - async () => await adminClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); - await AssertAPIError("missing-permission", - async () => await adminClient.CreateUser(new CreateApplicationUserRequest() - { - Email = "test4@gmail.com", - Password = "afewfoiewiou", - IsAdministrator = true - })); - - // However, should be ok with the unrestricted permissions of an admin - adminClient = await adminAcc.CreateClient(Policies.Unrestricted); - await adminClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); - // Even creating new admin should be ok - await adminClient.CreateUser(new CreateApplicationUserRequest() - { - Email = "admin4@gmail.com", + Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true - }); + })); + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = true }); - var user1Acc = tester.NewAccount(); - user1Acc.UserId = user1.Id; - user1Acc.IsAdmin = false; - var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); + var adminAcc = tester.NewAccount(); + adminAcc.UserId = admin.Id; + adminAcc.IsAdmin = true; + var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile); - // User1 trying to get server management would still fail to create user - await AssertHttpError(403, - async () => await user1Client.CreateUser( - new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); + // We should be forbidden to create a new user without proper admin permissions + await AssertHttpError(403, + async () => await adminClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); + await AssertAPIError("missing-permission", + async () => await adminClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "test4@gmail.com", + Password = "afewfoiewiou", + IsAdministrator = true + })); - // User1 should be able to create user if subscription unlocked - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); - await user1Client.CreateUser( - new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); + // However, should be ok with the unrestricted permissions of an admin + adminClient = await adminAcc.CreateClient(Policies.Unrestricted); + await adminClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); + // Even creating new admin should be ok + await adminClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin4@gmail.com", + Password = "afewfoiewiou", + IsAdministrator = true + }); - // But not an admin - await AssertHttpError(403, - async () => await user1Client.CreateUser(new CreateApplicationUserRequest() - { - Email = "admin8@gmail.com", - Password = "afewfoiewiou", - IsAdministrator = true - })); + var user1Acc = tester.NewAccount(); + user1Acc.UserId = user1.Id; + user1Acc.IsAdmin = false; + var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); - // If we set DisableNonAdminCreateUserApi = true, it should always fail to create a user unless you are an admin - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false, DisableNonAdminCreateUserApi = true }); - await AssertHttpError(403, - async () => - await unauthClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); - await AssertHttpError(403, - async () => - await user1Client.CreateUser( - new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); - await adminClient.CreateUser( - new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" }); + // User1 trying to get server management would still fail to create user + await AssertHttpError(403, + async () => await user1Client.CreateUser( + new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); - } + // User1 should be able to create user if subscription unlocked + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); + await user1Client.CreateUser( + new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); + + // But not an admin + await AssertHttpError(403, + async () => await user1Client.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin8@gmail.com", + Password = "afewfoiewiou", + IsAdministrator = true + })); + + // If we set DisableNonAdminCreateUserApi = true, it should always fail to create a user unless you are an admin + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false, DisableNonAdminCreateUserApi = true }); + await AssertHttpError(403, + async () => + await unauthClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); + await AssertHttpError(403, + async () => + await user1Client.CreateUser( + new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); + await adminClient.CreateUser( + new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" }); } [Fact] [Trait("Integration", "Integration")] public async Task CanUsePullPaymentViaAPI() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.Register(); + acc.CreateStore(); + var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId; + var client = await acc.CreateClient(); + var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.Register(); - acc.CreateStore(); - var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId; - var client = await acc.CreateClient(); - var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Test", - Amount = 12.3m, - Currency = "BTC", - PaymentMethods = new[] { "BTC" } - }); + Name = "Test", + Amount = 12.3m, + Currency = "BTC", + PaymentMethods = new[] { "BTC" } + }); - void VerifyResult() - { - Assert.Equal("Test", result.Name); - Assert.Null(result.Period); - // If it contains ? it means that we are resolving an unknown route with the link generator - Assert.DoesNotContain("?", result.ViewLink); - Assert.False(result.Archived); - Assert.Equal("BTC", result.Currency); - Assert.Equal(12.3m, result.Amount); - } - VerifyResult(); - - var unauthenticated = new BTCPayServerClient(tester.PayTester.ServerUri); - result = await unauthenticated.GetPullPayment(result.Id); - VerifyResult(); - await AssertHttpError(404, async () => await unauthenticated.GetPullPayment("lol")); - // Can't list pull payments unauthenticated - await AssertHttpError(401, async () => await unauthenticated.GetPullPayments(storeId)); - - var pullPayments = await client.GetPullPayments(storeId); - result = Assert.Single(pullPayments); - VerifyResult(); - - Thread.Sleep(1000); - var test2 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Test 2", - Amount = 12.3m, - Currency = "BTC", - PaymentMethods = new[] { "BTC" } - }); - - TestLogs.LogInformation("Can't archive without knowing the walletId"); - var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id)); - Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission); - TestLogs.LogInformation("Can't archive without permission"); - await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); - await client.ArchivePullPayment(storeId, result.Id); - result = await unauthenticated.GetPullPayment(result.Id); - Assert.True(result.Archived); - var pps = await client.GetPullPayments(storeId); - result = Assert.Single(pps); - Assert.Equal("Test 2", result.Name); - pps = await client.GetPullPayments(storeId, true); - Assert.Equal(2, pps.Length); - Assert.Equal("Test 2", pps[0].Name); - Assert.Equal("Test", pps[1].Name); - - var payouts = await unauthenticated.GetPayouts(pps[0].Id); - Assert.Empty(payouts); - - var destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); - await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() - { - Destination = destination, - Amount = 1_000_000m, - PaymentMethod = "BTC", - })); - - await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest() - { - Destination = destination, - PaymentMethod = "BTC" - })); - - var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() - { - Destination = destination, - PaymentMethod = "BTC" - }); - - payouts = await unauthenticated.GetPayouts(pps[0].Id); - var payout2 = Assert.Single(payouts); - Assert.Equal(payout.Amount, payout2.Amount); - Assert.Equal(payout.Id, payout2.Id); - Assert.Equal(destination, payout2.Destination); - Assert.Equal(PayoutState.AwaitingApproval, payout.State); - Assert.Equal("BTC", payout2.PaymentMethod); - Assert.Equal("BTC", payout2.CryptoCode); - Assert.Null(payout.PaymentMethodAmount); - - TestLogs.LogInformation("Can't overdraft"); - - var destination2 = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); - await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() - { - Destination = destination2, - Amount = 0.00001m, - PaymentMethod = "BTC" - })); - - TestLogs.LogInformation("Can't create too low payout"); - await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() - { - Destination = destination2, - PaymentMethod = "BTC" - })); - - TestLogs.LogInformation("Can archive payout"); - await client.CancelPayout(storeId, payout.Id); - payouts = await unauthenticated.GetPayouts(pps[0].Id); - Assert.Empty(payouts); - - payouts = await client.GetPayouts(pps[0].Id, true); - payout = Assert.Single(payouts); - Assert.Equal(PayoutState.Cancelled, payout.State); - - TestLogs.LogInformation("Can create payout after cancelling"); - payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() - { - Destination = destination, - PaymentMethod = "BTC" - }); - - var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0)); - var inFuture = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Starts in the future", - Amount = 12.3m, - StartsAt = start, - Currency = "BTC", - PaymentMethods = new[] { "BTC" } - }); - Assert.Equal(start, inFuture.StartsAt); - Assert.Null(inFuture.ExpiresAt); - await this.AssertAPIError("not-started", async () => await unauthenticated.CreatePayout(inFuture.Id, new CreatePayoutRequest() - { - Amount = 1.0m, - Destination = destination, - PaymentMethod = "BTC" - })); - - var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0)); - var inPast = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Will expires", - Amount = 12.3m, - ExpiresAt = expires, - Currency = "BTC", - PaymentMethods = new[] { "BTC" } - }); - await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest() - { - Amount = 1.0m, - Destination = destination, - PaymentMethod = "BTC" - })); - - await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Test 2", - Amount = 12.3m, - StartsAt = DateTimeOffset.UtcNow, - ExpiresAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(1) - })); - - - TestLogs.LogInformation("Create a pull payment with USD"); - var pp = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Test USD", - Amount = 5000m, - Currency = "USD", - PaymentMethods = new[] { "BTC" } - }); - - destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); - TestLogs.LogInformation("Try to pay it in BTC"); - payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest() - { - Destination = destination, - PaymentMethod = "BTC" - }); - await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() - { - Revision = -1 - })); - await this.AssertAPIError("rate-unavailable", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() - { - RateRule = "DONOTEXIST(BTC_USD)" - })); - payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() - { - Revision = payout.Revision - }); - Assert.Equal(PayoutState.AwaitingPayment, payout.State); - Assert.NotNull(payout.PaymentMethodAmount); - Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests - await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() - { - Revision = payout.Revision - })); - - // Create one pull payment with an amount of 9 decimals - var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() - { - Name = "Test 2", - Amount = 12.303228134m, - Currency = "BTC", - PaymentMethods = new[] { "BTC" } - }); - destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); - payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest() - { - Destination = destination, - PaymentMethod = "BTC" - }); - payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()); - // The payout should round the value of the payment down to the network of the payment method - Assert.Equal(12.30322814m, payout.PaymentMethodAmount); - Assert.Equal(12.303228134m, payout.Amount); - - await client.MarkPayoutPaid(storeId, payout.Id); - payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id); - Assert.Equal(PayoutState.Completed, payout.State); - await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id)); + void VerifyResult() + { + Assert.Equal("Test", result.Name); + Assert.Null(result.Period); + // If it contains ? it means that we are resolving an unknown route with the link generator + Assert.DoesNotContain("?", result.ViewLink); + Assert.False(result.Archived); + Assert.Equal("BTC", result.Currency); + Assert.Equal(12.3m, result.Amount); } + VerifyResult(); + + var unauthenticated = new BTCPayServerClient(tester.PayTester.ServerUri); + result = await unauthenticated.GetPullPayment(result.Id); + VerifyResult(); + await AssertHttpError(404, async () => await unauthenticated.GetPullPayment("lol")); + // Can't list pull payments unauthenticated + await AssertHttpError(401, async () => await unauthenticated.GetPullPayments(storeId)); + + var pullPayments = await client.GetPullPayments(storeId); + result = Assert.Single(pullPayments); + VerifyResult(); + + Thread.Sleep(1000); + var test2 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Test 2", + Amount = 12.3m, + Currency = "BTC", + PaymentMethods = new[] { "BTC" } + }); + + TestLogs.LogInformation("Can't archive without knowing the walletId"); + var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id)); + Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission); + TestLogs.LogInformation("Can't archive without permission"); + await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); + await client.ArchivePullPayment(storeId, result.Id); + result = await unauthenticated.GetPullPayment(result.Id); + Assert.True(result.Archived); + var pps = await client.GetPullPayments(storeId); + result = Assert.Single(pps); + Assert.Equal("Test 2", result.Name); + pps = await client.GetPullPayments(storeId, true); + Assert.Equal(2, pps.Length); + Assert.Equal("Test 2", pps[0].Name); + Assert.Equal("Test", pps[1].Name); + + var payouts = await unauthenticated.GetPayouts(pps[0].Id); + Assert.Empty(payouts); + + var destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); + await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() + { + Destination = destination, + Amount = 1_000_000m, + PaymentMethod = "BTC", + })); + + await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest() + { + Destination = destination, + PaymentMethod = "BTC" + })); + + var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() + { + Destination = destination, + PaymentMethod = "BTC" + }); + + payouts = await unauthenticated.GetPayouts(pps[0].Id); + var payout2 = Assert.Single(payouts); + Assert.Equal(payout.Amount, payout2.Amount); + Assert.Equal(payout.Id, payout2.Id); + Assert.Equal(destination, payout2.Destination); + Assert.Equal(PayoutState.AwaitingApproval, payout.State); + Assert.Equal("BTC", payout2.PaymentMethod); + Assert.Equal("BTC", payout2.CryptoCode); + Assert.Null(payout.PaymentMethodAmount); + + TestLogs.LogInformation("Can't overdraft"); + + var destination2 = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); + await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() + { + Destination = destination2, + Amount = 0.00001m, + PaymentMethod = "BTC" + })); + + TestLogs.LogInformation("Can't create too low payout"); + await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() + { + Destination = destination2, + PaymentMethod = "BTC" + })); + + TestLogs.LogInformation("Can archive payout"); + await client.CancelPayout(storeId, payout.Id); + payouts = await unauthenticated.GetPayouts(pps[0].Id); + Assert.Empty(payouts); + + payouts = await client.GetPayouts(pps[0].Id, true); + payout = Assert.Single(payouts); + Assert.Equal(PayoutState.Cancelled, payout.State); + + TestLogs.LogInformation("Can create payout after cancelling"); + payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() + { + Destination = destination, + PaymentMethod = "BTC" + }); + + var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0)); + var inFuture = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Starts in the future", + Amount = 12.3m, + StartsAt = start, + Currency = "BTC", + PaymentMethods = new[] { "BTC" } + }); + Assert.Equal(start, inFuture.StartsAt); + Assert.Null(inFuture.ExpiresAt); + await this.AssertAPIError("not-started", async () => await unauthenticated.CreatePayout(inFuture.Id, new CreatePayoutRequest() + { + Amount = 1.0m, + Destination = destination, + PaymentMethod = "BTC" + })); + + var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0)); + var inPast = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Will expires", + Amount = 12.3m, + ExpiresAt = expires, + Currency = "BTC", + PaymentMethods = new[] { "BTC" } + }); + await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest() + { + Amount = 1.0m, + Destination = destination, + PaymentMethod = "BTC" + })); + + await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Test 2", + Amount = 12.3m, + StartsAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(1) + })); + + + TestLogs.LogInformation("Create a pull payment with USD"); + var pp = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Test USD", + Amount = 5000m, + Currency = "USD", + PaymentMethods = new[] { "BTC" } + }); + + destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); + TestLogs.LogInformation("Try to pay it in BTC"); + payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest() + { + Destination = destination, + PaymentMethod = "BTC" + }); + await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() + { + Revision = -1 + })); + await this.AssertAPIError("rate-unavailable", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() + { + RateRule = "DONOTEXIST(BTC_USD)" + })); + payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() + { + Revision = payout.Revision + }); + Assert.Equal(PayoutState.AwaitingPayment, payout.State); + Assert.NotNull(payout.PaymentMethodAmount); + Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests + await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() + { + Revision = payout.Revision + })); + + // Create one pull payment with an amount of 9 decimals + var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() + { + Name = "Test 2", + Amount = 12.303228134m, + Currency = "BTC", + PaymentMethods = new[] { "BTC" } + }); + destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); + payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest() + { + Destination = destination, + PaymentMethod = "BTC" + }); + payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()); + // The payout should round the value of the payment down to the network of the payment method + Assert.Equal(12.30322814m, payout.PaymentMethodAmount); + Assert.Equal(12.303228134m, payout.Amount); + + await client.MarkPayoutPaid(storeId, payout.Id); + payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id); + Assert.Equal(PayoutState.Completed, payout.State); + await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id)); } private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset) @@ -620,59 +607,57 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task StoresControllerTests() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + var client = await user.CreateClient(Policies.Unrestricted); + + //create store + var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); + + //update store + var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }); + Assert.Equal("B", updatedStore.Name); + Assert.Equal("B", (await client.GetStore(newStore.Id)).Name); + + //list stores + var stores = await client.GetStores(); + var storeIds = stores.Select(data => data.Id); + var storeNames = stores.Select(data => data.Name); + Assert.NotNull(stores); + Assert.Equal(2, stores.Count()); + Assert.Contains(newStore.Id, storeIds); + Assert.Contains(user.StoreId, storeIds); + + //get store + var store = await client.GetStore(user.StoreId); + Assert.Equal(user.StoreId, store.Id); + Assert.Contains(store.Name, storeNames); + + //remove store + await client.RemoveStore(newStore.Id); + await AssertHttpError(403, async () => { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - var client = await user.CreateClient(Policies.Unrestricted); + await client.GetStore(newStore.Id); + }); + Assert.Single(await client.GetStores()); - //create store - var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); + newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); + var scopedClient = + await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString()); + Assert.Single(await scopedClient.GetStores()); - //update store - var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }); - Assert.Equal("B", updatedStore.Name); - Assert.Equal("B", (await client.GetStore(newStore.Id)).Name); + var noauth = await user.CreateClient(Array.Empty()); + await AssertAPIError("missing-permission", () => noauth.GetStores()); - //list stores - var stores = await client.GetStores(); - var storeIds = stores.Select(data => data.Id); - var storeNames = stores.Select(data => data.Name); - Assert.NotNull(stores); - Assert.Equal(2, stores.Count()); - Assert.Contains(newStore.Id, storeIds); - Assert.Contains(user.StoreId, storeIds); - - //get store - var store = await client.GetStore(user.StoreId); - Assert.Equal(user.StoreId, store.Id); - Assert.Contains(store.Name, storeNames); - - //remove store - await client.RemoveStore(newStore.Id); - await AssertHttpError(403, async () => - { - await client.GetStore(newStore.Id); - }); - Assert.Single(await client.GetStores()); - - newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); - var scopedClient = - await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString()); - Assert.Single(await scopedClient.GetStores()); - - var noauth = await user.CreateClient(Array.Empty()); - await AssertAPIError("missing-permission", () => noauth.GetStores()); - - // We strip the user's Owner right, so the key should not work - using var ctx = tester.PayTester.GetService().CreateContext(); - var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id); - storeEntity.Role = "Guest"; - await ctx.SaveChangesAsync(); - await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" })); - } + // We strip the user's Owner right, so the key should not work + using var ctx = tester.PayTester.GetService().CreateContext(); + var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id); + storeEntity.Role = "Guest"; + await ctx.SaveChangesAsync(); + await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" })); } private async Task AssertValidationError(string[] fields, Func act) @@ -698,66 +683,64 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task UsersControllerTests() { - using (var tester = CreateServerTester(newDb: true)) + using var tester = CreateServerTester(newDb: true); + tester.PayTester.DisableRegistration = true; + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + var clientProfile = await user.CreateClient(Policies.CanModifyProfile); + var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile); + var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); + var clientBasic = await user.CreateClient(); + + + var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); + Assert.NotNull(apiKeyProfileUserData); + Assert.Equal(apiKeyProfileUserData.Id, user.UserId); + Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); + Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles); + + await AssertHttpError(403, async () => await clientInsufficient.GetCurrentUser()); + await clientServer.GetCurrentUser(); + await clientProfile.GetCurrentUser(); + await clientBasic.GetCurrentUser(); + + await AssertHttpError(403, async () => + await clientInsufficient.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + })); + + var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { - tester.PayTester.DisableRegistration = true; - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - var clientProfile = await user.CreateClient(Policies.CanModifyProfile); - var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile); - var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); - var clientBasic = await user.CreateClient(); + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + }); + Assert.NotNull(newUser); + var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + }); + Assert.NotNull(newUser2); - var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); - Assert.NotNull(apiKeyProfileUserData); - Assert.Equal(apiKeyProfileUserData.Id, user.UserId); - Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); - Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles); - - await AssertHttpError(403, async () => await clientInsufficient.GetCurrentUser()); - await clientServer.GetCurrentUser(); - await clientProfile.GetCurrentUser(); - await clientBasic.GetCurrentUser(); - - await AssertHttpError(403, async () => - await clientInsufficient.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() - })); - - var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() + await AssertValidationError(new[] { "Email" }, async () => + await clientServer.CreateUser(new CreateApplicationUserRequest() { - Email = $"{Guid.NewGuid()}@g.com", + Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString() - }); - Assert.NotNull(newUser); + })); - var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() - }); - Assert.NotNull(newUser2); + await AssertValidationError(new[] { "Password" }, async () => + await clientServer.CreateUser( + new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", })); - await AssertValidationError(new[] { "Email" }, async () => - await clientServer.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}", - Password = Guid.NewGuid().ToString() - })); - - await AssertValidationError(new[] { "Password" }, async () => - await clientServer.CreateUser( - new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", })); - - await AssertValidationError(new[] { "Email" }, async () => - await clientServer.CreateUser( - new CreateApplicationUserRequest() { Password = Guid.NewGuid().ToString() })); - } + await AssertValidationError(new[] { "Email" }, async () => + await clientServer.CreateUser( + new CreateApplicationUserRequest() { Password = Guid.NewGuid().ToString() })); } [Fact(Timeout = TestTimeout)] @@ -865,132 +848,126 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task HealthControllerTests() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - var apiHealthData = await unauthClient.GetHealth(); - Assert.NotNull(apiHealthData); - Assert.True(apiHealthData.Synchronized); - } + var apiHealthData = await unauthClient.GetHealth(); + Assert.NotNull(apiHealthData); + Assert.True(apiHealthData.Synchronized); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ServerInfoControllerTests() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await AssertHttpError(401, async () => await unauthClient.GetServerInfo()); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); + await AssertHttpError(401, async () => await unauthClient.GetServerInfo()); - var user = tester.NewAccount(); - user.GrantAccess(); - var clientBasic = await user.CreateClient(); - var serverInfoData = await clientBasic.GetServerInfo(); - Assert.NotNull(serverInfoData); - Assert.NotNull(serverInfoData.Version); - Assert.NotNull(serverInfoData.Onion); - Assert.True(serverInfoData.FullySynched); - Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods); - Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods); - Assert.NotNull(serverInfoData.SyncStatus); - Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC")); - } + var user = tester.NewAccount(); + user.GrantAccess(); + var clientBasic = await user.CreateClient(); + var serverInfoData = await clientBasic.GetServerInfo(); + Assert.NotNull(serverInfoData); + Assert.NotNull(serverInfoData.Version); + Assert.NotNull(serverInfoData.Onion); + Assert.True(serverInfoData.FullySynched); + Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods); + Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods); + Assert.NotNull(serverInfoData.SyncStatus); + Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC")); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task PaymentControllerTests() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + var client = await user.CreateClient(Policies.Unrestricted); + var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests); + + //create payment request + + //validation errors + await AssertValidationError(new[] { "Amount" }, async () => { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - var client = await user.CreateClient(Policies.Unrestricted); - var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests); + await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A" }); + }); + await AssertValidationError(new[] { "Amount" }, async () => + { + await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() { Title = "A", Currency = "BTC", Amount = 0 }); + }); + await AssertValidationError(new[] { "Currency" }, async () => + { + await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); + }); + await AssertHttpError(403, async () => + { + await viewOnly.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); + }); + var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() { Title = "A", Currency = "USD", Amount = 1 }); - //create payment request + //list payment request + var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId); - //validation errors - await AssertValidationError(new[] { "Amount" }, async () => - { - await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A" }); - }); - await AssertValidationError(new[] { "Amount" }, async () => - { - await client.CreatePaymentRequest(user.StoreId, - new CreatePaymentRequestRequest() { Title = "A", Currency = "BTC", Amount = 0 }); - }); - await AssertValidationError(new[] { "Currency" }, async () => - { - await client.CreatePaymentRequest(user.StoreId, - new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); - }); - await AssertHttpError(403, async () => - { - await viewOnly.CreatePaymentRequest(user.StoreId, - new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); - }); - var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId, - new CreatePaymentRequestRequest() { Title = "A", Currency = "USD", Amount = 1 }); + Assert.NotNull(paymentRequests); + Assert.Single(paymentRequests); + Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id); - //list payment request - var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId); + //get payment request + var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); + Assert.Equal(newPaymentRequest.Title, paymentRequest.Title); + Assert.Equal(newPaymentRequest.StoreId, user.StoreId); - Assert.NotNull(paymentRequests); - Assert.Single(paymentRequests); - Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id); + //update payment request + var updateRequest = JObject.FromObject(paymentRequest).ToObject(); + updateRequest.Title = "B"; + await AssertHttpError(403, async () => + { + await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); + }); + await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); + paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); + Assert.Equal(updateRequest.Title, paymentRequest.Title); - //get payment request - var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); - Assert.Equal(newPaymentRequest.Title, paymentRequest.Title); - Assert.Equal(newPaymentRequest.StoreId, user.StoreId); + //archive payment request + await AssertHttpError(403, async () => + { + await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); + }); - //update payment request - var updateRequest = JObject.FromObject(paymentRequest).ToObject(); - updateRequest.Title = "B"; - await AssertHttpError(403, async () => - { - await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); - }); - await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); - paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); - Assert.Equal(updateRequest.Title, paymentRequest.Title); + await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); + Assert.DoesNotContain(paymentRequest.Id, + (await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id)); - //archive payment request - await AssertHttpError(403, async () => - { - await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); - }); + //let's test some payment stuff + await user.RegisterDerivationSchemeAsync("BTC"); + var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" }); - await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); - Assert.DoesNotContain(paymentRequest.Id, - (await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id)); - - //let's test some payment stuff - await user.RegisterDerivationSchemeAsync("BTC"); - var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId, - new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" }); - - var invoiceId = Assert.IsType(Assert.IsType(await user.GetController() - .PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value); - var invoice = user.BitPay.GetInvoice(invoiceId); - await tester.WaitForEvent(async () => - { - await tester.ExplorerNode.SendToAddressAsync( - BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue); - }); - await TestUtils.EventuallyAsync(async () => - { - Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status); - Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status); - }); - } + var invoiceId = Assert.IsType(Assert.IsType(await user.GetController() + .PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value); + var invoice = user.BitPay.GetInvoice(invoiceId); + await tester.WaitForEvent(async () => + { + await tester.ExplorerNode.SendToAddressAsync( + BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue); + }); + await TestUtils.EventuallyAsync(async () => + { + Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status); + Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status); + }); } [Fact(Timeout = TestTimeout)] @@ -1065,32 +1042,30 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanOverpayInvoice() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.RegisterDerivationSchemeAsync("BTC"); - var client = await user.CreateClient(); - var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" }); - var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id); - var method = methods.First(); - var amount = method.Amount; - Assert.Equal(amount, method.Due); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.RegisterDerivationSchemeAsync("BTC"); + var client = await user.CreateClient(); + var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 5000.0m, Currency = "USD" }); + var methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id); + var method = methods.First(); + var amount = method.Amount; + Assert.Equal(amount, method.Due); #pragma warning disable CS0618 // Type or member is obsolete - var btc = tester.NetworkProvider.BTC.NBitcoinNetwork; + var btc = tester.NetworkProvider.BTC.NBitcoinNetwork; #pragma warning restore CS0618 // Type or member is obsolete - await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(method.Destination, btc), Money.Coins(method.Due) + Money.Coins(1.0m)); - await TestUtils.EventuallyAsync(async () => - { - invoice = await client.GetInvoice(user.StoreId, invoice.Id); - Assert.True(invoice.Status == InvoiceStatus.Processing); - methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id); - method = methods.First(); - Assert.Equal(amount, method.Amount); - Assert.Equal(-1.0m, method.Due); - Assert.Equal(amount + 1.0m, method.TotalPaid); - }); - } + await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(method.Destination, btc), Money.Coins(method.Due) + Money.Coins(1.0m)); + await TestUtils.EventuallyAsync(async () => + { + invoice = await client.GetInvoice(user.StoreId, invoice.Id); + Assert.True(invoice.Status == InvoiceStatus.Processing); + methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id); + method = methods.First(); + Assert.Equal(amount, method.Amount); + Assert.Equal(-1.0m, method.Due); + Assert.Equal(amount + 1.0m, method.TotalPaid); + }); } [Fact(Timeout = TestTimeout)] @@ -1440,108 +1415,106 @@ namespace BTCPayServer.Tests [Trait("Lightning", "Lightning")] public async Task CanUseLightningAPI() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); + + var merchant = tester.NewAccount(); + merchant.GrantAccess(true); + merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); + var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}"); + var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60))); + // The default client is using charge, so we should not be able to query channels + var client = await user.CreateClient(Policies.CanUseInternalLightningNode); + + var info = await client.GetLightningNodeInfo("BTC"); + Assert.Single(info.NodeURIs); + Assert.NotEqual(0, info.BlockHeight); + + await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC")); + // Not permission for the store! + await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC")); + var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - user.GrantAccess(true); - user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); + var chargeInvoice = invoiceData; + Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); - var merchant = tester.NewAccount(); - merchant.GrantAccess(true); - merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); - var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}"); - var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60))); - // The default client is using charge, so we should not be able to query channels - var client = await user.CreateClient(Policies.CanUseInternalLightningNode); + client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); + // Not permission for the server + await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC")); - var info = await client.GetLightningNodeInfo("BTC"); - Assert.Single(info.NodeURIs); - Assert.NotEqual(0, info.BlockHeight); + var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); + Assert.Equal(2, data.Count()); + BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); - await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC")); - // Not permission for the store! - await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC")); - var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() - { - Amount = LightMoney.Satoshis(1000), - Description = "lol", - Expiry = TimeSpan.FromSeconds(400), - PrivateRouteHints = false - }); - var chargeInvoice = invoiceData; - Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); + invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); - client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); - // Not permission for the server - await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC")); + Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); - var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); - Assert.Equal(2, data.Count()); - BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); + await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = merchantInvoice.BOLT11 + }); + await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = "lol" + })); - invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() - { - Amount = LightMoney.Satoshis(1000), - Description = "lol", - Expiry = TimeSpan.FromSeconds(400), - PrivateRouteHints = false - }); + var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = -1, + Expiry = TimeSpan.FromSeconds(-1), + Description = null + })); + Assert.Equal(2, validationErr.ValidationErrors.Length); - Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); + var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); + Assert.NotNull(invoice.PaidAt); + Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); + // Amount received might be bigger because of internal implementation shit from lightning + Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); - await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() - { - BOLT11 = merchantInvoice.BOLT11 - }); - await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() - { - BOLT11 = "lol" - })); - - var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() - { - Amount = -1, - Expiry = TimeSpan.FromSeconds(-1), - Description = null - })); - Assert.Equal(2, validationErr.ValidationErrors.Length); - - var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); - Assert.NotNull(invoice.PaidAt); - Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); - // Amount received might be bigger because of internal implementation shit from lightning - Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); - - info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); - Assert.Single(info.NodeURIs); - Assert.NotEqual(0, info.BlockHeight); + info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); + Assert.Single(info.NodeURIs); + Assert.NotEqual(0, info.BlockHeight); - // As admin, can use the internal node through our store. - await user.MakeAdmin(true); - await user.RegisterInternalLightningNodeAsync("BTC"); - await client.GetLightningNodeInfo(user.StoreId, "BTC"); - // But if not admin anymore, nope - await user.MakeAdmin(false); - await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); - // However, even as a guest, you should be able to create an invoice - var guest = tester.NewAccount(); - guest.GrantAccess(false); - await user.AddGuest(guest.UserId); - client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore); - await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() - { - Amount = LightMoney.Satoshis(1000), - Description = "lol", - Expiry = TimeSpan.FromSeconds(600), - }); - client = await guest.CreateClient(Policies.CanUseLightningNodeInStore); - // Can use lightning node is only granted to store's owner - await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); - } + // As admin, can use the internal node through our store. + await user.MakeAdmin(true); + await user.RegisterInternalLightningNodeAsync("BTC"); + await client.GetLightningNodeInfo(user.StoreId, "BTC"); + // But if not admin anymore, nope + await user.MakeAdmin(false); + await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); + // However, even as a guest, you should be able to create an invoice + var guest = tester.NewAccount(); + guest.GrantAccess(false); + await user.AddGuest(guest.UserId); + client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore); + await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(600), + }); + client = await guest.CreateClient(Policies.CanUseLightningNodeInStore); + // Can use lightning node is only granted to store's owner + await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); } [Fact(Timeout = TestTimeout)] diff --git a/BTCPayServer.Tests/LanguageServiceTests.cs b/BTCPayServer.Tests/LanguageServiceTests.cs index 5bb7c6f6a..61b74fe98 100644 --- a/BTCPayServer.Tests/LanguageServiceTests.cs +++ b/BTCPayServer.Tests/LanguageServiceTests.cs @@ -18,34 +18,32 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanAutoDetectLanguage() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var languageService = tester.PayTester.GetService(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var languageService = tester.PayTester.GetService(); - // Most common format. First option does not have a quality score. Others do in descending order. - // Result should be nl-NL (because the default weight is 1 for nl) - var lang1 = languageService.FindLanguageInAcceptLanguageHeader("nl,fr;q=0.7,en;q=0.5"); - Assert.NotNull(lang1); - Assert.Equal("nl-NL", lang1?.Code); + // Most common format. First option does not have a quality score. Others do in descending order. + // Result should be nl-NL (because the default weight is 1 for nl) + var lang1 = languageService.FindLanguageInAcceptLanguageHeader("nl,fr;q=0.7,en;q=0.5"); + Assert.NotNull(lang1); + Assert.Equal("nl-NL", lang1?.Code); - // Most common format. First option does not have a quality score. Others do in descending order. This time the first option includes a country. - // Result should be nl-NL (because the default weight is 1 for nl-BE and it does not exist in BTCPay Server, but nl-NL does and applies too for language "nl") - var lang2 = languageService.FindLanguageInAcceptLanguageHeader("nl-BE,fr;q=0.7,en;q=0.5"); - Assert.NotNull(lang2); - Assert.Equal("nl-NL", lang2?.Code); + // Most common format. First option does not have a quality score. Others do in descending order. This time the first option includes a country. + // Result should be nl-NL (because the default weight is 1 for nl-BE and it does not exist in BTCPay Server, but nl-NL does and applies too for language "nl") + var lang2 = languageService.FindLanguageInAcceptLanguageHeader("nl-BE,fr;q=0.7,en;q=0.5"); + Assert.NotNull(lang2); + Assert.Equal("nl-NL", lang2?.Code); - // Unusual format, but still valid. All values have a quality score and not ordered. - // Result should be fr-FR (because 0.7 is the highest quality score) - var lang3 = languageService.FindLanguageInAcceptLanguageHeader("nl;q=0.1,fr;q=0.7,en;q=0.5"); - Assert.NotNull(lang3); - Assert.Equal("fr-FR", lang3?.Code); + // Unusual format, but still valid. All values have a quality score and not ordered. + // Result should be fr-FR (because 0.7 is the highest quality score) + var lang3 = languageService.FindLanguageInAcceptLanguageHeader("nl;q=0.1,fr;q=0.7,en;q=0.5"); + Assert.NotNull(lang3); + Assert.Equal("fr-FR", lang3?.Code); - // Unusual format, but still valid. Some language is given that we don't have and a wildcard for everything else. - // Result should be NULL, because "xx" does not exist and * is a wildcard and has no meaning. - var lang4 = languageService.FindLanguageInAcceptLanguageHeader("xx,*;q=0.5"); - Assert.Null(lang4); - } + // Unusual format, but still valid. Some language is given that we don't have and a wildcard for everything else. + // Result should be NULL, because "xx" does not exist and * is a wildcard and has no meaning. + var lang4 = languageService.FindLanguageInAcceptLanguageHeader("xx,*;q=0.5"); + Assert.Null(lang4); } } } diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs index 603345419..07f68b56a 100644 --- a/BTCPayServer.Tests/POSTests.cs +++ b/BTCPayServer.Tests/POSTests.cs @@ -22,23 +22,22 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanUsePoSApp1() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - var apps = user.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); - vm.AppName = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); - Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); - var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); - var vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(app.Id)).Model); - vmpos.Template = @" + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + var apps = user.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + vm.AppName = "test"; + vm.SelectedAppType = AppType.PointOfSale.ToString(); + Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); + var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + var app = appList.Apps[0]; + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + var vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(app.Id)).Model); + vmpos.Template = @" apple: price: 5.0 title: good apple @@ -49,25 +48,24 @@ donation: price: 1.02 custom: true "; - Assert.IsType(apps.UpdatePointOfSale(app.Id, vmpos).Result); - vmpos = Assert.IsType(Assert - .IsType(apps.UpdatePointOfSale(app.Id)).Model); - var publicApps = user.GetController(); - var vmview = - Assert.IsType(Assert - .IsType(publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).Result).Model); + Assert.IsType(apps.UpdatePointOfSale(app.Id, vmpos).Result); + vmpos = Assert.IsType(Assert + .IsType(apps.UpdatePointOfSale(app.Id)).Model); + var publicApps = user.GetController(); + var vmview = + Assert.IsType(Assert + .IsType(publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).Result).Model); - // apple shouldn't be available since we it's set to "disabled: true" above - Assert.Equal(2, vmview.Items.Length); - Assert.Equal("orange", vmview.Items[0].Title); - Assert.Equal("donation", vmview.Items[1].Title); - // orange is available - Assert.IsType(publicApps - .ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result); - // apple is not found - Assert.IsType(publicApps - .ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result); - } + // apple shouldn't be available since we it's set to "disabled: true" above + Assert.Equal(2, vmview.Items.Length); + Assert.Equal("orange", vmview.Items[0].Title); + Assert.Equal("donation", vmview.Items[1].Title); + // orange is available + Assert.IsType(publicApps + .ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result); + // apple is not found + Assert.IsType(publicApps + .ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result); } } } diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index e240753d9..b3480f368 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -23,36 +23,35 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanPlayWithPSBT() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + var invoice = user.BitPay.CreateInvoice(new Invoice() { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - var invoice = user.BitPay.CreateInvoice(new Invoice() - { - Price = 10, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some \", description", - FullNotifications = true - }, Facade.Merchant); - var cashCow = tester.ExplorerNode; - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m)); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal("paid", invoice.Status); - }); + Price = 10, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some \", description", + FullNotifications = true + }, Facade.Merchant); + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m)); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal("paid", invoice.Status); + }); - var walletController = user.GetController(); - var walletId = new WalletId(user.StoreId, "BTC"); - var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(); - var sendModel = new WalletSendModel() - { - Outputs = new List() + var walletController = user.GetController(); + var walletId = new WalletId(user.StoreId, "BTC"); + var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(); + var sendModel = new WalletSendModel() + { + Outputs = new List() { new WalletSendModel.TransactionOutput() { @@ -60,81 +59,80 @@ namespace BTCPayServer.Tests Amount = 0.1m, } }, - FeeSatoshiPerByte = 1, - CurrentBalance = 1.5m - }; + FeeSatoshiPerByte = 1, + CurrentBalance = 1.5m + }; - string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); - var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync(); - var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); - Assert.NotNull(vmPSBT.Decoded); + string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); + var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync(); + var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); + Assert.NotNull(vmPSBT.Decoded); - var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt")); - PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); + var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt")); + PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); - var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel + var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel + { + SigningContext = new SigningContextModel { - SigningContext = new SigningContextModel - { - PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) - } - }).AssertViewModelAsync(); - Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); - Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT); + PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } + }).AssertViewModelAsync(); + Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); + Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT); - var signedPSBT = unsignedPSBT.Clone(); - signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath); - vmPSBT.PSBT = signedPSBT.ToBase64(); - var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel + var signedPSBT = unsignedPSBT.Clone(); + signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath); + vmPSBT.PSBT = signedPSBT.ToBase64(); + var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel + { + SigningContext = new SigningContextModel { - SigningContext = new SigningContextModel - { - PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) - } - }).AssertViewModelAsync(); - Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination - Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); - Assert.Contains(psbtReady.Destinations, d => d.Positive); + PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } + }).AssertViewModelAsync(); + Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination + Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); + Assert.Contains(psbtReady.Destinations, d => d.Positive); - vmPSBT.PSBT = unsignedPSBT.ToBase64(); - var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync(); - Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT); - combineVM.PSBT = signedPSBT.ToBase64(); - var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); + vmPSBT.PSBT = unsignedPSBT.ToBase64(); + var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync(); + Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT); + combineVM.PSBT = signedPSBT.ToBase64(); + var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); - var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); - Assert.True(signedPSBT.TryFinalize(out _)); - Assert.True(signedPSBT2.TryFinalize(out _)); - Assert.Equal(signedPSBT, signedPSBT2); + var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); + Assert.True(signedPSBT.TryFinalize(out _)); + Assert.True(signedPSBT2.TryFinalize(out _)); + Assert.Equal(signedPSBT, signedPSBT2); - // Can use uploaded file? - combineVM.PSBT = null; - combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); - signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); - Assert.True(signedPSBT.TryFinalize(out _)); - Assert.True(signedPSBT2.TryFinalize(out _)); - Assert.Equal(signedPSBT, signedPSBT2); + // Can use uploaded file? + combineVM.PSBT = null; + combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); + signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); + Assert.True(signedPSBT.TryFinalize(out _)); + Assert.True(signedPSBT2.TryFinalize(out _)); + Assert.Equal(signedPSBT, signedPSBT2); - var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel - { - SigningContext = new SigningContextModel(signedPSBT) - })).AssertViewModel(); - Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); - Assert.Equal(signedPSBT.ToBase64(), psbt); - var redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); - Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); + var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel + { + SigningContext = new SigningContextModel(signedPSBT) + })).AssertViewModel(); + Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); + Assert.Equal(signedPSBT.ToBase64(), psbt); + var redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); + Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); - //test base64 psbt file - Assert.False(string.IsNullOrEmpty(Assert.IsType( - Assert.IsType( - await walletController.WalletPSBT(walletId, - new WalletPSBTViewModel - { - UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64()) - })).Model).PSBT)); - } + //test base64 psbt file + Assert.False(string.IsNullOrEmpty(Assert.IsType( + Assert.IsType( + await walletController.WalletPSBT(walletId, + new WalletPSBTViewModel + { + UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64()) + })).Model).PSBT)); } private static string AssertRedirectedPSBT(IActionResult view, string actionName) diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index f2f599f1a..20a75def8 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -46,98 +46,92 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanUseTheDelayedBroadcaster() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var network = tester.NetworkProvider.GetNetwork("BTC"); - var broadcaster = tester.PayTester.GetService(); - await broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromDays(500), RandomTransaction(network), network); - var tx = RandomTransaction(network); - await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network); - // twice on same tx should be noop - await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network); - broadcaster.Disable(); - Assert.Equal(0, await broadcaster.ProcessAll()); - broadcaster.Enable(); - Assert.Equal(1, await broadcaster.ProcessAll()); - Assert.Equal(0, await broadcaster.ProcessAll()); - } + using var tester = CreateServerTester(); + await tester.StartAsync(); + var network = tester.NetworkProvider.GetNetwork("BTC"); + var broadcaster = tester.PayTester.GetService(); + await broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromDays(500), RandomTransaction(network), network); + var tx = RandomTransaction(network); + await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network); + // twice on same tx should be noop + await broadcaster.Schedule(DateTimeOffset.UtcNow - TimeSpan.FromDays(5), tx, network); + broadcaster.Disable(); + Assert.Equal(0, await broadcaster.ProcessAll()); + broadcaster.Enable(); + Assert.Equal(1, await broadcaster.ProcessAll()); + Assert.Equal(0, await broadcaster.ProcessAll()); } [Fact] [Trait("Integration", "Integration")] public async Task CanUsePayjoinRepository() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var network = tester.NetworkProvider.GetNetwork("BTC"); - var repo = tester.PayTester.GetService(); - var outpoint = RandomOutpoint(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var network = tester.NetworkProvider.GetNetwork("BTC"); + var repo = tester.PayTester.GetService(); + var outpoint = RandomOutpoint(); - // Should not be locked - Assert.False(await repo.TryUnlock(outpoint)); + // Should not be locked + Assert.False(await repo.TryUnlock(outpoint)); - // Can lock input - Assert.True(await repo.TryLockInputs(new[] { outpoint })); - // Can't twice - Assert.False(await repo.TryLockInputs(new[] { outpoint })); - Assert.False(await repo.TryUnlock(outpoint)); + // Can lock input + Assert.True(await repo.TryLockInputs(new[] { outpoint })); + // Can't twice + Assert.False(await repo.TryLockInputs(new[] { outpoint })); + Assert.False(await repo.TryUnlock(outpoint)); - // Lock and unlock outpoint utxo - Assert.True(await repo.TryLock(outpoint)); - Assert.True(await repo.TryUnlock(outpoint)); - Assert.False(await repo.TryUnlock(outpoint)); + // Lock and unlock outpoint utxo + Assert.True(await repo.TryLock(outpoint)); + Assert.True(await repo.TryUnlock(outpoint)); + Assert.False(await repo.TryUnlock(outpoint)); - // Make sure that if any can't be locked, all are not locked - var outpoint1 = RandomOutpoint(); - var outpoint2 = RandomOutpoint(); - Assert.True(await repo.TryLockInputs(new[] { outpoint1 })); - Assert.False(await repo.TryLockInputs(new[] { outpoint1, outpoint2 })); - Assert.True(await repo.TryLockInputs(new[] { outpoint2 })); + // Make sure that if any can't be locked, all are not locked + var outpoint1 = RandomOutpoint(); + var outpoint2 = RandomOutpoint(); + Assert.True(await repo.TryLockInputs(new[] { outpoint1 })); + Assert.False(await repo.TryLockInputs(new[] { outpoint1, outpoint2 })); + Assert.True(await repo.TryLockInputs(new[] { outpoint2 })); - outpoint1 = RandomOutpoint(); - outpoint2 = RandomOutpoint(); - Assert.True(await repo.TryLockInputs(new[] { outpoint1 })); - Assert.False(await repo.TryLockInputs(new[] { outpoint2, outpoint1 })); - Assert.True(await repo.TryLockInputs(new[] { outpoint2 })); - } + outpoint1 = RandomOutpoint(); + outpoint2 = RandomOutpoint(); + Assert.True(await repo.TryLockInputs(new[] { outpoint1 })); + Assert.False(await repo.TryLockInputs(new[] { outpoint2, outpoint1 })); + Assert.True(await repo.TryLockInputs(new[] { outpoint2 })); } [Fact] [Trait("Integration", "Integration")] public async Task ChooseBestUTXOsForPayjoin() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var network = tester.NetworkProvider.GetNetwork("BTC"); - var controller = tester.PayTester.GetService(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var network = tester.NetworkProvider.GetNetwork("BTC"); + var controller = tester.PayTester.GetService(); - //Only one utxo, so obvious result - var utxos = new[] { FakeUTXO(1.0m) }; - var paymentAmount = 0.5m; - var otherOutputs = new[] { 0.5m }; - var inputs = new[] { 1m }; - var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); - Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType); - Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo)); + //Only one utxo, so obvious result + var utxos = new[] { FakeUTXO(1.0m) }; + var paymentAmount = 0.5m; + var otherOutputs = new[] { 0.5m }; + var inputs = new[] { 1m }; + var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); + Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType); + Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo)); - //no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant - utxos = new[] { FakeUTXO(0.3m), FakeUTXO(0.7m) }; - paymentAmount = 0.5m; - otherOutputs = new[] { 0.5m }; - inputs = new[] { 1m }; - result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); - Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType); + //no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant + utxos = new[] { FakeUTXO(0.3m), FakeUTXO(0.7m) }; + paymentAmount = 0.5m; + otherOutputs = new[] { 0.5m }; + inputs = new[] { 1m }; + result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); + Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType); - //when there is no change, anything works - utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) }; - paymentAmount = 0.5m; - otherOutputs = new decimal[0]; - inputs = new[] { 0.03m, 0.07m }; - result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); - Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType); - } + //when there is no change, anything works + utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) }; + paymentAmount = 0.5m; + otherOutputs = new decimal[0]; + inputs = new[] { 0.03m, 0.07m }; + result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs); + Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType); } @@ -168,61 +162,58 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanOnlyUseCorrectAddressFormatsForPayjoin() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var broadcaster = tester.PayTester.GetService(); + var payjoinRepository = tester.PayTester.GetService(); + broadcaster.Disable(); + var network = tester.NetworkProvider.GetNetwork("BTC"); + var btcPayWallet = tester.PayTester.GetService().GetWallet(network); + var cashCow = tester.ExplorerNode; + cashCow.Generate(2); // get some money in case + + var unsupportedFormats = new[] { ScriptPubKeyType.Legacy }; + + + foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType))) { - await tester.StartAsync(); - var broadcaster = tester.PayTester.GetService(); - var payjoinRepository = tester.PayTester.GetService(); - broadcaster.Disable(); - var network = tester.NetworkProvider.GetNetwork("BTC"); - var btcPayWallet = tester.PayTester.GetService().GetWallet(network); - var cashCow = tester.ExplorerNode; - cashCow.Generate(2); // get some money in case + if (senderAddressType == ScriptPubKeyType.TaprootBIP86) + continue; + var senderUser = tester.NewAccount(); + senderUser.GrantAccess(true); + senderUser.RegisterDerivationScheme("BTC", senderAddressType); - var unsupportedFormats = new[] { ScriptPubKeyType.Legacy }; - - - foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType))) + foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType))) { - if (senderAddressType == ScriptPubKeyType.TaprootBIP86) + if (receiverAddressType == ScriptPubKeyType.TaprootBIP86) continue; - var senderUser = tester.NewAccount(); - senderUser.GrantAccess(true); - senderUser.RegisterDerivationScheme("BTC", senderAddressType); + var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network); - foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType))) + TestLogs.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}"); + var receiverUser = tester.NewAccount(); + receiverUser.GrantAccess(true); + receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true); + await receiverUser.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true); + var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network); + + string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available"; + var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true }); + if (unsupportedFormats.Contains(receiverAddressType)) { - if (receiverAddressType == ScriptPubKeyType.TaprootBIP86) - continue; - var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network); - - TestLogs.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}"); - var receiverUser = tester.NewAccount(); - receiverUser.GrantAccess(true); - receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true); - await receiverUser.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true); - var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network); - - string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available"; - var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true }); - if (unsupportedFormats.Contains(receiverAddressType)) - { - Assert.Null(TestAccount.GetPayjoinBitcoinUrl(invoice, cashCow.Network)); - continue; - } - var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); - - txBuilder.AddCoins(senderCoin); - txBuilder.Send(invoiceAddress, invoice.BtcDue); - txBuilder.SetChange(await senderUser.GetNewAddress(network)); - txBuilder.SendEstimatedFees(new FeeRate(50m)); - var psbt = txBuilder.BuildPSBT(false); - psbt = await senderUser.Sign(psbt); - var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false); + Assert.Null(TestAccount.GetPayjoinBitcoinUrl(invoice, cashCow.Network)); + continue; } - } + var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); + txBuilder.AddCoins(senderCoin); + txBuilder.Send(invoiceAddress, invoice.BtcDue); + txBuilder.SetChange(await senderUser.GetNewAddress(network)); + txBuilder.SendEstimatedFees(new FeeRate(50m)); + var psbt = txBuilder.BuildPSBT(false); + psbt = await senderUser.Sign(psbt); + var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false); + } } } @@ -230,185 +221,181 @@ namespace BTCPayServer.Tests [Trait("Selenium", "Selenium")] public async Task CanUsePayjoinForTopUp() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + var receiver = s.CreateNewStore(); + var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit); + var receiverWalletId = new WalletId(receiver.storeId, "BTC"); + + var sender = s.CreateNewStore(); + var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit); + var senderWalletId = new WalletId(sender.storeId, "BTC"); + + await s.Server.ExplorerNode.GenerateAsync(1); + await s.FundStoreWallet(senderWalletId); + await s.FundStoreWallet(receiverWalletId); + + var invoiceId = s.CreateInvoice(receiver.storeId, null, "BTC"); + s.GoToInvoiceCheckout(invoiceId); + var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); + s.GoToWallet(senderWalletId, WalletsNavPages.Send); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.Clear()); + s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.SendKeys("0.023")); + + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + + await s.Server.WaitForEvent(() => { - await s.StartAsync(); - s.RegisterNewUser(true); - var receiver = s.CreateNewStore(); - var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit); - var receiverWalletId = new WalletId(receiver.storeId, "BTC"); + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); + return Task.CompletedTask; + }); - var sender = s.CreateNewStore(); - var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit); - var senderWalletId = new WalletId(sender.storeId, "BTC"); - - await s.Server.ExplorerNode.GenerateAsync(1); - await s.FundStoreWallet(senderWalletId); - await s.FundStoreWallet(receiverWalletId); - - var invoiceId = s.CreateInvoice(receiver.storeId, null, "BTC"); - s.GoToInvoiceCheckout(invoiceId); - var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) - .GetAttribute("href"); - Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); - s.GoToWallet(senderWalletId, WalletsNavPages.Send); - s.Driver.FindElement(By.Id("bip21parse")).Click(); - s.Driver.SwitchTo().Alert().SendKeys(bip21); - s.Driver.SwitchTo().Alert().Accept(); - s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.Clear()); - s.Driver.FindElementUntilNotStaled(By.Id("Outputs_0__Amount"), we => we.SendKeys("0.023")); - - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - - await s.Server.WaitForEvent(() => - { - s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); - return Task.CompletedTask; - }); - - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); - var invoiceRepository = s.Server.PayTester.GetService(); - await TestUtils.EventuallyAsync(async () => - { - var invoice = await invoiceRepository.GetInvoice(invoiceId); - Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); - Assert.Equal(0.023m, invoice.Price); - }); - } + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); + var invoiceRepository = s.Server.PayTester.GetService(); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await invoiceRepository.GetInvoice(invoiceId); + Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); + Assert.Equal(0.023m, invoice.Price); + }); } [Fact] [Trait("Selenium", "Selenium")] public async Task CanUsePayjoinViaUI() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + var invoiceRepository = s.Server.PayTester.GetService(); + s.RegisterNewUser(true); + + foreach (var format in new[] { ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH }) { - await s.StartAsync(); - var invoiceRepository = s.Server.PayTester.GetService(); - s.RegisterNewUser(true); + var cryptoCode = "BTC"; + var receiver = s.CreateNewStore(); + var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format); + var receiverWalletId = new WalletId(receiver.storeId, cryptoCode); - foreach (var format in new[] { ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH }) + //payjoin is enabled by default. + var invoiceId = s.CreateInvoice(receiver.storeId); + s.GoToInvoiceCheckout(invoiceId); + var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); + + s.GoToWalletSettings(receiver.storeId, cryptoCode); + Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); + + var sender = s.CreateNewStore(); + var senderSeed = s.GenerateWallet(cryptoCode, "", true, true, format); + var senderWalletId = new WalletId(sender.storeId, cryptoCode); + await s.Server.ExplorerNode.GenerateAsync(1); + await s.FundStoreWallet(senderWalletId); + + invoiceId = s.CreateInvoice(receiver.storeId); + s.GoToInvoiceCheckout(invoiceId); + bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); + + s.GoToWallet(senderWalletId, WalletsNavPages.Send); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) + .GetAttribute("value"))); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + await s.Server.WaitForEvent(() => { - var cryptoCode = "BTC"; - var receiver = s.CreateNewStore(); - var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format); - var receiverWalletId = new WalletId(receiver.storeId, cryptoCode); + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); + return Task.CompletedTask; + }); + //no funds in receiver wallet to do payjoin + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); + Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); + }); - //payjoin is enabled by default. - var invoiceId = s.CreateInvoice(receiver.storeId); - s.GoToInvoiceCheckout(invoiceId); - var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) - .GetAttribute("href"); - Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); + s.SelectStoreContext(receiver.storeId); + s.GoToInvoices(); + var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_details_{invoiceId}")) + .FindElement(By.ClassName("payment-value")); + Assert.False(paymentValueRowColumn.Text.Contains("payjoin", + StringComparison.InvariantCultureIgnoreCase)); - s.GoToWalletSettings(receiver.storeId, cryptoCode); - Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); + //let's do it all again, except now the receiver has funds and is able to payjoin + invoiceId = s.CreateInvoice(); + s.GoToInvoiceCheckout(invoiceId); + bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) + .GetAttribute("href"); + Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21); - var sender = s.CreateNewStore(); - var senderSeed = s.GenerateWallet(cryptoCode, "", true, true, format); - var senderWalletId = new WalletId(sender.storeId, cryptoCode); - await s.Server.ExplorerNode.GenerateAsync(1); - await s.FundStoreWallet(senderWalletId); + s.GoToWallet(senderWalletId, WalletsNavPages.Send); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) + .GetAttribute("value"))); + s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear(); + s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2"); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + var txId = await s.Server.WaitForEvent(() => + { + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); + return Task.CompletedTask; + }); + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await invoiceRepository.GetInvoice(invoiceId); + var payments = invoice.GetPayments(false); + Assert.Equal(2, payments.Count); + var originalPayment = payments[0]; + var coinjoinPayment = payments[1]; + Assert.Equal(-1, + ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount); + Assert.Equal(0, + ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount); + Assert.False(originalPayment.Accounted); + Assert.True(coinjoinPayment.Accounted); + Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value, + ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value); + Assert.Equal(originalPayment.GetCryptoPaymentData() + .AssertType() + .Value, + coinjoinPayment.GetCryptoPaymentData() + .AssertType() + .Value); + }); - invoiceId = s.CreateInvoice(receiver.storeId); - s.GoToInvoiceCheckout(invoiceId); - bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) - .GetAttribute("href"); - Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); + await TestUtils.EventuallyAsync(async () => + { + var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); + var dto = invoice.EntityToDTO(); + Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); + }); + s.GoToInvoices(receiver.storeId); + paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_details_{invoiceId}")) + .FindElement(By.ClassName("payment-value")); + Assert.False(paymentValueRowColumn.Text.Contains("payjoin", + StringComparison.InvariantCultureIgnoreCase)); - s.GoToWallet(senderWalletId, WalletsNavPages.Send); - s.Driver.FindElement(By.Id("bip21parse")).Click(); - s.Driver.SwitchTo().Alert().SendKeys(bip21); - s.Driver.SwitchTo().Alert().Accept(); - Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) - .GetAttribute("value"))); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - await s.Server.WaitForEvent(() => - { - s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); - return Task.CompletedTask; - }); - //no funds in receiver wallet to do payjoin - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning); - await TestUtils.EventuallyAsync(async () => - { - var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); - Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); - }); - - s.SelectStoreContext(receiver.storeId); - s.GoToInvoices(); - var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_details_{invoiceId}")) - .FindElement(By.ClassName("payment-value")); - Assert.False(paymentValueRowColumn.Text.Contains("payjoin", - StringComparison.InvariantCultureIgnoreCase)); - - //let's do it all again, except now the receiver has funds and is able to payjoin - invoiceId = s.CreateInvoice(); - s.GoToInvoiceCheckout(invoiceId); - bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) - .GetAttribute("href"); - Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21); - - s.GoToWallet(senderWalletId, WalletsNavPages.Send); - s.Driver.FindElement(By.Id("bip21parse")).Click(); - s.Driver.SwitchTo().Alert().SendKeys(bip21); - s.Driver.SwitchTo().Alert().Accept(); - Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) - .GetAttribute("value"))); - s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear(); - s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2"); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - var txId = await s.Server.WaitForEvent(() => - { - s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); - return Task.CompletedTask; - }); - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); - await TestUtils.EventuallyAsync(async () => - { - var invoice = await invoiceRepository.GetInvoice(invoiceId); - var payments = invoice.GetPayments(false); - Assert.Equal(2, payments.Count); - var originalPayment = payments[0]; - var coinjoinPayment = payments[1]; - Assert.Equal(-1, - ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount); - Assert.Equal(0, - ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount); - Assert.False(originalPayment.Accounted); - Assert.True(coinjoinPayment.Accounted); - Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value, - ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value); - Assert.Equal(originalPayment.GetCryptoPaymentData() - .AssertType() - .Value, - coinjoinPayment.GetCryptoPaymentData() - .AssertType() - .Value); - }); - - await TestUtils.EventuallyAsync(async () => - { - var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); - var dto = invoice.EntityToDTO(); - Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status); - }); - s.GoToInvoices(receiver.storeId); - paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_details_{invoiceId}")) - .FindElement(By.ClassName("payment-value")); - Assert.False(paymentValueRowColumn.Text.Contains("payjoin", - StringComparison.InvariantCultureIgnoreCase)); - - TestUtils.Eventually(() => - { - s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions); - Assert.Contains(invoiceId, s.Driver.PageSource); - Assert.Contains("payjoin", s.Driver.PageSource); + TestUtils.Eventually(() => + { + s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions); + Assert.Contains(invoiceId, s.Driver.PageSource); + Assert.Contains("payjoin", s.Driver.PageSource); //this label does not always show since input gets used // Assert.Contains("payjoin-exposed", s.Driver.PageSource); }); - } } } @@ -416,29 +403,28 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanUsePayjoin2() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var pjClient = tester.PayTester.GetService(); - var nbx = tester.PayTester.GetService().GetExplorerClient("BTC"); - var notifications = await nbx.CreateWebsocketNotificationSessionAsync(); - var alice = tester.NewAccount(); - await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); - await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme }); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var pjClient = tester.PayTester.GetService(); + var nbx = tester.PayTester.GetService().GetExplorerClient("BTC"); + var notifications = await nbx.CreateWebsocketNotificationSessionAsync(); + var alice = tester.NewAccount(); + await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); + await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme }); - BitcoinAddress address = null; - for (int i = 0; i < 5; i++) - { - address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address; - await tester.ExplorerNode.GenerateAsync(1); - tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m)); - await notifications.NextEventAsync(); - } - var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest); - var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest); - var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() - { - Destinations = + BitcoinAddress address = null; + for (int i = 0; i < 5; i++) + { + address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address; + await tester.ExplorerNode.GenerateAsync(1); + tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m)); + await notifications.NextEventAsync(); + } + var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest); + var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest); + var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() + { + Destinations = { new CreatePSBTDestination() { @@ -451,184 +437,183 @@ namespace BTCPayServer.Tests Destination = otherAddress } }, - FeePreference = new FeePreference() - { - ExplicitFee = Money.Satoshis(3000) - } - })).PSBT; - int paymentIndex = 0; - int changeIndex = 0; - int otherIndex = 0; - for (int i = 0; i < psbt.Outputs.Count; i++) + FeePreference = new FeePreference() { - if (psbt.Outputs[i].Value == Money.Coins(0.5m)) - paymentIndex = i; - else if (psbt.Outputs[i].Value == Money.Coins(0.1m)) - otherIndex = i; - else - changeIndex = i; + ExplicitFee = Money.Satoshis(3000) } - - var derivationSchemeSettings = alice.GetController().GetDerivationSchemeSettings(new WalletId(alice.StoreId, "BTC")); - var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings(); - psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); - using var fakeServer = new FakeServer(); - await fakeServer.Start(); - var bip21 = new BitcoinUrlBuilder($"bitcoin:{paymentAddress}?pj={fakeServer.ServerUri}", Network.RegTest); - var requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - var request = await fakeServer.GetNextRequest(); - Assert.Equal("1", request.Request.Query["v"][0]); - Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]); - Assert.Equal("1146", request.Request.Query["maxadditionalfeecontribution"][0]); - - TestLogs.LogInformation("The payjoin receiver tries to make us pay lots of fee"); - var originalPSBT = await ParsePSBT(request); - var proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1147); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - var ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("contribution is more than maxadditionalfeecontribution", ex.Message); - - TestLogs.LogInformation("The payjoin receiver tries to change one of our output"); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs[otherIndex].Value -= Money.Satoshis(1); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("The receiver decreased the value of one", ex.Message); - TestLogs.LogInformation("The payjoin receiver tries to pocket the fee"); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs[paymentIndex].Value += Money.Satoshis(1); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("The receiver decreased absolute fee", ex.Message); - - TestLogs.LogInformation("The payjoin receiver tries to remove one of our output"); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - var removedOutput = proposalTx.Outputs.First(o => o.ScriptPubKey == otherAddress.ScriptPubKey); - proposalTx.Outputs.Remove(removedOutput); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("Some of our outputs are not included in the proposal", ex.Message); - - TestLogs.LogInformation("The payjoin receiver tries to change their own output"); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs.First(o => o.ScriptPubKey == paymentAddress.ScriptPubKey).Value -= Money.Satoshis(1); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - await requesting; - - - TestLogs.LogInformation("The payjoin receiver tries to send money to himself"); - pjClient.MaxFeeBumpContribution = Money.Satoshis(1); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs[paymentIndex].Value += Money.Satoshis(1); - proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("is not only paying fee", ex.Message); - pjClient.MaxFeeBumpContribution = null; - TestLogs.LogInformation("The payjoin receiver can't use additional fee without adding inputs"); - pjClient.MinimumFeeRate = new FeeRate(50m); - requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); - request = await fakeServer.GetNextRequest(); - originalPSBT = await ParsePSBT(request); - proposalTx = originalPSBT.GetGlobalTransaction(); - proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1146); - await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); - fakeServer.Done(); - ex = await Assert.ThrowsAsync(async () => await requesting); - Assert.Contains("is not only paying for additional inputs", ex.Message); - pjClient.MinimumFeeRate = null; - - TestLogs.LogInformation("Make sure the receiver implementation do not take more fee than allowed"); - var bob = tester.NewAccount(); - await bob.GrantAccessAsync(); - await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); - - await notifications.DisposeAsync(); - notifications = await nbx.CreateWebsocketNotificationSessionAsync(); - await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme }); - address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; - tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); - await notifications.NextEventAsync(); - await bob.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true); - var invoice = bob.BitPay.CreateInvoice( - new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true }); - var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, - tester.ExplorerClient.Network.NBitcoinNetwork); - - psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() - { - Destinations = - { - new CreatePSBTDestination() - { - Amount = invoiceBIP21.Amount, - Destination = invoiceBIP21.Address - } - }, - FeePreference = new FeePreference() - { - ExplicitFee = Money.Satoshis(3001) - } - })).PSBT; - psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); - var endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest); - pjClient.MaxFeeBumpContribution = Money.Satoshis(50); - var proposal = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default); - Assert.True(proposal.TryGetFee(out var newFee)); - Assert.Equal(Money.Satoshis(3001 + 50), newFee); - proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); - proposal.Finalize(); - await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction()); - await notifications.NextEventAsync(); - - TestLogs.LogInformation("Abusing minFeeRate should give not enough money error"); - invoice = bob.BitPay.CreateInvoice( - new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true }); - invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, - tester.ExplorerClient.Network.NBitcoinNetwork); - psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() - { - Destinations = - { - new CreatePSBTDestination() - { - Amount = invoiceBIP21.Amount, - Destination = invoiceBIP21.Address - } - }, - FeePreference = new FeePreference() - { - ExplicitFee = Money.Satoshis(3001) - } - })).PSBT; - psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); - endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest); - pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m); - var ex2 = await Assert.ThrowsAsync(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default)); - Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError); + })).PSBT; + int paymentIndex = 0; + int changeIndex = 0; + int otherIndex = 0; + for (int i = 0; i < psbt.Outputs.Count; i++) + { + if (psbt.Outputs[i].Value == Money.Coins(0.5m)) + paymentIndex = i; + else if (psbt.Outputs[i].Value == Money.Coins(0.1m)) + otherIndex = i; + else + changeIndex = i; } + + var derivationSchemeSettings = alice.GetController().GetDerivationSchemeSettings(new WalletId(alice.StoreId, "BTC")); + var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings(); + psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); + using var fakeServer = new FakeServer(); + await fakeServer.Start(); + var bip21 = new BitcoinUrlBuilder($"bitcoin:{paymentAddress}?pj={fakeServer.ServerUri}", Network.RegTest); + var requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + var request = await fakeServer.GetNextRequest(); + Assert.Equal("1", request.Request.Query["v"][0]); + Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]); + Assert.Equal("1146", request.Request.Query["maxadditionalfeecontribution"][0]); + + TestLogs.LogInformation("The payjoin receiver tries to make us pay lots of fee"); + var originalPSBT = await ParsePSBT(request); + var proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1147); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + var ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("contribution is more than maxadditionalfeecontribution", ex.Message); + + TestLogs.LogInformation("The payjoin receiver tries to change one of our output"); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs[otherIndex].Value -= Money.Satoshis(1); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("The receiver decreased the value of one", ex.Message); + TestLogs.LogInformation("The payjoin receiver tries to pocket the fee"); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs[paymentIndex].Value += Money.Satoshis(1); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("The receiver decreased absolute fee", ex.Message); + + TestLogs.LogInformation("The payjoin receiver tries to remove one of our output"); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + var removedOutput = proposalTx.Outputs.First(o => o.ScriptPubKey == otherAddress.ScriptPubKey); + proposalTx.Outputs.Remove(removedOutput); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("Some of our outputs are not included in the proposal", ex.Message); + + TestLogs.LogInformation("The payjoin receiver tries to change their own output"); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs.First(o => o.ScriptPubKey == paymentAddress.ScriptPubKey).Value -= Money.Satoshis(1); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + await requesting; + + + TestLogs.LogInformation("The payjoin receiver tries to send money to himself"); + pjClient.MaxFeeBumpContribution = Money.Satoshis(1); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs[paymentIndex].Value += Money.Satoshis(1); + proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("is not only paying fee", ex.Message); + pjClient.MaxFeeBumpContribution = null; + TestLogs.LogInformation("The payjoin receiver can't use additional fee without adding inputs"); + pjClient.MinimumFeeRate = new FeeRate(50m); + requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default); + request = await fakeServer.GetNextRequest(); + originalPSBT = await ParsePSBT(request); + proposalTx = originalPSBT.GetGlobalTransaction(); + proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1146); + await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8); + fakeServer.Done(); + ex = await Assert.ThrowsAsync(async () => await requesting); + Assert.Contains("is not only paying for additional inputs", ex.Message); + pjClient.MinimumFeeRate = null; + + TestLogs.LogInformation("Make sure the receiver implementation do not take more fee than allowed"); + var bob = tester.NewAccount(); + await bob.GrantAccessAsync(); + await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); + + await notifications.DisposeAsync(); + notifications = await nbx.CreateWebsocketNotificationSessionAsync(); + await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme }); + address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; + tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); + await notifications.NextEventAsync(); + await bob.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true); + var invoice = bob.BitPay.CreateInvoice( + new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true }); + var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, + tester.ExplorerClient.Network.NBitcoinNetwork); + + psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() + { + Destinations = + { + new CreatePSBTDestination() + { + Amount = invoiceBIP21.Amount, + Destination = invoiceBIP21.Address + } + }, + FeePreference = new FeePreference() + { + ExplicitFee = Money.Satoshis(3001) + } + })).PSBT; + psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); + var endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest); + pjClient.MaxFeeBumpContribution = Money.Satoshis(50); + var proposal = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default); + Assert.True(proposal.TryGetFee(out var newFee)); + Assert.Equal(Money.Satoshis(3001 + 50), newFee); + proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); + proposal.Finalize(); + await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction()); + await notifications.NextEventAsync(); + + TestLogs.LogInformation("Abusing minFeeRate should give not enough money error"); + invoice = bob.BitPay.CreateInvoice( + new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true }); + invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, + tester.ExplorerClient.Network.NBitcoinNetwork); + psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest() + { + Destinations = + { + new CreatePSBTDestination() + { + Amount = invoiceBIP21.Amount, + Destination = invoiceBIP21.Address + } + }, + FeePreference = new FeePreference() + { + ExplicitFee = Money.Satoshis(3001) + } + })).PSBT; + psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath()); + endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest); + pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m); + var ex2 = await Assert.ThrowsAsync(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default)); + Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError); } private static async Task ParsePSBT(Microsoft.AspNetCore.Http.HttpContext request) diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs index 8c1746b7b..50714d9db 100644 --- a/BTCPayServer.Tests/PaymentRequestTests.cs +++ b/BTCPayServer.Tests/PaymentRequestTests.cs @@ -24,222 +24,216 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanCreateViewUpdateAndDeletePaymentRequest() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + + var user2 = tester.NewAccount(); + + await user2.GrantAccessAsync(); + + var paymentRequestController = user.GetController(); + var guestpaymentRequestController = user2.GetController(); + + var request = new UpdatePaymentRequestViewModel { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); + Title = "original juice", + Currency = "BTC", + Amount = 1, + StoreId = user.StoreId, + Description = "description" + }; + var id = Assert + .IsType(await paymentRequestController.EditPaymentRequest(null, request)) + .RouteValues.Values.Last().ToString(); - var user2 = tester.NewAccount(); + paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = request.StoreId }); - await user2.GrantAccessAsync(); + // Permission guard for guests editing + Assert + .IsType(guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); - var paymentRequestController = user.GetController(); - var guestpaymentRequestController = user2.GetController(); - - var request = new UpdatePaymentRequestViewModel - { - Title = "original juice", - Currency = "BTC", - Amount = 1, - StoreId = user.StoreId, - Description = "description" - }; - var id = Assert - .IsType(await paymentRequestController.EditPaymentRequest(null, request)) - .RouteValues.Values.Last().ToString(); - - paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = request.StoreId }); - - // Permission guard for guests editing - Assert - .IsType(guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); - - request.Title = "update"; - Assert.IsType(await paymentRequestController.EditPaymentRequest(id, request)); - - Assert.Equal(request.Title, - Assert.IsType(Assert - .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Title); - - Assert.False(string.IsNullOrEmpty(id)); + request.Title = "update"; + Assert.IsType(await paymentRequestController.EditPaymentRequest(id, request)); + Assert.Equal(request.Title, Assert.IsType(Assert - .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model); + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Title); - // Archive - Assert - .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); - Assert.True(Assert - .IsType(Assert - .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); + Assert.False(string.IsNullOrEmpty(id)); - Assert.Empty(Assert - .IsType(Assert - .IsType(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items); + Assert.IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model); - // Unarchive - Assert - .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); + // Archive + Assert + .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); + Assert.True(Assert + .IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); - Assert.False(Assert - .IsType(Assert - .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); + Assert.Empty(Assert + .IsType(Assert + .IsType(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items); - Assert.Single(Assert - .IsType(Assert - .IsType(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items); - } + // Unarchive + Assert + .IsType(await paymentRequestController.TogglePaymentRequestArchival(id)); + + Assert.False(Assert + .IsType(Assert + .IsType(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived); + + Assert.Single(Assert + .IsType(Assert + .IsType(await paymentRequestController.GetPaymentRequests(user.StoreId)).Model).Items); } [Fact(Timeout = 60 * 2 * 1000)] [Trait("Integration", "Integration")] public async Task CanPayPaymentRequestWhenPossible() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + + var paymentRequestController = user.GetController(); + + Assert.IsType( + await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString())); + + + var request = new UpdatePaymentRequestViewModel() { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); + Title = "original juice", + Currency = "BTC", + Amount = 1, + StoreId = user.StoreId, + Description = "description" + }; + var response = Assert + .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) + .RouteValues.Last(); - var paymentRequestController = user.GetController(); + var invoiceId = Assert + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value + .ToString(); - Assert.IsType( - await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString())); + var actionResult = Assert + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + Assert.Equal("Checkout", actionResult.ActionName); + Assert.Equal("UIInvoice", actionResult.ControllerName); + Assert.Contains(actionResult.RouteValues, + pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); - var request = new UpdatePaymentRequestViewModel() - { - Title = "original juice", - Currency = "BTC", - Amount = 1, - StoreId = user.StoreId, - Description = "description" - }; - var response = Assert - .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) - .RouteValues.Last(); + var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); + Assert.Equal(1, invoice.Price); - var invoiceId = Assert - .IsType( - await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value - .ToString(); + request = new UpdatePaymentRequestViewModel() + { + Title = "original juice with expiry", + Currency = "BTC", + Amount = 1, + ExpiryDate = DateTime.Today.Subtract(TimeSpan.FromDays(2)), + StoreId = user.StoreId, + Description = "description" + }; - var actionResult = Assert - .IsType( - await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + response = Assert + .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) + .RouteValues.Last(); - Assert.Equal("Checkout", actionResult.ActionName); - Assert.Equal("UIInvoice", actionResult.ControllerName); - Assert.Contains(actionResult.RouteValues, - pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); - - var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); - Assert.Equal(1, invoice.Price); - - request = new UpdatePaymentRequestViewModel() - { - Title = "original juice with expiry", - Currency = "BTC", - Amount = 1, - ExpiryDate = DateTime.Today.Subtract(TimeSpan.FromDays(2)), - StoreId = user.StoreId, - Description = "description" - }; - - response = Assert - .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) - .RouteValues.Last(); - - Assert - .IsType( - await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)); - } + Assert + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)); } [Fact(Timeout = 60 * 2 * 1000)] [Trait("Integration", "Integration")] public async Task CanCancelPaymentWhenPossible() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + + var paymentRequestController = user.GetController(); + + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false)); + + var request = new UpdatePaymentRequestViewModel() { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + Title = "original juice", + Currency = "BTC", + Amount = 1, + StoreId = user.StoreId, + Description = "description" + }; + var response = Assert + .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) + .RouteValues.Last(); + var invoiceId = response.Value.ToString(); + await paymentRequestController.PayPaymentRequest(invoiceId, false); + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(invoiceId, false)); - var paymentRequestController = user.GetController(); + request.AllowCustomPaymentAmounts = true; - Assert.IsType(await - paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false)); + response = Assert + .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) + .RouteValues.Last(); - var request = new UpdatePaymentRequestViewModel() - { - Title = "original juice", - Currency = "BTC", - Amount = 1, - StoreId = user.StoreId, - Description = "description" - }; - var response = Assert - .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) - .RouteValues.Last(); - var invoiceId = response.Value.ToString(); - await paymentRequestController.PayPaymentRequest(invoiceId, false); - Assert.IsType(await - paymentRequestController.CancelUnpaidPendingInvoice(invoiceId, false)); + var paymentRequestId = response.Value.ToString(); - request.AllowCustomPaymentAmounts = true; + invoiceId = Assert + .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) + .Value + .ToString(); - response = Assert - .IsType(paymentRequestController.EditPaymentRequest(null, request).Result) - .RouteValues.Last(); + var actionResult = Assert + .IsType( + await paymentRequestController.PayPaymentRequest(response.Value.ToString())); - var paymentRequestId = response.Value.ToString(); + Assert.Equal("Checkout", actionResult.ActionName); + Assert.Equal("UIInvoice", actionResult.ControllerName); + Assert.Contains(actionResult.RouteValues, + pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); - invoiceId = Assert - .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) - .Value - .ToString(); + var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); + Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.New), invoice.Status); + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); - var actionResult = Assert - .IsType( - await paymentRequestController.PayPaymentRequest(response.Value.ToString())); + invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); + Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.Invalid), invoice.Status); - Assert.Equal("Checkout", actionResult.ActionName); - Assert.Equal("UIInvoice", actionResult.ControllerName); - Assert.Contains(actionResult.RouteValues, - pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId); + Assert.IsType(await + paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); - var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); - Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.New), invoice.Status); - Assert.IsType(await - paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); + invoiceId = Assert + .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) + .Value + .ToString(); - invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant); - Assert.Equal(InvoiceState.ToString(InvoiceStatusLegacy.Invalid), invoice.Status); + await user.BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant); - Assert.IsType(await - paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false)); - - invoiceId = Assert - .IsType(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)) - .Value - .ToString(); - - await user.BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant); - - //a hack to generate invoices for the payment request is to manually create an invoice with an order id that matches: - user.BitPay.CreateInvoice(new Invoice(1, "USD") - { - OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(paymentRequestId) - }); - //shouldn't crash - await paymentRequestController.ViewPaymentRequest(paymentRequestId); - await paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId); - } + //a hack to generate invoices for the payment request is to manually create an invoice with an order id that matches: + user.BitPay.CreateInvoice(new Invoice(1, "USD") + { + OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(paymentRequestId) + }); + //shouldn't crash + await paymentRequestController.ViewPaymentRequest(paymentRequestId); + await paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId); } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 24e166302..8152717a2 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -49,305 +49,291 @@ namespace BTCPayServer.Tests [Fact(Timeout = TestTimeout)] public async Task CanNavigateServerSettings() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(true); - s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click(); - s.Driver.AssertNoError(); - s.ClickOnAllSectionLinks(); - s.Driver.FindElement(By.LinkText("Services")).Click(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click(); + s.Driver.AssertNoError(); + s.ClickOnAllSectionLinks(); + s.Driver.FindElement(By.LinkText("Services")).Click(); - TestLogs.LogInformation("Let's check if we can access the logs"); - s.Driver.FindElement(By.LinkText("Logs")).Click(); - s.Driver.FindElement(By.PartialLinkText(".log")).Click(); - Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource); - s.Driver.Quit(); - } + TestLogs.LogInformation("Let's check if we can access the logs"); + s.Driver.FindElement(By.LinkText("Logs")).Click(); + s.Driver.FindElement(By.PartialLinkText(".log")).Click(); + Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource); + s.Driver.Quit(); } [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanUseLndSeedBackup() { - using (var s = CreateSeleniumTester()) - { - s.Server.ActivateLightning(); - await s.StartAsync(); - s.RegisterNewUser(true); - s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click(); - s.Driver.AssertNoError(); - s.Driver.FindElement(By.LinkText("Services")).Click(); + using var s = CreateSeleniumTester(); + s.Server.ActivateLightning(); + await s.StartAsync(); + s.RegisterNewUser(true); + s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click(); + s.Driver.AssertNoError(); + s.Driver.FindElement(By.LinkText("Services")).Click(); - TestLogs.LogInformation("Let's if we can access LND's seed"); - Assert.Contains("server/services/lndseedbackup/BTC", s.Driver.PageSource); - s.Driver.Navigate().GoToUrl(s.Link("/server/services/lndseedbackup/BTC")); - s.Driver.FindElement(By.Id("details")).Click(); - var seedEl = s.Driver.FindElement(By.Id("Seed")); - Assert.True(seedEl.Displayed); - Assert.Contains("about over million", seedEl.Text, StringComparison.OrdinalIgnoreCase); - var passEl = s.Driver.FindElement(By.Id("WalletPassword")); - Assert.True(passEl.Displayed); - Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase); - s.Driver.FindElement(By.Id("delete")).Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - s.FindAlertMessage(); - seedEl = s.Driver.FindElement(By.Id("Seed")); - Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase); - } + TestLogs.LogInformation("Let's if we can access LND's seed"); + Assert.Contains("server/services/lndseedbackup/BTC", s.Driver.PageSource); + s.Driver.Navigate().GoToUrl(s.Link("/server/services/lndseedbackup/BTC")); + s.Driver.FindElement(By.Id("details")).Click(); + var seedEl = s.Driver.FindElement(By.Id("Seed")); + Assert.True(seedEl.Displayed); + Assert.Contains("about over million", seedEl.Text, StringComparison.OrdinalIgnoreCase); + var passEl = s.Driver.FindElement(By.Id("WalletPassword")); + Assert.True(passEl.Displayed); + Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase); + s.Driver.FindElement(By.Id("delete")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + s.FindAlertMessage(); + seedEl = s.Driver.FindElement(By.Id("Seed")); + Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase); } [Fact(Timeout = TestTimeout)] [Trait("Selenium", "Selenium")] public async Task CanChangeUserMail() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); - var tester = s.Server; - var u1 = tester.NewAccount(); - await u1.GrantAccessAsync(); - await u1.MakeAdmin(false); + 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); + var u2 = tester.NewAccount(); + await u2.GrantAccessAsync(); + await u2.MakeAdmin(false); - s.GoToLogin(); - s.Login(u1.RegisterDetails.Email, u1.RegisterDetails.Password); - s.GoToProfile(); - s.Driver.FindElement(By.Id("Email")).Clear(); - s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email); - s.Driver.FindElement(By.Id("save")).Click(); + s.GoToLogin(); + s.Login(u1.RegisterDetails.Email, u1.RegisterDetails.Password); + s.GoToProfile(); + s.Driver.FindElement(By.Id("Email")).Clear(); + s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email); + s.Driver.FindElement(By.Id("save")).Click(); - Assert.Contains("The email address is already in use with an other account.", - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text); + Assert.Contains("The email address is already in use with an other account.", + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text); - s.GoToProfile(); - s.Driver.FindElement(By.Id("Email")).Clear(); - var changedEmail = Guid.NewGuid() + "@lol.com"; - s.Driver.FindElement(By.Id("Email")).SendKeys(changedEmail); - s.Driver.FindElement(By.Id("save")).Click(); - s.FindAlertMessage(); + s.GoToProfile(); + s.Driver.FindElement(By.Id("Email")).Clear(); + var changedEmail = Guid.NewGuid() + "@lol.com"; + s.Driver.FindElement(By.Id("Email")).SendKeys(changedEmail); + s.Driver.FindElement(By.Id("save")).Click(); + s.FindAlertMessage(); - var manager = tester.PayTester.GetService>(); - Assert.NotNull(await manager.FindByNameAsync(changedEmail)); - Assert.NotNull(await manager.FindByEmailAsync(changedEmail)); - } + var manager = tester.PayTester.GetService>(); + Assert.NotNull(await manager.FindByNameAsync(changedEmail)); + Assert.NotNull(await manager.FindByEmailAsync(changedEmail)); } [Fact(Timeout = TestTimeout)] public async Task NewUserLogin() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - //Register & Log Out - var email = s.RegisterNewUser(); - s.Logout(); - s.Driver.AssertNoError(); - Assert.Contains("/login", s.Driver.Url); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + //Register & Log Out + var email = s.RegisterNewUser(); + s.Logout(); + s.Driver.AssertNoError(); + Assert.Contains("/login", s.Driver.Url); - s.GoToUrl("/UIManage/Index"); - Assert.Contains("ReturnUrl=%2FUIManage%2FIndex", s.Driver.Url); + s.GoToUrl("/UIManage/Index"); + Assert.Contains("ReturnUrl=%2FUIManage%2FIndex", s.Driver.Url); - // We should be redirected to login - //Same User Can Log Back In - s.Driver.FindElement(By.Id("Email")).SendKeys(email); - s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); - s.Driver.FindElement(By.Id("LoginButton")).Click(); + // We should be redirected to login + //Same User Can Log Back In + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); - // We should be redirected to invoice - Assert.EndsWith("/UIManage/Index", s.Driver.Url); + // We should be redirected to invoice + Assert.EndsWith("/UIManage/Index", s.Driver.Url); - // Should not be able to reach server settings - s.GoToUrl("/server/users"); - Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url); - s.GoToHome(); + // Should not be able to reach server settings + s.GoToUrl("/server/users"); + Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url); + s.GoToHome(); - //Change Password & Log Out - s.GoToProfile(ManageNavPages.ChangePassword); - s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456"); - s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???"); - s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???"); - s.Driver.FindElement(By.Id("UpdatePassword")).Click(); - s.Logout(); - s.Driver.AssertNoError(); + //Change Password & Log Out + s.GoToProfile(ManageNavPages.ChangePassword); + s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456"); + s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("UpdatePassword")).Click(); + s.Logout(); + s.Driver.AssertNoError(); - //Log In With New Password - s.Driver.FindElement(By.Id("Email")).SendKeys(email); - s.Driver.FindElement(By.Id("Password")).SendKeys("abc???"); - s.Driver.FindElement(By.Id("LoginButton")).Click(); + //Log In With New Password + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("abc???"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); - s.GoToProfile(); - s.ClickOnAllSectionLinks(); + s.GoToProfile(); + s.ClickOnAllSectionLinks(); - //let's test invite link - s.Logout(); - s.GoToRegister(); - s.RegisterNewUser(true); - s.GoToServer(ServerNavPages.Users); - s.Driver.FindElement(By.Id("CreateUser")).Click(); + //let's test invite link + s.Logout(); + s.GoToRegister(); + s.RegisterNewUser(true); + s.GoToServer(ServerNavPages.Users); + s.Driver.FindElement(By.Id("CreateUser")).Click(); - var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com"; - s.Driver.FindElement(By.Id("Email")).SendKeys(usr); - s.Driver.FindElement(By.Id("Save")).Click(); - var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text; + var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com"; + s.Driver.FindElement(By.Id("Email")).SendKeys(usr); + s.Driver.FindElement(By.Id("Save")).Click(); + var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text; - s.Logout(); - s.Driver.Navigate().GoToUrl(url); - Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type")); - Assert.Equal(usr, s.Driver.FindElement(By.Id("Email")).GetAttribute("value")); + s.Logout(); + s.Driver.Navigate().GoToUrl(url); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type")); + Assert.Equal(usr, s.Driver.FindElement(By.Id("Email")).GetAttribute("value")); - s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); - s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); - s.Driver.FindElement(By.Id("SetPassword")).Click(); - s.FindAlertMessage(); - s.Driver.FindElement(By.Id("Email")).SendKeys(usr); - s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); - s.Driver.FindElement(By.Id("LoginButton")).Click(); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); + s.Driver.FindElement(By.Id("SetPassword")).Click(); + s.FindAlertMessage(); + s.Driver.FindElement(By.Id("Email")).SendKeys(usr); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); - // We should be logged in now - s.Driver.FindElement(By.Id("mainNav")); + // We should be logged in now + s.Driver.FindElement(By.Id("mainNav")); - //let's test delete user quickly while we're at it - s.GoToProfile(); - s.Driver.FindElement(By.Id("delete-user")).Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + //let's test delete user quickly while we're at it + s.GoToProfile(); + s.Driver.FindElement(By.Id("delete-user")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - Assert.Contains("/login", s.Driver.Url); - } + Assert.Contains("/login", s.Driver.Url); } [Fact(Timeout = TestTimeout)] public async Task CanUseSSHService() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + var settings = s.Server.PayTester.GetService(); + var policies = await settings.GetSettingAsync() ?? new PoliciesSettings(); + policies.DisableSSHService = false; + await settings.UpdateSetting(policies); + s.RegisterNewUser(isAdmin: true); + s.Driver.Navigate().GoToUrl(s.Link("/server/services")); + Assert.Contains("server/services/ssh", s.Driver.PageSource); + using (var client = await s.Server.PayTester.GetService().SSHSettings + .ConnectAsync()) { - await s.StartAsync(); - var settings = s.Server.PayTester.GetService(); - var policies = await settings.GetSettingAsync() ?? new PoliciesSettings(); - policies.DisableSSHService = false; - await settings.UpdateSetting(policies); - s.RegisterNewUser(isAdmin: true); - s.Driver.Navigate().GoToUrl(s.Link("/server/services")); - Assert.Contains("server/services/ssh", s.Driver.PageSource); - using (var client = await s.Server.PayTester.GetService().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); - } - - s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); - s.Driver.AssertNoError(); - s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); - s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2"); - s.Driver.FindElement(By.Id("submit")).Click(); - s.Driver.AssertNoError(); - - var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; - // 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(s.Driver.PageSource.Contains("authorized_keys has been updated", - StringComparison.OrdinalIgnoreCase)); - - s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); - s.Driver.FindElement(By.Id("submit")).Click(); - - text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; - Assert.DoesNotContain("test2", text); - - // Let's try to disable it now - s.Driver.FindElement(By.Id("disable")).Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DISABLE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); - Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase)); - - policies = await settings.GetSettingAsync(); - Assert.True(policies.DisableSSHService); - - policies.DisableSSHService = false; - await settings.UpdateSetting(policies); + var result = await client.RunBash("echo hello"); + Assert.Equal(string.Empty, result.Error); + Assert.Equal("hello\n", result.Output); + Assert.Equal(0, result.ExitStatus); } + + s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); + s.Driver.AssertNoError(); + s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); + s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2"); + s.Driver.FindElement(By.Id("submit")).Click(); + s.Driver.AssertNoError(); + + var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; + // 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(s.Driver.PageSource.Contains("authorized_keys has been updated", + StringComparison.OrdinalIgnoreCase)); + + s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); + s.Driver.FindElement(By.Id("submit")).Click(); + + text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; + Assert.DoesNotContain("test2", text); + + // Let's try to disable it now + s.Driver.FindElement(By.Id("disable")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DISABLE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); + Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase)); + + policies = await settings.GetSettingAsync(); + Assert.True(policies.DisableSSHService); + + policies.DisableSSHService = false; + await settings.UpdateSetting(policies); } [Fact(Timeout = TestTimeout)] public async Task CanSetupEmailServer() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(isAdmin: true); + s.Driver.Navigate().GoToUrl(s.Link("/server/emails")); + if (s.Driver.PageSource.Contains("Configured")) { - await s.StartAsync(); - s.RegisterNewUser(isAdmin: true); - s.Driver.Navigate().GoToUrl(s.Link("/server/emails")); - if (s.Driver.PageSource.Contains("Configured")) - { - s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); - s.FindAlertMessage(); - } - CanSetupEmailCore(s); - s.CreateNewStore(); - s.GoToUrl($"stores/{s.StoreId}/emails"); - CanSetupEmailCore(s); + s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); + s.FindAlertMessage(); } + CanSetupEmailCore(s); + s.CreateNewStore(); + s.GoToUrl($"stores/{s.StoreId}/emails"); + CanSetupEmailCore(s); } [Fact(Timeout = TestTimeout)] public async Task CanUseDynamicDns() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(isAdmin: true); + s.Driver.Navigate().GoToUrl(s.Link("/server/services")); + Assert.Contains("Dynamic DNS", s.Driver.PageSource); + + s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns")); + s.Driver.AssertNoError(); + if (s.Driver.PageSource.Contains("pouet.hello.com")) { - await s.StartAsync(); - s.RegisterNewUser(isAdmin: true); - s.Driver.Navigate().GoToUrl(s.Link("/server/services")); - Assert.Contains("Dynamic DNS", s.Driver.PageSource); - - s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns")); - s.Driver.AssertNoError(); - if (s.Driver.PageSource.Contains("pouet.hello.com")) - { - // Cleanup old test run - s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete")); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - } - - s.Driver.FindElement(By.Id("AddDynamicDNS")).Click(); - s.Driver.AssertNoError(); - // We will just cheat for test purposes by only querying the server - s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/")); - s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com"); - s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog"); - s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter); - s.Driver.AssertNoError(); - Assert.Contains("The Dynamic DNS has been successfully queried", s.Driver.PageSource); - Assert.EndsWith("/server/services/dynamic-dns", s.Driver.Url); - - // Try to do the same thing should fail (hostname already exists) - s.Driver.FindElement(By.Id("AddDynamicDNS")).Click(); - s.Driver.AssertNoError(); - s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/")); - s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com"); - s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog"); - s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter); - s.Driver.AssertNoError(); - Assert.Contains("This hostname already exists", s.Driver.PageSource); - - // Delete it - s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns")); - Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource); + // Cleanup old test run s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete")); s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - s.Driver.AssertNoError(); - - Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource); } + + s.Driver.FindElement(By.Id("AddDynamicDNS")).Click(); + s.Driver.AssertNoError(); + // We will just cheat for test purposes by only querying the server + s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/")); + s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com"); + s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog"); + s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter); + s.Driver.AssertNoError(); + Assert.Contains("The Dynamic DNS has been successfully queried", s.Driver.PageSource); + Assert.EndsWith("/server/services/dynamic-dns", s.Driver.Url); + + // Try to do the same thing should fail (hostname already exists) + s.Driver.FindElement(By.Id("AddDynamicDNS")).Click(); + s.Driver.AssertNoError(); + s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/")); + s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com"); + s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("MyLog"); + s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("MyLog" + Keys.Enter); + s.Driver.AssertNoError(); + Assert.Contains("This hostname already exists", s.Driver.PageSource); + + // Delete it + s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns")); + Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource); + s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete")); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + s.Driver.AssertNoError(); + + Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource); } [Fact(Timeout = TestTimeout)] public async Task CanCreateInvoiceInUI() @@ -384,700 +370,678 @@ namespace BTCPayServer.Tests [Fact(Timeout = TestTimeout)] public async Task CanSetupStoreViaGuide() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(); - s.GoToUrl("/"); - - Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present"); - Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorCreate\""), "Store selector create button should be present"); - - // verify steps for store creation are displayed correctly - s.Driver.FindElement(By.Id("SetupGuide-Store")).Click(); - Assert.Contains("/stores/create", s.Driver.Url); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(); + s.GoToUrl("/"); - s.CreateNewStore(); - s.GoToUrl("/"); - - Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should be present"); - Assert.False(s.Driver.PageSource.Contains("id=\"SetupGuide\""), "Setup guide should not be present"); - } + Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present"); + Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorCreate\""), "Store selector create button should be present"); + + // verify steps for store creation are displayed correctly + s.Driver.FindElement(By.Id("SetupGuide-Store")).Click(); + Assert.Contains("/stores/create", s.Driver.Url); + + s.CreateNewStore(); + s.GoToUrl("/"); + + Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should be present"); + Assert.False(s.Driver.PageSource.Contains("id=\"SetupGuide\""), "Setup guide should not be present"); } [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanCreateStores() { - using (var s = CreateSeleniumTester()) - { - s.Server.ActivateLightning(); - await s.StartAsync(); - var alice = s.RegisterNewUser(true); - (string storeName, string storeId) = s.CreateNewStore(); - var storeUrl = $"/stores/{storeId}"; - var onchainHint = "Set up your wallet to receive payments at your store."; - var offchainHint = "A connection to a Lightning node is required to receive Lightning payments."; + using var s = CreateSeleniumTester(); + s.Server.ActivateLightning(); + await s.StartAsync(); + var alice = s.RegisterNewUser(true); + (string storeName, string storeId) = s.CreateNewStore(); + var storeUrl = $"/stores/{storeId}"; + var onchainHint = "Set up your wallet to receive payments at your store."; + var offchainHint = "A connection to a Lightning node is required to receive Lightning payments."; - // verify that hints are displayed on the store page - Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not present"); - Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present"); - - s.GoToStore(); - Assert.Contains(storeName, s.Driver.PageSource); - Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point"); - Assert.True(s.Driver.PageSource.Contains(offchainHint), - "Lightning hint should be present at this point"); + // verify that hints are displayed on the store page + Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not present"); + Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present"); - // verify steps for wallet setup are displayed correctly - s.GoToStore(StoreNavPages.Dashboard); - Assert.True(s.Driver.FindElement(By.Id("SetupGuide-StoreDone")).Displayed); - Assert.True(s.Driver.FindElement(By.Id("SetupGuide-Wallet")).Displayed); - Assert.True(s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Displayed); + s.GoToStore(); + Assert.Contains(storeName, s.Driver.PageSource); + Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point"); + Assert.True(s.Driver.PageSource.Contains(offchainHint), + "Lightning hint should be present at this point"); - // setup onchain wallet - s.Driver.FindElement(By.Id("SetupGuide-Wallet")).Click(); - Thread.Sleep(10000); - s.AddDerivationScheme(); - s.Driver.AssertNoError(); - Assert.False(s.Driver.PageSource.Contains(onchainHint), - "Wallet hint not dismissed on derivation scheme add"); + // verify steps for wallet setup are displayed correctly + s.GoToStore(StoreNavPages.Dashboard); + Assert.True(s.Driver.FindElement(By.Id("SetupGuide-StoreDone")).Displayed); + Assert.True(s.Driver.FindElement(By.Id("SetupGuide-Wallet")).Displayed); + Assert.True(s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Displayed); - s.GoToStore(StoreNavPages.Dashboard); - Assert.True(s.Driver.FindElement(By.Id("SetupGuide-WalletDone")).Displayed); - - // setup offchain wallet - s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click(); - s.AddLightningNode(); - s.Driver.AssertNoError(); - var successAlert = s.FindAlertMessage(); - Assert.Contains("BTC Lightning node updated.", successAlert.Text); - Assert.False(s.Driver.PageSource.Contains(offchainHint), - "Lightning hint should be dismissed at this point"); + // setup onchain wallet + s.Driver.FindElement(By.Id("SetupGuide-Wallet")).Click(); + Thread.Sleep(10000); + s.AddDerivationScheme(); + s.Driver.AssertNoError(); + Assert.False(s.Driver.PageSource.Contains(onchainHint), + "Wallet hint not dismissed on derivation scheme add"); - s.ClickOnAllSectionLinks(); - - s.GoToStore(StoreNavPages.Dashboard); - Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed); - - s.GoToInvoices(); - Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource); - var invoiceId = s.CreateInvoice(); - s.FindAlertMessage(); - s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); - var invoiceUrl = s.Driver.Url; + s.GoToStore(StoreNavPages.Dashboard); + Assert.True(s.Driver.FindElement(By.Id("SetupGuide-WalletDone")).Displayed); - //let's test archiving an invoice - Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); - s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); - Assert.Contains("Unarchive", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); + // setup offchain wallet + s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click(); + s.AddLightningNode(); + s.Driver.AssertNoError(); + var successAlert = s.FindAlertMessage(); + Assert.Contains("BTC Lightning node updated.", successAlert.Text); + Assert.False(s.Driver.PageSource.Contains(offchainHint), + "Lightning hint should be dismissed at this point"); - //check that it no longer appears in list - s.GoToInvoices(); - Assert.DoesNotContain(invoiceId, s.Driver.PageSource); + s.ClickOnAllSectionLinks(); - //ok, let's unarchive and see that it shows again - s.Driver.Navigate().GoToUrl(invoiceUrl); - s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); - s.FindAlertMessage(); - Assert.DoesNotContain("Unarchive", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); - s.GoToInvoices(); - Assert.Contains(invoiceId, s.Driver.PageSource); - - // archive via list - s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click(); - s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click(); - Assert.Contains("1 invoice archived", s.FindAlertMessage().Text); - Assert.DoesNotContain(invoiceId, s.Driver.PageSource); - - // unarchive via list - s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click(); - s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click(); - Assert.Contains(invoiceId, s.Driver.PageSource); - s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click(); - s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click(); - Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text); - Assert.Contains(invoiceId, s.Driver.PageSource); + s.GoToStore(StoreNavPages.Dashboard); + Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed); - // When logout out we should not be able to access store and invoice details - s.Logout(); - s.GoToUrl(storeUrl); - Assert.Contains("ReturnUrl", s.Driver.Url); - s.Driver.Navigate().GoToUrl(invoiceUrl); - Assert.Contains("ReturnUrl", s.Driver.Url); - s.GoToRegister(); + s.GoToInvoices(); + Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource); + var invoiceId = s.CreateInvoice(); + s.FindAlertMessage(); + s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); + var invoiceUrl = s.Driver.Url; - // When logged in as different user we should not be able to access store and invoice details - var bob = s.RegisterNewUser(); - s.GoToUrl(storeUrl); - Assert.Contains("ReturnUrl", s.Driver.Url); - s.Driver.Navigate().GoToUrl(invoiceUrl); - s.AssertAccessDenied(); - s.GoToHome(); - s.Logout(); + //let's test archiving an invoice + Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); + s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); + Assert.Contains("Unarchive", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); - // Let's add Bob as a guest to alice's store - s.LogIn(alice); - s.GoToUrl(storeUrl + "/users"); - s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter); - Assert.Contains("User added successfully", s.Driver.PageSource); - s.Logout(); + //check that it no longer appears in list + s.GoToInvoices(); + Assert.DoesNotContain(invoiceId, s.Driver.PageSource); - // Bob should not have access to store, but should have access to invoice - s.LogIn(bob); - s.GoToUrl(storeUrl); - Assert.Contains("ReturnUrl", s.Driver.Url); - s.GoToUrl(invoiceUrl); - s.Driver.AssertNoError(); + //ok, let's unarchive and see that it shows again + s.Driver.Navigate().GoToUrl(invoiceUrl); + s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); + s.FindAlertMessage(); + Assert.DoesNotContain("Unarchive", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); + s.GoToInvoices(); + Assert.Contains(invoiceId, s.Driver.PageSource); - // Alice should be able to delete the store - s.Logout(); - s.LogIn(alice); - s.GoToStore(StoreNavPages.GeneralSettings); - s.Driver.FindElement(By.Id("DeleteStore")).Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - s.GoToUrl(storeUrl); - Assert.Contains("ReturnUrl", s.Driver.Url); - } + // archive via list + s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click(); + s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); + s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click(); + Assert.Contains("1 invoice archived", s.FindAlertMessage().Text); + Assert.DoesNotContain(invoiceId, s.Driver.PageSource); + + // unarchive via list + s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click(); + s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click(); + Assert.Contains(invoiceId, s.Driver.PageSource); + s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click(); + s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); + s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click(); + Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text); + Assert.Contains(invoiceId, s.Driver.PageSource); + + // When logout out we should not be able to access store and invoice details + s.Logout(); + s.GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.Driver.Navigate().GoToUrl(invoiceUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.GoToRegister(); + + // When logged in as different user we should not be able to access store and invoice details + var bob = s.RegisterNewUser(); + s.GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.Driver.Navigate().GoToUrl(invoiceUrl); + s.AssertAccessDenied(); + s.GoToHome(); + s.Logout(); + + // Let's add Bob as a guest to alice's store + s.LogIn(alice); + s.GoToUrl(storeUrl + "/users"); + s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter); + Assert.Contains("User added successfully", s.Driver.PageSource); + s.Logout(); + + // Bob should not have access to store, but should have access to invoice + s.LogIn(bob); + s.GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.GoToUrl(invoiceUrl); + s.Driver.AssertNoError(); + + // Alice should be able to delete the store + s.Logout(); + s.LogIn(alice); + s.GoToStore(StoreNavPages.GeneralSettings); + s.Driver.FindElement(By.Id("DeleteStore")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + s.GoToUrl(storeUrl); + Assert.Contains("ReturnUrl", s.Driver.Url); } [Fact(Timeout = TestTimeout)] public async Task CanUsePairing() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.Driver.Navigate().GoToUrl(s.Link("/api-access-request")); - Assert.Contains("ReturnUrl", s.Driver.Url); - s.GoToRegister(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.Driver.Navigate().GoToUrl(s.Link("/api-access-request")); + Assert.Contains("ReturnUrl", s.Driver.Url); + s.GoToRegister(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme(); - s.Driver.FindElement(By.Id("SectionNav-Tokens")).Click(); - s.Driver.FindElement(By.Id("CreateNewToken")).Click(); - s.Driver.FindElement(By.Id("RequestPairing")).Click(); - var pairingCode = AssertUrlHasPairingCode(s); + s.Driver.FindElement(By.Id("SectionNav-Tokens")).Click(); + s.Driver.FindElement(By.Id("CreateNewToken")).Click(); + s.Driver.FindElement(By.Id("RequestPairing")).Click(); + var pairingCode = AssertUrlHasPairingCode(s); - s.Driver.FindElement(By.Id("ApprovePairing")).Click(); - s.FindAlertMessage(); - Assert.Contains(pairingCode, s.Driver.PageSource); + s.Driver.FindElement(By.Id("ApprovePairing")).Click(); + s.FindAlertMessage(); + Assert.Contains(pairingCode, s.Driver.PageSource); - var client = new NBitpayClient.Bitpay(new 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); + var client = new NBitpayClient.Bitpay(new 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 Key(), s.ServerUri); + client = new NBitpayClient.Bitpay(new Key(), s.ServerUri); - var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant); - s.Driver.Navigate().GoToUrl(code.CreateLink(s.ServerUri)); - s.Driver.FindElement(By.Id("ApprovePairing")).Click(); + var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant); + s.Driver.Navigate().GoToUrl(code.CreateLink(s.ServerUri)); + s.Driver.FindElement(By.Id("ApprovePairing")).Click(); - await client.CreateInvoiceAsync( - new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true }, - NBitpayClient.Facade.Merchant); + await client.CreateInvoiceAsync( + new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true }, + NBitpayClient.Facade.Merchant); - s.Driver.Navigate().GoToUrl(s.Link("/api-tokens")); - s.Driver.FindElement(By.Id("RequestPairing")).Click(); - s.Driver.FindElement(By.Id("ApprovePairing")).Click(); - AssertUrlHasPairingCode(s); - } + s.Driver.Navigate().GoToUrl(s.Link("/api-tokens")); + s.Driver.FindElement(By.Id("RequestPairing")).Click(); + s.Driver.FindElement(By.Id("ApprovePairing")).Click(); + AssertUrlHasPairingCode(s); } [Fact(Timeout = TestTimeout)] public async Task CanCreateAppPoS() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(); - s.CreateNewStore(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(); + s.CreateNewStore(); - s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click(); - s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid()); - s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale"); - s.Driver.FindElement(By.Id("Create")).Click(); - s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click(); - s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money"); - s.Driver.FindElement(By.Id("SaveItemChanges")).Click(); - s.Driver.FindElement(By.Id("ToggleRawEditor")).Click(); + s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click(); + s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid()); + s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale"); + s.Driver.FindElement(By.Id("Create")).Click(); + s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click(); + s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money"); + s.Driver.FindElement(By.Id("SaveItemChanges")).Click(); + s.Driver.FindElement(By.Id("ToggleRawEditor")).Click(); - var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value"); - Assert.Contains("buyButtonText: Take my money", template); + var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value"); + Assert.Contains("buyButtonText: Take my money", template); - s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart"); - s.Driver.FindElement(By.Id("SaveSettings")).Click(); - s.Driver.FindElement(By.Id("ViewApp")).Click(); + s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart"); + s.Driver.FindElement(By.Id("SaveSettings")).Click(); + s.Driver.FindElement(By.Id("ViewApp")).Click(); - var posBaseUrl = s.Driver.Url.Replace("/Cart", ""); - Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS"); - Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view"); - Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view"); + var posBaseUrl = s.Driver.Url.Replace("/Cart", ""); + Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS"); + Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view"); + Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view"); - s.Driver.Url = posBaseUrl + "/static"; - Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view"); + s.Driver.Url = posBaseUrl + "/static"; + Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view"); - s.Driver.Url = posBaseUrl + "/cart"; - Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view"); - } + s.Driver.Url = posBaseUrl + "/cart"; + Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view"); } [Fact(Timeout = TestTimeout)] public async Task CanCreateCrowdfundingApp() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme(); - s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click(); - s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid()); - s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund"); - s.Driver.FindElement(By.Id("Create")).Click(); - s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter"); - s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC"); - s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY"); - s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700"); - s.Driver.FindElement(By.Id("SaveSettings")).Click(); - s.Driver.FindElement(By.Id("ViewApp")).Click(); - Assert.Equal("currently active!", - s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text); - } + s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click(); + s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid()); + s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund"); + s.Driver.FindElement(By.Id("Create")).Click(); + s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter"); + s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC"); + s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY"); + s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700"); + s.Driver.FindElement(By.Id("SaveSettings")).Click(); + s.Driver.FindElement(By.Id("ViewApp")).Click(); + Assert.Equal("currently active!", + s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text); } [Fact(Timeout = TestTimeout)] public async Task CanCreatePayRequest() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(); - s.CreateNewStore(); - s.AddDerivationScheme(); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(); + s.CreateNewStore(); + s.AddDerivationScheme(); - s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click(); - s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click(); - s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123"); - s.Driver.FindElement(By.Id("Amount")).SendKeys("700"); - s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); - s.Driver.FindElement(By.Id("SaveButton")).Click(); - var aaa = s.Driver.PageSource; - var url = s.Driver.Url; - s.Driver.FindElement(By.Id("ViewAppButton")).Click(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text); - Assert.Equal("Pay Invoice", - s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim()); + s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click(); + s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click(); + s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123"); + s.Driver.FindElement(By.Id("Amount")).SendKeys("700"); + s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); + s.Driver.FindElement(By.Id("SaveButton")).Click(); + var aaa = s.Driver.PageSource; + var url = s.Driver.Url; + s.Driver.FindElement(By.Id("ViewAppButton")).Click(); + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text); + Assert.Equal("Pay Invoice", + s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim()); - // expire - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); - s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'"); - s.Driver.FindElement(By.Id("SaveButton")).Click(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - s.Driver.Navigate().Refresh(); - Assert.Equal("Expired", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text); + // expire + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); + s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'"); + s.Driver.FindElement(By.Id("SaveButton")).Click(); + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + s.Driver.Navigate().Refresh(); + Assert.Equal("Expired", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text); - // unexpire - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); - s.Driver.FindElement(By.Id("ClearExpiryDate")).Click(); - s.Driver.FindElement(By.Id("SaveButton")).Click(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - s.Driver.Navigate().Refresh(); - s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']")); - Assert.Equal("Pay Invoice", - s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim()); - } + // unexpire + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); + s.Driver.FindElement(By.Id("ClearExpiryDate")).Click(); + s.Driver.FindElement(By.Id("SaveButton")).Click(); + s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); + s.Driver.Navigate().Refresh(); + s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']")); + Assert.Equal("Pay Invoice", + s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim()); } [Fact(Timeout = TestTimeout)] public async Task CanUseCoinSelection() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + (_, string storeId) = s.CreateNewStore(); + s.GenerateWallet("BTC", "", false, true); + var walletId = new WalletId(storeId, "BTC"); + s.GoToWallet(walletId, WalletsNavPages.Receive); + s.Driver.FindElement(By.Id("generateButton")).Click(); + var addressStr = s.Driver.FindElement(By.Id("address")).GetProperty("value"); + 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.StartAsync(); - s.RegisterNewUser(true); - (_, string storeId) = s.CreateNewStore(); - s.GenerateWallet("BTC", "", false, true); - var walletId = new WalletId(storeId, "BTC"); - s.GoToWallet(walletId, WalletsNavPages.Receive); - s.Driver.FindElement(By.Id("generateButton")).Click(); - var addressStr = s.Driver.FindElement(By.Id("address")).GetProperty("value"); - 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 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))); - await TestUtils.EventuallyAsync(async () => - { - var store = await s.Server.PayTester.StoreRepository.FindStore(storeId); - var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider) - .OfType() - .Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode); - var wallet = s.Server.PayTester.GetService().GetWallet(walletId.CryptoCode); - wallet.InvalidateCache(x.AccountDerivation); - Assert.Contains( - await wallet.GetUnspentCoins(x.AccountDerivation), - coin => coin.OutPoint == spentOutpoint); - }); - await s.Server.ExplorerNode.GenerateAsync(1); - s.GoToWallet(walletId); - s.Driver.WaitForAndClick(By.Id("toggleInputSelection")); - s.Driver.WaitForElement(By.Id(spentOutpoint.ToString())); - Assert.Equal("true", - s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant()); - var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString())); - s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click(); - var inputSelectionSelect = s.Driver.FindElement(By.Name("SelectedInputs")); - Assert.Single(inputSelectionSelect.FindElements(By.CssSelector("[selected]"))); - - var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(s, 0, bob, 0.3m); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); - var happyElement = s.FindAlertMessage(); - var happyText = happyElement.Text; - var txid = 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); + await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m)); } + + 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))); + await TestUtils.EventuallyAsync(async () => + { + var store = await s.Server.PayTester.StoreRepository.FindStore(storeId); + var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider) + .OfType() + .Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode); + var wallet = s.Server.PayTester.GetService().GetWallet(walletId.CryptoCode); + wallet.InvalidateCache(x.AccountDerivation); + Assert.Contains( + await wallet.GetUnspentCoins(x.AccountDerivation), + coin => coin.OutPoint == spentOutpoint); + }); + await s.Server.ExplorerNode.GenerateAsync(1); + s.GoToWallet(walletId); + s.Driver.WaitForAndClick(By.Id("toggleInputSelection")); + s.Driver.WaitForElement(By.Id(spentOutpoint.ToString())); + Assert.Equal("true", + s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant()); + var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString())); + s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click(); + var inputSelectionSelect = s.Driver.FindElement(By.Name("SelectedInputs")); + Assert.Single(inputSelectionSelect.FindElements(By.CssSelector("[selected]"))); + + var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(s, 0, bob, 0.3m); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); + var happyElement = s.FindAlertMessage(); + var happyText = happyElement.Text; + var txid = 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(Timeout = TestTimeout)] public async Task CanUseWebhooks() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + s.CreateNewStore(); + s.GoToStore(StoreNavPages.Webhooks); + + TestLogs.LogInformation("Let's create two webhooks"); + for (var i = 0; i < 2; i++) { - await s.StartAsync(); - s.RegisterNewUser(true); - s.CreateNewStore(); - s.GoToStore(StoreNavPages.Webhooks); - - TestLogs.LogInformation("Let's create two webhooks"); - for (var i = 0; i < 2; i++) - { - s.Driver.FindElement(By.Id("CreateWebhook")).Click(); - s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}"); - new SelectElement(s.Driver.FindElement(By.Id("Everything"))).SelectByValue("false"); - s.Driver.FindElement(By.Id("InvoiceCreated")).Click(); - s.Driver.FindElement(By.Id("InvoiceProcessing")).Click(); - s.Driver.FindElement(By.Name("add")).Click(); - } - - TestLogs.LogInformation("Let's delete one of them"); - var deletes = s.Driver.FindElements(By.LinkText("Delete")); - Assert.Equal(2, deletes.Count); - deletes[0].Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - deletes = s.Driver.FindElements(By.LinkText("Delete")); - Assert.Single(deletes); - s.FindAlertMessage(); - - TestLogs.LogInformation("Let's try to update one of them"); - s.Driver.FindElement(By.LinkText("Modify")).Click(); - - using FakeServer server = new FakeServer(); - await server.Start(); - s.Driver.FindElement(By.Name("PayloadUrl")).Clear(); - s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri); - s.Driver.FindElement(By.Name("Secret")).Clear(); - s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld"); - s.Driver.FindElement(By.Name("update")).Click(); - s.FindAlertMessage(); - s.Driver.FindElement(By.LinkText("Modify")).Click(); - foreach (var value in Enum.GetValues(typeof(WebhookEventType))) - { - // Here we make sure we did not forget an event type in the list - // However, maybe some event should not appear here because not at the store level. - // Fix as needed. - Assert.Contains($"value=\"{value}\"", s.Driver.PageSource); - } - - // This one should be checked - Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource); - Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource); - // This one never been checked - Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource); - - s.Driver.FindElement(By.Name("update")).Click(); - s.FindAlertMessage(); - Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource); - - TestLogs.LogInformation("Let's see if we can generate an event"); - s.GoToStore(); - s.AddDerivationScheme(); - 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(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}"; - Assert.Equal(expectedSig, actualSig); - request.Response.StatusCode = 200; - server.Done(); - - TestLogs.LogInformation("Let's make a failed event"); - 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); - s.GoToStore(StoreNavPages.Webhooks); - s.Driver.FindElement(By.LinkText("Modify")).Click(); - var elements = s.Driver.FindElements(By.ClassName("redeliver")); - // One worked, one failed - s.Driver.FindElement(By.ClassName("fa-times")); - s.Driver.FindElement(By.ClassName("fa-check")); - elements[0].Click(); - - s.FindAlertMessage(); - request = await server.GetNextRequest(); - request.Response.StatusCode = 404; - server.Done(); - - TestLogs.LogInformation("Can we browse the json content?"); - CanBrowseContent(s); - - s.GoToInvoices(); - s.Driver.FindElement(By.LinkText("Details")).Click(); - CanBrowseContent(s); - var element = s.Driver.FindElement(By.ClassName("redeliver")); - element.Click(); - - 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"); - s.GoToStore(StoreNavPages.GeneralSettings); - s.Driver.FindElement(By.Id("DeleteStore")).Click(); - s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); - s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); - s.FindAlertMessage(); + s.Driver.FindElement(By.Id("CreateWebhook")).Click(); + s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}"); + new SelectElement(s.Driver.FindElement(By.Id("Everything"))).SelectByValue("false"); + s.Driver.FindElement(By.Id("InvoiceCreated")).Click(); + s.Driver.FindElement(By.Id("InvoiceProcessing")).Click(); + s.Driver.FindElement(By.Name("add")).Click(); } + + TestLogs.LogInformation("Let's delete one of them"); + var deletes = s.Driver.FindElements(By.LinkText("Delete")); + Assert.Equal(2, deletes.Count); + deletes[0].Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + deletes = s.Driver.FindElements(By.LinkText("Delete")); + Assert.Single(deletes); + s.FindAlertMessage(); + + TestLogs.LogInformation("Let's try to update one of them"); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + + using FakeServer server = new FakeServer(); + await server.Start(); + s.Driver.FindElement(By.Name("PayloadUrl")).Clear(); + s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri); + s.Driver.FindElement(By.Name("Secret")).Clear(); + s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld"); + s.Driver.FindElement(By.Name("update")).Click(); + s.FindAlertMessage(); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + foreach (var value in Enum.GetValues(typeof(WebhookEventType))) + { + // Here we make sure we did not forget an event type in the list + // However, maybe some event should not appear here because not at the store level. + // Fix as needed. + Assert.Contains($"value=\"{value}\"", s.Driver.PageSource); + } + + // This one should be checked + Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource); + Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource); + // This one never been checked + Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource); + + s.Driver.FindElement(By.Name("update")).Click(); + s.FindAlertMessage(); + Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource); + + TestLogs.LogInformation("Let's see if we can generate an event"); + s.GoToStore(); + s.AddDerivationScheme(); + 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(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}"; + Assert.Equal(expectedSig, actualSig); + request.Response.StatusCode = 200; + server.Done(); + + TestLogs.LogInformation("Let's make a failed event"); + 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); + s.GoToStore(StoreNavPages.Webhooks); + s.Driver.FindElement(By.LinkText("Modify")).Click(); + var elements = s.Driver.FindElements(By.ClassName("redeliver")); + // One worked, one failed + s.Driver.FindElement(By.ClassName("fa-times")); + s.Driver.FindElement(By.ClassName("fa-check")); + elements[0].Click(); + + s.FindAlertMessage(); + request = await server.GetNextRequest(); + request.Response.StatusCode = 404; + server.Done(); + + TestLogs.LogInformation("Can we browse the json content?"); + CanBrowseContent(s); + + s.GoToInvoices(); + s.Driver.FindElement(By.LinkText("Details")).Click(); + CanBrowseContent(s); + var element = s.Driver.FindElement(By.ClassName("redeliver")); + element.Click(); + + 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"); + s.GoToStore(StoreNavPages.GeneralSettings); + s.Driver.FindElement(By.Id("DeleteStore")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE"); + s.Driver.FindElement(By.Id("ConfirmContinue")).Click(); + s.FindAlertMessage(); } [Fact(Timeout = TestTimeout)] public async Task CanImportMnemonic() { - using (var s = CreateSeleniumTester()) + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + foreach (var isHotwallet in new[] { false, true }) { - await s.StartAsync(); - s.RegisterNewUser(true); - foreach (var isHotwallet in new[] { false, true }) - { - var cryptoCode = "BTC"; - (_, string storeId) = s.CreateNewStore(); - s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", privkeys: isHotwallet); - s.GoToWalletSettings(storeId, cryptoCode); - if (isHotwallet) - Assert.Contains("View seed", s.Driver.PageSource); - else - Assert.DoesNotContain("View seed", s.Driver.PageSource); - } + var cryptoCode = "BTC"; + (_, string storeId) = s.CreateNewStore(); + s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", privkeys: isHotwallet); + s.GoToWalletSettings(storeId, cryptoCode); + if (isHotwallet) + Assert.Contains("View seed", s.Driver.PageSource); + else + Assert.DoesNotContain("View seed", s.Driver.PageSource); } } [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"; + 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); + // 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 receive wallet page - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); + //let's test quickly the receive wallet page + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); + s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); - //you cannot use the Sign with NBX option without saving private keys when generating the wallet. - Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); + //you cannot use the Sign with NBX option without saving private keys when generating the wallet. + Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); - s.Driver.FindElement(By.Id("SectionNav-Receive")).Click(); - //generate a receiving address - s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); - var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); - //unreserve - s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click(); - //generate it again, should be the same one as before as nothing got used in the meantime - s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); - Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); + s.Driver.FindElement(By.Id("SectionNav-Receive")).Click(); + //generate a receiving address + s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); + Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); + var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); + //unreserve + s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click(); + //generate it again, should be the same one as before as nothing got used in the meantime + s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); + Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); + Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); - //send money to addr and ensure it changed - var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); - await sess.ListenAllTrackedSourceAsync(); - var nextEvent = sess.NextEventAsync(); - await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), - Money.Parse("0.1")); - await nextEvent; - 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("value")); - receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); + //send money to addr and ensure it changed + var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); + await sess.ListenAllTrackedSourceAsync(); + var nextEvent = sess.NextEventAsync(); + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), + Money.Parse("0.1")); + await nextEvent; + 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("value")); + receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); - //change the wallet and ensure old address is not there and generating a new one does not result in the prev one - s.GoToStore(storeId); - s.GenerateWallet(cryptoCode, "", true); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Receive")).Click(); - s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); + //change the wallet and ensure old address is not there and generating a new one does not result in the prev one + s.GoToStore(storeId); + s.GenerateWallet(cryptoCode, "", true); + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); + s.Driver.FindElement(By.Id("SectionNav-Receive")).Click(); + s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); + Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); - var invoiceId = s.CreateInvoice(storeId); - var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); - var address = invoice.EntityToDTO().Addresses["BTC"]; + var invoiceId = s.CreateInvoice(storeId); + var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + var address = invoice.EntityToDTO().Addresses["BTC"]; - //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); + //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 - var root = mnemonic.DeriveExtKey(); - invoiceId = s.CreateInvoice(storeId); - invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); - address = invoice.EntityToDTO().Addresses["BTC"]; - 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); + //lets import and save private keys + var root = mnemonic.DeriveExtKey(); + invoiceId = s.CreateInvoice(storeId); + invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + address = invoice.EntityToDTO().Addresses["BTC"]; + 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(); + s.GoToStore(storeId); + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); + s.ClickOnAllSectionLinks(); - // Make sure wallet info is correct - s.GoToWalletSettings(storeId, 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 wallet info is correct + s.GoToWalletSettings(storeId, 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")); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - // Make sure we can rescan, because we are admin! - s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click(); - Assert.Contains("The batch size make sure", s.Driver.PageSource); + // Make sure we can rescan, because we are admin! + s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click(); + Assert.Contains("The batch size make sure", s.Driver.PageSource); - // Check the tx sent earlier arrived - s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click(); + // Check the tx sent earlier arrived + s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click(); - var walletTransactionLink = s.Driver.Url; - Assert.Contains(tx.ToString(), s.Driver.PageSource); + var walletTransactionLink = s.Driver.Url; + Assert.Contains(tx.ToString(), s.Driver.PageSource); - // Send to bob - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); - var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(s, 0, bob, 1); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); + // Send to bob + s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(s, 0, bob, 1); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); - // 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(walletTransactionLink, s.Driver.Url); + // 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(walletTransactionLink, s.Driver.Url); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); + s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); - var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(s, 0, jack, 0.01m); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); + var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(s, 0, jack, 0.01m); + s.Driver.FindElement(By.Id("SignTransaction")).Click(); - 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(walletTransactionLink, s.Driver.Url); + 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(walletTransactionLink, s.Driver.Url); - var bip21 = invoice.EntityToDTO().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("SectionNav-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")); + var bip21 = invoice.EntityToDTO().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("SectionNav-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.GoToWalletSettings(storeId, cryptoCode); - var settingsUrl = s.Driver.Url; - s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ViewSeed")).Click(); + s.GoToWalletSettings(storeId, cryptoCode); + var settingsUrl = 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); + // 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(settingsUrl, s.Driver.Url); - } + // 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(settingsUrl, s.Driver.Url); } [Fact(Timeout = TestTimeout)] public async Task CanImportWallet() { - using (var s = CreateSeleniumTester()) - { - await s.StartAsync(); - s.RegisterNewUser(true); - (_, string storeId) = s.CreateNewStore(); - const string cryptoCode = "BTC"; - var mnemonic = s.GenerateWallet(cryptoCode, "click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel"); + using var s = CreateSeleniumTester(); + await s.StartAsync(); + s.RegisterNewUser(true); + (_, string storeId) = s.CreateNewStore(); + const string cryptoCode = "BTC"; + var mnemonic = s.GenerateWallet(cryptoCode, "click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel"); - // Make sure wallet info is correct - s.GoToWalletSettings(storeId, 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 wallet info is correct + s.GoToWalletSettings(storeId, 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")); } [Fact] diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index 49fc997cd..605814c6d 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -37,40 +37,38 @@ namespace BTCPayServer.Tests [FactWithSecret("AzureBlobStorageConnectionString")] public async Task CanUseAzureBlobStorage() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - var azureBlobStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) - .Model); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + var azureBlobStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model); - azureBlobStorageConfiguration.ConnectionString = FactWithSecretAttribute.GetFromSecrets("AzureBlobStorageConnectionString"); - azureBlobStorageConfiguration.ContainerName = "testscontainer"; - Assert.IsType( - await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration)); + azureBlobStorageConfiguration.ConnectionString = FactWithSecretAttribute.GetFromSecrets("AzureBlobStorageConnectionString"); + azureBlobStorageConfiguration.ContainerName = "testscontainer"; + Assert.IsType( + await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration)); - var shouldBeRedirectingToAzureStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.AzureBlobStorage, - shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]); + var shouldBeRedirectingToAzureStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage, + shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]); - //seems like azure config worked, let's see if the conn string was actually saved + //seems like azure config worked, let's see if the conn string was actually saved - Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert - .IsType(Assert - .IsType( - await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) - .Model).ConnectionString); + Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert + .IsType(Assert + .IsType( + await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model).ConnectionString); - await UnitTest1.CanUploadRemoveFiles(controller); - } + await UnitTest1.CanUploadRemoveFiles(controller); } [Fact] @@ -338,24 +336,22 @@ namespace BTCPayServer.Tests [Fact] public async Task CanUseExchangeSpecificRate() { - using (var tester = CreateServerTester()) - { - tester.PayTester.MockRates = false; - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - List rates = new List(); - rates.Add(await CreateInvoice(tester, user, "coingecko")); - var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY"); - var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY"); - Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache - rates.Add(bitflyer); + using var tester = CreateServerTester(); + tester.PayTester.MockRates = false; + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + List rates = new List(); + rates.Add(await CreateInvoice(tester, user, "coingecko")); + var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY"); + var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY"); + Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache + rates.Add(bitflyer); - foreach (var rate in rates) - { - Assert.Single(rates.Where(r => r == rate)); - } + foreach (var rate in rates) + { + Assert.Single(rates.Where(r => r == rate)); } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index fe83b3744..880318667 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -134,70 +134,66 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CheckSwaggerIsConformToSchema() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + + var sresp = Assert + .IsType(await tester.PayTester.GetController(acc.UserId, acc.StoreId) + .Swagger()).Value.ToJson(); + JObject swagger = JObject.Parse(sresp); + var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json"))); + IList errors; + bool valid = swagger.IsValid(schema, out errors); + //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. + var matchedError = errors.Where(error => + error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf).ToList(); + foreach (ValidationError validationError in matchedError) { - await tester.StartAsync(); - var acc = tester.NewAccount(); - - var sresp = Assert - .IsType(await tester.PayTester.GetController(acc.UserId, acc.StoreId) - .Swagger()).Value.ToJson(); - JObject swagger = JObject.Parse(sresp); - var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json"))); - IList errors; - bool valid = swagger.IsValid(schema, out errors); - //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. - var matchedError = errors.Where(error => - error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf).ToList(); - foreach (ValidationError validationError in matchedError) - { - errors.Remove(validationError); - } - valid = !errors.Any(); - - Assert.Empty(errors); - Assert.True(valid); + errors.Remove(validationError); } + valid = !errors.Any(); + + Assert.Empty(errors); + Assert.True(valid); } [Fact] [Trait("Integration", "Integration")] public async Task EnsureSwaggerPermissionsDocumented() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); - var description = - "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n#OTHERPERMISSIONS#\n\nThe following permissions are available if the user is an administrator:\n\n#SERVERPERMISSIONS#\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n#STOREPERMISSIONS#\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n"; + var description = + "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n#OTHERPERMISSIONS#\n\nThe following permissions are available if the user is an administrator:\n\n#SERVERPERMISSIONS#\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n#STOREPERMISSIONS#\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n"; - var storePolicies = - UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => - Policies.IsStorePolicy(pair.Key) && !pair.Key.EndsWith(":", StringComparison.InvariantCulture)); - var serverPolicies = - UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => - Policies.IsServerPolicy(pair.Key)); - var otherPolicies = - UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => - !Policies.IsStorePolicy(pair.Key) && !Policies.IsServerPolicy(pair.Key)); + var storePolicies = + UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => + Policies.IsStorePolicy(pair.Key) && !pair.Key.EndsWith(":", StringComparison.InvariantCulture)); + var serverPolicies = + UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => + Policies.IsServerPolicy(pair.Key)); + var otherPolicies = + UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair => + !Policies.IsStorePolicy(pair.Key) && !Policies.IsServerPolicy(pair.Key)); - description = description.Replace("#OTHERPERMISSIONS#", - string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))) - .Replace("#SERVERPERMISSIONS#", - string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))) - .Replace("#STOREPERMISSIONS#", - string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))); - TestLogs.LogInformation(description); + description = description.Replace("#OTHERPERMISSIONS#", + string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))) + .Replace("#SERVERPERMISSIONS#", + string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))) + .Replace("#STOREPERMISSIONS#", + string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}"))); + TestLogs.LogInformation(description); - var sresp = Assert - .IsType(await tester.PayTester.GetController(acc.UserId, acc.StoreId) - .Swagger()).Value.ToJson(); + var sresp = Assert + .IsType(await tester.PayTester.GetController(acc.UserId, acc.StoreId) + .Swagger()).Value.ToJson(); - JObject json = JObject.Parse(sresp); + JObject json = JObject.Parse(sresp); - Assert.Equal(description, json["components"]["securitySchemes"]["API Key"]["description"].Value()); - } + Assert.Equal(description, json["components"]["securitySchemes"]["API Key"]["description"].Value()); } private async Task CheckDeadLinks(Regex regex, HttpClient httpClient, string file) @@ -271,84 +267,80 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanAcceptInvoiceWithTolerance2() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - // Set tolerance to 50% - var stores = user.GetController(); - var response = stores.PaymentMethods(); - var vm = Assert.IsType(Assert.IsType(response).Model); - Assert.Equal(0.0, vm.PaymentTolerance); - vm.PaymentTolerance = 50.0; - Assert.IsType(stores.PaymentMethods(vm).Result); + // Set tolerance to 50% + var stores = user.GetController(); + var response = stores.PaymentMethods(); + var vm = Assert.IsType(Assert.IsType(response).Model); + Assert.Equal(0.0, vm.PaymentTolerance); + vm.PaymentTolerance = 50.0; + Assert.IsType(stores.PaymentMethods(vm).Result); - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - // Pays 75% - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); - tester.ExplorerNode.SendToAddress(invoiceAddress, - Money.Satoshis(invoice.BtcDue.Satoshi * 0.75m)); - - TestUtils.Eventually(() => + var invoice = user.BitPay.CreateInvoice( + new Invoice() { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("paid", localInvoice.Status); - }); - } + Buyer = new Buyer() { email = "test@fwf.com" }, + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + // Pays 75% + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network); + tester.ExplorerNode.SendToAddress(invoiceAddress, + Money.Satoshis(invoice.BtcDue.Satoshi * 0.75m)); + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("paid", localInvoice.Status); + }); } [Fact] [Trait("Integration", "Integration")] public async Task CanThrowBitpay404Error() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Buyer = new Buyer() { email = "test@fwf.com" }, + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + try { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Buyer = new Buyer() { email = "test@fwf.com" }, - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - try - { - var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123"); - } - catch (BitPayException ex) - { - Assert.Equal("Object not found", ex.Errors.First()); - } - var req = new HttpRequestMessage(HttpMethod.Get, "/invoices/Cy9jfK82eeEED1T3qhwF3Y"); - req.Headers.TryAddWithoutValidation("Authorization", "Basic dGVzdA=="); - req.Content = new StringContent("{}", Encoding.UTF8, "application/json"); - var result = await tester.PayTester.HttpClient.SendAsync(req); - Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); - Assert.Equal(0, result.Content.Headers.ContentLength.Value); + var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123"); } + catch (BitPayException ex) + { + Assert.Equal("Object not found", ex.Errors.First()); + } + var req = new HttpRequestMessage(HttpMethod.Get, "/invoices/Cy9jfK82eeEED1T3qhwF3Y"); + req.Headers.TryAddWithoutValidation("Authorization", "Basic dGVzdA=="); + req.Content = new StringContent("{}", Encoding.UTF8, "application/json"); + var result = await tester.PayTester.HttpClient.SendAsync(req); + Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); + Assert.Equal(0, result.Content.Headers.ContentLength.Value); } [Fact(Timeout = 60 * 2 * 1000)] @@ -418,44 +410,42 @@ namespace BTCPayServer.Tests [Trait("Lightning", "Lightning")] public async Task CanSetLightningServer() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(true); + var storeController = user.GetController(); + var storeResponse = storeController.PaymentMethods(); + Assert.IsType(storeResponse); + Assert.IsType(await storeController.SetupLightningNode(user.StoreId, "BTC")); + + var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - user.GrantAccess(true); - var storeController = user.GetController(); - var storeResponse = storeController.PaymentMethods(); - Assert.IsType(storeResponse); - Assert.IsType(await storeController.SetupLightningNode(user.StoreId, "BTC")); + ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true", + SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :( + }, "test", "BTC").GetAwaiter().GetResult(); + Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage)); + storeController.TempData.Clear(); + Assert.True(storeController.ModelState.IsValid); - var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel + Assert.IsType(storeController.SetupLightningNode(user.StoreId, + new LightningNodeViewModel { - ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true", - SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :( - }, "test", "BTC").GetAwaiter().GetResult(); - Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage)); - storeController.TempData.Clear(); - Assert.True(storeController.ModelState.IsValid); + ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true" + }, "save", "BTC").GetAwaiter().GetResult()); - Assert.IsType(storeController.SetupLightningNode(user.StoreId, - new LightningNodeViewModel - { - ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true" - }, "save", "BTC").GetAwaiter().GetResult()); + // Make sure old connection string format does not work + Assert.IsType(storeController.SetupLightningNode(user.StoreId, + new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, + "save", "BTC").GetAwaiter().GetResult()); - // Make sure old connection string format does not work - Assert.IsType(storeController.SetupLightningNode(user.StoreId, - new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, - "save", "BTC").GetAwaiter().GetResult()); - - storeResponse = storeController.PaymentMethods(); - var storeVm = - Assert.IsType(Assert - .IsType(storeResponse).Model); - Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); - } + storeResponse = storeController.PaymentMethods(); + var storeVm = + Assert.IsType(Assert + .IsType(storeResponse).Model); + Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); } [Fact(Timeout = 60 * 2 * 1000)] @@ -487,22 +477,20 @@ namespace BTCPayServer.Tests // For easier debugging and testing // LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue; - using (var tester = CreateServerTester()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - user.GrantAccess(true); - user.RegisterLightningNode("BTC", type); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterLightningNode("BTC", type); + user.RegisterDerivationScheme("BTC"); - await CanSendLightningPaymentCore(tester, user); + await CanSendLightningPaymentCore(tester, user); - await Task.WhenAll(Enumerable.Range(0, 5) - .Select(_ => CanSendLightningPaymentCore(tester, user)) - .ToArray()); - } + await Task.WhenAll(Enumerable.Range(0, 5) + .Select(_ => CanSendLightningPaymentCore(tester, user)) + .ToArray()); } async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user) { @@ -531,410 +519,392 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanUseServerInitiatedPairingCode() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.Register(); - acc.CreateStore(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.Register(); + acc.CreateStore(); - var controller = acc.GetController(); - var token = (RedirectToActionResult)await controller.CreateToken2( - new Models.StoreViewModels.CreateTokenViewModel() - { - Label = "bla", - PublicKey = null, - StoreId = acc.StoreId - }); + var controller = acc.GetController(); + var token = (RedirectToActionResult)await controller.CreateToken2( + new Models.StoreViewModels.CreateTokenViewModel() + { + Label = "bla", + PublicKey = null, + StoreId = acc.StoreId + }); - var pairingCode = (string)token.RouteValues["pairingCode"]; + var pairingCode = (string)token.RouteValues["pairingCode"]; - acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult(); - Assert.True(acc.BitPay.TestAccess(Facade.Merchant)); - } + acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult(); + Assert.True(acc.BitPay.TestAccess(Facade.Merchant)); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanSendIPN() { - using (var callbackServer = new CustomServer()) + using var callbackServer = new CustomServer(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.GrantAccess(); + acc.RegisterDerivationScheme("BTC"); + await acc.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed); + var invoice = acc.BitPay.CreateInvoice(new Invoice { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.GrantAccess(); - acc.RegisterDerivationScheme("BTC"); - await acc.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed); - var invoice = acc.BitPay.CreateInvoice(new Invoice - { - Price = 5.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - NotificationURL = callbackServer.GetUri().AbsoluteUri, - ItemDesc = "Some description", - FullNotifications = true, - ExtendedNotifications = true - }); + Price = 5.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + NotificationURL = callbackServer.GetUri().AbsoluteUri, + ItemDesc = "Some description", + FullNotifications = true, + ExtendedNotifications = true + }); #pragma warning disable CS0618 - BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21, - tester.NetworkProvider.BTC.NBitcoinNetwork); - bool receivedPayment = false; - bool paid = false; - bool confirmed = false; - bool completed = false; - while (!completed || !confirmed) + BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21, + tester.NetworkProvider.BTC.NBitcoinNetwork); + bool receivedPayment = false; + bool paid = false; + bool confirmed = false; + bool completed = false; + while (!completed || !confirmed) + { + var request = await callbackServer.GetNextRequest(); + if (request.ContainsKey("event")) + { + var evtName = request["event"]["name"].Value(); + switch (evtName) { - var request = await callbackServer.GetNextRequest(); - if (request.ContainsKey("event")) - { - var evtName = request["event"]["name"].Value(); - switch (evtName) - { - case InvoiceEvent.Created: - tester.ExplorerNode.SendToAddress(url.Address, url.Amount); - break; - case InvoiceEvent.ReceivedPayment: - receivedPayment = true; - break; - case InvoiceEvent.PaidInFull: - Assert.True(receivedPayment); - tester.ExplorerNode.Generate(6); - paid = true; - break; - case InvoiceEvent.Confirmed: - Assert.True(paid); - confirmed = true; - break; - case InvoiceEvent.Completed: - Assert.True( - paid); //TODO: Fix, out of order event mean we can receive invoice_confirmed after invoice_complete - completed = true; - break; - default: - Assert.False(true, $"{evtName} was not expected"); - break; - } - } + case InvoiceEvent.Created: + tester.ExplorerNode.SendToAddress(url.Address, url.Amount); + break; + case InvoiceEvent.ReceivedPayment: + receivedPayment = true; + break; + case InvoiceEvent.PaidInFull: + Assert.True(receivedPayment); + tester.ExplorerNode.Generate(6); + paid = true; + break; + case InvoiceEvent.Confirmed: + Assert.True(paid); + confirmed = true; + break; + case InvoiceEvent.Completed: + Assert.True( + paid); //TODO: Fix, out of order event mean we can receive invoice_confirmed after invoice_complete + completed = true; + break; + default: + Assert.False(true, $"{evtName} was not expected"); + break; } - var invoice2 = acc.BitPay.GetInvoice(invoice.Id); - Assert.NotNull(invoice2); } } + var invoice2 = acc.BitPay.GetInvoice(invoice.Id); + Assert.NotNull(invoice2); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CantPairTwiceWithSamePubkey() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.Register(); - acc.CreateStore(); - var store = acc.GetController(); - var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); - Assert.IsType(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter() - .GetResult()); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.Register(); + acc.CreateStore(); + var store = acc.GetController(); + var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); + Assert.IsType(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter() + .GetResult()); - pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); - acc.CreateStore(); - var store2 = acc.GetController(); - await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id); - Assert.Contains(nameof(PairingResult.ReusedKey), - (string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase); - } + pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); + acc.CreateStore(); + var store2 = acc.GetController(); + await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id); + Assert.Contains(nameof(PairingResult.ReusedKey), + (string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanUseTorClient() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var httpFactory = tester.PayTester.GetService(); - var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); - Assert.NotNull(client); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var httpFactory = tester.PayTester.GetService(); + var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); + Assert.NotNull(client); - TestLogs.LogInformation("Querying an clearnet site over tor"); - var response = await client.GetAsync("https://check.torproject.org/"); - response.EnsureSuccessStatusCode(); - var result = await response.Content.ReadAsStringAsync(); - Assert.DoesNotContain("You are not using Tor.", result); - Assert.Contains("Congratulations. This browser is configured to use Tor.", result); + TestLogs.LogInformation("Querying an clearnet site over tor"); + var response = await client.GetAsync("https://check.torproject.org/"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("You are not using Tor.", result); + Assert.Contains("Congratulations. This browser is configured to use Tor.", result); - TestLogs.LogInformation("Querying a tor website"); - response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); - response.EnsureSuccessStatusCode(); - result = await response.Content.ReadAsStringAsync(); - Assert.Contains("Bitcoin", result); + TestLogs.LogInformation("Querying a tor website"); + response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); + response.EnsureSuccessStatusCode(); + result = await response.Content.ReadAsStringAsync(); + Assert.Contains("Bitcoin", result); - TestLogs.LogInformation("...twice"); - response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); - response.EnsureSuccessStatusCode(); - client.Dispose(); + TestLogs.LogInformation("...twice"); + response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); + response.EnsureSuccessStatusCode(); + client.Dispose(); - TestLogs.LogInformation("...three times, but with a new httpclient"); - client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); - response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); - response.EnsureSuccessStatusCode(); + TestLogs.LogInformation("...three times, but with a new httpclient"); + client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); + response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); + response.EnsureSuccessStatusCode(); - TestLogs.LogInformation("Querying an onion address which can't be found"); - await Assert.ThrowsAsync(() => client.GetAsync("http://dwoduwoi.onion/")); + TestLogs.LogInformation("Querying an onion address which can't be found"); + await Assert.ThrowsAsync(() => client.GetAsync("http://dwoduwoi.onion/")); - TestLogs.LogInformation("Querying valid onion but unreachable"); - await Assert.ThrowsAsync(() => client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/")); - } + TestLogs.LogInformation("Querying valid onion but unreachable"); + await Assert.ThrowsAsync(() => client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/")); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanRescanWallet() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.GrantAccess(); + acc.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit); + var btcDerivationScheme = acc.DerivationScheme; + + var walletController = acc.GetController(); + + var walletId = new WalletId(acc.StoreId, "BTC"); + acc.IsAdmin = true; + walletController = acc.GetController(); + + var rescan = + Assert.IsType(Assert + .IsType(walletController.WalletRescan(walletId).Result).Model); + Assert.True(rescan.Ok); + Assert.True(rescan.IsFullySync); + Assert.True(rescan.IsSupportedByCurrency); + Assert.True(rescan.IsServerAdmin); + + rescan.GapLimit = 100; + + // Sending a coin + var txId = tester.ExplorerNode.SendToAddress( + btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m)); + tester.ExplorerNode.Generate(1); + var transactions = Assert.IsType(Assert + .IsType(walletController.WalletTransactions(walletId).Result).Model); + Assert.Empty(transactions.Transactions); + + Assert.IsType(walletController.WalletRescan(walletId, rescan).Result); + + while (true) { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.GrantAccess(); - acc.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit); - var btcDerivationScheme = acc.DerivationScheme; - - var walletController = acc.GetController(); - - var walletId = new WalletId(acc.StoreId, "BTC"); - acc.IsAdmin = true; - walletController = acc.GetController(); - - var rescan = - Assert.IsType(Assert - .IsType(walletController.WalletRescan(walletId).Result).Model); - Assert.True(rescan.Ok); - Assert.True(rescan.IsFullySync); - Assert.True(rescan.IsSupportedByCurrency); - Assert.True(rescan.IsServerAdmin); - - rescan.GapLimit = 100; - - // Sending a coin - var txId = tester.ExplorerNode.SendToAddress( - btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m)); - tester.ExplorerNode.Generate(1); - var transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); - Assert.Empty(transactions.Transactions); - - Assert.IsType(walletController.WalletRescan(walletId, rescan).Result); - - while (true) + rescan = Assert.IsType(Assert + .IsType(walletController.WalletRescan(walletId).Result).Model); + if (rescan.Progress == null && rescan.LastSuccess != null) { - rescan = Assert.IsType(Assert - .IsType(walletController.WalletRescan(walletId).Result).Model); - if (rescan.Progress == null && rescan.LastSuccess != null) - { - if (rescan.LastSuccess.Found == 0) - continue; - // Scan over - break; - } - else - { - Assert.Null(rescan.TimeOfScan); - Assert.NotNull(rescan.RemainingTime); - Assert.NotNull(rescan.Progress); - Thread.Sleep(100); - } + if (rescan.LastSuccess.Found == 0) + continue; + // Scan over + break; + } + else + { + Assert.Null(rescan.TimeOfScan); + Assert.NotNull(rescan.RemainingTime); + Assert.NotNull(rescan.Progress); + Thread.Sleep(100); } - - Assert.Null(rescan.PreviousError); - Assert.NotNull(rescan.TimeOfScan); - Assert.Equal(1, rescan.LastSuccess.Found); - transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); - var tx = Assert.Single(transactions.Transactions); - Assert.Equal(tx.Id, txId.ToString()); - - // Hijack the test to see if we can add label and comments - Assert.IsType( - await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello-pouet")); - Assert.IsType( - await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test")); - Assert.IsType( - await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2")); - Assert.IsType( - await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello")); - - transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); - tx = Assert.Single(transactions.Transactions); - - Assert.Equal("hello", tx.Comment); - Assert.Contains("test", tx.Labels.Select(l => l.Text)); - Assert.Contains("test2", tx.Labels.Select(l => l.Text)); - Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count()); - - Assert.IsType( - await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2")); - - transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); - tx = Assert.Single(transactions.Transactions); - - Assert.Equal("hello", tx.Comment); - Assert.Contains("test", tx.Labels.Select(l => l.Text)); - Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text)); - Assert.Single(tx.Labels.GroupBy(l => l.Color)); - - var walletInfo = await tester.PayTester.GetService().GetWalletInfo(walletId); - Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed } + + Assert.Null(rescan.PreviousError); + Assert.NotNull(rescan.TimeOfScan); + Assert.Equal(1, rescan.LastSuccess.Found); + transactions = Assert.IsType(Assert + .IsType(walletController.WalletTransactions(walletId).Result).Model); + var tx = Assert.Single(transactions.Transactions); + Assert.Equal(tx.Id, txId.ToString()); + + // Hijack the test to see if we can add label and comments + Assert.IsType( + await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello-pouet")); + Assert.IsType( + await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test")); + Assert.IsType( + await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2")); + Assert.IsType( + await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello")); + + transactions = Assert.IsType(Assert + .IsType(walletController.WalletTransactions(walletId).Result).Model); + tx = Assert.Single(transactions.Transactions); + + Assert.Equal("hello", tx.Comment); + Assert.Contains("test", tx.Labels.Select(l => l.Text)); + Assert.Contains("test2", tx.Labels.Select(l => l.Text)); + Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count()); + + Assert.IsType( + await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2")); + + transactions = Assert.IsType(Assert + .IsType(walletController.WalletTransactions(walletId).Result).Model); + tx = Assert.Single(transactions.Transactions); + + Assert.Equal("hello", tx.Comment); + Assert.Contains("test", tx.Labels.Select(l => l.Text)); + Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text)); + Assert.Single(tx.Labels.GroupBy(l => l.Color)); + + var walletInfo = await tester.PayTester.GetService().GetWalletInfo(walletId); + Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanListInvoices() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - await acc.GrantAccessAsync(); - acc.RegisterDerivationScheme("BTC"); - // First we try payment with a merchant having only BTC - var invoice = await acc.BitPay.CreateInvoiceAsync( - new Invoice - { - Price = 500, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - - var cashCow = tester.ExplorerNode; - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); - await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); - TestUtils.Eventually(() => + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(); + acc.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice = await acc.BitPay.CreateInvoiceAsync( + new Invoice { - invoice = acc.BitPay.GetInvoice(invoice.Id); - Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); - }); + Price = 500, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); - AssertSearchInvoice(acc, true, invoice.Id, null); - AssertSearchInvoice(acc, true, invoice.Id, null, acc.StoreId); - AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}"); - AssertSearchInvoice(acc, false, invoice.Id, "storeid:doesnotexist"); - AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}"); - AssertSearchInvoice(acc, true, invoice.Id, "exceptionstatus:paidPartial"); - AssertSearchInvoice(acc, false, invoice.Id, "exceptionstatus:paidOver"); - AssertSearchInvoice(acc, true, invoice.Id, "unusual:true"); - AssertSearchInvoice(acc, false, invoice.Id, "unusual:false"); + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); + await cashCow.SendToAddressAsync(invoiceAddress, firstPayment); + TestUtils.Eventually(() => + { + invoice = acc.BitPay.GetInvoice(invoice.Id); + Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); + }); - var time = invoice.InvoiceTime; - AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}"); - AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}"); - AssertSearchInvoice(acc, false, invoice.Id, - $"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}"); - AssertSearchInvoice(acc, false, invoice.Id, - $"enddate:{time.AddSeconds(-1).ToString("yyyy-MM-dd HH:mm:ss")}"); - } + AssertSearchInvoice(acc, true, invoice.Id, null); + AssertSearchInvoice(acc, true, invoice.Id, null, acc.StoreId); + AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}"); + AssertSearchInvoice(acc, false, invoice.Id, "storeid:doesnotexist"); + AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}"); + AssertSearchInvoice(acc, true, invoice.Id, "exceptionstatus:paidPartial"); + AssertSearchInvoice(acc, false, invoice.Id, "exceptionstatus:paidOver"); + AssertSearchInvoice(acc, true, invoice.Id, "unusual:true"); + AssertSearchInvoice(acc, false, invoice.Id, "unusual:false"); + + var time = invoice.InvoiceTime; + AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}"); + AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}"); + AssertSearchInvoice(acc, false, invoice.Id, + $"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}"); + AssertSearchInvoice(acc, false, invoice.Id, + $"enddate:{time.AddSeconds(-1).ToString("yyyy-MM-dd HH:mm:ss")}"); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanListNotifications() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.GrantAccess(true); - acc.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.GrantAccess(true); + acc.RegisterDerivationScheme("BTC"); - const string newVersion = "1.0.4.4"; - var ctrl = acc.GetController(); - var resp = await ctrl.Generate(newVersion); + const string newVersion = "1.0.4.4"; + var ctrl = acc.GetController(); + var resp = await ctrl.Generate(newVersion); - var vm = Assert.IsType( - Assert.IsType(await ctrl.Index()).Model); + var vm = Assert.IsType( + Assert.IsType(await ctrl.Index()).Model); - Assert.True(vm.Skip == 0); - Assert.True(vm.Count == 50); - Assert.True(vm.Total == 1); - Assert.True(vm.Items.Count == 1); + Assert.True(vm.Skip == 0); + Assert.True(vm.Count == 50); + Assert.True(vm.Total == 1); + Assert.True(vm.Items.Count == 1); - var fn = vm.Items.First(); - var now = DateTimeOffset.UtcNow; - Assert.True(fn.Created >= now.AddSeconds(-3)); - Assert.True(fn.Created <= now); - Assert.Equal($"New version {newVersion} released!", fn.Body); - Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); - Assert.False(fn.Seen); - } + var fn = vm.Items.First(); + var now = DateTimeOffset.UtcNow; + Assert.True(fn.Created >= now.AddSeconds(-3)); + Assert.True(fn.Created <= now); + Assert.Equal($"New version {newVersion} released!", fn.Body); + Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); + Assert.False(fn.Seen); } [Fact] [Trait("Integration", "Integration")] public async Task CanGetRates() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.GrantAccess(); - acc.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var acc = tester.NewAccount(); + acc.GrantAccess(); + acc.RegisterDerivationScheme("BTC"); - var rateController = acc.GetController(); - var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController - .GetBaseCurrencyRates("BTC", default) - .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); - Assert.NotNull(GetBaseCurrencyRatesResult); - Assert.NotNull(GetBaseCurrencyRatesResult.Data); - var rate = Assert.Single(GetBaseCurrencyRatesResult.Data); - Assert.Equal("BTC", rate.Code); + var rateController = acc.GetController(); + var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController + .GetBaseCurrencyRates("BTC", default) + .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); + Assert.NotNull(GetBaseCurrencyRatesResult); + Assert.NotNull(GetBaseCurrencyRatesResult.Data); + var rate = Assert.Single(GetBaseCurrencyRatesResult.Data); + Assert.Equal("BTC", rate.Code); - var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) - .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); - // We don't have any default currencies, so this should be failing - Assert.Null(GetRatesResult?.Data); + var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) + .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); + // We don't have any default currencies, so this should be failing + Assert.Null(GetRatesResult?.Data); - var store = acc.GetController(); - var ratesVM = (RatesViewModel)(Assert.IsType(store.Rates()).Model); - ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; - await store.Rates(ratesVM); - store = acc.GetController(); - rateController = acc.GetController(); - GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) - .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); - // Now we should have a result - Assert.NotNull(GetRatesResult); - Assert.NotNull(GetRatesResult.Data); - Assert.Equal(2, GetRatesResult.Data.Length); + var store = acc.GetController(); + var ratesVM = (RatesViewModel)(Assert.IsType(store.Rates()).Model); + ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; + await store.Rates(ratesVM); + store = acc.GetController(); + rateController = acc.GetController(); + GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) + .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); + // Now we should have a result + Assert.NotNull(GetRatesResult); + Assert.NotNull(GetRatesResult.Data); + Assert.Equal(2, GetRatesResult.Data.Length); - var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController - .GetCurrencyPairRate("BTC", "LTC", default) - .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); + var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController + .GetCurrencyPairRate("BTC", "LTC", default) + .GetAwaiter().GetResult()).Value.ToJson()).ToObject>(); - Assert.NotNull(GetCurrencyPairRateResult); - Assert.NotNull(GetCurrencyPairRateResult.Data); - Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code); + Assert.NotNull(GetCurrencyPairRateResult); + Assert.NotNull(GetCurrencyPairRateResult.Data); + Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code); - // Should be OK because the request is signed, so we can know the store - var rates = acc.BitPay.GetRates(); - HttpClient client = new HttpClient(); - // Unauthentified requests should also be ok - var response = - await client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}"); - response.EnsureSuccessStatusCode(); - response = await client.GetAsync( - $"http://127.0.0.1:{tester.PayTester.Port}/rates?storeId={acc.StoreId}"); - response.EnsureSuccessStatusCode(); - } + // Should be OK because the request is signed, so we can know the store + var rates = acc.BitPay.GetRates(); + HttpClient client = new HttpClient(); + // Unauthentified requests should also be ok + var response = + await client.GetAsync($"http://127.0.0.1:{tester.PayTester.Port}/api/rates?storeId={acc.StoreId}"); + response.EnsureSuccessStatusCode(); + response = await client.GetAsync( + $"http://127.0.0.1:{tester.PayTester.Port}/rates?storeId={acc.StoreId}"); + response.EnsureSuccessStatusCode(); } private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter, string storeId = null) @@ -950,156 +920,154 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanRBFPayment() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - await user.SetNetworkFeeMode(NetworkFeeMode.Always); - var invoice = - user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); - var payment1 = invoice.BtcDue + Money.Coins(0.0001m); - var payment2 = invoice.BtcDue; + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); + var invoice = + user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); + var payment1 = invoice.BtcDue + Money.Coins(0.0001m); + var payment2 = invoice.BtcDue; - var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[] - { + var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[] + { invoice.BitcoinAddress, payment1.ToString(), null, //comment null, //comment_to false, //subtractfeefromamount true, //replaceable - }).ResultString); - TestLogs.LogInformation( - $"Let's send a first payment of {payment1} for the {invoice.BtcDue} invoice ({tx1})"); - var invoiceAddress = - BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); + }).ResultString); + TestLogs.LogInformation( + $"Let's send a first payment of {payment1} for the {invoice.BtcDue} invoice ({tx1})"); + var invoiceAddress = + BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); - TestLogs.LogInformation($"The invoice should be paidOver"); - TestUtils.Eventually(() => + TestLogs.LogInformation($"The invoice should be paidOver"); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(payment1, invoice.BtcPaid); + Assert.Equal("paid", invoice.Status); + Assert.Equal("paidOver", invoice.ExceptionStatus.ToString()); + invoiceAddress = + BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); + }); + + var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1)); + foreach (var input in tx.Inputs) + { + input.ScriptSig = Script.Empty; //Strip signatures + } + + var output = tx.Outputs.First(o => o.Value == payment1); + output.Value = payment2; + output.ScriptPubKey = invoiceAddress.ScriptPubKey; + + using (var cts = new CancellationTokenSource(10000)) + using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession()) + { + listener.ListenAllDerivationSchemes(); + var replaced = tester.ExplorerNode.SignRawTransaction(tx); + Thread.Sleep(1000); // Make sure the replacement has a different timestamp + var tx2 = tester.ExplorerNode.SendRawTransaction(replaced); + TestLogs.LogInformation( + $"Let's RBF with a payment of {payment2} ({tx2}), waiting for NBXplorer to pick it up"); + Assert.Equal(tx2, + ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash); + } + + TestLogs.LogInformation($"The invoice should now not be paidOver anymore"); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(payment2, invoice.BtcPaid); + Assert.Equal("False", invoice.ExceptionStatus.ToString()); + }); + + + TestLogs.LogInformation( + $"Let's test out rbf payments where the payment gets sent elsehwere instead"); + var invoice2 = + user.BitPay.CreateInvoice(new Invoice() { Price = 0.01m, Currency = "BTC" }, Facade.Merchant); + + var invoice2Address = + BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); + uint256 invoice2tx1Id = + await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, new NBitcoin.RPC.SendToAddressParameters() + { + Replaceable = true + }); + Transaction invoice2Tx1 = null; + TestUtils.Eventually(() => + { + invoice2 = user.BitPay.GetInvoice(invoice2.Id); + Assert.Equal("paid", invoice2.Status); + invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id)); + }); + var invoice2Tx2 = invoice2Tx1.Clone(); + foreach (var input in invoice2Tx2.Inputs) + { + input.ScriptSig = Script.Empty; //Strip signatures + input.WitScript = WitScript.Empty; //Strip signatures + } + + output = invoice2Tx2.Outputs.First(o => + o.ScriptPubKey == invoice2Address.ScriptPubKey); + output.Value -= new Money(10_000, MoneyUnit.Satoshi); + output.ScriptPubKey = new Key().GetScriptPubKey(ScriptPubKeyType.Legacy); + invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2); + await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2); + tester.ExplorerNode.Generate(1); + await TestUtils.EventuallyAsync(async () => + { + var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); + Assert.Equal(InvoiceStatusLegacy.New, i.Status); + Assert.Single(i.GetPayments(false)); + Assert.False(i.GetPayments(false).First().Accounted); + }); + + TestLogs.LogInformation("Let's test if we can RBF a normal payment without adding fees to the invoice"); + await user.SetNetworkFeeMode(NetworkFeeMode.MultiplePaymentsOnly); + invoice = user.BitPay.CreateInvoice(new Invoice { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); + payment1 = invoice.BtcDue; + tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[] + { + invoice.BitcoinAddress, payment1.ToString(), null, //comment + null, //comment_to + false, //subtractfeefromamount + true, //replaceable + }).ResultString); + TestLogs.LogInformation($"Paid {tx1}"); + TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.Equal(payment1, invoice.BtcPaid); Assert.Equal("paid", invoice.Status); - Assert.Equal("paidOver", invoice.ExceptionStatus.ToString()); - invoiceAddress = - BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); - }); - - var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1)); - foreach (var input in tx.Inputs) - { - input.ScriptSig = Script.Empty; //Strip signatures - } - - var output = tx.Outputs.First(o => o.Value == payment1); - output.Value = payment2; - output.ScriptPubKey = invoiceAddress.ScriptPubKey; - - using (var cts = new CancellationTokenSource(10000)) - using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession()) - { - listener.ListenAllDerivationSchemes(); - var replaced = tester.ExplorerNode.SignRawTransaction(tx); - Thread.Sleep(1000); // Make sure the replacement has a different timestamp - var tx2 = tester.ExplorerNode.SendRawTransaction(replaced); - TestLogs.LogInformation( - $"Let's RBF with a payment of {payment2} ({tx2}), waiting for NBXplorer to pick it up"); - Assert.Equal(tx2, - ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash); - } - - TestLogs.LogInformation($"The invoice should now not be paidOver anymore"); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(payment2, invoice.BtcPaid); Assert.Equal("False", invoice.ExceptionStatus.ToString()); - }); - - - TestLogs.LogInformation( - $"Let's test out rbf payments where the payment gets sent elsehwere instead"); - var invoice2 = - user.BitPay.CreateInvoice(new Invoice() { Price = 0.01m, Currency = "BTC" }, Facade.Merchant); - - var invoice2Address = - BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); - uint256 invoice2tx1Id = - await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, new NBitcoin.RPC.SendToAddressParameters() - { - Replaceable = true - }); - Transaction invoice2Tx1 = null; - TestUtils.Eventually(() => - { - invoice2 = user.BitPay.GetInvoice(invoice2.Id); - Assert.Equal("paid", invoice2.Status); - invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id)); - }); - var invoice2Tx2 = invoice2Tx1.Clone(); - foreach (var input in invoice2Tx2.Inputs) - { - input.ScriptSig = Script.Empty; //Strip signatures - input.WitScript = WitScript.Empty; //Strip signatures } - - output = invoice2Tx2.Outputs.First(o => - o.ScriptPubKey == invoice2Address.ScriptPubKey); - output.Value -= new Money(10_000, MoneyUnit.Satoshi); - output.ScriptPubKey = new Key().GetScriptPubKey(ScriptPubKeyType.Legacy); - invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2); - await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2); - tester.ExplorerNode.Generate(1); - await TestUtils.EventuallyAsync(async () => - { - var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); - Assert.Equal(InvoiceStatusLegacy.New, i.Status); - Assert.Single(i.GetPayments(false)); - Assert.False(i.GetPayments(false).First().Accounted); - }); - - TestLogs.LogInformation("Let's test if we can RBF a normal payment without adding fees to the invoice"); - await user.SetNetworkFeeMode(NetworkFeeMode.MultiplePaymentsOnly); - invoice = user.BitPay.CreateInvoice(new Invoice { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); - payment1 = invoice.BtcDue; - tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[] - { - invoice.BitcoinAddress, payment1.ToString(), null, //comment - null, //comment_to - false, //subtractfeefromamount - true, //replaceable - }).ResultString); - TestLogs.LogInformation($"Paid {tx1}"); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(payment1, invoice.BtcPaid); - Assert.Equal("paid", invoice.Status); - Assert.Equal("False", invoice.ExceptionStatus.ToString()); - } - ); - var tx1Bump = new uint256(tester.ExplorerNode.SendCommand("bumpfee", new object[] - { + ); + var tx1Bump = new uint256(tester.ExplorerNode.SendCommand("bumpfee", new object[] + { tx1.ToString(), - }).Result["txid"].Value()); - TestLogs.LogInformation($"Bumped with {tx1Bump}"); - await TestUtils.EventuallyAsync(async () => - { - var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); - var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray(); - var payments = invoiceEntity.GetPayments(false).ToArray(); - Assert.Equal(tx1, btcPayments[0].Outpoint.Hash); - Assert.False(payments[0].Accounted); - Assert.Equal(tx1Bump, payments[1].Outpoint.Hash); - Assert.True(payments[1].Accounted); - Assert.Equal(0.0m, payments[1].NetworkFee); - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal(payment1, invoice.BtcPaid); - Assert.Equal("paid", invoice.Status); - Assert.Equal("False", invoice.ExceptionStatus.ToString()); - } - ); - } + }).Result["txid"].Value()); + TestLogs.LogInformation($"Bumped with {tx1Bump}"); + await TestUtils.EventuallyAsync(async () => + { + var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); + var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray(); + var payments = invoiceEntity.GetPayments(false).ToArray(); + Assert.Equal(tx1, btcPayments[0].Outpoint.Hash); + Assert.False(payments[0].Accounted); + Assert.Equal(tx1Bump, payments[1].Outpoint.Hash); + Assert.True(payments[1].Accounted); + Assert.Equal(0.0m, payments[1].NetworkFee); + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(payment1, invoice.BtcPaid); + Assert.Equal("paid", invoice.Status); + Assert.Equal("False", invoice.ExceptionStatus.ToString()); + } + ); } // [Fact(Timeout = TestTimeout)] @@ -1135,213 +1103,204 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async void CheckCORSSetOnBitpayAPI() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + foreach (var req in new[] { "invoices/", "invoices", "rates", "tokens" }.Select(async path => + { + using HttpClient client = new HttpClient(); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Options, + tester.PayTester.ServerUri.AbsoluteUri + path); + message.Headers.Add("Access-Control-Request-Headers", "test"); + var response = await client.SendAsync(message); + response.EnsureSuccessStatusCode(); + Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Origin", out var val)); + Assert.Equal("*", val.FirstOrDefault()); + Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Headers", out val)); + Assert.Equal("test", val.FirstOrDefault()); + }).ToList()) { - await tester.StartAsync(); - foreach (var req in new[] { "invoices/", "invoices", "rates", "tokens" }.Select(async path => - { - using (HttpClient client = new HttpClient()) - { - HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Options, - tester.PayTester.ServerUri.AbsoluteUri + path); - message.Headers.Add("Access-Control-Request-Headers", "test"); - var response = await client.SendAsync(message); - response.EnsureSuccessStatusCode(); - Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Origin", out var val)); - Assert.Equal("*", val.FirstOrDefault()); - Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Headers", out val)); - Assert.Equal("test", val.FirstOrDefault()); - } - }).ToList()) - { - await req; - } - - HttpClient client2 = new HttpClient(); - HttpRequestMessage message2 = new HttpRequestMessage(HttpMethod.Options, - tester.PayTester.ServerUri.AbsoluteUri + "rates"); - var response2 = await client2.SendAsync(message2); - Assert.True(response2.Headers.TryGetValues("Access-Control-Allow-Origin", out var val2)); - Assert.Equal("*", val2.FirstOrDefault()); + await req; } + + HttpClient client2 = new HttpClient(); + HttpRequestMessage message2 = new HttpRequestMessage(HttpMethod.Options, + tester.PayTester.ServerUri.AbsoluteUri + "rates"); + var response2 = await client2.SendAsync(message2); + Assert.True(response2.Headers.TryGetValues("Access-Control-Allow-Origin", out var val2)); + Assert.Equal("*", val2.FirstOrDefault()); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task TestAccessBitpayAPI() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - Assert.False(user.BitPay.TestAccess(Facade.Merchant)); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + Assert.False(user.BitPay.TestAccess(Facade.Merchant)); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - Assert.True(user.BitPay.TestAccess(Facade.Merchant)); + Assert.True(user.BitPay.TestAccess(Facade.Merchant)); - // Test request pairing code client side - var storeController = user.GetController(); - storeController - .CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId }) - .GetAwaiter().GetResult(); - Assert.NotNull(storeController.GeneratedPairingCode); + // Test request pairing code client side + var storeController = user.GetController(); + storeController + .CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId }) + .GetAwaiter().GetResult(); + Assert.NotNull(storeController.GeneratedPairingCode); - var k = new Key(); - var bitpay = new Bitpay(k, tester.PayTester.ServerUri); - bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait(); - Assert.True(bitpay.TestAccess(Facade.Merchant)); - Assert.True(bitpay.TestAccess(Facade.PointOfSale)); - // Same with new instance - bitpay = new Bitpay(k, tester.PayTester.ServerUri); - Assert.True(bitpay.TestAccess(Facade.Merchant)); - Assert.True(bitpay.TestAccess(Facade.PointOfSale)); + var k = new Key(); + var bitpay = new Bitpay(k, tester.PayTester.ServerUri); + bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait(); + Assert.True(bitpay.TestAccess(Facade.Merchant)); + Assert.True(bitpay.TestAccess(Facade.PointOfSale)); + // Same with new instance + bitpay = new Bitpay(k, tester.PayTester.ServerUri); + Assert.True(bitpay.TestAccess(Facade.Merchant)); + Assert.True(bitpay.TestAccess(Facade.PointOfSale)); - // Can generate API Key - var repo = tester.PayTester.GetService(); - Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); - Assert.IsType(user.GetController() - .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); + // Can generate API Key + var repo = tester.PayTester.GetService(); + Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.IsType(user.GetController() + .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); - var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); - /////// + var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + /////// - // Generating a new one remove the previous - Assert.IsType(user.GetController() - .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); - var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); - Assert.NotEqual(apiKey, apiKey2); - //////// + // Generating a new one remove the previous + Assert.IsType(user.GetController() + .GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); + var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.NotEqual(apiKey, apiKey2); + //////// - apiKey = apiKey2; + apiKey = apiKey2; - // Can create an invoice with this new API Key - HttpClient client = new HttpClient(); - HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, - tester.PayTester.ServerUri.AbsoluteUri + "invoices"); - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", - Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey))); - var invoice = new Invoice() { Price = 5000.0m, Currency = "USD" }; - message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, - "application/json"); - var result = client.SendAsync(message).GetAwaiter().GetResult(); - result.EnsureSuccessStatusCode(); - ///////////////////// + // Can create an invoice with this new API Key + HttpClient client = new HttpClient(); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, + tester.PayTester.ServerUri.AbsoluteUri + "invoices"); + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", + Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey))); + var invoice = new Invoice() { Price = 5000.0m, Currency = "USD" }; + message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, + "application/json"); + var result = client.SendAsync(message).GetAwaiter().GetResult(); + result.EnsureSuccessStatusCode(); + ///////////////////// - // Have error 403 with bad signature - client = new HttpClient(); - HttpRequestMessage mess = - new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens"); - mess.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); - mess.Headers.Add("x-signature", - "3045022100caa123193afc22ef93d9c6b358debce6897c09dd9869fe6fe029c9cb43623fac022000b90c65c50ba8bbbc6ebee8878abe5659e17b9f2e1b27d95eda4423da5608fe"); - mess.Headers.Add("x-identity", - "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99"); - mess.Method = HttpMethod.Get; - result = client.SendAsync(mess).GetAwaiter().GetResult(); - Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode); - // - } + // Have error 403 with bad signature + client = new HttpClient(); + HttpRequestMessage mess = + new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens"); + mess.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); + mess.Headers.Add("x-signature", + "3045022100caa123193afc22ef93d9c6b358debce6897c09dd9869fe6fe029c9cb43623fac022000b90c65c50ba8bbbc6ebee8878abe5659e17b9f2e1b27d95eda4423da5608fe"); + mess.Headers.Add("x-identity", + "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99"); + mess.Method = HttpMethod.Get; + result = client.SendAsync(mess).GetAwaiter().GetResult(); + Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode); + + // } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanUseAnyoneCanCreateInvoice() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - TestLogs.LogInformation("StoreId without anyone can create invoice = 403"); - var response = await tester.PayTester.HttpClient.SendAsync( - new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}") - { - Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, - "application/json"), - }); - Assert.Equal(403, (int)response.StatusCode); + TestLogs.LogInformation("StoreId without anyone can create invoice = 403"); + var response = await tester.PayTester.HttpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}") + { + Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, + "application/json"), + }); + Assert.Equal(403, (int)response.StatusCode); - TestLogs.LogInformation( - "No store without anyone can create invoice = 404 because the bitpay API can't know the storeid"); - response = await tester.PayTester.HttpClient.SendAsync( - new HttpRequestMessage(HttpMethod.Post, $"invoices") - { - Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, - "application/json"), - }); - Assert.Equal(404, (int)response.StatusCode); + TestLogs.LogInformation( + "No store without anyone can create invoice = 404 because the bitpay API can't know the storeid"); + response = await tester.PayTester.HttpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Post, $"invoices") + { + Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, + "application/json"), + }); + Assert.Equal(404, (int)response.StatusCode); - await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true); + await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true); - TestLogs.LogInformation("Bad store with anyone can create invoice = 403"); - response = await tester.PayTester.HttpClient.SendAsync( - new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId=badid") - { - Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, - "application/json"), - }); - Assert.Equal(403, (int)response.StatusCode); + TestLogs.LogInformation("Bad store with anyone can create invoice = 403"); + response = await tester.PayTester.HttpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId=badid") + { + Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, + "application/json"), + }); + Assert.Equal(403, (int)response.StatusCode); - TestLogs.LogInformation("Good store with anyone can create invoice = 200"); - response = await tester.PayTester.HttpClient.SendAsync( - new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}") - { - Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, - "application/json"), - }); - Assert.Equal(200, (int)response.StatusCode); - } + TestLogs.LogInformation("Good store with anyone can create invoice = 200"); + response = await tester.PayTester.HttpClient.SendAsync( + new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}") + { + Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, + "application/json"), + }); + Assert.Equal(200, (int)response.StatusCode); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanTweakRate() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - // First we try payment with a merchant having only BTC - var invoice1 = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + // First we try payment with a merchant having only BTC + var invoice1 = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice); - var storeController = user.GetController(); - var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; - Assert.Equal(0.0, vm.Spread); - vm.Spread = 40; - await storeController.Rates(vm); + var storeController = user.GetController(); + var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; + Assert.Equal(0.0, vm.Spread); + vm.Spread = 40; + await storeController.Rates(vm); - var invoice2 = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 5000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); + var invoice2 = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); - var expectedRate = 5000.0m * 0.6m; - var expectedCoins = invoice2.Price / expectedRate; - Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m)); - } + var expectedRate = 5000.0m * 0.6m; + var expectedCoins = invoice2.Price / expectedRate; + Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m)); } @@ -1349,27 +1308,25 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanCreateTopupInvoices() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - var rng = new Random(); - var seed = rng.Next(); - rng = new Random(seed); - TestLogs.LogInformation("Seed: " + seed); - foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) - { - await user.SetNetworkFeeMode(networkFeeMode); - await AssertTopUpBtcPrice(tester, user, Money.Coins(1.0m), 5000.0m, networkFeeMode); - await AssertTopUpBtcPrice(tester, user, Money.Coins(1.23456789m), 5000.0m * 1.23456789m, networkFeeMode); - // Check if there is no strange roundup issues - var v = (decimal)(rng.NextDouble() + 1.0); - v = Money.Coins(v).ToDecimal(MoneyUnit.BTC); - await AssertTopUpBtcPrice(tester, user, Money.Coins(v), 5000.0m * v, networkFeeMode); - } + var rng = new Random(); + var seed = rng.Next(); + rng = new Random(seed); + TestLogs.LogInformation("Seed: " + seed); + foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) + { + await user.SetNetworkFeeMode(networkFeeMode); + await AssertTopUpBtcPrice(tester, user, Money.Coins(1.0m), 5000.0m, networkFeeMode); + await AssertTopUpBtcPrice(tester, user, Money.Coins(1.23456789m), 5000.0m * 1.23456789m, networkFeeMode); + // Check if there is no strange roundup issues + var v = (decimal)(rng.NextDouble() + 1.0); + v = Money.Coins(v).ToDecimal(MoneyUnit.BTC); + await AssertTopUpBtcPrice(tester, user, Money.Coins(v), 5000.0m * v, networkFeeMode); } } @@ -1432,247 +1389,239 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanModifyRates() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - var store = user.GetController(); - var rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); - Assert.False(rateVm.ShowScripting); - Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, rateVm.PreferredExchange); - Assert.Equal(0.0, rateVm.Spread); - Assert.Null(rateVm.TestRateRules); + var store = user.GetController(); + var rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.False(rateVm.ShowScripting); + Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, rateVm.PreferredExchange); + Assert.Equal(0.0, rateVm.Spread); + Assert.Null(rateVm.TestRateRules); - rateVm.PreferredExchange = "bitflyer"; - Assert.IsType(await store.Rates(rateVm, "Save")); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); - Assert.Equal("bitflyer", rateVm.PreferredExchange); + rateVm.PreferredExchange = "bitflyer"; + Assert.IsType(await store.Rates(rateVm, "Save")); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal("bitflyer", rateVm.PreferredExchange); - rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; - rateVm.Spread = 10; - store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) - .Model); - Assert.NotNull(rateVm.TestRateRules); - Assert.Equal(2, rateVm.TestRateRules.Count); - Assert.False(rateVm.TestRateRules[0].Error); - Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, - StringComparison.OrdinalIgnoreCase); - Assert.True(rateVm.TestRateRules[1].Error); - Assert.IsType(await store.Rates(rateVm, "Save")); + rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; + rateVm.Spread = 10; + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) + .Model); + Assert.NotNull(rateVm.TestRateRules); + Assert.Equal(2, rateVm.TestRateRules.Count); + Assert.False(rateVm.TestRateRules[0].Error); + Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, + StringComparison.OrdinalIgnoreCase); + Assert.True(rateVm.TestRateRules[1].Error); + Assert.IsType(await store.Rates(rateVm, "Save")); - Assert.IsType(store.ShowRateRulesPost(true).Result); - Assert.IsType(await store.Rates(rateVm, "Save")); - store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); - Assert.Equal(rateVm.StoreId, user.StoreId); - Assert.Equal(rateVm.DefaultScript, rateVm.Script); - Assert.True(rateVm.ShowScripting); - rateVm.ScriptTest = "BTC_JPY"; - rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) - .Model); - Assert.True(rateVm.ShowScripting); - Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, - StringComparison.OrdinalIgnoreCase); + Assert.IsType(store.ShowRateRulesPost(true).Result); + Assert.IsType(await store.Rates(rateVm, "Save")); + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal(rateVm.StoreId, user.StoreId); + Assert.Equal(rateVm.DefaultScript, rateVm.Script); + Assert.True(rateVm.ShowScripting); + rateVm.ScriptTest = "BTC_JPY"; + rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) + .Model); + Assert.True(rateVm.ShowScripting); + Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, + StringComparison.OrdinalIgnoreCase); - rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; - rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + - "X_CAD = ndax(X_CAD);\n" + - "X_X = coingecko(X_X);"; - rateVm.Spread = 50; - rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) - .Model); - Assert.True(rateVm.TestRateRules.All(t => !t.Error)); - Assert.IsType(await store.Rates(rateVm, "Save")); - store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); - Assert.Equal(50, rateVm.Spread); - Assert.True(rateVm.ShowScripting); - Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase); - } + rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; + rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + + "X_CAD = ndax(X_CAD);\n" + + "X_X = coingecko(X_X);"; + rateVm.Spread = 50; + rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")) + .Model); + Assert.True(rateVm.TestRateRules.All(t => !t.Error)); + Assert.IsType(await store.Rates(rateVm, "Save")); + store = user.GetController(); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + Assert.Equal(50, rateVm.Spread); + Assert.True(rateVm.ShowScripting); + Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase); } [Fact] [Trait("Integration", "Integration")] public async Task CanUseDefaultCurrency() { - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(true); + user.RegisterDerivationScheme("BTC"); + await user.ModifyPayment(s => { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(true); - user.RegisterDerivationScheme("BTC"); - await user.ModifyPayment(s => - { - Assert.Equal("USD", s.DefaultCurrency); - s.DefaultCurrency = "EUR"; - }); - var client = await user.CreateClient(); + Assert.Equal("USD", s.DefaultCurrency); + s.DefaultCurrency = "EUR"; + }); + var client = await user.CreateClient(); - // with greenfield - var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()); - Assert.Equal("EUR", invoice.Currency); - Assert.Equal(InvoiceType.TopUp, invoice.Type); + // with greenfield + var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()); + Assert.Equal("EUR", invoice.Currency); + Assert.Equal(InvoiceType.TopUp, invoice.Type); - // with bitpay api - var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice()); - Assert.Equal("EUR", invoice2.Currency); + // with bitpay api + var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice()); + Assert.Equal("EUR", invoice2.Currency); - // via UI - var controller = user.GetController(); - var model = await controller.CreateInvoice(); - (await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType(); - invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId); - Assert.Equal("EUR", invoice.Currency); - Assert.Equal(InvoiceType.TopUp, invoice.Type); + // via UI + var controller = user.GetController(); + var model = await controller.CreateInvoice(); + (await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType(); + invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId); + Assert.Equal("EUR", invoice.Currency); + Assert.Equal(InvoiceType.TopUp, invoice.Type); - // Check that the SendWallet use the default currency - var walletController = user.GetController(); - var walletSend = await walletController.WalletSend(new WalletId(user.StoreId, "BTC")).AssertViewModelAsync(); - Assert.Equal("EUR", walletSend.Fiat); - } + // Check that the SendWallet use the default currency + var walletController = user.GetController(); + var walletSend = await walletController.WalletSend(new WalletId(user.StoreId, "BTC")).AssertViewModelAsync(); + Assert.Equal("EUR", walletSend.Fiat); } [Fact] [Trait("Lightning", "Lightning")] public async Task CanSetPaymentMethodLimits() { - using (var tester = CreateServerTester()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(true); - user.RegisterDerivationScheme("BTC"); - await user.RegisterLightningNodeAsync("BTC"); + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterDerivationScheme("BTC"); + await user.RegisterLightningNodeAsync("BTC"); - var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(); - var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString(); + var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(); + var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString(); - // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice - var vm = Assert.IsType(Assert - .IsType(user.GetController().CheckoutAppearance()).Model); - Assert.Equal(2, vm.PaymentMethodCriteria.Count); - var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); - Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); - criteria.Value = "5 USD"; - criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; - Assert.IsType(user.GetController().CheckoutAppearance(vm) - .Result); + // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice + var vm = Assert.IsType(Assert + .IsType(user.GetController().CheckoutAppearance()).Model); + Assert.Equal(2, vm.PaymentMethodCriteria.Count); + var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); + Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); + criteria.Value = "5 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; + Assert.IsType(user.GetController().CheckoutAppearance(vm) + .Result); - var invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 4.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); + var invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 4.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); - Assert.Single(invoice.CryptoInfo); - Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); + Assert.Single(invoice.CryptoInfo); + Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); - // Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963 - // We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default - // payment method should be LN. - vm = Assert.IsType(Assert - .IsType(user.GetController().CheckoutAppearance()).Model); - vm.DefaultPaymentMethod = lnMethod; - criteria = vm.PaymentMethodCriteria.First(); - criteria.Value = "150 USD"; - criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; - criteria = vm.PaymentMethodCriteria.Skip(1).First(); - criteria.Value = "5 USD"; - criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; - Assert.IsType(user.GetController().CheckoutAppearance(vm) - .Result); - invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 50m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - var checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); - Assert.Equal(lnMethod, checkout.PaymentMethodId); + // Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963 + // We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default + // payment method should be LN. + vm = Assert.IsType(Assert + .IsType(user.GetController().CheckoutAppearance()).Model); + vm.DefaultPaymentMethod = lnMethod; + criteria = vm.PaymentMethodCriteria.First(); + criteria.Value = "150 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; + criteria = vm.PaymentMethodCriteria.Skip(1).First(); + criteria.Value = "5 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; + Assert.IsType(user.GetController().CheckoutAppearance(vm) + .Result); + invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 50m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + var checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); + Assert.Equal(lnMethod, checkout.PaymentMethodId); - // If we change store's default, it should change the checkout's default - vm.DefaultPaymentMethod = btcMethod; - Assert.IsType(user.GetController().CheckoutAppearance(vm) - .Result); - checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); - Assert.Equal(btcMethod, checkout.PaymentMethodId); - } + // If we change store's default, it should change the checkout's default + vm.DefaultPaymentMethod = btcMethod; + Assert.IsType(user.GetController().CheckoutAppearance(vm) + .Result); + checkout = (await user.GetController().Checkout(invoice.Id)).AssertViewModel(); + Assert.Equal(btcMethod, checkout.PaymentMethodId); } [Fact] [Trait("Integration", "Integration")] public async Task CanSetUnifiedQrCode() { - using (var tester = CreateServerTester()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - var cryptoCode = "BTC"; - await user.GrantAccessAsync(true); - user.RegisterDerivationScheme(cryptoCode, ScriptPubKeyType.Segwit); - user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning); + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + var cryptoCode = "BTC"; + await user.GrantAccessAsync(true); + user.RegisterDerivationScheme(cryptoCode, ScriptPubKeyType.Segwit); + user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning); - var invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 5.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); + var invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 5.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); - // validate that invoice data model doesn't have lightning string initially - var res = await user.GetController().Checkout(invoice.Id); - var paymentMethodFirst = Assert.IsType( - Assert.IsType(res).Model - ); - Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); + // validate that invoice data model doesn't have lightning string initially + var res = await user.GetController().Checkout(invoice.Id); + var paymentMethodFirst = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); - // enable unified QR code in settings - var vm = Assert.IsType(Assert - .IsType(await user.GetController().LightningSettings(user.StoreId, cryptoCode)).Model - ); - vm.OnChainWithLnInvoiceFallback = true; - Assert.IsType( - user.GetController().LightningSettings(vm).Result - ); + // enable unified QR code in settings + var vm = Assert.IsType(Assert + .IsType(await user.GetController().LightningSettings(user.StoreId, cryptoCode)).Model + ); + vm.OnChainWithLnInvoiceFallback = true; + Assert.IsType( + user.GetController().LightningSettings(vm).Result + ); - // validate that QR code now has both onchain and offchain payment urls - res = await user.GetController().Checkout(invoice.Id); - var paymentMethodSecond = Assert.IsType( - Assert.IsType(res).Model - ); - Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR); - Assert.StartsWith("bitcoin:", paymentMethodSecond.InvoiceBitcoinUrlQR); - var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0]; + // validate that QR code now has both onchain and offchain payment urls + res = await user.GetController().Checkout(invoice.Id); + var paymentMethodSecond = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR); + Assert.StartsWith("bitcoin:", paymentMethodSecond.InvoiceBitcoinUrlQR); + var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0]; - // Standard for all uppercase characters in QR codes is still not implemented in all wallets - // But we're proceeding with BECH32 being uppercase - Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); + // Standard for all uppercase characters in QR codes is still not implemented in all wallets + // But we're proceeding with BECH32 being uppercase + Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); - // Fallback lightning invoice should be uppercase inside the QR code. - var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1]; - Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback); - } + // Fallback lightning invoice should be uppercase inside the QR code. + var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1]; + Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback); } [Fact(Timeout = 60 * 2 * 1000)] @@ -1680,79 +1629,76 @@ namespace BTCPayServer.Tests [Trait("Lightning", "Lightning")] public async Task CanSetPaymentMethodLimitsLightning() { - using (var tester = CreateServerTester()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - var cryptoCode = "BTC"; - user.GrantAccess(true); - user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); - var vm = user.GetController().CheckoutAppearance().AssertViewModel(); - var criteria = Assert.Single(vm.PaymentMethodCriteria); - Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); - criteria.Value = "2 USD"; - criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; - Assert.IsType(user.GetController().CheckoutAppearance(vm) - .Result); + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + var cryptoCode = "BTC"; + user.GrantAccess(true); + user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); + var vm = user.GetController().CheckoutAppearance().AssertViewModel(); + var criteria = Assert.Single(vm.PaymentMethodCriteria); + Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); + criteria.Value = "2 USD"; + criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; + Assert.IsType(user.GetController().CheckoutAppearance(vm) + .Result); - var invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 1.5m, - Currency = "USD" - }, Facade.Merchant); + var invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 1.5m, + Currency = "USD" + }, Facade.Merchant); - Assert.Single(invoice.CryptoInfo); - Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); + Assert.Single(invoice.CryptoInfo); + Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); - // Activating LNUrl, we should still have only 1 payment criteria that can be set. - user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); - var lnSettingsVm = await user.GetController().LightningSettings(user.StoreId, cryptoCode).AssertViewModelAsync(); - lnSettingsVm.LNURLEnabled = true; - lnSettingsVm.LNURLStandardInvoiceEnabled = true; - Assert.IsType(user.GetController().LightningSettings(lnSettingsVm).Result); - vm = user.GetController().CheckoutAppearance().AssertViewModel(); - criteria = Assert.Single(vm.PaymentMethodCriteria); - Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); - Assert.IsType(user.GetController().CheckoutAppearance(vm).Result); + // Activating LNUrl, we should still have only 1 payment criteria that can be set. + user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge); + var lnSettingsVm = await user.GetController().LightningSettings(user.StoreId, cryptoCode).AssertViewModelAsync(); + lnSettingsVm.LNURLEnabled = true; + lnSettingsVm.LNURLStandardInvoiceEnabled = true; + Assert.IsType(user.GetController().LightningSettings(lnSettingsVm).Result); + vm = user.GetController().CheckoutAppearance().AssertViewModel(); + criteria = Assert.Single(vm.PaymentMethodCriteria); + Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); + Assert.IsType(user.GetController().CheckoutAppearance(vm).Result); - // However, creating an invoice should show LNURL - invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 1.5m, - Currency = "USD" - }, Facade.Merchant); - Assert.Equal(2, invoice.CryptoInfo.Length); + // However, creating an invoice should show LNURL + invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 1.5m, + Currency = "USD" + }, Facade.Merchant); + Assert.Equal(2, invoice.CryptoInfo.Length); - // Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available - Assert.Throws(() => user.BitPay.CreateInvoice( - new Invoice - { - Price = 2.5m, - Currency = "USD" - }, Facade.Merchant)); - } + // Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available + Assert.Throws(() => user.BitPay.CreateInvoice( + new Invoice + { + Price = 2.5m, + Currency = "USD" + }, Facade.Merchant)); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task PosDataParser_ParsesCorrectly_Slower() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - var controller = tester.PayTester.GetController(null); + var controller = tester.PayTester.GetController(null); - var testCases = - new List<(string input, Dictionary expectedOutput)>() - { + var testCases = + new List<(string input, Dictionary expectedOutput)>() + { {(null, new Dictionary())}, {("", new Dictionary())}, {("{}", new Dictionary())}, @@ -1767,24 +1713,23 @@ namespace BTCPayServer.Tests ("{ invalidjson file here}", new Dictionary() {{String.Empty, "{ invalidjson file here}"}}) } - }; + }; - var tasks = new List(); - foreach (var valueTuple in testCases) - { - tasks.Add(user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC") { PosData = valueTuple.input }) - .ContinueWith(async task => - { - var result = await controller.Invoice(task.Result.Id); - var viewModel = - Assert.IsType( - Assert.IsType(result).Model); - Assert.Equal(valueTuple.expectedOutput, viewModel.PosData); - })); - } - - await Task.WhenAll(tasks); + var tasks = new List(); + foreach (var valueTuple in testCases) + { + tasks.Add(user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC") { PosData = valueTuple.input }) + .ContinueWith(async task => + { + var result = await controller.Invoice(task.Result.Id); + var viewModel = + Assert.IsType( + Assert.IsType(result).Model); + Assert.Equal(valueTuple.expectedOutput, viewModel.PosData); + })); } + + await Task.WhenAll(tasks); } [Fact(Timeout = LongRunningTestTimeout)] @@ -1798,13 +1743,86 @@ namespace BTCPayServer.Tests return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture); } - using (var tester = CreateServerTester()) + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); + var invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 10, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some \", description", + FullNotifications = true + }, Facade.Merchant); + + var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100); + // ensure 0 invoices exported because there are no payments yet + var jsonResult = user.GetController().Export("json").GetAwaiter().GetResult(); + var result = Assert.IsType(jsonResult); + Assert.Equal("application/json", result.ContentType); + Assert.Equal("[]", result.Content); + + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + // + var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee; + cashCow.SendToAddress(invoiceAddress, firstPayment); + Thread.Sleep(1000); // prevent race conditions, ordering payments + // look if you can reduce thread sleep, this was min value for me + + // should reduce invoice due by 0 USD because payment = network fee + cashCow.SendToAddress(invoiceAddress, networkFee); + Thread.Sleep(1000); + + // pay remaining amount + cashCow.SendToAddress(invoiceAddress, 4 * networkFee); + Thread.Sleep(1000); + + TestUtils.Eventually(() => { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - await user.SetNetworkFeeMode(NetworkFeeMode.Always); + var jsonResultPaid = + user.GetController().Export("json").GetAwaiter().GetResult(); + var paidresult = Assert.IsType(jsonResultPaid); + Assert.Equal("application/json", paidresult.ContentType); + + var parsedJson = JsonConvert.DeserializeObject(paidresult.Content); + Assert.Equal(3, parsedJson.Length); + + var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate; + var pay1str = parsedJson[0].ToString(); + Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); + Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue")); + Assert.Contains("\"InvoicePrice\": 10.0", pay1str); + Assert.Contains("\"ConversionRate\": 5000.0", pay1str); + Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str); + + var pay2str = parsedJson[1].ToString(); + Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue")); + + var pay3str = parsedJson[2].ToString(); + Assert.Contains("\"InvoiceDue\": 0", pay3str); + }); + } + + [Fact(Timeout = LongRunningTestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanChangeNetworkFeeMode() + { + using var tester = CreateServerTester(); + var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) + { + TestLogs.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}"); + await user.SetNetworkFeeMode(networkFeeMode); var invoice = user.BitPay.CreateInvoice( new Invoice { @@ -1815,145 +1833,68 @@ namespace BTCPayServer.Tests ItemDesc = "Some \", description", FullNotifications = true }, Facade.Merchant); + var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) + .GetPaymentMethods()[btc] + .GetPaymentMethodDetails() + .AssertType() + .GetNextNetworkFee(); + var firstPaymentFee = nextNetworkFee; + switch (networkFeeMode) + { + case NetworkFeeMode.Never: + case NetworkFeeMode.MultiplePaymentsOnly: + Assert.Equal(0.0m, nextNetworkFee); + break; + case NetworkFeeMode.Always: + Assert.NotEqual(0.0m, nextNetworkFee); + break; + } - var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100); - // ensure 0 invoices exported because there are no payments yet - var jsonResult = user.GetController().Export("json").GetAwaiter().GetResult(); - var result = Assert.IsType(jsonResult); - Assert.Equal("application/json", result.ContentType); - Assert.Equal("[]", result.Content); - + var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC); var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - // - var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee; - cashCow.SendToAddress(invoiceAddress, firstPayment); - Thread.Sleep(1000); // prevent race conditions, ordering payments - // look if you can reduce thread sleep, this was min value for me - // should reduce invoice due by 0 USD because payment = network fee - cashCow.SendToAddress(invoiceAddress, networkFee); - Thread.Sleep(1000); + var due = Money.Parse(invoice.CryptoInfo[0].Due); + var productPartDue = (invoice.Price / invoice.Rate); + TestLogs.LogInformation( + $"Product part due is {productPartDue} and due {due} with network fee {nextNetworkFee}"); + Assert.Equal(productPartDue + nextNetworkFee, due.ToDecimal(MoneyUnit.BTC)); + var firstPayment = productPartDue - missingMoney; + cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment)); - // pay remaining amount - cashCow.SendToAddress(invoiceAddress, 4 * networkFee); - Thread.Sleep(1000); - - TestUtils.Eventually(() => + await TestUtils.EventuallyAsync(async () => { - var jsonResultPaid = - user.GetController().Export("json").GetAwaiter().GetResult(); - var paidresult = Assert.IsType(jsonResultPaid); - Assert.Equal("application/json", paidresult.ContentType); - - var parsedJson = JsonConvert.DeserializeObject(paidresult.Content); - Assert.Equal(3, parsedJson.Length); - - var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate; - var pay1str = parsedJson[0].ToString(); - Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); - Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue")); - Assert.Contains("\"InvoicePrice\": 10.0", pay1str); - Assert.Contains("\"ConversionRate\": 5000.0", pay1str); - Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str); - - var pay2str = parsedJson[1].ToString(); - Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue")); - - var pay3str = parsedJson[2].ToString(); - Assert.Contains("\"InvoiceDue\": 0", pay3str); - }); - } - } - - [Fact(Timeout = LongRunningTestTimeout)] - [Trait("Integration", "Integration")] - public async Task CanChangeNetworkFeeMode() - { - using (var tester = CreateServerTester()) - { - var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) - { - TestLogs.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}"); - await user.SetNetworkFeeMode(networkFeeMode); - var invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 10, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some \", description", - FullNotifications = true - }, Facade.Merchant); - var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) + invoice = user.BitPay.GetInvoice(invoice.Id); + due = Money.Parse(invoice.CryptoInfo[0].Due); + TestLogs.LogInformation($"Remaining due after first payment: {due}"); + Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid)); + nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) .GetPaymentMethods()[btc] .GetPaymentMethodDetails() .AssertType() .GetNextNetworkFee(); - var firstPaymentFee = nextNetworkFee; switch (networkFeeMode) { case NetworkFeeMode.Never: - case NetworkFeeMode.MultiplePaymentsOnly: Assert.Equal(0.0m, nextNetworkFee); break; + case NetworkFeeMode.MultiplePaymentsOnly: case NetworkFeeMode.Always: Assert.NotEqual(0.0m, nextNetworkFee); break; } - var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC); - var cashCow = tester.ExplorerNode; - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - - var due = Money.Parse(invoice.CryptoInfo[0].Due); - var productPartDue = (invoice.Price / invoice.Rate); - TestLogs.LogInformation( - $"Product part due is {productPartDue} and due {due} with network fee {nextNetworkFee}"); - Assert.Equal(productPartDue + nextNetworkFee, due.ToDecimal(MoneyUnit.BTC)); - var firstPayment = productPartDue - missingMoney; - cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment)); - - await TestUtils.EventuallyAsync(async () => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - due = Money.Parse(invoice.CryptoInfo[0].Due); - TestLogs.LogInformation($"Remaining due after first payment: {due}"); - Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid)); - nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) - .GetPaymentMethods()[btc] - .GetPaymentMethodDetails() - .AssertType() - .GetNextNetworkFee(); - switch (networkFeeMode) - { - case NetworkFeeMode.Never: - Assert.Equal(0.0m, nextNetworkFee); - break; - case NetworkFeeMode.MultiplePaymentsOnly: - case NetworkFeeMode.Always: - Assert.NotEqual(0.0m, nextNetworkFee); - break; - } - - Assert.Equal(missingMoney + firstPaymentFee + nextNetworkFee, due.ToDecimal(MoneyUnit.BTC)); - Assert.Equal(firstPayment + missingMoney + firstPaymentFee + nextNetworkFee, - Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC)); - }); - cashCow.SendToAddress(invoiceAddress, due); - TestLogs.LogInformation($"After payment of {due}, the invoice should be paid"); - TestUtils.Eventually(() => - { - invoice = user.BitPay.GetInvoice(invoice.Id); - Assert.Equal("paid", invoice.Status); - }); - } + Assert.Equal(missingMoney + firstPaymentFee + nextNetworkFee, due.ToDecimal(MoneyUnit.BTC)); + Assert.Equal(firstPayment + missingMoney + firstPaymentFee + nextNetworkFee, + Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC)); + }); + cashCow.SendToAddress(invoiceAddress, due); + TestLogs.LogInformation($"After payment of {due}, the invoice should be paid"); + TestUtils.Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal("paid", invoice.Status); + }); } } @@ -1961,81 +1902,77 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanExportInvoicesCsv() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - user.RegisterDerivationScheme("BTC"); - await user.SetNetworkFeeMode(NetworkFeeMode.Always); - var invoice = user.BitPay.CreateInvoice( - new Invoice - { - Price = 500, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some \", description", - FullNotifications = true - }, Facade.Merchant); - - var cashCow = tester.ExplorerNode; - var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m); - cashCow.SendToAddress(invoiceAddress, firstPayment); - TestUtils.Eventually(() => + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + user.RegisterDerivationScheme("BTC"); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); + var invoice = user.BitPay.CreateInvoice( + new Invoice { - var exportResultPaid = - user.GetController().Export("csv").GetAwaiter().GetResult(); - var paidresult = Assert.IsType(exportResultPaid); - Assert.Equal("application/csv", paidresult.ContentType); - Assert.Contains($",orderId,{invoice.Id},", paidresult.Content); - Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content); - Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same + Price = 500, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some \", description", + FullNotifications = true + }, Facade.Merchant); + + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); + var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m); + cashCow.SendToAddress(invoiceAddress, firstPayment); + TestUtils.Eventually(() => + { + var exportResultPaid = + user.GetController().Export("csv").GetAwaiter().GetResult(); + var paidresult = Assert.IsType(exportResultPaid); + Assert.Equal("application/csv", paidresult.ContentType); + Assert.Contains($",orderId,{invoice.Id},", paidresult.Content); + Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content); + Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial", - paidresult.Content); - }); - } + paidresult.Content); + }); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanCreateAndDeleteApps() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - await user.GrantAccessAsync(); - var user2 = tester.NewAccount(); - await user2.GrantAccessAsync(); - var apps = user.GetController(); - var apps2 = user2.GetController(); - var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); - Assert.NotNull(vm.SelectedAppType); - Assert.Null(vm.AppName); - vm.AppName = "test"; - vm.SelectedAppType = AppType.PointOfSale.ToString(); - var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); - Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName); - var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - var appList2 = - Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); - var app = appList.Apps[0]; - apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); - Assert.Single(appList.Apps); - Assert.Empty(appList2.Apps); - Assert.Equal("test", appList.Apps[0].AppName); - Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); - Assert.True(app.IsOwner); - Assert.Equal(user.StoreId, appList.Apps[0].StoreId); - Assert.IsType(apps2.DeleteApp(appList.Apps[0].Id)); - Assert.IsType(apps.DeleteApp(appList.Apps[0].Id)); - redirectToAction = Assert.IsType(apps.DeleteAppPost(appList.Apps[0].Id).Result); - Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); - appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); - Assert.Empty(appList.Apps); - } + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + var user2 = tester.NewAccount(); + await user2.GrantAccessAsync(); + var apps = user.GetController(); + var apps2 = user2.GetController(); + var vm = Assert.IsType(Assert.IsType(apps.CreateApp(user.StoreId)).Model); + Assert.NotNull(vm.SelectedAppType); + Assert.Null(vm.AppName); + vm.AppName = "test"; + vm.SelectedAppType = AppType.PointOfSale.ToString(); + var redirectToAction = Assert.IsType(apps.CreateApp(user.StoreId, vm).Result); + Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName); + var appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + var appList2 = + Assert.IsType(Assert.IsType(apps2.ListApps(user2.StoreId).Result).Model); + var app = appList.Apps[0]; + apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName }); + Assert.Single(appList.Apps); + Assert.Empty(appList2.Apps); + Assert.Equal("test", appList.Apps[0].AppName); + Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id); + Assert.True(app.IsOwner); + Assert.Equal(user.StoreId, appList.Apps[0].StoreId); + Assert.IsType(apps2.DeleteApp(appList.Apps[0].Id)); + Assert.IsType(apps.DeleteApp(appList.Apps[0].Id)); + redirectToAction = Assert.IsType(apps.DeleteAppPost(appList.Apps[0].Id).Result); + Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName); + appList = Assert.IsType(Assert.IsType(apps.ListApps(user.StoreId).Result).Model); + Assert.Empty(appList.Apps); } [Fact(Timeout = LongRunningTestTimeout)] @@ -2043,418 +1980,408 @@ namespace BTCPayServer.Tests [Trait("Lightning", "Lightning")] public async Task CanCreateStrangeInvoice() { - using (var tester = CreateServerTester()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(true); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterDerivationScheme("BTC"); - DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21); + DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21); - // This should fail, the amount is too low to be above the dust limit of bitcoin - var ex = Assert.Throws(() => user.BitPay.CreateInvoice( - new Invoice() - { - Price = 0.000000012m, - Currency = "USD", - FullNotifications = true, - ExpirationTime = expiration - }, Facade.Merchant)); - Assert.Contains("dust threshold", ex.Message); - await user.RegisterLightningNodeAsync("BTC"); - - var invoice1 = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 0.000000012m, - Currency = "USD", - FullNotifications = true, - ExpirationTime = expiration - }, Facade.Merchant); - - Assert.Equal(expiration.ToUnixTimeSeconds(), invoice1.ExpirationTime.ToUnixTimeSeconds()); - var invoice2 = user.BitPay.CreateInvoice(new Invoice() { Price = 0.000000019m, Currency = "USD" }, - Facade.Merchant); - Assert.Equal(0.000000012m, invoice1.Price); - Assert.Equal(0.000000019m, invoice2.Price); - - // Should round up to 1 because 0.000000019 is unsignificant - var invoice3 = user.BitPay.CreateInvoice( - new Invoice() { Price = 1.000000019m, Currency = "USD", FullNotifications = true }, Facade.Merchant); - Assert.Equal(1m, invoice3.Price); - - // Should not round up at 8 digit because the 9th is insignificant - var invoice4 = user.BitPay.CreateInvoice( - new Invoice() { Price = 1.000000019m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); - Assert.Equal(1.00000002m, invoice4.Price); - - // But not if the 9th is insignificant - invoice4 = user.BitPay.CreateInvoice( - new Invoice() { Price = 0.000000019m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); - Assert.Equal(0.000000019m, invoice4.Price); - - var invoice = user.BitPay.CreateInvoice( - new Invoice() { Price = -0.1m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); - Assert.Equal(0.0m, invoice.Price); - - // Should round down to 50.51, taxIncluded should be also clipped to this value because taxIncluded can't be higher than the price. - var invoice5 = user.BitPay.CreateInvoice( - new Invoice() { Price = 50.513m, Currency = "USD", FullNotifications = true, TaxIncluded = 50.516m }, Facade.Merchant); - Assert.Equal(50.51m, invoice5.Price); - Assert.Equal(50.51m, invoice5.TaxIncluded); - - var greenfield = await user.CreateClient(); - var invoice5g = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest() + // This should fail, the amount is too low to be above the dust limit of bitcoin + var ex = Assert.Throws(() => user.BitPay.CreateInvoice( + new Invoice() { - Amount = 50.513m, + Price = 0.000000012m, Currency = "USD", - Metadata = new JObject() { new JProperty("taxIncluded", 50.516m), new JProperty("orderId", "000000161") } - }); - Assert.Equal(50.51m, invoice5g.Amount); - Assert.Equal(50.51m, (decimal)invoice5g.Metadata["taxIncluded"]); - Assert.Equal("000000161", (string)invoice5g.Metadata["orderId"]); + FullNotifications = true, + ExpirationTime = expiration + }, Facade.Merchant)); + Assert.Contains("dust threshold", ex.Message); + await user.RegisterLightningNodeAsync("BTC"); - var zeroInvoice = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest() + var invoice1 = user.BitPay.CreateInvoice( + new Invoice() { - Amount = 0m, - Currency = "USD" - }); - Assert.Equal(InvoiceStatus.New, zeroInvoice.Status); - await TestUtils.EventuallyAsync(async () => - { - zeroInvoice = await greenfield.GetInvoice(user.StoreId, zeroInvoice.Id); - Assert.Equal(InvoiceStatus.Settled, zeroInvoice.Status); - }); + Price = 0.000000012m, + Currency = "USD", + FullNotifications = true, + ExpirationTime = expiration + }, Facade.Merchant); - var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id); - Assert.Empty(zeroInvoicePM); - } + Assert.Equal(expiration.ToUnixTimeSeconds(), invoice1.ExpirationTime.ToUnixTimeSeconds()); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() { Price = 0.000000019m, Currency = "USD" }, + Facade.Merchant); + Assert.Equal(0.000000012m, invoice1.Price); + Assert.Equal(0.000000019m, invoice2.Price); + + // Should round up to 1 because 0.000000019 is unsignificant + var invoice3 = user.BitPay.CreateInvoice( + new Invoice() { Price = 1.000000019m, Currency = "USD", FullNotifications = true }, Facade.Merchant); + Assert.Equal(1m, invoice3.Price); + + // Should not round up at 8 digit because the 9th is insignificant + var invoice4 = user.BitPay.CreateInvoice( + new Invoice() { Price = 1.000000019m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); + Assert.Equal(1.00000002m, invoice4.Price); + + // But not if the 9th is insignificant + invoice4 = user.BitPay.CreateInvoice( + new Invoice() { Price = 0.000000019m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); + Assert.Equal(0.000000019m, invoice4.Price); + + var invoice = user.BitPay.CreateInvoice( + new Invoice() { Price = -0.1m, Currency = "BTC", FullNotifications = true }, Facade.Merchant); + Assert.Equal(0.0m, invoice.Price); + + // Should round down to 50.51, taxIncluded should be also clipped to this value because taxIncluded can't be higher than the price. + var invoice5 = user.BitPay.CreateInvoice( + new Invoice() { Price = 50.513m, Currency = "USD", FullNotifications = true, TaxIncluded = 50.516m }, Facade.Merchant); + Assert.Equal(50.51m, invoice5.Price); + Assert.Equal(50.51m, invoice5.TaxIncluded); + + var greenfield = await user.CreateClient(); + var invoice5g = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest() + { + Amount = 50.513m, + Currency = "USD", + Metadata = new JObject() { new JProperty("taxIncluded", 50.516m), new JProperty("orderId", "000000161") } + }); + Assert.Equal(50.51m, invoice5g.Amount); + Assert.Equal(50.51m, (decimal)invoice5g.Metadata["taxIncluded"]); + Assert.Equal("000000161", (string)invoice5g.Metadata["orderId"]); + + var zeroInvoice = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest() + { + Amount = 0m, + Currency = "USD" + }); + Assert.Equal(InvoiceStatus.New, zeroInvoice.Status); + await TestUtils.EventuallyAsync(async () => + { + zeroInvoice = await greenfield.GetInvoice(user.StoreId, zeroInvoice.Id); + Assert.Equal(InvoiceStatus.Settled, zeroInvoice.Status); + }); + + var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id); + Assert.Empty(zeroInvoicePM); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task InvoiceFlowThroughDifferentStatesCorrectly() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - await user.SetupWebhook(); - var invoice = user.BitPay.CreateInvoice( - new Invoice() - { - Price = 5000.0m, - TaxIncluded = 1000.0m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - var repo = tester.PayTester.GetService(); - var ctx = tester.PayTester.GetService().CreateContext(); - Assert.Equal(0, invoice.CryptoInfo[0].TxCount); - Assert.True(invoice.MinerFees.ContainsKey("BTC")); - Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m }); - TestUtils.Eventually(() => - { - var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() - { - StoreId = new[] { user.StoreId }, - TextSearch = invoice.OrderId - }).GetAwaiter().GetResult(); - Assert.Single(textSearchResult); - textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() - { - StoreId = new[] { user.StoreId }, - TextSearch = invoice.Id - }).GetAwaiter().GetResult(); - - Assert.Single(textSearchResult); - }); - - invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal(1000.0m, invoice.TaxIncluded); - Assert.Equal(5000.0m, invoice.Price); - Assert.Equal(Money.Coins(0), invoice.BtcPaid); - Assert.Equal("new", invoice.Status); - Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); - - Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime)); - Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2))); - Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5))); - Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), - invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0))); - Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), - invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1))); - - - var firstPayment = Money.Coins(0.04m); - - var txFee = Money.Zero; - - var cashCow = tester.ExplorerNode; - - var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - Assert.True(IsMapped(invoice, ctx)); - cashCow.SendToAddress(invoiceAddress, firstPayment); - - var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); - Assert.Single(invoiceEntity.HistoricalAddresses); - Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned); - - Money secondPayment = Money.Zero; - - TestUtils.Eventually(() => - { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("new", localInvoice.Status); - Assert.Equal(firstPayment, localInvoice.BtcPaid); - txFee = localInvoice.BtcDue - invoice.BtcDue; - Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString()); - Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount); - Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address - Assert.True(IsMapped(invoice, ctx)); - Assert.True(IsMapped(localInvoice, ctx)); - - invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); - var historical1 = - invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress); - Assert.NotNull(historical1.UnAssigned); - var historical2 = - invoiceEntity.HistoricalAddresses.FirstOrDefault(h => - h.GetAddress() == localInvoice.BitcoinAddress); - Assert.Null(historical2.UnAssigned); - invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network); - secondPayment = localInvoice.BtcDue; - }); - - cashCow.SendToAddress(invoiceAddress, secondPayment); - - TestUtils.Eventually(() => - { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("paid", localInvoice.Status); - Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount); - Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid); - Assert.Equal(Money.Zero, localInvoice.BtcDue); - Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated - Assert.True(IsMapped(localInvoice, ctx)); - Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value); - }); - - cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed - - TestUtils.Eventually(() => - { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("confirmed", localInvoice.Status); - }); - - cashCow.Generate(5); //Now should be complete - - TestUtils.Eventually(() => - { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("complete", localInvoice.Status); - Assert.NotEqual(0.0m, localInvoice.Rate); - }); - - invoice = user.BitPay.CreateInvoice(new Invoice() + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + await user.SetupWebhook(); + var invoice = user.BitPay.CreateInvoice( + new Invoice() { Price = 5000.0m, + TaxIncluded = 1000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", - //RedirectURL = redirect + "redirect", - //NotificationURL = CallbackUri + "/notification", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); - invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); - - var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1)); - - TestUtils.Eventually(() => + var repo = tester.PayTester.GetService(); + var ctx = tester.PayTester.GetService().CreateContext(); + Assert.Equal(0, invoice.CryptoInfo[0].TxCount); + Assert.True(invoice.MinerFees.ContainsKey("BTC")); + Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m }); + TestUtils.Eventually(() => + { + var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("paid", localInvoice.Status); - Assert.Equal(Money.Zero, localInvoice.BtcDue); - Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); - - var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() - { - StoreId = new[] { user.StoreId }, - TextSearch = txId.ToString() - }).GetAwaiter().GetResult(); - Assert.Single(textSearchResult); - }); - - cashCow.Generate(1); - - TestUtils.Eventually(() => + StoreId = new[] { user.StoreId }, + TextSearch = invoice.OrderId + }).GetAwaiter().GetResult(); + Assert.Single(textSearchResult); + textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { - var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); - Assert.Equal("confirmed", localInvoice.Status); - Assert.Equal(Money.Zero, localInvoice.BtcDue); - Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); + StoreId = new[] { user.StoreId }, + TextSearch = invoice.Id + }).GetAwaiter().GetResult(); + + Assert.Single(textSearchResult); + }); + + invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal(1000.0m, invoice.TaxIncluded); + Assert.Equal(5000.0m, invoice.Price); + Assert.Equal(Money.Coins(0), invoice.BtcPaid); + Assert.Equal("new", invoice.Status); + Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); + + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime)); + Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2))); + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5))); + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), + invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0))); + Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), + invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1))); + + + var firstPayment = Money.Coins(0.04m); + + var txFee = Money.Zero; + + var cashCow = tester.ExplorerNode; + + var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + Assert.True(IsMapped(invoice, ctx)); + cashCow.SendToAddress(invoiceAddress, firstPayment); + + var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); + Assert.Single(invoiceEntity.HistoricalAddresses); + Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned); + + Money secondPayment = Money.Zero; + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("new", localInvoice.Status); + Assert.Equal(firstPayment, localInvoice.BtcPaid); + txFee = localInvoice.BtcDue - invoice.BtcDue; + Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString()); + Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount); + Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address + Assert.True(IsMapped(invoice, ctx)); + Assert.True(IsMapped(localInvoice, ctx)); + + invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult(); + var historical1 = + invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress); + Assert.NotNull(historical1.UnAssigned); + var historical2 = + invoiceEntity.HistoricalAddresses.FirstOrDefault(h => + h.GetAddress() == localInvoice.BitcoinAddress); + Assert.Null(historical2.UnAssigned); + invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network); + secondPayment = localInvoice.BtcDue; + }); + + cashCow.SendToAddress(invoiceAddress, secondPayment); + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("paid", localInvoice.Status); + Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount); + Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid); + Assert.Equal(Money.Zero, localInvoice.BtcDue); + Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated + Assert.True(IsMapped(localInvoice, ctx)); + Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value); + }); + + cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("confirmed", localInvoice.Status); + }); + + cashCow.Generate(5); //Now should be complete + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("complete", localInvoice.Status); + Assert.NotEqual(0.0m, localInvoice.Rate); + }); + + invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + //RedirectURL = redirect + "redirect", + //NotificationURL = CallbackUri + "/notification", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + + var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1)); + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("paid", localInvoice.Status); + Assert.Equal(Money.Zero, localInvoice.BtcDue); + Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); + + var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() + { + StoreId = new[] { user.StoreId }, + TextSearch = txId.ToString() + }).GetAwaiter().GetResult(); + Assert.Single(textSearchResult); + }); + + cashCow.Generate(1); + + TestUtils.Eventually(() => + { + var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); + Assert.Equal("confirmed", localInvoice.Status); + Assert.Equal(Money.Zero, localInvoice.BtcDue); + Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); + }); + + // Test on the webhooks + user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, + c => + { + Assert.False(c.ManuallyMarked); }); + user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, + c => + { + Assert.True(c.OverPaid); + }); + user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment, + c => + { + Assert.False(c.AfterExpiration); + Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod); + Assert.NotNull(c.Payment); + Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); + Assert.StartsWith(txId.ToString(), c.Payment.Id); - // Test on the webhooks - user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, - c => - { - Assert.False(c.ManuallyMarked); - }); - user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, - c => - { - Assert.True(c.OverPaid); - }); - user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment, - c => - { - Assert.False(c.AfterExpiration); - Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod); - Assert.NotNull(c.Payment); - Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); - Assert.StartsWith(txId.ToString(), c.Payment.Id); - - }); - user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled, - c => - { - Assert.False(c.AfterExpiration); - Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod); - Assert.NotNull(c.Payment); - Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); - Assert.StartsWith(txId.ToString(), c.Payment.Id); - }); - } + }); + user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled, + c => + { + Assert.False(c.AfterExpiration); + Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod); + Assert.NotNull(c.Payment); + Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); + Assert.StartsWith(txId.ToString(), c.Payment.Id); + }); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CheckLogsRoute() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); - var serverController = user.GetController(); - var vm = Assert.IsType( - Assert.IsType(await serverController.LogsView()).Model); - } + var serverController = user.GetController(); + var vm = Assert.IsType( + Assert.IsType(await serverController.LogsView()).Model); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); - var accountController = tester.PayTester.GetController(); + var accountController = tester.PayTester.GetController(); - //no 2fa or fido2 enabled, login should work - Assert.Equal(nameof(UIHomeController.Index), - Assert.IsType(await accountController.Login(new LoginViewModel() - { - Email = user.RegisterDetails.Email, - Password = user.RegisterDetails.Password - })).ActionName); - - var listController = user.GetController(); - var manageController = user.GetController(); - - //by default no fido2 devices available - Assert.Empty(Assert - .IsType(Assert - .IsType(await listController.TwoFactorAuthentication()).Model).Credentials); - Assert.IsType(Assert - .IsType(await manageController.Create(new AddFido2CredentialViewModel - { - Name = "label" - })).Model); - - //sending an invalid response model back to server, should error out - Assert.IsType(await manageController.CreateResponse("sdsdsa", "sds")); - var statusModel = manageController.TempData.GetStatusMessageModel(); - Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity); - - var contextFactory = tester.PayTester.GetService(); - - //add a fake fido2 device in db directly since emulating a fido2 device is hard and annoying - using (var context = contextFactory.CreateContext()) - { - var newDevice = new Fido2Credential() - { - Id = Guid.NewGuid().ToString(), - Name = "fake", - Type = Fido2Credential.CredentialType.FIDO2, - ApplicationUserId = user.UserId - }; - newDevice.SetBlob(new Fido2CredentialBlob() { }); - await context.Fido2Credentials.AddAsync(newDevice); - await context.SaveChangesAsync(); - - Assert.NotNull(newDevice.Id); - Assert.NotEmpty(Assert - .IsType(Assert - .IsType(await listController.TwoFactorAuthentication()).Model).Credentials); - } - - //check if we are showing the fido2 login screen now - var secondLoginResult = Assert.IsType(await accountController.Login(new LoginViewModel() + //no 2fa or fido2 enabled, login should work + Assert.Equal(nameof(UIHomeController.Index), + Assert.IsType(await accountController.Login(new LoginViewModel() { Email = user.RegisterDetails.Email, Password = user.RegisterDetails.Password - })); + })).ActionName); - Assert.Equal("SecondaryLogin", secondLoginResult.ViewName); - var vm = Assert.IsType(secondLoginResult.Model); - //2fa was never enabled for user so this should be empty - Assert.Null(vm.LoginWith2FaViewModel); - Assert.NotNull(vm.LoginWithFido2ViewModel); + var listController = user.GetController(); + var manageController = user.GetController(); + + //by default no fido2 devices available + Assert.Empty(Assert + .IsType(Assert + .IsType(await listController.TwoFactorAuthentication()).Model).Credentials); + Assert.IsType(Assert + .IsType(await manageController.Create(new AddFido2CredentialViewModel + { + Name = "label" + })).Model); + + //sending an invalid response model back to server, should error out + Assert.IsType(await manageController.CreateResponse("sdsdsa", "sds")); + var statusModel = manageController.TempData.GetStatusMessageModel(); + Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity); + + var contextFactory = tester.PayTester.GetService(); + + //add a fake fido2 device in db directly since emulating a fido2 device is hard and annoying + using (var context = contextFactory.CreateContext()) + { + var newDevice = new Fido2Credential() + { + Id = Guid.NewGuid().ToString(), + Name = "fake", + Type = Fido2Credential.CredentialType.FIDO2, + ApplicationUserId = user.UserId + }; + newDevice.SetBlob(new Fido2CredentialBlob() { }); + await context.Fido2Credentials.AddAsync(newDevice); + await context.SaveChangesAsync(); + + Assert.NotNull(newDevice.Id); + Assert.NotEmpty(Assert + .IsType(Assert + .IsType(await listController.TwoFactorAuthentication()).Model).Credentials); } + + //check if we are showing the fido2 login screen now + var secondLoginResult = Assert.IsType(await accountController.Login(new LoginViewModel() + { + Email = user.RegisterDetails.Email, + Password = user.RegisterDetails.Password + })); + + Assert.Equal("SecondaryLogin", secondLoginResult.ViewName); + var vm = Assert.IsType(secondLoginResult.Model); + //2fa was never enabled for user so this should be empty + Assert.Null(vm.LoginWith2FaViewModel); + Assert.NotNull(vm.LoginWithFido2ViewModel); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async void CheckOnionlocationForNonOnionHtmlRequests() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var url = tester.PayTester.ServerUri.AbsoluteUri; + using var tester = CreateServerTester(); + await tester.StartAsync(); + var url = tester.PayTester.ServerUri.AbsoluteUri; - // check onion location is present for HTML page request - using var htmlRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); - htmlRequest.Headers.TryAddWithoutValidation("Accept", "text/html,*/*"); + // check onion location is present for HTML page request + using var htmlRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); + htmlRequest.Headers.TryAddWithoutValidation("Accept", "text/html,*/*"); - var htmlResponse = await tester.PayTester.HttpClient.SendAsync(htmlRequest); - htmlResponse.EnsureSuccessStatusCode(); - Assert.True(htmlResponse.Headers.TryGetValues("Onion-Location", out var onionLocation)); - Assert.StartsWith("http://wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", onionLocation.FirstOrDefault() ?? "no-onion-location-header"); + var htmlResponse = await tester.PayTester.HttpClient.SendAsync(htmlRequest); + htmlResponse.EnsureSuccessStatusCode(); + Assert.True(htmlResponse.Headers.TryGetValues("Onion-Location", out var onionLocation)); + Assert.StartsWith("http://wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", onionLocation.FirstOrDefault() ?? "no-onion-location-header"); - // no onion location for other mime types - using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); - otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*"); + // no onion location for other mime types + using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); + otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*"); - var otherResponse = await tester.PayTester.HttpClient.SendAsync(otherRequest); - otherResponse.EnsureSuccessStatusCode(); - Assert.False(otherResponse.Headers.Contains("Onion-Location")); - } + var otherResponse = await tester.PayTester.HttpClient.SendAsync(otherRequest); + otherResponse.EnsureSuccessStatusCode(); + Assert.False(otherResponse.Headers.Contains("Onion-Location")); } private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) @@ -2479,87 +2406,84 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanCheckForNewVersion() { - using (var tester = CreateServerTester(newDb: true)) - { - await tester.StartAsync(); + using var tester = CreateServerTester(newDb: true); + await tester.StartAsync(); - var acc = tester.NewAccount(); - acc.GrantAccess(true); + var acc = tester.NewAccount(); + acc.GrantAccess(true); - var settings = tester.PayTester.GetService(); - await settings.UpdateSetting(new PoliciesSettings() { CheckForNewVersions = true }); + var settings = tester.PayTester.GetService(); + await settings.UpdateSetting(new PoliciesSettings() { CheckForNewVersions = true }); - var mockEnv = tester.PayTester.GetService(); - var mockSender = tester.PayTester.GetService(); + var mockEnv = tester.PayTester.GetService(); + var mockSender = tester.PayTester.GetService(); - var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher(), BTCPayLogs); - await svc.ProcessVersionCheck(); + var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher(), BTCPayLogs); + await svc.ProcessVersionCheck(); - // since last version present in database was null, it should've been updated with version mock returned - var lastVersion = await settings.GetSettingAsync(); - Assert.Equal(MockVersionFetcher.MOCK_NEW_VERSION, lastVersion.LastVersion); + // since last version present in database was null, it should've been updated with version mock returned + var lastVersion = await settings.GetSettingAsync(); + Assert.Equal(MockVersionFetcher.MOCK_NEW_VERSION, lastVersion.LastVersion); - // we should also have notification in UI - var ctrl = acc.GetController(); - var newVersion = MockVersionFetcher.MOCK_NEW_VERSION; + // we should also have notification in UI + var ctrl = acc.GetController(); + var newVersion = MockVersionFetcher.MOCK_NEW_VERSION; - var vm = Assert.IsType( - Assert.IsType(await ctrl.Index()).Model); + var vm = Assert.IsType( + Assert.IsType(await ctrl.Index()).Model); - Assert.True(vm.Skip == 0); - Assert.True(vm.Count == 50); - Assert.True(vm.Total == 1); - Assert.True(vm.Items.Count == 1); + Assert.True(vm.Skip == 0); + Assert.True(vm.Count == 50); + Assert.True(vm.Total == 1); + Assert.True(vm.Items.Count == 1); - var fn = vm.Items.First(); - var now = DateTimeOffset.UtcNow; - Assert.True(fn.Created >= now.AddSeconds(-3)); - Assert.True(fn.Created <= now); - Assert.Equal($"New version {newVersion} released!", fn.Body); - Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); - Assert.False(fn.Seen); - } + var fn = vm.Items.First(); + var now = DateTimeOffset.UtcNow; + Assert.True(fn.Created >= now.AddSeconds(-3)); + Assert.True(fn.Created <= now); + Assert.Equal($"New version {newVersion} released!", fn.Body); + Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink); + Assert.False(fn.Seen); } [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanDoLightningInternalNodeMigration() { - using (var tester = CreateServerTester(newDb: true)) - { - tester.ActivateLightning(LightningConnectionType.CLightning); - await tester.StartAsync(); - var acc = tester.NewAccount(); - await acc.GrantAccessAsync(true); - await acc.CreateStoreAsync(); + using var tester = CreateServerTester(newDb: true); + tester.ActivateLightning(LightningConnectionType.CLightning); + await tester.StartAsync(); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(true); + await acc.CreateStoreAsync(); - // Test if legacy DerivationStrategy column is converted to DerivationStrategies - var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav"; - var derivation = $"{xpub}-[legacy]"; - store.DerivationStrategy = derivation; - await tester.PayTester.StoreRepository.UpdateStore(store); - await RestartMigration(tester); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - Assert.True(string.IsNullOrEmpty(store.DerivationStrategy)); - var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First(); - Assert.Equal(derivation, v.AccountDerivation.ToString()); - Assert.Equal(derivation, v.AccountOriginal.ToString()); - Assert.Equal(xpub, v.SigningKey.ToString()); - Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString()); + // Test if legacy DerivationStrategy column is converted to DerivationStrategies + var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav"; + var derivation = $"{xpub}-[legacy]"; + store.DerivationStrategy = derivation; + await tester.PayTester.StoreRepository.UpdateStore(store); + await RestartMigration(tester); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + Assert.True(string.IsNullOrEmpty(store.DerivationStrategy)); + var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First(); + Assert.Equal(derivation, v.AccountDerivation.ToString()); + Assert.Equal(derivation, v.AccountOriginal.ToString()); + Assert.Equal(xpub, v.SigningKey.ToString()); + Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString()); - await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); - Assert.NotNull(lnMethod.GetExternalLightningUrl()); - await RestartMigration(tester); + await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); + Assert.NotNull(lnMethod.GetExternalLightningUrl()); + await RestartMigration(tester); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); - Assert.Null(lnMethod.GetExternalLightningUrl()); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); + Assert.Null(lnMethod.GetExternalLightningUrl()); - // Test if legacy lightning charge settings are converted to LightningConnectionString - store.DerivationStrategies = new JObject() + // Test if legacy lightning charge settings are converted to LightningConnectionString + store.DerivationStrategies = new JObject() { new JProperty("BTC_LightningLike", new JObject() { @@ -2570,20 +2494,20 @@ namespace BTCPayServer.Tests new JProperty("PaymentId", "someshit"), }) }.ToString(); - await tester.PayTester.StoreRepository.UpdateStore(store); - await RestartMigration(tester); + await tester.PayTester.StoreRepository.UpdateStore(store); + await RestartMigration(tester); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); - Assert.NotNull(lnMethod.GetExternalLightningUrl()); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); + Assert.NotNull(lnMethod.GetExternalLightningUrl()); - var url = lnMethod.GetExternalLightningUrl(); - Assert.Equal(LightningConnectionType.Charge, url.ConnectionType); - Assert.Equal("pass", url.Password); - Assert.Equal("usr", url.Username); + var url = lnMethod.GetExternalLightningUrl(); + Assert.Equal(LightningConnectionType.Charge, url.ConnectionType); + Assert.Equal("pass", url.Password); + Assert.Equal("usr", url.Username); - // Test if lightning connection strings get migrated to internal - store.DerivationStrategies = new JObject() + // Test if lightning connection strings get migrated to internal + store.DerivationStrategies = new JObject() { new JProperty("BTC_LightningLike", new JObject() { @@ -2591,12 +2515,11 @@ namespace BTCPayServer.Tests new JProperty("LightningConnectionString", tester.PayTester.IntegratedLightning), }) }.ToString(); - await tester.PayTester.StoreRepository.UpdateStore(store); - await RestartMigration(tester); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); - Assert.True(lnMethod.IsInternalNode); - } + await tester.PayTester.StoreRepository.UpdateStore(store); + await RestartMigration(tester); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType().First(); + Assert.True(lnMethod.IsInternalNode); } @@ -2604,72 +2527,69 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task CanDoInvoiceMigrations() { - using (var tester = CreateServerTester(newDb: true)) - { - await tester.StartAsync(); + using var tester = CreateServerTester(newDb: true); + await tester.StartAsync(); - var acc = tester.NewAccount(); - await acc.GrantAccessAsync(true); - await acc.CreateStoreAsync(); - await acc.RegisterDerivationSchemeAsync("BTC"); - var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(true); + await acc.CreateStoreAsync(); + await acc.RegisterDerivationSchemeAsync("BTC"); + var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - var blob = store.GetStoreBlob(); - var serializer = new Serializer(null); + var blob = store.GetStoreBlob(); + var serializer = new Serializer(null); - blob.AdditionalData = new Dictionary(); - blob.AdditionalData.Add("rateRules", JToken.Parse( - serializer.ToString(new List() - { + blob.AdditionalData = new Dictionary(); + blob.AdditionalData.Add("rateRules", JToken.Parse( + serializer.ToString(new List() + { new MigrationStartupTask.RateRule_Obsolete() { Multiplier = 2 } - }))); - blob.AdditionalData.Add("walletKeyPathRoots", JToken.Parse( - serializer.ToString(new Dictionary() - { + }))); + blob.AdditionalData.Add("walletKeyPathRoots", JToken.Parse( + serializer.ToString(new Dictionary() + { { new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), new KeyPath("44'/0'/0'").ToString() } - }))); + }))); - blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse( - serializer.ToString((bool?)true))); + blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse( + serializer.ToString((bool?)true))); - blob.AdditionalData.Add("onChainMinValue", JToken.Parse( - serializer.ToString(new CurrencyValue() - { - Currency = "USD", - Value = 5m - }.ToString()))); - blob.AdditionalData.Add("lightningMaxValue", JToken.Parse( - serializer.ToString(new CurrencyValue() - { - Currency = "USD", - Value = 5m - }.ToString()))); + blob.AdditionalData.Add("onChainMinValue", JToken.Parse( + serializer.ToString(new CurrencyValue() + { + Currency = "USD", + Value = 5m + }.ToString()))); + blob.AdditionalData.Add("lightningMaxValue", JToken.Parse( + serializer.ToString(new CurrencyValue() + { + Currency = "USD", + Value = 5m + }.ToString()))); - store.SetStoreBlob(blob); - await tester.PayTester.StoreRepository.UpdateStore(store); - await RestartMigration(tester); + store.SetStoreBlob(blob); + await tester.PayTester.StoreRepository.UpdateStore(store); + await RestartMigration(tester); - store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); + store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); - blob = store.GetStoreBlob(); - Assert.Empty(blob.AdditionalData); - Assert.Single(blob.PaymentMethodCriteria); - Assert.Contains(blob.PaymentMethodCriteria, - criteria => criteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) && - criteria.Above && criteria.Value.Value == 5m && criteria.Value.Currency == "USD"); - Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode); - Assert.Contains(store.GetSupportedPaymentMethods(tester.NetworkProvider), method => - method is DerivationSchemeSettings dss && - method.PaymentId == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) && - dss.AccountKeyPath == new KeyPath("44'/0'/0'")); - - } + blob = store.GetStoreBlob(); + Assert.Empty(blob.AdditionalData); + Assert.Single(blob.PaymentMethodCriteria); + Assert.Contains(blob.PaymentMethodCriteria, + criteria => criteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) && + criteria.Above && criteria.Value.Value == 5m && criteria.Value.Currency == "USD"); + Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode); + Assert.Contains(store.GetSupportedPaymentMethods(tester.NetworkProvider), method => + method is DerivationSchemeSettings dss && + method.PaymentId == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) && + dss.AccountKeyPath == new KeyPath("44'/0'/0'")); } private static async Task RestartMigration(ServerTester tester) @@ -2685,159 +2605,152 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task EmailSenderTests() { - using (var tester = CreateServerTester(newDb: true)) + using var tester = CreateServerTester(newDb: true); + await tester.StartAsync(); + + var acc = tester.NewAccount(); + acc.GrantAccess(true); + + var settings = tester.PayTester.GetService(); + var emailSenderFactory = tester.PayTester.GetService(); + + Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()); + Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); + + + await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = false }); + await settings.UpdateSetting(new EmailSettings() { - await tester.StartAsync(); + From = "admin@admin.com", + Login = "admin@admin.com", + Password = "admin@admin.com", + Port = 1234, + Server = "admin.com", + }); + Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login); + Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login); - var acc = tester.NewAccount(); - acc.GrantAccess(true); + await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = true }); + Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login); + Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); - var settings = tester.PayTester.GetService(); - var emailSenderFactory = tester.PayTester.GetService(); + Assert.IsType(await acc.GetController().Emails(acc.StoreId, new EmailsViewModel(new EmailSettings() + { + From = "store@store.com", + Login = "store@store.com", + Password = "store@store.com", + Port = 1234, + Server = "store.com" + }), "")); - Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()); - Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); - - - await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = false }); - await settings.UpdateSetting(new EmailSettings() - { - From = "admin@admin.com", - Login = "admin@admin.com", - Password = "admin@admin.com", - Port = 1234, - Server = "admin.com", - }); - Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login); - Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login); - - await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = true }); - Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login); - Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); - - Assert.IsType(await acc.GetController().Emails(acc.StoreId, new EmailsViewModel(new EmailSettings() - { - From = "store@store.com", - Login = "store@store.com", - Password = "store@store.com", - Port = 1234, - Server = "store.com" - }), "")); - - Assert.Equal("store@store.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login); - - } + Assert.Equal("store@store.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login); } [Fact(Timeout = TestUtils.TestTimeout)] [Trait("Integration", "Integration")] public async Task CanConfigureStorage() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - //Once we select a provider, redirect to its view - var localResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.FileSystem - })); - Assert.Equal(nameof(UIServerController.StorageProvider), localResult.ActionName); - Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]); + //Once we select a provider, redirect to its view + var localResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.FileSystem + })); + Assert.Equal(nameof(UIServerController.StorageProvider), localResult.ActionName); + Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]); - var AmazonS3result = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.AmazonS3 - })); - Assert.Equal(nameof(UIServerController.StorageProvider), AmazonS3result.ActionName); - Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]); + var AmazonS3result = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.AmazonS3 + })); + Assert.Equal(nameof(UIServerController.StorageProvider), AmazonS3result.ActionName); + Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]); - var GoogleResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.GoogleCloudStorage - })); - Assert.Equal(nameof(UIServerController.StorageProvider), GoogleResult.ActionName); - Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]); + var GoogleResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.GoogleCloudStorage + })); + Assert.Equal(nameof(UIServerController.StorageProvider), GoogleResult.ActionName); + Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]); - var AzureResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.AzureBlobStorage - })); - Assert.Equal(nameof(UIServerController.StorageProvider), AzureResult.ActionName); - Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]); + var AzureResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.AzureBlobStorage + })); + Assert.Equal(nameof(UIServerController.StorageProvider), AzureResult.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]); - //Cool, we get redirected to the config pages - //Let's configure this stuff + //Cool, we get redirected to the config pages + //Let's configure this stuff - //Let's try and cheat and go to an invalid storage provider config - Assert.Equal(nameof(Storage), (Assert - .IsType(await controller.StorageProvider("I am not a real provider")) - .ActionName)); + //Let's try and cheat and go to an invalid storage provider config + Assert.Equal(nameof(Storage), (Assert + .IsType(await controller.StorageProvider("I am not a real provider")) + .ActionName)); - //ok no more messing around, let's configure this shit. - var fileSystemStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) - .Model); + //ok no more messing around, let's configure this shit. + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); - //local file system does not need config, easy days! - Assert.IsType( - await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + //local file system does not need config, easy days! + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); - //ok cool, let's see if this got set right - var shouldBeRedirectingToLocalStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.FileSystem, - shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + //ok cool, let's see if this got set right + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); - //if we tell the settings page to force, it should allow us to select a new provider - Assert.IsType(Assert.IsType(await controller.Storage(true)).Model); + //if we tell the settings page to force, it should allow us to select a new provider + Assert.IsType(Assert.IsType(await controller.Storage(true)).Model); - //awesome, now let's see if the files result says we're all set up - var viewFilesViewModel = - Assert.IsType(Assert.IsType(await controller.Files()).Model); - Assert.True(viewFilesViewModel.StorageConfigured); - Assert.Empty(viewFilesViewModel.Files); - } + //awesome, now let's see if the files result says we're all set up + var viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files()).Model); + Assert.True(viewFilesViewModel.StorageConfigured); + Assert.Empty(viewFilesViewModel.Files); } [Fact] [Trait("Integration", "Integration")] public async void CanUseLocalProviderFiles() { - using (var tester = CreateServerTester()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + using var tester = CreateServerTester(); + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - var fileSystemStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) - .Model); - Assert.IsType( - await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); - var shouldBeRedirectingToLocalStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.FileSystem, - shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); - await CanUploadRemoveFiles(controller); - } + await CanUploadRemoveFiles(controller); } internal static async Task CanUploadRemoveFiles(UIServerController controller) diff --git a/BTCPayServer.Tests/Utils.cs b/BTCPayServer.Tests/Utils.cs index 3e7e95dd4..e5cdf6e0e 100644 --- a/BTCPayServer.Tests/Utils.cs +++ b/BTCPayServer.Tests/Utils.cs @@ -16,23 +16,21 @@ namespace BTCPayServer.Tests { lock (_portLock) { - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + while (true) { - while (true) + try { - try + var port = _nextPort++; + socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + return port; + } + catch (SocketException) + { + // Retry unless exhausted + if (_nextPort == 65536) { - var port = _nextPort++; - socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); - return port; - } - catch (SocketException) - { - // Retry unless exhausted - if (_nextPort == 65536) - { - throw; - } + throw; } } } diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index 2285011de..8bf0866d4 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -871,11 +871,9 @@ namespace BTCPayServer.Controllers { try { - using (var sshClient = await _Options.SSHSettings.ConnectAsync()) - { - var result = await sshClient.RunBash("cat ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10)); - vm.SSHKeyFileContent = result.Output; - } + using var sshClient = await _Options.SSHSettings.ConnectAsync(); + var result = await sshClient.RunBash("cat ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10)); + vm.SSHKeyFileContent = result.Output; } catch { } } @@ -1106,17 +1104,13 @@ namespace BTCPayServer.Controllers return NotFound(); try { - using (var fileStream = new FileStream( + using var fileStream = new FileStream( fi.FullName, FileMode.Open, FileAccess.Read, - FileShare.ReadWrite)) - { - using (var reader = new StreamReader(fileStream)) - { - vm.Log = await reader.ReadToEndAsync(); - } - } + FileShare.ReadWrite); + using var reader = new StreamReader(fileStream); + vm.Log = await reader.ReadToEndAsync(); } catch { diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index fc9891659..50f8a0417 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -810,10 +810,8 @@ namespace BTCPayServer.Controllers private async Task ReadAllText(IFormFile file) { - using (var stream = new StreamReader(file.OpenReadStream())) - { - return await stream.ReadToEndAsync(); - } + using var stream = new StreamReader(file.OpenReadStream()); + return await stream.ReadToEndAsync(); } private string WalletWarning(bool isHotWallet, string info) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 0aebb64b8..ab911f557 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -125,11 +125,9 @@ namespace BTCPayServer { if (webSocket.State == WebSocketState.Open) { - using (CancellationTokenSource cts = new CancellationTokenSource()) - { - cts.CancelAfter(5000); - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token); - } + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(5000); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token); } } catch { } diff --git a/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs b/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs index f8469ab5c..2920f0ba3 100644 --- a/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs +++ b/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs @@ -40,12 +40,10 @@ retry: Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ..."); try { - using (var connection = await _options.SSHSettings.ConnectAsync(_cancellationTokenSource.Token)) - { - await connection.DisconnectAsync(_cancellationTokenSource.Token); - Logs.Configuration.LogInformation($"SSH connection succeeded"); - canUseSSH = true; - } + using var connection = await _options.SSHSettings.ConnectAsync(_cancellationTokenSource.Token); + await connection.DisconnectAsync(_cancellationTokenSource.Token); + Logs.Configuration.LogInformation($"SSH connection succeeded"); + canUseSSH = true; } catch (Renci.SshNet.Common.SshAuthenticationException ex) { diff --git a/BTCPayServer/HostedServices/DynamicDnsHostedService.cs b/BTCPayServer/HostedServices/DynamicDnsHostedService.cs index 1ca4869b3..da78d91eb 100644 --- a/BTCPayServer/HostedServices/DynamicDnsHostedService.cs +++ b/BTCPayServer/HostedServices/DynamicDnsHostedService.cs @@ -59,13 +59,11 @@ namespace BTCPayServer.HostedServices } } } - using (var delayCancel = CancellationTokenSource.CreateLinkedTokenSource(Cancellation)) - { - var delay = Task.Delay(Period, delayCancel.Token); - var changed = SettingsRepository.WaitSettingsChanged(Cancellation); - await Task.WhenAny(delay, changed); - delayCancel.Cancel(); - } + using var delayCancel = CancellationTokenSource.CreateLinkedTokenSource(Cancellation); + var delay = Task.Delay(Period, delayCancel.Token); + var changed = SettingsRepository.WaitSettingsChanged(Cancellation); + await Task.WhenAny(delay, changed); + delayCancel.Cancel(); } } } diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index 116140618..c2f380052 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -463,164 +463,154 @@ retry: private async Task ConvertConvertWalletKeyPathRoots() { bool save = false; - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) { - foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) - { #pragma warning disable CS0618 // Type or member is obsolete - var blob = store.GetStoreBlob(); + var blob = store.GetStoreBlob(); - if (blob.AdditionalData.TryGetValue("walletKeyPathRoots", out var walletKeyPathRootsJToken)) + if (blob.AdditionalData.TryGetValue("walletKeyPathRoots", out var walletKeyPathRootsJToken)) + { + var walletKeyPathRoots = walletKeyPathRootsJToken.ToObject>(); + + if (!(walletKeyPathRoots?.Any() is true)) + continue; + foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider) + .OfType()) { - var walletKeyPathRoots = walletKeyPathRootsJToken.ToObject>(); - - if (!(walletKeyPathRoots?.Any() is true)) - continue; - foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider) - .OfType()) + if (walletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), + out var root)) { - if (walletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), - out var root)) - { - scheme.AccountKeyPath = new NBitcoin.KeyPath(root); - store.SetSupportedPaymentMethod(scheme); - save = true; - } + scheme.AccountKeyPath = new NBitcoin.KeyPath(root); + store.SetSupportedPaymentMethod(scheme); + save = true; } - - blob.AdditionalData.Remove("walletKeyPathRoots"); - store.SetStoreBlob(blob); } -#pragma warning restore CS0618 // Type or member is obsolete + + blob.AdditionalData.Remove("walletKeyPathRoots"); + store.SetStoreBlob(blob); } - if (save) - await ctx.SaveChangesAsync(); +#pragma warning restore CS0618 // Type or member is obsolete } + if (save) + await ctx.SaveChangesAsync(); } private async Task ConvertCrowdfundOldSettings() { - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var app in await ctx.Apps.Where(a => a.AppType == "Crowdfund").ToArrayAsync()) { - foreach (var app in await ctx.Apps.Where(a => a.AppType == "Crowdfund").ToArrayAsync()) - { - var settings = app.GetSettings(); + var settings = app.GetSettings(); #pragma warning disable CS0618 // Type or member is obsolete - if (settings.UseAllStoreInvoices) + if (settings.UseAllStoreInvoices) #pragma warning restore CS0618 // Type or member is obsolete - { - app.TagAllInvoices = true; - } + { + app.TagAllInvoices = true; } - await ctx.SaveChangesAsync(); } + await ctx.SaveChangesAsync(); } private async Task MigratePaymentMethodCriteria() { - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) { - foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) + var blob = store.GetStoreBlob(); + + CurrencyValue onChainMinValue = null; + CurrencyValue lightningMaxValue = null; + if (blob.AdditionalData.TryGetValue("onChainMinValue", out var onChainMinValueJToken)) { - var blob = store.GetStoreBlob(); - - CurrencyValue onChainMinValue = null; - CurrencyValue lightningMaxValue = null; - if (blob.AdditionalData.TryGetValue("onChainMinValue", out var onChainMinValueJToken)) - { - CurrencyValue.TryParse(onChainMinValueJToken.Value(), out onChainMinValue); - blob.AdditionalData.Remove("onChainMinValue"); - } - if (blob.AdditionalData.TryGetValue("lightningMaxValue", out var lightningMaxValueJToken)) - { - CurrencyValue.TryParse(lightningMaxValueJToken.Value(), out lightningMaxValue); - blob.AdditionalData.Remove("lightningMaxValue"); - } - blob.PaymentMethodCriteria = store.GetEnabledPaymentIds(_NetworkProvider).Select(paymentMethodId => - { - var matchedFromBlob = - blob.PaymentMethodCriteria?.SingleOrDefault(criteria => criteria.PaymentMethod == paymentMethodId && criteria.Value != null); - return matchedFromBlob switch - { - null when paymentMethodId.PaymentType == LightningPaymentType.Instance && - lightningMaxValue != null => new PaymentMethodCriteria() - { - Above = false, - PaymentMethod = paymentMethodId, - Value = lightningMaxValue - }, - null when paymentMethodId.PaymentType == BitcoinPaymentType.Instance && - onChainMinValue != null => new PaymentMethodCriteria() - { - Above = true, - PaymentMethod = paymentMethodId, - Value = onChainMinValue - }, - _ => new PaymentMethodCriteria() - { - PaymentMethod = paymentMethodId, - Above = matchedFromBlob?.Above ?? true, - Value = matchedFromBlob?.Value - } - }; - }).ToList(); - - store.SetStoreBlob(blob); + CurrencyValue.TryParse(onChainMinValueJToken.Value(), out onChainMinValue); + blob.AdditionalData.Remove("onChainMinValue"); } + if (blob.AdditionalData.TryGetValue("lightningMaxValue", out var lightningMaxValueJToken)) + { + CurrencyValue.TryParse(lightningMaxValueJToken.Value(), out lightningMaxValue); + blob.AdditionalData.Remove("lightningMaxValue"); + } + blob.PaymentMethodCriteria = store.GetEnabledPaymentIds(_NetworkProvider).Select(paymentMethodId => + { + var matchedFromBlob = + blob.PaymentMethodCriteria?.SingleOrDefault(criteria => criteria.PaymentMethod == paymentMethodId && criteria.Value != null); + return matchedFromBlob switch + { + null when paymentMethodId.PaymentType == LightningPaymentType.Instance && + lightningMaxValue != null => new PaymentMethodCriteria() + { + Above = false, + PaymentMethod = paymentMethodId, + Value = lightningMaxValue + }, + null when paymentMethodId.PaymentType == BitcoinPaymentType.Instance && + onChainMinValue != null => new PaymentMethodCriteria() + { + Above = true, + PaymentMethod = paymentMethodId, + Value = onChainMinValue + }, + _ => new PaymentMethodCriteria() + { + PaymentMethod = paymentMethodId, + Above = matchedFromBlob?.Above ?? true, + Value = matchedFromBlob?.Value + } + }; + }).ToList(); - await ctx.SaveChangesAsync(); + store.SetStoreBlob(blob); } + + await ctx.SaveChangesAsync(); } private async Task ConvertNetworkFeeProperty() { - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) { - foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) + var blob = store.GetStoreBlob(); + if (blob.AdditionalData.TryGetValue("networkFeeDisabled", out var networkFeeModeJToken)) { - var blob = store.GetStoreBlob(); - if (blob.AdditionalData.TryGetValue("networkFeeDisabled", out var networkFeeModeJToken)) + var networkFeeMode = networkFeeModeJToken.ToObject(); + if (networkFeeMode != null) { - var networkFeeMode = networkFeeModeJToken.ToObject(); - if (networkFeeMode != null) - { - blob.NetworkFeeMode = networkFeeMode.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always; - } - - blob.AdditionalData.Remove("networkFeeDisabled"); - store.SetStoreBlob(blob); + blob.NetworkFeeMode = networkFeeMode.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always; } + + blob.AdditionalData.Remove("networkFeeDisabled"); + store.SetStoreBlob(blob); } - await ctx.SaveChangesAsync(); } + await ctx.SaveChangesAsync(); } private async Task ConvertMultiplierToSpread() { - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) { - foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) + var blob = store.GetStoreBlob(); + decimal multiplier = 1.0m; + if (blob.AdditionalData.TryGetValue("rateRules", out var rateRulesJToken)) { - var blob = store.GetStoreBlob(); - decimal multiplier = 1.0m; - if (blob.AdditionalData.TryGetValue("rateRules", out var rateRulesJToken)) + var rateRules = new Serializer(null).ToObject>(rateRulesJToken.ToString()); + if (rateRules != null && rateRules.Count != 0) { - var rateRules = new Serializer(null).ToObject>(rateRulesJToken.ToString()); - if (rateRules != null && rateRules.Count != 0) + foreach (var rule in rateRules) { - foreach (var rule in rateRules) - { - multiplier = rule.Apply(null, multiplier); - } + multiplier = rule.Apply(null, multiplier); } - - blob.AdditionalData.Remove("rateRules"); - blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m))); - store.SetStoreBlob(blob); } + + blob.AdditionalData.Remove("rateRules"); + blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m))); + store.SetStoreBlob(blob); } - await ctx.SaveChangesAsync(); } + await ctx.SaveChangesAsync(); } public class RateRule_Obsolete @@ -646,22 +636,20 @@ retry: private async Task DeprecatedLightningConnectionStringCheck() { - using (var ctx = _DBContextFactory.CreateContext()) + using var ctx = _DBContextFactory.CreateContext(); + foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) { - foreach (var store in await ctx.Stores.AsQueryable().ToArrayAsync()) + foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType()) { - foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType()) + var lightning = method.GetExternalLightningUrl(); + if (lightning?.IsLegacy is true) { - var lightning = method.GetExternalLightningUrl(); - if (lightning?.IsLegacy is true) - { - method.SetLightningUrl(lightning); - store.SetSupportedPaymentMethod(method); - } + method.SetLightningUrl(lightning); + store.SetSupportedPaymentMethod(method); } } - await ctx.SaveChangesAsync(); } + await ctx.SaveChangesAsync(); } } } diff --git a/BTCPayServer/Hosting/ResourceBundleProvider.cs b/BTCPayServer/Hosting/ResourceBundleProvider.cs index bb92a9801..430b743f2 100644 --- a/BTCPayServer/Hosting/ResourceBundleProvider.cs +++ b/BTCPayServer/Hosting/ResourceBundleProvider.cs @@ -20,17 +20,15 @@ namespace BTCPayServer.Hosting { _BundlesByName = new Lazy>(() => { - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json")) - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - var content = reader.ReadToEnd(); - return JArray.Parse(content).OfType() - .Select(jobj => new Bundle() - { - Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value(), - OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value()) - }).ToDictionary(o => o.Name, o => o); - } + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"); + using var reader = new StreamReader(stream, Encoding.UTF8); + var content = reader.ReadToEnd(); + return JArray.Parse(content).OfType() + .Select(jobj => new Bundle() + { + Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value(), + OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value()) + }).ToDictionary(o => o.Name, o => o); }, true); } else diff --git a/BTCPayServer/Models/GetTokensResponse.cs b/BTCPayServer/Models/GetTokensResponse.cs index a0c0f5921..72dc6ddc8 100644 --- a/BTCPayServer/Models/GetTokensResponse.cs +++ b/BTCPayServer/Models/GetTokensResponse.cs @@ -43,10 +43,8 @@ namespace BTCPayServer.Models } context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json")); var str = JsonConvert.SerializeObject(jobj); - await using (var writer = new StreamWriter(context.HttpContext.Response.Body, new UTF8Encoding(false), 1024 * 10, true)) - { - await writer.WriteAsync(str); - } + await using var writer = new StreamWriter(context.HttpContext.Response.Body, new UTF8Encoding(false), 1024 * 10, true); + await writer.WriteAsync(str); } } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 32b371547..2d18b98fc 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -130,42 +130,40 @@ namespace BTCPayServer.Payments.Lightning try { - using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) + using var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT); + var client = CreateLightningClient(supportedPaymentMethod, network); + LightningNodeInformation info; + try { - var client = CreateLightningClient(supportedPaymentMethod, network); - LightningNodeInformation info; - try - { - info = await client.GetInfo(cts.Token); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" + - (!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : "")); - } - - var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion) - ? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray() - : info.NodeInfoList.Select(i => i).ToArray(); - - // Maybe the user does not have an easily accessible ln node. Node info should be optional. The UI also supports this. - // if (!nodeInfo.Any()) - // { - // throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); - // } - - var blocksGap = summary.Status.ChainHeight - info.BlockHeight; - if (blocksGap > 10) - { - throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)"); - } - - return nodeInfo; + info = await client.GetInfo(cts.Token); } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" + + (!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : "")); + } + + var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion) + ? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray() + : info.NodeInfoList.Select(i => i).ToArray(); + + // Maybe the user does not have an easily accessible ln node. Node info should be optional. The UI also supports this. + // if (!nodeInfo.Any()) + // { + // throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); + // } + + var blocksGap = summary.Status.ChainHeight - info.BlockHeight; + if (blocksGap > 10) + { + throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)"); + } + + return nodeInfo; } catch (Exception e) when (!throws) { @@ -197,9 +195,7 @@ namespace BTCPayServer.Payments.Lightning if (!Utils.TryParseEndpoint(nodeInfo.Host, nodeInfo.Port, out var endpoint)) throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}"); - using (var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation)) - { - } + using var tcp = await _socketFactory.ConnectAsync(endpoint, cancellation); } catch (Exception ex) { diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 0a49d5e9c..519aef55c 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -458,38 +458,36 @@ namespace BTCPayServer.Payments.Lightning try { var lightningClient = _lightningClientFactory.Create(ConnectionString, _network); - using (var session = await lightningClient.Listen(cancellation)) + using var session = await lightningClient.Listen(cancellation); + // Just in case the payment arrived after our last poll but before we listened. + await PollAllListenedInvoices(cancellation); + if (_ErrorAlreadyLogged) { - // Just in case the payment arrived after our last poll but before we listened. - await PollAllListenedInvoices(cancellation); - if (_ErrorAlreadyLogged) + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Could reconnect successfully to {ConnectionString.BaseUri}"); + } + _ErrorAlreadyLogged = false; + while (!_ListenedInvoices.IsEmpty) + { + var notification = await session.WaitInvoice(cancellation); + if (!_ListenedInvoices.TryGetValue(notification.Id, out var listenedInvoice)) + continue; + if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId && + (notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11 || + BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash == + listenedInvoice.PaymentMethodDetails.GetPaymentHash(_network.NBitcoinNetwork))) { - Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Could reconnect successfully to {ConnectionString.BaseUri}"); - } - _ErrorAlreadyLogged = false; - while (!_ListenedInvoices.IsEmpty) - { - var notification = await session.WaitInvoice(cancellation); - if (!_ListenedInvoices.TryGetValue(notification.Id, out var listenedInvoice)) - continue; - if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId && - (notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11 || - BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash == - listenedInvoice.PaymentMethodDetails.GetPaymentHash(_network.NBitcoinNetwork))) + if (notification.Status == LightningInvoiceStatus.Paid && + notification.PaidAt.HasValue && notification.Amount != null) { - if (notification.Status == LightningInvoiceStatus.Paid && - notification.PaidAt.HasValue && notification.Amount != null) + if (await AddPayment(notification, listenedInvoice.InvoiceId, listenedInvoice.PaymentMethod.GetId().PaymentType)) { - if (await AddPayment(notification, listenedInvoice.InvoiceId, listenedInvoice.PaymentMethod.GetId().PaymentType)) - { - Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); - } - _ListenedInvoices.TryRemove(notification.Id, out var _); - } - else if (notification.Status == LightningInvoiceStatus.Expired) - { - _ListenedInvoices.TryRemove(notification.Id, out var _); + Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); } + _ListenedInvoices.TryRemove(notification.Id, out var _); + } + else if (notification.Status == LightningInvoiceStatus.Expired) + { + _ListenedInvoices.TryRemove(notification.Id, out var _); } } } diff --git a/BTCPayServer/Security/Bitpay/TokenRepository.cs b/BTCPayServer/Security/Bitpay/TokenRepository.cs index 0571ad882..aab971533 100644 --- a/BTCPayServer/Security/Bitpay/TokenRepository.cs +++ b/BTCPayServer/Security/Bitpay/TokenRepository.cs @@ -29,21 +29,17 @@ namespace BTCPayServer.Security.Bitpay { if (sin == null) return Array.Empty(); - using (var ctx = _Factory.CreateContext()) - { - return (await ctx.PairedSINData.Where(p => p.SIN == sin) - .ToArrayAsync()) - .Select(p => CreateTokenEntity(p)) - .ToArray(); - } + using var ctx = _Factory.CreateContext(); + return (await ctx.PairedSINData.Where(p => p.SIN == sin) + .ToArrayAsync()) + .Select(p => CreateTokenEntity(p)) + .ToArray(); } public async Task GetStoreIdFromAPIKey(string apiKey) { - using (var ctx = _Factory.CreateContext()) - { - return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync(); - } + using var ctx = _Factory.CreateContext(); + return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync(); } public async Task GenerateLegacyAPIKey(string storeId) @@ -57,16 +53,14 @@ namespace BTCPayServer.Security.Bitpay generated[i] = chars[(int)(RandomUtils.GetUInt32() % generated.Length)]; } - using (var ctx = _Factory.CreateContext()) + using var ctx = _Factory.CreateContext(); + var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync(); + if (existing.Any()) { - var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync(); - if (existing.Any()) - { - ctx.ApiKeys.RemoveRange(existing); - } - ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId }); - await ctx.SaveChangesAsync().ConfigureAwait(false); + ctx.ApiKeys.RemoveRange(existing); } + ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId }); + await ctx.SaveChangesAsync().ConfigureAwait(false); } public async Task RevokeLegacyAPIKeys(string storeId) @@ -77,19 +71,15 @@ namespace BTCPayServer.Security.Bitpay return; } - using (var ctx = _Factory.CreateContext()) - { - ctx.ApiKeys.RemoveRange(keys.Select(s => new APIKeyData() { Id = s })); - await ctx.SaveChangesAsync(); - } + using var ctx = _Factory.CreateContext(); + ctx.ApiKeys.RemoveRange(keys.Select(s => new APIKeyData() { Id = s })); + await ctx.SaveChangesAsync(); } public async Task GetLegacyAPIKeys(string storeId) { - using (var ctx = _Factory.CreateContext()) - { - return await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).Select(c => c.Id).ToArrayAsync(); - } + using var ctx = _Factory.CreateContext(); + return await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).Select(c => c.Id).ToArrayAsync(); } private BitTokenEntity CreateTokenEntity(PairedSINData data) @@ -131,41 +121,35 @@ namespace BTCPayServer.Security.Bitpay public async Task UpdatePairingCode(PairingCodeEntity pairingCodeEntity) { - using (var ctx = _Factory.CreateContext()) - { - var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id); - pairingCode.Label = pairingCodeEntity.Label; - await ctx.SaveChangesAsync(); - return CreatePairingCodeEntity(pairingCode); - } + using var ctx = _Factory.CreateContext(); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id); + pairingCode.Label = pairingCodeEntity.Label; + await ctx.SaveChangesAsync(); + return CreatePairingCodeEntity(pairingCode); } public async Task PairWithStoreAsync(string pairingCodeId, string storeId) { - using (var ctx = _Factory.CreateContext()) - { - var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); - if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) - return PairingResult.Expired; - pairingCode.StoreDataId = storeId; - var result = await ActivateIfComplete(ctx, pairingCode); - await ctx.SaveChangesAsync(); - return result; - } + using var ctx = _Factory.CreateContext(); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); + if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) + return PairingResult.Expired; + pairingCode.StoreDataId = storeId; + var result = await ActivateIfComplete(ctx, pairingCode); + await ctx.SaveChangesAsync(); + return result; } public async Task PairWithSINAsync(string pairingCodeId, string sin) { - using (var ctx = _Factory.CreateContext()) - { - var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); - if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) - return PairingResult.Expired; - pairingCode.SIN = sin; - var result = await ActivateIfComplete(ctx, pairingCode); - await ctx.SaveChangesAsync(); - return result; - } + using var ctx = _Factory.CreateContext(); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); + if (pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) + return PairingResult.Expired; + pairingCode.SIN = sin; + var result = await ActivateIfComplete(ctx, pairingCode); + await ctx.SaveChangesAsync(); + return result; } @@ -195,20 +179,16 @@ namespace BTCPayServer.Security.Bitpay public async Task GetTokensByStoreIdAsync(string storeId) { - using (var ctx = _Factory.CreateContext()) - { - return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync()) - .Select(c => CreateTokenEntity(c)) - .ToArray(); - } + using var ctx = _Factory.CreateContext(); + return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync()) + .Select(c => CreateTokenEntity(c)) + .ToArray(); } public async Task GetPairingAsync(string pairingCode) { - using (var ctx = _Factory.CreateContext()) - { - return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode)); - } + using var ctx = _Factory.CreateContext(); + return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode)); } private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data) @@ -229,26 +209,22 @@ namespace BTCPayServer.Security.Bitpay public async Task DeleteToken(string tokenId) { - using (var ctx = _Factory.CreateContext()) - { - var token = await ctx.PairedSINData.FindAsync(tokenId); - if (token == null) - return false; - ctx.PairedSINData.Remove(token); - await ctx.SaveChangesAsync(); - return true; - } + using var ctx = _Factory.CreateContext(); + var token = await ctx.PairedSINData.FindAsync(tokenId); + if (token == null) + return false; + ctx.PairedSINData.Remove(token); + await ctx.SaveChangesAsync(); + return true; } public async Task GetToken(string tokenId) { - using (var ctx = _Factory.CreateContext()) - { - var token = await ctx.PairedSINData.FindAsync(tokenId); - if (token == null) - return null; - return CreateTokenEntity(token); - } + using var ctx = _Factory.CreateContext(); + var token = await ctx.PairedSINData.FindAsync(tokenId); + if (token == null) + return null; + return CreateTokenEntity(token); } } diff --git a/BTCPayServer/Security/GreenField/APIKeyRepository.cs b/BTCPayServer/Security/GreenField/APIKeyRepository.cs index c278c4ae9..8025325af 100644 --- a/BTCPayServer/Security/GreenField/APIKeyRepository.cs +++ b/BTCPayServer/Security/GreenField/APIKeyRepository.cs @@ -28,19 +28,17 @@ namespace BTCPayServer.Security.Greenfield public async Task> GetKeys(APIKeyQuery query) { - using (var context = _applicationDbContextFactory.CreateContext()) + using var context = _applicationDbContextFactory.CreateContext(); + var queryable = context.ApiKeys.AsQueryable(); + if (query != null) { - var queryable = context.ApiKeys.AsQueryable(); - if (query != null) + if (query.UserId != null && query.UserId.Any()) { - if (query.UserId != null && query.UserId.Any()) - { - queryable = queryable.Where(data => query.UserId.Contains(data.UserId)); - } + queryable = queryable.Where(data => query.UserId.Contains(data.UserId)); } - - return await queryable.ToListAsync(); } + + return await queryable.ToListAsync(); } public async Task CreateKey(APIKeyData key) @@ -50,11 +48,9 @@ namespace BTCPayServer.Security.Greenfield throw new InvalidOperationException("cannot save a bitpay legacy api key with this repository"); } - using (var context = _applicationDbContextFactory.CreateContext()) - { - await context.ApiKeys.AddAsync(key); - await context.SaveChangesAsync(); - } + using var context = _applicationDbContextFactory.CreateContext(); + await context.ApiKeys.AddAsync(key); + await context.SaveChangesAsync(); } public async Task Remove(string id, string getUserId) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index f9d5ca0a5..f92b694a8 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -209,53 +209,47 @@ namespace BTCPayServer.Services.Apps public async Task GetOwnedStores(string userId) { - using (var ctx = _ContextFactory.CreateContext()) - { - return await ctx.UserStore - .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) - .Select(u => u.StoreData) - .ToArrayAsync(); - } + using var ctx = _ContextFactory.CreateContext(); + return await ctx.UserStore + .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) + .Select(u => u.StoreData) + .ToArrayAsync(); } public async Task DeleteApp(AppData appData) { - using (var ctx = _ContextFactory.CreateContext()) - { - ctx.Apps.Add(appData); - ctx.Entry(appData).State = EntityState.Deleted; - return await ctx.SaveChangesAsync() == 1; - } + using var ctx = _ContextFactory.CreateContext(); + ctx.Apps.Add(appData); + ctx.Entry(appData).State = EntityState.Deleted; + return await ctx.SaveChangesAsync() == 1; } public async Task GetAllApps(string userId, bool allowNoUser = false, string storeId = null) { - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + var listApps = await ctx.UserStore + .Where(us => + (allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) && + (storeId == null || us.StoreDataId == storeId)) + .Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, + (us, app) => + new ListAppsViewModel.ListAppViewModel() + { + IsOwner = us.Role == StoreRoles.Owner, + StoreId = us.StoreDataId, + StoreName = us.StoreData.StoreName, + AppName = app.Name, + AppType = app.AppType, + Id = app.Id + }) + .ToArrayAsync(); + + foreach (ListAppsViewModel.ListAppViewModel app in listApps) { - var listApps = await ctx.UserStore - .Where(us => - (allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) && - (storeId == null || us.StoreDataId == storeId)) - .Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, - (us, app) => - new ListAppsViewModel.ListAppViewModel() - { - IsOwner = us.Role == StoreRoles.Owner, - StoreId = us.StoreDataId, - StoreName = us.StoreData.StoreName, - AppName = app.Name, - AppType = app.AppType, - Id = app.Id - }) - .ToArrayAsync(); - - foreach (ListAppsViewModel.ListAppViewModel app in listApps) - { - app.ViewStyle = await GetAppViewStyleAsync(app.Id, app.AppType); - } - - return listApps; + app.ViewStyle = await GetAppViewStyleAsync(app.Id, app.AppType); } + + return listApps; } public async Task GetAppViewStyleAsync(string appId, string appType) @@ -284,33 +278,29 @@ namespace BTCPayServer.Services.Apps public async Task> GetApps(string[] appIds, bool includeStore = false) { - using (var ctx = _ContextFactory.CreateContext()) - { - var query = ctx.Apps - .Where(us => appIds.Contains(us.Id)); + using var ctx = _ContextFactory.CreateContext(); + var query = ctx.Apps + .Where(us => appIds.Contains(us.Id)); - if (includeStore) - { - query = query.Include(data => data.StoreData); - } - return await query.ToListAsync(); + if (includeStore) + { + query = query.Include(data => data.StoreData); } + return await query.ToListAsync(); } public async Task GetApp(string appId, AppType? appType, bool includeStore = false) { - using (var ctx = _ContextFactory.CreateContext()) - { - var query = ctx.Apps - .Where(us => us.Id == appId && - (appType == null || us.AppType == appType.ToString())); + using var ctx = _ContextFactory.CreateContext(); + var query = ctx.Apps + .Where(us => us.Id == appId && + (appType == null || us.AppType == appType.ToString())); - if (includeStore) - { - query = query.Include(data => data.StoreData); - } - return await query.FirstOrDefaultAsync(); + if (includeStore) + { + query = query.Include(data => data.StoreData); } + return await query.FirstOrDefaultAsync(); } public Task GetStore(AppData app) @@ -525,39 +515,35 @@ namespace BTCPayServer.Services.Apps { if (userId == null || appId == null) return null; - using (var ctx = _ContextFactory.CreateContext()) - { - var app = await ctx.UserStore - .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) - .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) - .FirstOrDefaultAsync(); - if (app == null) - return null; - if (type != null && type.Value.ToString() != app.AppType) - return null; - return app; - } + using var ctx = _ContextFactory.CreateContext(); + var app = await ctx.UserStore + .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) + .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) + .FirstOrDefaultAsync(); + if (app == null) + return null; + if (type != null && type.Value.ToString() != app.AppType) + return null; + return app; } public async Task UpdateOrCreateApp(AppData app) { - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + if (string.IsNullOrEmpty(app.Id)) { - if (string.IsNullOrEmpty(app.Id)) - { - app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)); - app.Created = DateTimeOffset.UtcNow; - await ctx.Apps.AddAsync(app); - } - else - { - ctx.Apps.Update(app); - ctx.Entry(app).Property(data => data.Created).IsModified = false; - ctx.Entry(app).Property(data => data.Id).IsModified = false; - ctx.Entry(app).Property(data => data.AppType).IsModified = false; - } - await ctx.SaveChangesAsync(); + app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)); + app.Created = DateTimeOffset.UtcNow; + await ctx.Apps.AddAsync(app); } + else + { + ctx.Apps.Update(app); + ctx.Entry(app).Property(data => data.Created).IsModified = false; + ctx.Entry(app).Property(data => data.Id).IsModified = false; + ctx.Entry(app).Property(data => data.AppType).IsModified = false; + } + await ctx.SaveChangesAsync(); } private static bool TryParseJson(string json, out JObject result) diff --git a/BTCPayServer/Services/Cheater.cs b/BTCPayServer/Services/Cheater.cs index e64967e44..6fe76cc75 100644 --- a/BTCPayServer/Services/Cheater.cs +++ b/BTCPayServer/Services/Cheater.cs @@ -28,14 +28,12 @@ namespace BTCPayServer.Services public async Task UpdateInvoiceExpiry(string invoiceId, DateTimeOffset dateTimeOffset) { - using (var ctx = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - // TODO change the expiry time. But how? - await ctx.SaveChangesAsync().ConfigureAwait(false); - } + using var ctx = _applicationDbContextFactory.CreateContext(); + var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null) + return; + // TODO change the expiry time. But how? + await ctx.SaveChangesAsync().ConfigureAwait(false); } Task IHostedService.StartAsync(CancellationToken cancellationToken) diff --git a/BTCPayServer/Services/DelayedTransactionBroadcaster.cs b/BTCPayServer/Services/DelayedTransactionBroadcaster.cs index 19628bdcc..be72f9c46 100644 --- a/BTCPayServer/Services/DelayedTransactionBroadcaster.cs +++ b/BTCPayServer/Services/DelayedTransactionBroadcaster.cs @@ -44,21 +44,19 @@ namespace BTCPayServer.Services { ArgumentNullException.ThrowIfNull(transaction); ArgumentNullException.ThrowIfNull(network); - using (var db = _dbContextFactory.CreateContext()) + using var db = _dbContextFactory.CreateContext(); + db.PlannedTransactions.Add(new PlannedTransaction() + { + Id = $"{network.CryptoCode}-{transaction.GetHash()}", + BroadcastAt = broadcastTime, + Blob = transaction.ToBytes() + }); + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateException) { - db.PlannedTransactions.Add(new PlannedTransaction() - { - Id = $"{network.CryptoCode}-{transaction.GetHash()}", - BroadcastAt = broadcastTime, - Blob = transaction.ToBytes() - }); - try - { - await db.SaveChangesAsync(); - } - catch (DbUpdateException) - { - } } } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 327d2d45a..90e925ad2 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -67,38 +67,32 @@ namespace BTCPayServer.Services.Invoices public async Task RemovePendingInvoice(string invoiceId) { Logs.PayServer.LogInformation($"Remove pending invoice {invoiceId}"); - using (var ctx = _applicationDbContextFactory.CreateContext()) + using var ctx = _applicationDbContextFactory.CreateContext(); + ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); + try { - ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); - try - { - await ctx.SaveChangesAsync(); - return true; - } - catch (DbUpdateException) { return false; } + await ctx.SaveChangesAsync(); + return true; } + catch (DbUpdateException) { return false; } } public async Task> GetInvoicesFromAddresses(string[] addresses) { - using (var db = _applicationDbContextFactory.CreateContext()) - { - return (await db.AddressInvoices - .Include(a => a.InvoiceData.Payments) + using var db = _applicationDbContextFactory.CreateContext(); + return (await db.AddressInvoices + .Include(a => a.InvoiceData.Payments) #pragma warning disable CS0618 .Where(a => addresses.Contains(a.Address)) #pragma warning restore CS0618 .Select(a => a.InvoiceData) - .ToListAsync()).Select(ToEntity); - } + .ToListAsync()).Select(ToEntity); } public async Task GetPendingInvoices() { - using (var ctx = _applicationDbContextFactory.CreateContext()) - { - return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync(); - } + using var ctx = _applicationDbContextFactory.CreateContext(); + return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync(); } public async Task> GetWebhookDeliveries(string invoiceId) @@ -115,40 +109,34 @@ namespace BTCPayServer.Services.Invoices public async Task GetAppsTaggingStore(string storeId) { ArgumentNullException.ThrowIfNull(storeId); - using (var ctx = _applicationDbContextFactory.CreateContext()) - { - return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync(); - } + using var ctx = _applicationDbContextFactory.CreateContext(); + return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync(); } public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) { - using (var ctx = _applicationDbContextFactory.CreateContext()) + using var ctx = _applicationDbContextFactory.CreateContext(); + var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null) + return; + if (invoiceData.CustomerEmail == null && data.Email != null) { - var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - if (invoiceData.CustomerEmail == null && data.Email != null) - { - invoiceData.CustomerEmail = data.Email; - AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail); - } - await ctx.SaveChangesAsync().ConfigureAwait(false); + invoiceData.CustomerEmail = data.Email; + AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail); } + await ctx.SaveChangesAsync().ConfigureAwait(false); } public async Task ExtendInvoiceMonitor(string invoiceId) { - using (var ctx = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await ctx.Invoices.FindAsync(invoiceId); + using var ctx = _applicationDbContextFactory.CreateContext(); + var invoiceData = await ctx.Invoices.FindAsync(invoiceId); - var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); - invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); - invoiceData.Blob = ToBytes(invoice, null); + var invoice = invoiceData.GetBlob(_btcPayNetworkProvider); + invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1); + invoiceData.Blob = ToBytes(invoice, null); - await ctx.SaveChangesAsync(); - } + await ctx.SaveChangesAsync(); } public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null) @@ -316,50 +304,45 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) { - using (var context = _applicationDbContextFactory.CreateContext()) + using var context = _applicationDbContextFactory.CreateContext(); + var invoice = await context.Invoices.FindAsync(invoiceId); + if (invoice == null) + return; + var network = paymentMethod.Network; + var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); + var newDetails = paymentMethod.GetPaymentMethodDetails(); + var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); + if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) { - var invoice = await context.Invoices.FindAsync(invoiceId); - if (invoice == null) - return; - var network = paymentMethod.Network; - var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); - var newDetails = paymentMethod.GetPaymentMethodDetails(); - var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); - if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) + await context.AddressInvoices.AddAsync(new AddressInvoiceData() { - await context.AddressInvoices.AddAsync(new AddressInvoiceData() - { - InvoiceDataId = invoiceId, - CreatedTime = DateTimeOffset.UtcNow - } - .Set(GetDestination(paymentMethod), paymentMethod.GetId())); - await context.HistoricalAddressInvoices.AddAsync(new HistoricalAddressInvoiceData() - { - InvoiceDataId = invoiceId, - Assigned = DateTimeOffset.UtcNow - }.SetAddress(paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(), network.CryptoCode)); + InvoiceDataId = invoiceId, + CreatedTime = DateTimeOffset.UtcNow } - invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.Blob = ToBytes(invoiceEntity, network); - AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination()); - await context.SaveChangesAsync(); - + .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + await context.HistoricalAddressInvoices.AddAsync(new HistoricalAddressInvoiceData() + { + InvoiceDataId = invoiceId, + Assigned = DateTimeOffset.UtcNow + }.SetAddress(paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(), network.CryptoCode)); } + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.Blob = ToBytes(invoiceEntity, network); + AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination()); + await context.SaveChangesAsync(); } public async Task AddPendingInvoiceIfNotPresent(string invoiceId) { - using (var context = _applicationDbContextFactory.CreateContext()) + using var context = _applicationDbContextFactory.CreateContext(); + if (!context.PendingInvoices.Any(a => a.Id == invoiceId)) { - if (!context.PendingInvoices.Any(a => a.Id == invoiceId)) + context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId }); + try { - context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId }); - try - { - await context.SaveChangesAsync(); - } - catch (DbUpdateException) { } // Already exists + await context.SaveChangesAsync(); } + catch (DbUpdateException) { } // Already exists } } @@ -419,80 +402,70 @@ namespace BTCPayServer.Services.Invoices public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - invoiceData.Status = InvoiceState.ToString(invoiceState.Status); - invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus); - await context.SaveChangesAsync().ConfigureAwait(false); - } + using var context = _applicationDbContextFactory.CreateContext(); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null) + return; + invoiceData.Status = InvoiceState.ToString(invoiceState.Status); + invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus); + await context.SaveChangesAsync().ConfigureAwait(false); } internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice) { if (invoice.Type != InvoiceType.TopUp) throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice)); - using (var context = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null) - return; - var blob = invoiceData.GetBlob(_btcPayNetworkProvider); - blob.Price = invoice.Price; - AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) }); - invoiceData.Blob = ToBytes(blob, null); - await context.SaveChangesAsync().ConfigureAwait(false); - } + using var context = _applicationDbContextFactory.CreateContext(); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null) + return; + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); + blob.Price = invoice.Price; + AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) }); + invoiceData.Blob = ToBytes(blob, null); + await context.SaveChangesAsync().ConfigureAwait(false); } public async Task MassArchive(string[] invoiceIds, bool archive = true) { - using (var context = _applicationDbContextFactory.CreateContext()) + using var context = _applicationDbContextFactory.CreateContext(); + var items = context.Invoices.Where(a => invoiceIds.Contains(a.Id)); + if (items == null) { - var items = context.Invoices.Where(a => invoiceIds.Contains(a.Id)); - if (items == null) - { - return; - } - - foreach (InvoiceData invoice in items) - { - invoice.Archived = archive; - } - - await context.SaveChangesAsync(); + return; } + + foreach (InvoiceData invoice in items) + { + invoice.Archived = archive; + } + + await context.SaveChangesAsync(); } public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); - if (invoiceData == null || invoiceData.Archived == archived || - (storeId != null && - !invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) - return; - invoiceData.Archived = archived; - await context.SaveChangesAsync().ConfigureAwait(false); - } + using var context = _applicationDbContextFactory.CreateContext(); + var invoiceData = await context.FindAsync(invoiceId).ConfigureAwait(false); + if (invoiceData == null || invoiceData.Archived == archived || + (storeId != null && + !invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase))) + return; + invoiceData.Archived = archived; + await context.SaveChangesAsync().ConfigureAwait(false); } public async Task UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var invoiceData = await GetInvoiceRaw(invoiceId, context); - if (invoiceData == null || (storeId != null && - !invoiceData.StoreDataId.Equals(storeId, - StringComparison.InvariantCultureIgnoreCase))) - return null; - var blob = invoiceData.GetBlob(_btcPayNetworkProvider); - blob.Metadata = InvoiceMetadata.FromJObject(metadata); - invoiceData.Blob = ToBytes(blob); - await context.SaveChangesAsync().ConfigureAwait(false); - return ToEntity(invoiceData); - } + using var context = _applicationDbContextFactory.CreateContext(); + var invoiceData = await GetInvoiceRaw(invoiceId, context); + if (invoiceData == null || (storeId != null && + !invoiceData.StoreDataId.Equals(storeId, + StringComparison.InvariantCultureIgnoreCase))) + return null; + var blob = invoiceData.GetBlob(_btcPayNetworkProvider); + blob.Metadata = InvoiceMetadata.FromJObject(metadata); + invoiceData.Blob = ToBytes(blob); + await context.SaveChangesAsync().ConfigureAwait(false); + return ToEntity(invoiceData); } public async Task MarkInvoiceStatus(string invoiceId, InvoiceStatus status) { @@ -541,25 +514,21 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoice(string id, bool includeAddressData = false) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var res = await GetInvoiceRaw(id, context, includeAddressData); - return res == null ? null : ToEntity(res); - } + using var context = _applicationDbContextFactory.CreateContext(); + var res = await GetInvoiceRaw(id, context, includeAddressData); + return res == null ? null : ToEntity(res); } public async Task GetInvoices(string[] invoiceIds) { var invoiceIdSet = invoiceIds.ToHashSet(); - using (var context = _applicationDbContextFactory.CreateContext()) - { - IQueryable query = - context - .Invoices - .Include(o => o.Payments) - .Where(o => invoiceIdSet.Contains(o.Id)); + using var context = _applicationDbContextFactory.CreateContext(); + IQueryable query = + context + .Invoices + .Include(o => o.Payments) + .Where(o => invoiceIdSet.Contains(o.Id)); - return (await query.ToListAsync()).Select(o => ToEntity(o)).ToArray(); - } + return (await query.ToListAsync()).Select(o => ToEntity(o)).ToArray(); } private async Task GetInvoiceRaw(string id, ApplicationDbContext dbContext, bool includeAddressData = false) @@ -710,26 +679,22 @@ namespace BTCPayServer.Services.Invoices public async Task GetInvoicesTotal(InvoiceQuery queryObject) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var query = GetInvoiceQuery(context, queryObject); - return await query.CountAsync(); - } + using var context = _applicationDbContextFactory.CreateContext(); + var query = GetInvoiceQuery(context, queryObject); + return await query.CountAsync(); } public async Task GetInvoices(InvoiceQuery queryObject) { - using (var context = _applicationDbContextFactory.CreateContext()) - { - var query = GetInvoiceQuery(context, queryObject); - query = query.Include(o => o.Payments); - if (queryObject.IncludeAddresses) - query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices); - if (queryObject.IncludeEvents) - query = query.Include(o => o.Events); - var data = await query.ToArrayAsync().ConfigureAwait(false); - return data.Select(ToEntity).ToArray(); - } + using var context = _applicationDbContextFactory.CreateContext(); + var query = GetInvoiceQuery(context, queryObject); + query = query.Include(o => o.Payments); + if (queryObject.IncludeAddresses) + query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices); + if (queryObject.IncludeEvents) + query = query.Include(o => o.Events); + var data = await query.ToArrayAsync().ConfigureAwait(false); + return data.Select(ToEntity).ToArray(); } private string NormalizeExceptionStatus(string status) diff --git a/BTCPayServer/Services/LanguageService.cs b/BTCPayServer/Services/LanguageService.cs index d0ab87018..f8d5165e1 100644 --- a/BTCPayServer/Services/LanguageService.cs +++ b/BTCPayServer/Services/LanguageService.cs @@ -37,11 +37,9 @@ namespace BTCPayServer.Services var result = new List(); foreach (var file in files) { - using (var stream = new StreamReader(file)) - { - var json = stream.ReadToEnd(); - result.Add(JObject.Parse(json).ToObject()); - } + using var stream = new StreamReader(file); + var json = stream.ReadToEnd(); + result.Add(JObject.Parse(json).ToObject()); } _languages = result.ToArray(); diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index 21e26fb12..3d23e9902 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -30,12 +30,10 @@ namespace BTCPayServer.Services.Mails Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); return; } - using (var smtp = await emailSettings.CreateSmtpClient()) - { - var mail = emailSettings.CreateMailMessage(new MailboxAddress(email, email), subject, message, true); - await smtp.SendAsync(mail, cancellationToken); - await smtp.DisconnectAsync(true, cancellationToken); - } + using var smtp = await emailSettings.CreateSmtpClient(); + var mail = emailSettings.CreateMailMessage(new MailboxAddress(email, email), subject, message, true); + await smtp.SendAsync(mail, cancellationToken); + await smtp.DisconnectAsync(true, cancellationToken); }, TimeSpan.Zero); } diff --git a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs index b468b0f73..57fa6459c 100644 --- a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs +++ b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs @@ -22,21 +22,19 @@ namespace BTCPayServer.Services.PaymentRequests public async Task CreateOrUpdatePaymentRequest(PaymentRequestData entity) { - using (var context = _ContextFactory.CreateContext()) + using var context = _ContextFactory.CreateContext(); + if (string.IsNullOrEmpty(entity.Id)) { - if (string.IsNullOrEmpty(entity.Id)) - { - entity.Id = Guid.NewGuid().ToString(); - await context.PaymentRequests.AddAsync(entity); - } - else - { - context.PaymentRequests.Update(entity); - } - - await context.SaveChangesAsync(); - return entity; + entity.Id = Guid.NewGuid().ToString(); + await context.PaymentRequests.AddAsync(entity); } + else + { + context.PaymentRequests.Update(entity); + } + + await context.SaveChangesAsync(); + return entity; } public async Task FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default) @@ -46,15 +44,13 @@ namespace BTCPayServer.Services.PaymentRequests return null; } - using (var context = _ContextFactory.CreateContext()) - { - var result = await context.PaymentRequests.Include(x => x.StoreData) - .Where(data => - string.IsNullOrEmpty(userId) || - (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))) - .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); - return result; - } + using var context = _ContextFactory.CreateContext(); + var result = await context.PaymentRequests.Include(x => x.StoreData) + .Where(data => + string.IsNullOrEmpty(userId) || + (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))) + .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + return result; } public async Task IsPaymentRequestAdmin(string paymentRequestId, string userId) @@ -63,76 +59,70 @@ namespace BTCPayServer.Services.PaymentRequests { return false; } - using (var context = _ContextFactory.CreateContext()) - { - return await context.PaymentRequests.Include(x => x.StoreData) - .AnyAsync(data => - data.Id == paymentRequestId && - (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))); - } + using var context = _ContextFactory.CreateContext(); + return await context.PaymentRequests.Include(x => x.StoreData) + .AnyAsync(data => + data.Id == paymentRequestId && + (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))); } public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default) { - using (var context = _ContextFactory.CreateContext()) - { - var invoiceData = await context.FindAsync(paymentRequestId); - if (invoiceData == null) - return; - invoiceData.Status = status; - await context.SaveChangesAsync(cancellationToken); - } + using var context = _ContextFactory.CreateContext(); + var invoiceData = await context.FindAsync(paymentRequestId); + if (invoiceData == null) + return; + invoiceData.Status = status; + await context.SaveChangesAsync(cancellationToken); } public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default) { - using (var context = _ContextFactory.CreateContext()) + using var context = _ContextFactory.CreateContext(); + var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable(); + + if (!query.IncludeArchived) { - var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable(); - - if (!query.IncludeArchived) - { - queryable = queryable.Where(data => !data.Archived); - } - if (!string.IsNullOrEmpty(query.StoreId)) - { - queryable = queryable.Where(data => - data.StoreDataId == query.StoreId); - } - - if (query.Status != null && query.Status.Any()) - { - queryable = queryable.Where(data => - query.Status.Contains(data.Status)); - } - - if (query.Ids != null && query.Ids.Any()) - { - queryable = queryable.Where(data => - query.Ids.Contains(data.Id)); - } - - if (!string.IsNullOrEmpty(query.UserId)) - { - queryable = queryable.Where(i => - i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId)); - } - - var total = await queryable.CountAsync(cancellationToken); - - queryable = queryable.OrderByDescending(u => u.Created); - - if (query.Skip.HasValue) - { - queryable = queryable.Skip(query.Skip.Value); - } - - if (query.Count.HasValue) - { - queryable = queryable.Take(query.Count.Value); - } - return (total, await queryable.ToArrayAsync(cancellationToken)); + queryable = queryable.Where(data => !data.Archived); } + if (!string.IsNullOrEmpty(query.StoreId)) + { + queryable = queryable.Where(data => + data.StoreDataId == query.StoreId); + } + + if (query.Status != null && query.Status.Any()) + { + queryable = queryable.Where(data => + query.Status.Contains(data.Status)); + } + + if (query.Ids != null && query.Ids.Any()) + { + queryable = queryable.Where(data => + query.Ids.Contains(data.Id)); + } + + if (!string.IsNullOrEmpty(query.UserId)) + { + queryable = queryable.Where(i => + i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId)); + } + + var total = await queryable.CountAsync(cancellationToken); + + queryable = queryable.OrderByDescending(u => u.Created); + + if (query.Skip.HasValue) + { + queryable = queryable.Skip(query.Skip.Value); + } + + if (query.Count.HasValue) + { + queryable = queryable.Take(query.Count.Value); + } + return (total, await queryable.ToArrayAsync(cancellationToken)); } public async Task GetInvoicesForPaymentRequest(string paymentRequestId, diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index da06544d5..b1b96c694 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -26,11 +26,9 @@ namespace BTCPayServer.Services.Stores { if (storeId == null) return null; - using (var ctx = _ContextFactory.CreateContext()) - { - var result = await ctx.FindAsync(storeId).ConfigureAwait(false); - return result; - } + using var ctx = _ContextFactory.CreateContext(); + var result = await ctx.FindAsync(storeId).ConfigureAwait(false); + return result; } public async Task FindStore(string storeId, string userId) @@ -62,34 +60,30 @@ namespace BTCPayServer.Services.Stores public async Task GetStoreUsers(string storeId) { ArgumentNullException.ThrowIfNull(storeId); - using (var ctx = _ContextFactory.CreateContext()) - { - return await ctx - .UserStore - .Where(u => u.StoreDataId == storeId) - .Select(u => new StoreUser() - { - Id = u.ApplicationUserId, - Email = u.ApplicationUser.Email, - Role = u.Role - }).ToArrayAsync(); - } + using var ctx = _ContextFactory.CreateContext(); + return await ctx + .UserStore + .Where(u => u.StoreDataId == storeId) + .Select(u => new StoreUser() + { + Id = u.ApplicationUserId, + Email = u.ApplicationUser.Email, + Role = u.Role + }).ToArrayAsync(); } public async Task GetStoresByUserId(string userId, IEnumerable storeIds = null) { - using (var ctx = _ContextFactory.CreateContext()) - { - return (await ctx.UserStore - .Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId))) - .Select(u => new { u.StoreData, u.Role }) - .ToArrayAsync()) - .Select(u => - { - u.StoreData.Role = u.Role; - return u.StoreData; - }).ToArray(); - } + using var ctx = _ContextFactory.CreateContext(); + return (await ctx.UserStore + .Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId))) + .Select(u => new { u.StoreData, u.Role }) + .ToArrayAsync()) + .Select(u => + { + u.StoreData.Role = u.Role; + return u.StoreData; + }).ToArray(); } public async Task GetStoreByInvoiceId(string invoiceId) @@ -102,34 +96,30 @@ namespace BTCPayServer.Services.Stores public async Task AddStoreUser(string storeId, string userId, string role) { - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role }; + ctx.UserStore.Add(userStore); + try { - var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role }; - ctx.UserStore.Add(userStore); - try - { - await ctx.SaveChangesAsync(); - return true; - } - catch (Microsoft.EntityFrameworkCore.DbUpdateException) - { - return false; - } + await ctx.SaveChangesAsync(); + return true; + } + catch (Microsoft.EntityFrameworkCore.DbUpdateException) + { + return false; } } public async Task CleanUnreachableStores() { - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + if (!ctx.Database.SupportDropForeignKey()) + return; + foreach (var store in await ctx.Stores.Where(s => !s.UserStores.Where(u => u.Role == StoreRoles.Owner).Any()).ToArrayAsync()) { - if (!ctx.Database.SupportDropForeignKey()) - return; - foreach (var store in await ctx.Stores.Where(s => !s.UserStores.Where(u => u.Role == StoreRoles.Owner).Any()).ToArrayAsync()) - { - ctx.Stores.Remove(store); - } - await ctx.SaveChangesAsync(); + ctx.Stores.Remove(store); } + await ctx.SaveChangesAsync(); } public async Task RemoveStoreUser(string storeId, string userId) @@ -147,18 +137,16 @@ namespace BTCPayServer.Services.Stores private async Task DeleteStoreIfOrphan(string storeId) { - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + if (ctx.Database.SupportDropForeignKey()) { - if (ctx.Database.SupportDropForeignKey()) + if (!await ctx.UserStore.Where(u => u.StoreDataId == storeId && u.Role == StoreRoles.Owner).AnyAsync()) { - if (!await ctx.UserStore.Where(u => u.StoreDataId == storeId && u.Role == StoreRoles.Owner).AnyAsync()) + var store = await ctx.Stores.FindAsync(storeId); + if (store != null) { - var store = await ctx.Stores.FindAsync(storeId); - if (store != null) - { - ctx.Stores.Remove(store); - await ctx.SaveChangesAsync(); - } + ctx.Stores.Remove(store); + await ctx.SaveChangesAsync(); } } } @@ -182,22 +170,20 @@ namespace BTCPayServer.Services.Stores if (string.IsNullOrEmpty(storeData.StoreName)) throw new ArgumentException("name should not be empty", nameof(storeData.StoreName)); ArgumentNullException.ThrowIfNull(ownerId); - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + storeData.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)); + var userStore = new UserStore { - storeData.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)); - var userStore = new UserStore - { - StoreDataId = storeData.Id, - ApplicationUserId = ownerId, - Role = StoreRoles.Owner, - }; + StoreDataId = storeData.Id, + ApplicationUserId = ownerId, + Role = StoreRoles.Owner, + }; - SetNewStoreHints(ref storeData); + SetNewStoreHints(ref storeData); - ctx.Add(storeData); - ctx.Add(userStore); - await ctx.SaveChangesAsync(); - } + ctx.Add(storeData); + ctx.Add(userStore); + await ctx.SaveChangesAsync(); } public async Task CreateStore(string ownerId, string name) @@ -343,41 +329,35 @@ namespace BTCPayServer.Services.Stores public async Task UpdateStore(StoreData store) { - using (var ctx = _ContextFactory.CreateContext()) - { - var existing = await ctx.FindAsync(store.Id); - ctx.Entry(existing).CurrentValues.SetValues(store); - await ctx.SaveChangesAsync().ConfigureAwait(false); - } + using var ctx = _ContextFactory.CreateContext(); + var existing = await ctx.FindAsync(store.Id); + ctx.Entry(existing).CurrentValues.SetValues(store); + await ctx.SaveChangesAsync().ConfigureAwait(false); } public async Task DeleteStore(string storeId) { - using (var ctx = _ContextFactory.CreateContext()) - { - if (!ctx.Database.SupportDropForeignKey()) - return false; - var store = await ctx.Stores.FindAsync(storeId); - if (store == null) - return false; - var webhooks = await ctx.StoreWebhooks - .Where(o => o.StoreId == storeId) - .Select(o => o.Webhook) - .ToArrayAsync(); - foreach (var w in webhooks) - ctx.Webhooks.Remove(w); - ctx.Stores.Remove(store); - await ctx.SaveChangesAsync(); - return true; - } + using var ctx = _ContextFactory.CreateContext(); + if (!ctx.Database.SupportDropForeignKey()) + return false; + var store = await ctx.Stores.FindAsync(storeId); + if (store == null) + return false; + var webhooks = await ctx.StoreWebhooks + .Where(o => o.StoreId == storeId) + .Select(o => o.Webhook) + .ToArrayAsync(); + foreach (var w in webhooks) + ctx.Webhooks.Remove(w); + ctx.Stores.Remove(store); + await ctx.SaveChangesAsync(); + return true; } public bool CanDeleteStores() { - using (var ctx = _ContextFactory.CreateContext()) - { - return ctx.Database.SupportDropForeignKey(); - } + using var ctx = _ContextFactory.CreateContext(); + return ctx.Database.SupportDropForeignKey(); } } } diff --git a/BTCPayServer/Services/WalletRepository.cs b/BTCPayServer/Services/WalletRepository.cs index aa245ae78..80f3814a7 100644 --- a/BTCPayServer/Services/WalletRepository.cs +++ b/BTCPayServer/Services/WalletRepository.cs @@ -19,77 +19,69 @@ namespace BTCPayServer.Services public async Task SetWalletInfo(WalletId walletId, WalletBlobInfo blob) { ArgumentNullException.ThrowIfNull(walletId); - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + var walletData = new WalletData() { Id = walletId.ToString() }; + walletData.SetBlobInfo(blob); + var entity = await ctx.Wallets.AddAsync(walletData); + entity.State = EntityState.Modified; + try { - var walletData = new WalletData() { Id = walletId.ToString() }; - walletData.SetBlobInfo(blob); - var entity = await ctx.Wallets.AddAsync(walletData); - entity.State = EntityState.Modified; - try - { - await ctx.SaveChangesAsync(); - } - catch (DbUpdateException) // Does not exists - { - entity.State = EntityState.Added; - await ctx.SaveChangesAsync(); - } + await ctx.SaveChangesAsync(); + } + catch (DbUpdateException) // Does not exists + { + entity.State = EntityState.Added; + await ctx.SaveChangesAsync(); } } public async Task> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null) { ArgumentNullException.ThrowIfNull(walletId); - using (var ctx = _ContextFactory.CreateContext()) - { - return (await ctx.WalletTransactions - .Where(w => w.WalletDataId == walletId.ToString()) - .Where(data => transactionIds == null || transactionIds.Contains(data.TransactionId)) - .Select(w => w) - .ToArrayAsync()) - .ToDictionary(w => w.TransactionId, w => w.GetBlobInfo()); - } + using var ctx = _ContextFactory.CreateContext(); + return (await ctx.WalletTransactions + .Where(w => w.WalletDataId == walletId.ToString()) + .Where(data => transactionIds == null || transactionIds.Contains(data.TransactionId)) + .Select(w => w) + .ToArrayAsync()) + .ToDictionary(w => w.TransactionId, w => w.GetBlobInfo()); } public async Task GetWalletInfo(WalletId walletId) { ArgumentNullException.ThrowIfNull(walletId); - using (var ctx = _ContextFactory.CreateContext()) - { - var data = await ctx.Wallets - .Where(w => w.Id == walletId.ToString()) - .Select(w => w) - .FirstOrDefaultAsync(); - return data?.GetBlobInfo() ?? new WalletBlobInfo(); - } + using var ctx = _ContextFactory.CreateContext(); + var data = await ctx.Wallets + .Where(w => w.Id == walletId.ToString()) + .Select(w => w) + .FirstOrDefaultAsync(); + return data?.GetBlobInfo() ?? new WalletBlobInfo(); } public async Task SetWalletTransactionInfo(WalletId walletId, string transactionId, WalletTransactionInfo walletTransactionInfo) { ArgumentNullException.ThrowIfNull(walletId); ArgumentNullException.ThrowIfNull(transactionId); - using (var ctx = _ContextFactory.CreateContext()) + using var ctx = _ContextFactory.CreateContext(); + var walletData = new WalletTransactionData() { WalletDataId = walletId.ToString(), TransactionId = transactionId }; + walletData.SetBlobInfo(walletTransactionInfo); + var entity = await ctx.WalletTransactions.AddAsync(walletData); + entity.State = EntityState.Modified; + try { - var walletData = new WalletTransactionData() { WalletDataId = walletId.ToString(), TransactionId = transactionId }; - walletData.SetBlobInfo(walletTransactionInfo); - var entity = await ctx.WalletTransactions.AddAsync(walletData); - entity.State = EntityState.Modified; + await ctx.SaveChangesAsync(); + } + catch (DbUpdateException) // Does not exists + { + entity.State = EntityState.Added; try { await ctx.SaveChangesAsync(); } - catch (DbUpdateException) // Does not exists + catch (DbUpdateException) // the Wallet does not exists in the DB { - entity.State = EntityState.Added; - try - { - await ctx.SaveChangesAsync(); - } - catch (DbUpdateException) // the Wallet does not exists in the DB - { - await SetWalletInfo(walletId, new WalletBlobInfo()); - await ctx.SaveChangesAsync(); - } + await SetWalletInfo(walletId, new WalletBlobInfo()); + await ctx.SaveChangesAsync(); } } } diff --git a/BTCPayServer/Storage/Services/StoredFileRepository.cs b/BTCPayServer/Storage/Services/StoredFileRepository.cs index 75db8c31b..024804e68 100644 --- a/BTCPayServer/Storage/Services/StoredFileRepository.cs +++ b/BTCPayServer/Storage/Services/StoredFileRepository.cs @@ -29,35 +29,29 @@ namespace BTCPayServer.Storage.Services filesQuery = new FilesQuery(); } - using (var context = _ApplicationDbContextFactory.CreateContext()) - { - return await context.Files - .Include(file => file.ApplicationUser) - .Where(file => - (!filesQuery.Id.Any() || filesQuery.Id.Contains(file.Id)) && - (!filesQuery.UserIds.Any() || filesQuery.UserIds.Contains(file.ApplicationUserId))) - .OrderByDescending(file => file.Timestamp) - .ToListAsync(); - } + using var context = _ApplicationDbContextFactory.CreateContext(); + return await context.Files + .Include(file => file.ApplicationUser) + .Where(file => + (!filesQuery.Id.Any() || filesQuery.Id.Contains(file.Id)) && + (!filesQuery.UserIds.Any() || filesQuery.UserIds.Contains(file.ApplicationUserId))) + .OrderByDescending(file => file.Timestamp) + .ToListAsync(); } public async Task RemoveFile(StoredFile file) { - using (var context = _ApplicationDbContextFactory.CreateContext()) - { - context.Attach(file); - context.Files.Remove(file); - await context.SaveChangesAsync(); - } + using var context = _ApplicationDbContextFactory.CreateContext(); + context.Attach(file); + context.Files.Remove(file); + await context.SaveChangesAsync(); } public async Task AddFile(StoredFile storedFile) { - using (var context = _ApplicationDbContextFactory.CreateContext()) - { - await context.AddAsync(storedFile); - await context.SaveChangesAsync(); - } + using var context = _ApplicationDbContextFactory.CreateContext(); + await context.AddAsync(storedFile); + await context.SaveChangesAsync(); } public class FilesQuery diff --git a/BTCPayServer/WebSocketHelper.cs b/BTCPayServer/WebSocketHelper.cs index b9d96fc86..ee67837d6 100644 --- a/BTCPayServer/WebSocketHelper.cs +++ b/BTCPayServer/WebSocketHelper.cs @@ -94,13 +94,9 @@ namespace BTCPayServer public async Task Send(string evt, CancellationToken cancellation = default) { var bytes = UTF8.GetBytes(evt); - using (var cts = new CancellationTokenSource(5000)) - { - using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cancellation)) - { - await Socket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, cts2.Token); - } - } + using var cts = new CancellationTokenSource(5000); + using var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + await Socket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, cts2.Token); } public async Task DisposeAsync(CancellationToken cancellation)