btcpayserver/BTCPayServer.Tests/UnitTest1.cs

2935 lines
141 KiB
C#
Raw Normal View History

2020-06-29 04:44:35 +02:00
using System;
2020-06-28 10:55:27 +02:00
using System.Collections.Generic;
using System.Globalization;
using System.IO;
2017-09-13 08:47:34 +02:00
using System.Linq;
2020-06-28 10:55:27 +02:00
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
2017-09-13 08:47:34 +02:00
using System.Threading;
2020-06-28 10:55:27 +02:00
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Events;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
2020-06-28 10:55:27 +02:00
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ManageViewModels;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Models.ServerViewModels;
2018-01-17 07:59:31 +01:00
using BTCPayServer.Models.StoreViewModels;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Plugins.PayButton;
using BTCPayServer.Plugins.PointOfSale;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Plugins.PointOfSale.Controllers;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Services;
2018-04-03 09:53:55 +02:00
using BTCPayServer.Services.Apps;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
2020-12-08 08:12:29 +01:00
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Services.Rates;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using ExchangeSharp;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
2020-06-28 10:55:27 +02:00
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
2020-06-28 10:55:27 +02:00
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer;
2020-06-28 10:55:27 +02:00
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
2020-03-18 12:08:09 +01:00
using Newtonsoft.Json.Schema;
2020-06-28 10:55:27 +02:00
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
2020-06-28 10:55:27 +02:00
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Tests
{
2021-11-23 05:57:45 +01:00
[Collection(nameof(NonParallelizableCollectionDefinition))]
2021-11-22 09:16:08 +01:00
public class UnitTest1 : UnitTestBase
{
public const int LongRunningTestTimeout = 60_000; // 60s
2021-11-22 09:16:08 +01:00
public UnitTest1(ITestOutputHelper helper) : base(helper)
{
}
class DockerImage
{
public string User { get; private set; }
public string Name { get; private set; }
public string Tag { get; private set; }
public string Source { get; set; }
public static DockerImage Parse(string str)
{
//${BTCPAY_IMAGE: -btcpayserver / btcpayserver:1.0.3.21}
var variableMatch = Regex.Match(str, @"\$\{[^-]+-([^\}]+)\}");
if (variableMatch.Success)
{
str = variableMatch.Groups[1].Value;
}
DockerImage img = new DockerImage();
var match = Regex.Match(str, "([^/]*/)?([^:]+):?(.*)");
if (!match.Success)
throw new FormatException();
img.User = match.Groups[1].Length == 0 ? string.Empty : match.Groups[1].Value.Substring(0, match.Groups[1].Value.Length - 1);
img.Name = match.Groups[2].Value;
img.Tag = match.Groups[3].Value;
if (img.Tag == string.Empty)
img.Tag = "latest";
return img;
}
public override string ToString()
{
return ToString(true);
}
public string ToString(bool includeTag)
{
StringBuilder builder = new StringBuilder();
if (!String.IsNullOrWhiteSpace(User))
builder.Append($"{User}/");
builder.Append($"{Name}");
if (includeTag)
{
if (!String.IsNullOrWhiteSpace(Tag))
builder.Append($":{Tag}");
}
return builder.ToString();
}
}
2020-03-18 12:08:09 +01:00
[Fact]
2020-04-05 09:43:49 +02:00
[Trait("Integration", "Integration")]
2020-03-18 12:08:09 +01:00
public async Task CheckSwaggerIsConformToSchema()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
2022-01-14 09:50:29 +01:00
JObject swagger = JObject.Parse(sresp);
var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json")));
IList<ValidationError> 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.
2022-01-14 09:50:29 +01:00
var matchedError = errors.Where(error =>
error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf).ToList();
foreach (ValidationError validationError in matchedError)
2020-03-20 17:37:39 +01:00
{
2022-01-14 09:50:29 +01:00
errors.Remove(validationError);
2020-03-20 17:37:39 +01:00
}
2022-01-14 09:50:29 +01:00
valid = !errors.Any();
Assert.Empty(errors);
Assert.True(valid);
2020-03-18 12:08:09 +01:00
}
2021-12-31 08:59:02 +01:00
[Fact]
[Trait("Integration", "Integration")]
public async Task EnsureSwaggerPermissionsDocumented()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
var description = UtilitiesTests.GetSecuritySchemeDescription();
2022-01-14 09:50:29 +01:00
TestLogs.LogInformation(description);
var sresp = Assert
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
2022-01-14 09:50:29 +01:00
JObject json = JObject.Parse(sresp);
// If this test fail, run `UpdateSwagger` once.
if (description != json["components"]["securitySchemes"]["API_Key"]["description"].Value<string>())
{
Assert.Fail("Please run manually the test `UpdateSwagger` once");
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanStoreArbitrarySettingsWithStore()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
var settingsRepo = tester.PayTester.ServiceProvider.GetRequiredService<IStoreRepository>();
var arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId, "arbitrary");
Assert.Null(arbValue);
await settingsRepo.UpdateSetting(user.StoreId, "arbitrary", "saved");
arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId, "arbitrary");
Assert.Equal("saved", arbValue);
await settingsRepo.UpdateSetting<TestData>(user.StoreId, "arbitrary", new TestData() { Name = "hello" });
var arbData = await settingsRepo.GetSettingAsync<TestData>(user.StoreId, "arbitrary");
Assert.Equal("hello", arbData.Name);
var client = await user.CreateClient();
await client.RemoveStore(user.StoreId);
tester.Stores.Clear();
arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId, "arbitrary");
Assert.Null(arbValue);
}
class TestData
{
public string Name { get; set; }
}
2020-03-18 12:08:09 +01:00
2021-11-22 09:16:08 +01:00
private async Task CheckDeadLinks(Regex regex, HttpClient httpClient, string file)
2019-09-02 09:32:38 +02:00
{
List<Task> checkLinks = new List<Task>();
var text = await File.ReadAllTextAsync(file);
var urlBlacklist = new string[]
{
2020-03-31 11:13:22 +02:00
"https://www.btse.com", // not allowing to be hit from circleci
2021-06-14 11:32:04 +02:00
"https://www.bitpay.com", // not allowing to be hit from circleci
"https://support.bitpay.com"
};
2019-09-02 09:32:38 +02:00
foreach (var match in regex.Matches(text).OfType<Match>())
{
var url = match.Groups[1].Value;
2021-10-15 07:19:40 +02:00
if (urlBlacklist.Any(a => url.StartsWith(a.ToLowerInvariant())))
continue;
checkLinks.Add(AssertLinkNotDead(httpClient, url, file));
2019-09-02 09:32:38 +02:00
}
2019-09-02 09:32:38 +02:00
await Task.WhenAll(checkLinks);
}
2021-11-22 09:16:08 +01:00
private async Task AssertLinkNotDead(HttpClient httpClient, string url, string file)
2019-09-02 09:32:38 +02:00
{
var uri = new Uri(url);
2019-09-02 09:32:38 +02:00
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.TryAddWithoutValidation("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
request.Headers.TryAddWithoutValidation("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0");
var response = await httpClient.SendAsync(request);
2021-11-04 16:36:05 +01:00
if (response.StatusCode == HttpStatusCode.ServiceUnavailable) // Temporary issue
{
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"Unavailable: {url} ({file})");
2021-11-04 16:36:05 +01:00
return;
}
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
if (uri.Fragment.Length != 0)
{
var fragment = uri.Fragment.Substring(1);
var contents = await response.Content.ReadAsStringAsync();
Assert.Matches($"id=\"{fragment}\"", contents);
}
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"OK: {url} ({file})");
2019-09-02 09:32:38 +02:00
}
catch (Exception ex) when (ex is MatchesException)
{
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"FAILED: {url} ({file}) – anchor not found: {uri.Fragment}");
throw;
}
catch (Exception ex)
2019-09-02 09:32:38 +02:00
{
2023-11-21 06:11:17 +01:00
var details = ex.Message;
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"FAILED: {url} ({file}) {details}");
2020-06-24 03:34:09 +02:00
2019-09-02 09:32:38 +02:00
throw;
}
}
[Fact]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanAcceptInvoiceWithTolerance2()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// Set tolerance to 50%
var stores = user.GetController<UIStoresController>();
var response = stores.GeneralSettings();
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
2022-01-14 09:50:29 +01:00
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.GeneralSettings(vm).Result);
2022-01-14 09:50:29 +01:00
var invoice = await user.BitPay.CreateInvoiceAsync(
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
// Pays 75%
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
await tester.ExplorerNode.SendToAddressAsync(invoiceAddress,
2022-01-14 09:50:29 +01:00
Money.Satoshis(invoice.BtcDue.Satoshi * 0.75m));
2022-01-14 09:50:29 +01:00
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
});
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanThrowBitpay404Error()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
2022-01-14 09:50:29 +01:00
var invoice = user.BitPay.CreateInvoice(
new Invoice()
{
2022-01-14 09:50:29 +01:00
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
try
{
2024-01-18 01:47:39 +01:00
user.BitPay.GetInvoice(invoice.Id + "123");
2022-01-14 09:50:29 +01:00
}
catch (BitPayException ex)
{
Assert.Equal("Object not found", ex.Errors.First());
}
2022-01-14 09:50:29 +01:00
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);
}
2021-12-31 08:59:02 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task EnsureNewLightningInvoiceOnPartialPayment()
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
2020-10-01 13:15:46 +02:00
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m));
2021-12-31 08:59:02 +01:00
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance);
await tester.ExplorerNode.GenerateAsync(1);
Invoice newInvoice = null;
await TestUtils.EventuallyAsync(async () =>
{
await Task.Delay(1000); // wait a bit for payment to process before fetching new invoice
newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
Assert.NotEqual(newBolt11, oldBolt11);
Assert.Equal(newInvoice.BtcDue.ToDecimal(MoneyUnit.BTC),
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
}, 40000);
2021-12-31 08:59:02 +01:00
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork)tester.DefaultNetwork)} via lightning");
2020-09-16 14:50:06 +02:00
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.SendLightningPaymentAsync(newInvoice);
}, evt => evt.InvoiceId == invoice.Id);
var fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed });
Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus);
2020-11-09 08:48:45 +01:00
//BTCPay will attempt to cancel previous bolt11 invoices so that there are less weird edge case scenarios
TestLogs.LogInformation($"Attempting to pay invoice {invoice.Id} original full amount bolt11 invoice");
var res = await tester.SendLightningPaymentAsync(invoice);
Assert.Equal(PayResult.Error, res.Result);
2021-12-31 08:59:02 +01:00
//NOTE: Eclair does not support cancelling invoice so the below test case would make sense for it
2021-11-22 09:16:08 +01:00
// TestLogs.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
// evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
// {
// await tester.SendLightningPaymentAsync(invoice);
// }, evt => evt.InvoiceId == invoice.Id);
// Assert.Equal(evt.InvoiceId, invoice.Id);
// fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
// Assert.Equal(3, fetchedInvoice.Payments.Count);
}
2019-04-05 09:28:18 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
2018-10-28 14:46:03 +01:00
public async Task CanSetLightningServer()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
2022-01-14 09:50:29 +01:00
var storeController = user.GetController<UIStoresController>();
var storeResponse = storeController.GeneralSettings();
2022-01-14 09:50:29 +01:00
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
2022-01-14 09:50:29 +01:00
storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
{
2022-01-14 09:50:29 +01:00
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);
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel
{
2022-01-14 09:50:29 +01:00
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<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
storeResponse = storeController.LightningSettings(user.StoreId, "BTC");
2022-01-14 09:50:29 +01:00
var storeVm =
Assert.IsType<LightningSettingsViewModel>(Assert
2022-01-14 09:50:29 +01:00
.IsType<ViewResult>(storeResponse).Model);
Assert.NotEmpty(storeVm.ConnectionString);
2017-12-13 07:49:19 +01:00
}
2019-03-27 10:57:51 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentCLightning()
{
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
2019-03-27 10:57:51 +01:00
[Fact(Timeout = 60 * 2 * 1000)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSendLightningPaymentLnd()
2018-05-31 23:07:59 +02:00
{
await ProcessLightningPayment(LightningConnectionType.LndREST);
2018-05-31 23:07:59 +02:00
}
async Task ProcessLightningPayment(string type)
{
// For easier debugging and testing
2018-06-16 00:20:56 +02:00
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
2022-01-14 09:50:29 +01:00
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 Task.WhenAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
.ToArray());
}
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
{
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
{
2018-05-11 15:38:31 +02:00
Price = 0.01m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"Trying to send Lightning payment to {invoice.Id}");
await tester.SendLightningPaymentAsync(invoice);
2021-11-22 09:16:08 +01:00
TestLogs.LogInformation($"Lightning payment to {invoice.Id} is sent");
2019-01-05 19:47:39 +01:00
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
2020-01-10 03:13:54 +01:00
// C-Lightning may overpay for privacy
2020-06-24 03:34:09 +02:00
Assert.Contains(localInvoice.ExceptionStatus.ToString(), new[] { "False", "paidOver" });
});
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanUseServerInitiatedPairingCode()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
2022-01-14 09:50:29 +01:00
var controller = acc.GetController<UIStoresController>();
var token = (RedirectToActionResult)await controller.CreateToken2(
new Models.StoreViewModels.CreateTokenViewModel()
{
Label = "bla",
PublicKey = null,
StoreId = acc.StoreId
});
2022-01-14 09:50:29 +01:00
var pairingCode = (string)token.RouteValues["pairingCode"];
await acc.BitPay.AuthorizeClient(new PairingCode(pairingCode));
2022-01-14 09:50:29 +01:00
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-01-06 07:04:30 +01:00
public async Task CanSendIPN()
{
2022-01-14 09:50:29 +01:00
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
{
2022-01-14 09:50:29 +01:00
Price = 5.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true,
ExtendedNotifications = true
});
2021-11-22 16:49:51 +01:00
#pragma warning disable CS0618
2022-01-14 09:50:29 +01:00
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 || !receivedPayment)
2022-01-14 09:50:29 +01:00
{
var request = await callbackServer.GetNextRequest();
if (request.ContainsKey("event"))
{
var evtName = request["event"]["name"].Value<string>();
switch (evtName)
{
2022-01-14 09:50:29 +01:00
case InvoiceEvent.Created:
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
break;
case InvoiceEvent.ReceivedPayment:
receivedPayment = true;
break;
case InvoiceEvent.PaidInFull:
// TODO, we should check that ReceivedPayment is sent after PaidInFull
// for now, we can't ensure this because the ReceivedPayment events isn't sent by the
// InvoiceWatcher, contrary to all other events
2022-01-14 09:50:29 +01:00
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.Fail($"{evtName} was not expected");
2022-01-14 09:50:29 +01:00
break;
2019-01-06 07:04:30 +01:00
}
}
}
2022-01-14 09:50:29 +01:00
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CantPairTwiceWithSamePubkey()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
var store = acc.GetController<UIStoresController>();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter()
.GetResult());
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
acc.CreateStore();
var store2 = acc.GetController<UIStoresController>();
await store2.Pair(pairingCode.ToString(), store2.CurrentStore.Id);
Assert.Contains(nameof(PairingResult.ReusedKey),
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
}
2022-10-07 09:04:22 +02:00
[Fact(Timeout = LongRunningTestTimeout * 2)]
2022-11-22 13:58:15 +01:00
[Trait("Flaky", "Flaky")]
public async Task CanUseTorClient()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
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 a tor website");
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
2022-01-25 04:27:44 +01:00
if (response.IsSuccessStatusCode) // Sometimes the site goes down
{
result = await response.Content.ReadAsStringAsync();
Assert.Contains("Bitcoin", result);
}
2022-01-14 09:50:29 +01:00
TestLogs.LogInformation("...twice");
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
client.Dispose();
TestLogs.LogInformation("...three times, but with a new httpclient");
client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
TestLogs.LogInformation("Querying an onion address which can't be found");
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync("http://dwoduwoi.onion/"));
2022-01-25 04:27:44 +01:00
2022-01-14 09:50:29 +01:00
TestLogs.LogInformation("Querying valid onion but unreachable");
2022-01-25 04:27:44 +01:00
using var cts = new CancellationTokenSource(10_000);
try
{
await client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/", cts.Token);
}
catch (HttpRequestException)
{
}
catch when (cts.Token.IsCancellationRequested) // Ignore timeouts
{
}
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
public async Task CanRescanWallet()
2018-10-26 16:07:39 +02:00
{
2022-01-14 09:50:29 +01:00
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<UIWalletsController>();
var walletId = new WalletId(acc.StoreId, "BTC");
acc.IsAdmin = true;
walletController = acc.GetController<UIWalletsController>();
var rescan =
Assert.IsType<RescanWalletModel>(Assert
.IsType<ViewResult>(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<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
2022-01-14 09:50:29 +01:00
Assert.Empty(transactions.Transactions);
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
while (true)
2018-10-26 16:07:39 +02:00
{
2022-01-14 09:50:29 +01:00
rescan = Assert.IsType<RescanWalletModel>(Assert
.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
if (rescan.Progress == null && rescan.LastSuccess != null)
2018-10-26 16:07:39 +02:00
{
2022-01-14 09:50:29 +01:00
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);
2018-10-26 16:07:39 +02:00
}
}
2022-01-14 09:50:29 +01:00
Assert.Null(rescan.PreviousError);
Assert.NotNull(rescan.TimeOfScan);
Assert.Equal(1, rescan.LastSuccess.Found);
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
2022-01-14 09:50:29 +01:00
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<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello-pouet"));
Assert.IsType<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test"));
Assert.IsType<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2"));
Assert.IsType<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
2022-01-14 09:50:29 +01:00
tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Tags.Select(l => l.Text));
Assert.Contains("test2", tx.Tags.Select(l => l.Text));
Assert.Equal(2, tx.Tags.GroupBy(l => l.Color).Count());
2022-01-14 09:50:29 +01:00
Assert.IsType<RedirectToActionResult>(
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
2022-01-14 09:50:29 +01:00
tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Tags.Select(l => l.Text));
Assert.DoesNotContain("test2", tx.Tags.Select(l => l.Text));
Assert.Single(tx.Tags.GroupBy(l => l.Color));
2018-10-26 16:07:39 +02:00
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanListInvoices()
2018-05-06 06:16:39 +02:00
{
2022-01-14 09:50:29 +01:00
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
2018-05-06 06:16:39 +02:00
{
2022-01-14 09:50:29 +01:00
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
2018-05-06 06:16:39 +02:00
2022-01-14 09:50:29 +01:00
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);
});
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")}");
2018-05-06 06:16:39 +02:00
}
[Fact]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanGetRates()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
var rateController = acc.GetController<BitpayRateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController
.GetBaseCurrencyRates("BTC", default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
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<DataWrapper<Rate[]>>();
// We don't have any default currencies, so this should be failing
Assert.Null(GetRatesResult?.Data);
var store = acc.GetController<UIStoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates()).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
await store.Rates(ratesVM);
store = acc.GetController<UIStoresController>();
rateController = acc.GetController<BitpayRateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// 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<DataWrapper<Rate>>();
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
2024-01-18 01:47:39 +01:00
acc.BitPay.GetRates();
2022-01-14 09:50:29 +01:00
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)
2018-05-06 06:16:39 +02:00
{
var result =
(InvoicesModel)((ViewResult)acc.GetController<UIInvoiceController>(storeId is not null)
.ListInvoices(new InvoicesModel { SearchTerm = filter, StoreId = storeId }).Result).Model;
2018-05-06 06:16:39 +02:00
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
}
// [Fact(Timeout = TestTimeout)]
[Fact()]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanRBFPayment()
2017-11-06 09:31:02 +01:00
{
2022-01-14 09:50:29 +01:00
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[]
2017-11-06 09:31:02 +01:00
{
invoice.BitcoinAddress, payment1.ToString(), null, //comment
2017-11-06 09:31:02 +01:00
null, //comment_to
false, //subtractfeefromamount
true, //replaceable
2022-01-14 09:50:29 +01:00
}).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(() =>
{
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);
2022-01-14 09:50:29 +01:00
});
2017-11-06 09:31:02 +01:00
2022-01-14 09:50:29 +01:00
var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1));
foreach (var input in tx.Inputs)
{
input.ScriptSig = Script.Empty; //Strip signatures
}
2017-11-06 09:31:02 +01:00
2022-01-14 09:50:29 +01:00
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
2022-01-14 09:50:29 +01:00
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);
}
2018-10-28 15:43:48 +01:00
2022-01-14 09:50:29 +01:00
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());
});
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
var invoice2Address =
BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
uint256 invoice2tx1Id =
await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, new NBitcoin.RPC.SendToAddressParameters()
{
2022-01-14 09:50:29 +01:00
Replaceable = true
});
2022-01-14 09:50:29 +01:00
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
}
2022-01-14 09:50:29 +01:00
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);
});
2022-01-14 09:50:29 +01:00
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
2022-01-14 09:50:29 +01:00
}).ResultString);
TestLogs.LogInformation($"Paid {tx1}");
TestUtils.Eventually(() =>
{
2022-01-14 09:50:29 +01:00
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[]
{
tx1.ToString(),
2022-01-14 09:50:29 +01:00
}).Result["txid"].Value<string>());
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());
}
);
2017-11-06 09:31:02 +01:00
}
2020-11-09 08:48:45 +01:00
[Fact(Timeout = TestUtils.TestTimeout)]
2020-09-16 11:49:47 +02:00
[Trait("Integration", "Integration")]
public async Task CanSaveKeyPathForOnChainPayments()
{
2021-11-22 09:16:08 +01:00
using var tester = CreateServerTester();
2020-09-16 11:49:47 +02:00
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.RegisterDerivationSchemeAsync("BTC");
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.01m, "BTC"));
await tester.WaitForEvent<InvoiceEvent>(async () =>
{
2024-01-18 01:47:39 +01:00
await tester.ExplorerNode.SendToAddressAsync(
2020-09-16 11:49:47 +02:00
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest),
Money.Coins(0.01m));
});
2020-11-09 08:48:45 +01:00
2020-09-16 11:49:47 +02:00
var payments = Assert.IsType<InvoiceDetailsModel>(
2022-01-07 04:32:00 +01:00
Assert.IsType<ViewResult>(await user.GetController<UIInvoiceController>().Invoice(invoice.Id)).Model)
2020-09-16 11:49:47 +02:00
.Payments;
Assert.Single(payments);
var paymentData = payments.First().GetCryptoPaymentData() as BitcoinLikePaymentData;
Assert.NotNull(paymentData.KeyPath);
}
2017-11-06 09:31:02 +01:00
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
{
2022-01-14 09:50:29 +01:00
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())
{
2022-01-14 09:50:29 +01:00
await req;
}
2022-01-14 09:50:29 +01:00
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)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task TestAccessBitpayAPI()
{
2022-01-14 09:50:29 +01:00
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));
// Test request pairing code client side
var storeController = user.GetController<UIStoresController>();
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));
HttpClient client = new HttpClient();
var token = (await bitpay.GetAccessTokenAsync(Facade.Merchant)).Value;
var getRates = tester.PayTester.ServerUri.AbsoluteUri + $"rates/?cryptoCode=BTC&token={token}";
var req = new HttpRequestMessage(HttpMethod.Get, getRates);
req.Headers.Add("x-signature", NBitpayClient.Extensions.BitIdExtensions.GetBitIDSignature(k, getRates, null));
req.Headers.Add("x-identity", k.PubKey.ToHex());
var resp = await client.SendAsync(req);
resp.EnsureSuccessStatusCode();
2022-01-14 09:50:29 +01:00
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.NotEqual(apiKey, apiKey2);
////////
apiKey = apiKey2;
// Can create an invoice with this new API Key
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);
//
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseAnyoneCanCreateInvoice()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
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("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)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanTweakRate()
2018-01-17 07:59:31 +01:00
{
2022-01-14 09:50:29 +01:00
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);
2018-01-17 07:59:31 +01:00
2022-01-14 09:50:29 +01:00
var storeController = user.GetController<UIStoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
await storeController.Rates(vm);
2018-01-17 07:59:31 +01:00
2022-01-14 09:50:29 +01:00
var invoice2 = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
2018-01-17 07:59:31 +01:00
2022-01-14 09:50:29 +01:00
var expectedRate = 5000.0m * 0.6m;
var expectedCoins = invoice2.Price / expectedRate;
Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
2018-01-17 07:59:31 +01:00
}
2021-08-03 10:03:00 +02:00
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateTopupInvoices()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
2022-01-14 09:50:29 +01:00
user.RegisterDerivationScheme("BTC");
await tester.ExplorerNode.EnsureGenerateAsync(1);
2022-01-14 09:50:29 +01:00
var rng = new Random();
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
2021-08-03 10:03:00 +02:00
{
2022-01-14 09:50:29 +01:00
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);
2021-08-03 10:03:00 +02:00
}
}
private async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
2021-08-03 10:03:00 +02:00
{
var cashCow = tester.ExplorerNode;
// First we try payment with a merchant having only BTC
var client = await user.CreateClient();
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = null,
Currency = "USD"
});
Assert.Equal(0m, invoice.Amount);
Assert.Equal(InvoiceType.TopUp, invoice.Type);
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
var paid = btcSent;
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network);
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
if (networkFeeMode != NetworkFeeMode.Always)
{
networkFee = 0.0m;
}
await cashCow.SendToAddressAsync(invoiceAddress, paid);
2021-08-03 10:03:00 +02:00
await TestUtils.EventuallyAsync(async () =>
{
try
{
var bitpayinvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.NotEqual(0.0m, bitpayinvoice.Price);
var due = Money.Parse(bitpayinvoice.CryptoInfo[0].CryptoPaid);
Assert.Equal(paid, due);
Assert.Equal(expectedPriceWithoutNetworkFee - networkFee * bitpayinvoice.Rate, bitpayinvoice.Price);
Assert.Equal(Money.Zero, bitpayinvoice.BtcDue);
Assert.Equal("paid", bitpayinvoice.Status);
Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString());
// Check if we index by price correctly once we know it
var invoices = await client.GetInvoices(user.StoreId, textSearch: $"{bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture)}");
Assert.Contains(invoices, inv => inv.Id == bitpayinvoice.Id);
}
catch (JsonSerializationException)
{
Assert.Fail("The bitpay's amount is not set");
2021-08-03 10:03:00 +02:00
}
});
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanModifyRates()
2018-05-03 18:46:52 +02:00
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var store = user.GetController<UIStoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
rateVm.Spread = 10;
store = user.GetController<UIStoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
store = user.GetController<UIStoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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<RatesViewModel>(Assert.IsType<ViewResult>(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 = bitpay(DOGE_BTC) * BTC_X;\n" +
2022-01-14 09:50:29 +01:00
"X_CAD = ndax(X_CAD);\n" +
"X_X = coingecko(X_X);";
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test"))
.Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
store = user.GetController<UIStoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
2018-05-03 18:46:52 +02:00
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseDefaultCurrency()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
user.RegisterDerivationScheme("BTC");
await user.ModifyPayment(s =>
{
2022-01-14 09:50:29 +01:00
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 bitpay api
var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice());
Assert.Equal("EUR", invoice2.Currency);
// via UI
var controller = user.GetController<UIInvoiceController>();
2024-01-18 01:47:39 +01:00
await controller.CreateInvoice();
2022-01-14 09:50:29 +01:00
(await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType<RedirectToActionResult>();
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<UIWalletsController>();
var walletSend = await walletController.WalletSend(new WalletId(user.StoreId, "BTC")).AssertViewModelAsync<WalletSendModel>();
Assert.Equal("EUR", walletSend.Fiat);
}
[Fact]
[Trait("Lightning", "Lightning")]
2018-10-28 14:46:03 +01:00
public async Task CanSetPaymentMethodLimits()
2018-04-03 10:39:28 +02:00
{
2022-01-14 09:50:29 +01:00
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();
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().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<RedirectToActionResult>(user.GetController<UIStoresController>().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);
2018-04-03 10:39:28 +02:00
2022-01-14 09:50:29 +01:00
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<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().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<RedirectToActionResult>(user.GetController<UIStoresController>().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<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(lnMethod, checkout.PaymentMethodId);
// If we change store's default, it should change the checkout's default
vm.DefaultPaymentMethod = btcMethod;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
.Result);
checkout = (await user.GetController<UIInvoiceController>().Checkout(invoice.Id)).AssertViewModel<PaymentModel>();
Assert.Equal(btcMethod, checkout.PaymentMethodId);
}
2018-04-03 10:39:28 +02:00
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetUnifiedQrCode()
{
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
var invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
2022-01-14 09:50:29 +01:00
// validate that invoice data model doesn't have lightning string initially
var res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodFirst = Assert.IsType<PaymentModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<LightningSettingsViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().LightningSettings(user.StoreId, cryptoCode)).Model
2022-01-14 09:50:29 +01:00
);
vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>(
user.GetController<UIStoresController>().LightningSettings(vm).Result
);
// validate that QR code now has both onchain and offchain payment urls
res = await user.GetController<UIInvoiceController>().Checkout(invoice.Id);
var paymentMethodUnified = Assert.IsType<PaymentModel>(
2022-01-14 09:50:29 +01:00
Assert.IsType<ViewResult>(res).Model
);
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
Assert.StartsWith("bitcoin:BCRT", paymentMethodUnified.InvoiceBitcoinUrlQR);
Assert.Contains("&lightning=lnbcrt", paymentMethodUnified.InvoiceBitcoinUrl);
Assert.Contains("&lightning=LNBCRT", paymentMethodUnified.InvoiceBitcoinUrlQR);
// Check correct casing: Addresses in payment URI need to be …
// - lowercase in link version
// - uppercase in QR version
2023-04-10 04:07:03 +02:00
2022-01-14 09:50:29 +01:00
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
// But we're proceeding with BECH32 being uppercase
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress}", paymentMethodUnified.InvoiceBitcoinUrl.Split('?')[0]);
Assert.Equal($"bitcoin:{paymentMethodUnified.BtcAddress.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR.Split('?')[0]);
// Fallback lightning invoice should be uppercase inside the QR code, lowercase in payment URI
var lightningFallback = paymentMethodUnified.InvoiceBitcoinUrl.Split(new[] { "&lightning=" }, StringSplitOptions.None)[1];
Assert.NotNull(lightningFallback);
Assert.Contains($"&lightning={lightningFallback}", paymentMethodUnified.InvoiceBitcoinUrl);
Assert.Contains($"&lightning={lightningFallback.ToUpperInvariant()}", paymentMethodUnified.InvoiceBitcoinUrlQR);
}
2018-04-03 10:39:28 +02:00
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanSetPaymentMethodLimitsLightning()
{
2022-01-14 09:50:29 +01:00
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);
2023-04-25 01:51:38 +02:00
user.SetLNUrl(cryptoCode, false);
2022-01-14 09:50:29 +01:00
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
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<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
.Result);
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);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode);
2023-04-25 01:51:38 +02:00
user.SetLNUrl(cryptoCode, true);
2022-01-14 09:50:29 +01:00
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().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);
2022-01-14 09:50:29 +01:00
// Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available
Assert.Throws<BitPayException>(() => user.BitPay.CreateInvoice(
new Invoice
{
Price = 2.5m,
Currency = "USD"
}, Facade.Merchant));
2018-04-03 10:39:28 +02:00
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task PosDataParser_ParsesCorrectly_Slower()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
2018-11-30 09:34:43 +01:00
2022-01-14 09:50:29 +01:00
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
2022-01-14 09:50:29 +01:00
var testCases =
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{("{ \"key\": \"value\"}", new Dictionary<string, object>() {{"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
2022-01-14 09:50:29 +01:00
};
2022-01-14 09:50:29 +01:00
foreach (var valueTuple in testCases)
{
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC") { PosData = valueTuple.input });
var result = await controller.Invoice(invoice.Id);
var viewModel = result.AssertViewModel<InvoiceDetailsModel>();
Assert.Equal(valueTuple.expectedOutput, viewModel.AdditionalData["posData"]);
}
}
2022-01-14 09:50:29 +01:00
[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<NetworkFeeMode>())
2018-11-30 09:34:43 +01:00
{
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.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;
}
2018-11-30 09:34:43 +01:00
2022-01-14 09:50:29 +01:00
var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC);
2018-11-30 09:34:43 +01:00
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
2018-12-18 18:56:12 +01:00
2022-01-14 09:50:29 +01:00
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));
2022-01-14 09:50:29 +01:00
await TestUtils.EventuallyAsync(async () =>
{
2022-01-14 09:50:29 +01:00
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<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
switch (networkFeeMode)
{
case NetworkFeeMode.Never:
Assert.Equal(0.0m, nextNetworkFee);
break;
2022-01-14 09:50:29 +01:00
case NetworkFeeMode.MultiplePaymentsOnly:
case NetworkFeeMode.Always:
Assert.NotEqual(0.0m, nextNetworkFee);
break;
}
2022-01-14 09:50:29 +01:00
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);
});
}
}
2018-11-30 09:34:43 +01:00
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task CanCreateAndDeleteApps()
2018-04-03 09:53:55 +02:00
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
var user2 = tester.NewAccount();
await user2.GrantAccessAsync();
var stores = user.GetController<UIStoresController>();
2022-01-14 09:50:29 +01:00
var apps = user.GetController<UIAppsController>();
var apps2 = user2.GetController<UIAppsController>();
2022-07-18 20:51:53 +02:00
var pos = user.GetController<UIPointOfSaleController>();
var appType = PointOfSaleAppType.AppType;
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId, appType)).Model);
Assert.Equal(appType, vm.SelectedAppType);
2022-01-14 09:50:29 +01:00
Assert.Null(vm.AppName);
vm.AppName = "test";
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
2022-01-14 09:50:29 +01:00
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
var app = appList.Apps[0];
2022-07-18 20:51:53 +02:00
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
2022-01-14 09:50:29 +01:00
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
2023-05-26 16:49:32 +02:00
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
2022-01-14 09:50:29 +01:00
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(stores.Dashboard), redirectToAction.ActionName);
2022-01-14 09:50:29 +01:00
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
2018-04-03 09:53:55 +02:00
}
[Fact(Timeout = LongRunningTestTimeout)]
2019-01-15 14:12:29 +01:00
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
2019-10-07 09:04:25 +02:00
public async Task CanCreateStrangeInvoice()
2019-01-15 14:12:29 +01:00
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
var btcpayClient = await user.CreateClient();
2022-01-14 09:50:29 +01:00
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
2022-01-14 09:50:29 +01:00
// This should fail, the amount is too low to be above the dust limit of bitcoin
var ex = Assert.Throws<BitPayException>(() => user.BitPay.CreateInvoice(
new Invoice()
{
2022-01-14 09:50:29 +01:00
Price = 0.000000012m,
Currency = "USD",
2022-01-14 09:50:29 +01:00
FullNotifications = true,
ExpirationTime = expiration
}, Facade.Merchant));
Assert.Contains("dust threshold", ex.Message);
await user.RegisterLightningNodeAsync("BTC");
var invoice1 = user.BitPay.CreateInvoice(
new Invoice()
{
2022-01-14 09:50:29 +01:00
Price = 0.000000012m,
Currency = "USD",
FullNotifications = true,
ExpirationTime = expiration
}, Facade.Merchant);
2022-01-14 09:50:29 +01:00
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);
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Amount = GreenfieldConstants.MaxAmount,
Currency = "USD"
});
var repo = tester.PayTester.GetService<InvoiceRepository>();
var entity = (await repo.GetInvoice(invoice6.Id));
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
entity.GetPaymentMethods().First().Calculate();
// Shouldn't be possible as we clamp the value, but existing invoice may have that
entity.Price = decimal.MaxValue;
entity.GetPaymentMethods().First().Calculate();
2019-01-15 14:12:29 +01:00
}
[Fact(Timeout = LongRunningTestTimeout)]
2018-10-28 14:51:02 +01:00
[Trait("Integration", "Integration")]
2019-10-07 09:04:25 +02:00
public async Task InvoiceFlowThroughDifferentStatesCorrectly()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
2022-01-14 09:50:29 +01:00
user.RegisterDerivationScheme("BTC");
await user.SetupWebhook();
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
2022-01-14 09:50:29 +01:00
{
Price = 5000.0m,
TaxIncluded = 1000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().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(() =>
{
2022-01-14 09:50:29 +01:00
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
2022-01-14 09:50:29 +01:00
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();
2022-01-14 09:50:29 +01:00
Assert.Single(textSearchResult);
});
invoice = await user.BitPay.GetInvoiceAsync(invoice.Id, Facade.Merchant);
2022-01-14 09:50:29 +01:00
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);
2022-01-14 09:50:29 +01:00
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)));
2022-01-14 09:50:29 +01:00
var firstPayment = Money.Coins(0.04m);
var txFee = Money.Zero;
var cashCow = tester.ExplorerNode;
2022-01-14 09:50:29 +01:00
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment);
2022-01-14 09:50:29 +01:00
var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
2022-01-14 09:50:29 +01:00
Money secondPayment = Money.Zero;
2022-01-14 09:50:29 +01:00
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.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
Assert.True(IsMapped(invoice, ctx));
2022-01-14 09:50:29 +01:00
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
});
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
2022-01-14 09:50:29 +01:00
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));
2022-01-14 09:50:29 +01:00
Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value);
});
await cashCow.GenerateAsync(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
2022-01-14 09:50:29 +01:00
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
await cashCow.GenerateAsync(5); //Now should be complete
2022-01-14 09:50:29 +01:00
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
Assert.NotEqual(0.0m, localInvoice.Rate);
});
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
2022-01-14 09:50:29 +01:00
{
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 = await cashCow.SendToAddressAsync(invoiceAddress, invoice.BtcDue + Money.Coins(1));
2022-01-14 09:50:29 +01:00
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()
{
2022-01-14 09:50:29 +01:00
StoreId = new[] { user.StoreId },
TextSearch = txId.ToString()
}).GetAwaiter().GetResult();
Assert.Single(textSearchResult);
});
await cashCow.GenerateAsync(2);
2022-01-14 09:50:29 +01:00
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);
});
2022-01-14 09:50:29 +01:00
// Test on the webhooks
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
c =>
{
Assert.False(c.ManuallyMarked);
Assert.True(c.OverPaid);
2022-01-14 09:50:29 +01:00
});
user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
c =>
{
2022-01-14 09:50:29 +01:00
Assert.True(c.OverPaid);
});
2022-01-14 09:50:29 +01:00
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(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);
2020-11-13 08:28:15 +01:00
2022-01-14 09:50:29 +01:00
});
user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(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()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<UIServerController>();
2024-01-18 01:47:39 +01:00
Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
2018-11-30 09:34:43 +01:00
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
2022-01-14 09:50:29 +01:00
var accountController = tester.PayTester.GetController<UIAccountController>();
2022-01-14 09:50:29 +01:00
//no 2fa or fido2 enabled, login should work
Assert.Equal(nameof(UIHomeController.Index),
Assert.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
})).ActionName);
2022-01-14 09:50:29 +01:00
var listController = user.GetController<UIManageController>();
2022-01-14 10:34:30 +01:00
var manageController = user.GetController<UIFido2Controller>();
2019-11-07 10:35:47 +01:00
2022-01-14 09:50:29 +01:00
//by default no fido2 devices available
Assert.Empty(Assert
.IsType<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
Assert.IsType<CredentialCreateOptions>(Assert
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel
{
Name = "label"
})).Model);
2022-01-14 09:50:29 +01:00
//sending an invalid response model back to server, should error out
Assert.IsType<RedirectToActionResult>(await manageController.CreateResponse("sdsdsa", "sds"));
var statusModel = manageController.TempData.GetStatusMessageModel();
Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity);
2022-01-14 09:50:29 +01:00
var contextFactory = tester.PayTester.GetService<ApplicationDbContextFactory>();
2022-01-14 09:50:29 +01:00
//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()
{
2022-01-14 09:50:29 +01:00
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<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
}
2022-01-14 09:50:29 +01:00
//check if we are showing the fido2 login screen now
var secondLoginResult = Assert.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
}));
Assert.Equal("SecondaryLogin", secondLoginResult.ViewName);
var vm = Assert.IsType<SecondaryLoginViewModel>(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()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var url = tester.PayTester.ServerUri.AbsoluteUri;
2022-01-14 09:50:29 +01:00
// 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,*/*");
2022-01-14 09:50:29 +01:00
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");
2022-01-14 09:50:29 +01:00
// no onion location for other mime types
using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*");
2022-01-14 09:50:29 +01:00
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)
{
2018-04-10 12:07:57 +02:00
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return (ctx.AddressInvoices.Where(i => i.InvoiceDataId == invoice.Id).ToArrayAsync().GetAwaiter()
.GetResult())
.Where(i => i.GetAddress() == h).Any();
}
class MockVersionFetcher : GithubVersionFetcher
{
public const string MOCK_NEW_VERSION = "9.9.9.9";
public override Task<string> Fetch(CancellationToken cancellation)
{
return Task.FromResult(MOCK_NEW_VERSION);
}
public MockVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options, ILogger<GithubVersionFetcher> logger, SettingsRepository settingsRepository, BTCPayServerEnvironment environment, NotificationSender notificationSender) : base(httpClientFactory, options, logger, settingsRepository, environment, notificationSender)
{
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCheckForNewVersion()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
2022-01-14 09:50:29 +01:00
var acc = tester.NewAccount();
acc.GrantAccess(true);
2022-01-14 09:50:29 +01:00
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { CheckForNewVersions = true });
2022-01-14 09:50:29 +01:00
var mockEnv = tester.PayTester.GetService<BTCPayServerEnvironment>();
var mockSender = tester.PayTester.GetService<Services.Notifications.NotificationSender>();
var svc = new MockVersionFetcher(tester.PayTester.GetService<IHttpClientFactory>(),
tester.PayTester.GetService<BTCPayServerOptions>(),
tester.PayTester.GetService<ILogger<GithubVersionFetcher>>(),
settings,
mockEnv,
mockSender);
await svc.Do(CancellationToken.None);
2022-01-14 09:50:29 +01:00
// since last version present in database was null, it should've been updated with version mock returned
var lastVersion = await settings.GetSettingAsync<NewVersionCheckerDataHolder>();
Assert.Equal(MockVersionFetcher.MOCK_NEW_VERSION, lastVersion.LastVersion);
2022-01-14 09:50:29 +01:00
// we should also have notification in UI
var ctrl = acc.GetController<UINotificationsController>();
var newVersion = MockVersionFetcher.MOCK_NEW_VERSION;
2022-01-14 09:50:29 +01:00
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
2022-01-14 09:50:29 +01:00
Assert.True(vm.Skip == 0);
Assert.True(vm.Count == 50);
Assert.Null(vm.Total);
2022-01-14 09:50:29 +01:00
Assert.True(vm.Items.Count == 1);
2022-01-14 09:50:29 +01:00
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 CanFixMappedDomainAppType()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
const string id = "BTCPayServer.Services.PoliciesSettings";
using (var ctx = f.CreateContext())
{
// remove existing policies setting
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
if (setting != null) ctx.Settings.Remove(setting);
// create legacy policies setting that needs migration
setting = new SettingData { Id = id, Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString() };
ctx.Settings.Add(setting);
await ctx.SaveChangesAsync();
}
await RestartMigration(tester);
using (var ctx = f.CreateContext())
{
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
var o = JObject.Parse(setting.Value);
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
Assert.Equal("PointOfSale", o["AppType"].Value<string>());
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()
{
2022-01-14 09:50:29 +01:00
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());
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.Null(lnMethod.GetExternalLightningUrl());
// Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("LightningChargeUrl", "http://mycharge.com/"),
new JProperty("Username", "usr"),
new JProperty("Password", "pass"),
new JProperty("CryptoCode", "BTC"),
new JProperty("PaymentId", "someshit"),
})
}.ToString();
2022-01-14 09:50:29 +01:00
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
2022-01-14 09:50:29 +01:00
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.NotNull(lnMethod.GetExternalLightningUrl());
2022-01-14 09:50:29 +01:00
var url = lnMethod.GetExternalLightningUrl();
2024-01-18 01:47:39 +01:00
LightningConnectionStringHelper.ExtractValues(url, out var connType);
Assert.Equal(LightningConnectionType.Charge, connType);
var client = Assert.IsType<ChargeClient>(tester.PayTester.GetService<LightningClientFactoryService>()
.Create(url, tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")));
var auth = Assert.IsType<ChargeAuthentication.UserPasswordAuthentication>(client.ChargeAuthentication);
Assert.Equal("pass", auth.NetworkCredential.Password);
Assert.Equal("usr", auth.NetworkCredential.UserName);
2022-01-14 09:50:29 +01:00
// Test if lightning connection strings get migrated to internal
store.DerivationStrategies = new JObject()
{
new JProperty("BTC_LightningLike", new JObject()
{
new JProperty("CryptoCode", "BTC"),
new JProperty("LightningConnectionString", tester.PayTester.IntegratedLightning),
})
}.ToString();
2022-01-14 09:50:29 +01:00
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
Assert.True(lnMethod.IsInternalNode);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
[Obsolete]
public async Task CanDoLabelMigrations()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var dbf = tester.PayTester.GetService<ApplicationDbContextFactory>();
int walletCount = 1000;
var wallet = "walletttttttttttttttttttttttttttt";
using (var db = dbf.CreateContext())
{
for (int i = 0; i < walletCount; i++)
{
var walletData = new WalletData() { Id = $"S-{wallet}{i}-BTC" };
walletData.Blob = ZipUtils.Zip("{\"LabelColors\": { \"label1\" : \"black\", \"payout\":\"green\" }}");
db.Wallets.Add(walletData);
}
await db.SaveChangesAsync();
}
uint256 firstTxId = null;
using (var db = dbf.CreateContext())
{
int transactionCount = 10_000;
for (int i = 0; i < transactionCount; i++)
{
var txId = RandomUtils.GetUInt256();
var wt = new WalletTransactionData()
{
WalletDataId = $"S-{wallet}{i % walletCount}-BTC",
TransactionId = txId.ToString(),
};
firstTxId ??= txId;
if (i != 10)
wt.Blob = ZipUtils.Zip("{\"Comment\":\"test\"}");
if (i % 1240 != 0)
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"}]";
}
else if (i == 0)
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}, " +
"{\"type\":\"payout\", \"text\":\"payout\", \"pullPaymentPayouts\":{\"pp1\":[\"p1\",\"p2\"],\"pp2\":[\"p3\"]}}]";
}
else
{
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}]";
}
db.WalletTransactions.Add(wt);
}
await db.SaveChangesAsync();
}
await RestartMigration(tester);
var migrator = tester.PayTester.GetService<IEnumerable<IHostedService>>().OfType<DbMigrationsHostedService>().First();
await migrator.MigratedTransactionLabels(0);
var walletRepo = tester.PayTester.GetService<WalletRepository>();
var wi1 = await walletRepo.GetWalletLabels(new WalletId($"{wallet}0", "BTC"));
Assert.Equal(3, wi1.Length);
Assert.Contains(wi1, o => o.Label == "label1" && o.Color == "black");
Assert.Contains(wi1, o => o.Label == "labelo0" && o.Color == "#000");
Assert.Contains(wi1, o => o.Label == "payout" && o.Color == "green");
var txInfo = await walletRepo.GetWalletTransactionsInfo(new WalletId($"{wallet}0", "BTC"), new[] { firstTxId.ToString() });
Assert.Equal("test", txInfo.Values.First().Comment);
// Should have the 2 raw labels, and one legacy label for payouts
Assert.Equal(3, txInfo.Values.First().LegacyLabels.Count);
var payoutLabel = txInfo.Values.First().LegacyLabels.Select(l => l.Value).OfType<PayoutLabel>().First();
Assert.Equal(2, payoutLabel.PullPaymentPayouts.Count);
Assert.Equal(2, payoutLabel.PullPaymentPayouts["pp1"].Count);
Assert.Single(payoutLabel.PullPaymentPayouts["pp2"]);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoRateSourceMigration()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.CreateStoreAsync();
var db = tester.PayTester.GetService<ApplicationDbContextFactory>();
using var ctx = db.CreateContext();
var store = (await ctx.Stores.AsNoTracking().ToListAsync())[0];
var b = store.GetStoreBlob();
b.PreferredExchange = "coinaverage";
store.SetStoreBlob(b);
await ctx.SaveChangesAsync();
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\"='20230123062447_migrateoldratesource'");
await ctx.Database.MigrateAsync();
store = (await ctx.Stores.AsNoTracking().ToListAsync())[0];
b = store.GetStoreBlob();
Assert.Equal("coingecko", b.PreferredExchange);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoInvoiceMigrations()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
2022-01-14 09:50:29 +01:00
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
await acc.CreateStoreAsync();
await acc.RegisterDerivationSchemeAsync("BTC");
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
2022-01-14 09:50:29 +01:00
var blob = store.GetStoreBlob();
var serializer = new Serializer(null);
2022-01-14 09:50:29 +01:00
blob.AdditionalData = new Dictionary<string, JToken>();
blob.AdditionalData.Add("rateRules", JToken.Parse(
serializer.ToString(new List<MigrationStartupTask.RateRule_Obsolete>()
{
new MigrationStartupTask.RateRule_Obsolete()
{
Multiplier = 2
}
2022-01-14 09:50:29 +01:00
})));
blob.AdditionalData.Add("walletKeyPathRoots", JToken.Parse(
serializer.ToString(new Dictionary<string, string>()
{
{
new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(),
new KeyPath("44'/0'/0'").ToString()
}
2022-01-14 09:50:29 +01:00
})));
2022-01-14 09:50:29 +01:00
blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse(
serializer.ToString((bool?)true)));
2022-01-14 09:50:29 +01:00
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 = 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'"));
}
private static async Task RestartMigration(ServerTester tester)
{
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
}
[Fact(Timeout = LongRunningTestTimeout)]
2020-12-08 08:12:29 +01:00
[Trait("Integration", "Integration")]
public async Task EmailSenderTests()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
2020-12-08 08:12:29 +01:00
2022-01-14 09:50:29 +01:00
var acc = tester.NewAccount();
acc.GrantAccess(true);
2021-12-31 08:59:02 +01:00
2022-01-14 09:50:29 +01:00
var settings = tester.PayTester.GetService<SettingsRepository>();
var emailSenderFactory = tester.PayTester.GetService<EmailSenderFactory>();
2020-12-08 08:12:29 +01:00
2022-01-14 09:50:29 +01:00
Assert.Null(await Assert.IsType<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings());
Assert.Null(await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
2021-12-31 08:59:02 +01:00
2020-12-08 08:12:29 +01:00
2022-01-14 09:50:29 +01:00
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<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Equal("admin@admin.com", (await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
2021-12-31 08:59:02 +01:00
2022-01-14 09:50:29 +01:00
await settings.UpdateSetting(new PoliciesSettings() { DisableStoresToUseServerEmailSettings = true });
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
Assert.Null(await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
2020-12-08 08:12:29 +01:00
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings()
2022-01-14 09:50:29 +01:00
{
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<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
2020-12-08 08:12:29 +01:00
}
[Fact(Timeout = TestUtils.TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanConfigureStorage()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
2022-01-14 09:50:29 +01:00
//Once we select a provider, redirect to its view
var localResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.FileSystem
}));
Assert.Equal(nameof(UIServerController.StorageProvider), localResult.ActionName);
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
2022-01-14 09:50:29 +01:00
var AmazonS3result = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AmazonS3
}));
Assert.Equal(nameof(UIServerController.StorageProvider), AmazonS3result.ActionName);
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
2022-01-14 09:50:29 +01:00
var GoogleResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.GoogleCloudStorage
}));
Assert.Equal(nameof(UIServerController.StorageProvider), GoogleResult.ActionName);
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
2022-01-14 09:50:29 +01:00
var AzureResult = Assert
.IsType<RedirectToActionResult>(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
//Let's try and cheat and go to an invalid storage provider config
Assert.Equal(nameof(Storage), (Assert
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
.ActionName));
//ok no more messing around, let's configure this shit.
2022-01-14 09:50:29 +01:00
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
//local file system does not need config, easy days!
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
//ok cool, let's see if this got set right
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(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<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
//awesome, now let's see if the files result says we're all set up
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
Assert.True(viewFilesViewModel.StorageConfigured);
Assert.Empty(viewFilesViewModel.Files);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
{
2022-01-14 09:50:29 +01:00
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
2023-08-05 10:44:59 +02:00
await user.GrantAccessAsync();
2022-01-14 09:50:29 +01:00
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
2022-01-14 09:50:29 +01:00
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
2022-01-14 09:50:29 +01:00
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
2022-01-14 09:50:29 +01:00
await CanUploadRemoveFiles(controller);
}
2022-01-07 04:32:00 +01:00
internal static async Task CanUploadRemoveFiles(UIServerController controller)
{
var fileContent = "content";
List<IFormFile> fileList = new List<IFormFile>();
fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent));
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFiles(fileList));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds"));
string[] uploadFileList = (string[])uploadFormFileResult.RouteValues["fileIds"];
var fileId = uploadFileList[0];
Assert.Equal("Files", uploadFormFileResult.ActionName);
//check if file was uploaded and saved in db
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId));
Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]);
//verify file is available and the same
2021-12-27 05:46:31 +01:00
using var net = new HttpClient();
var data = await net.GetStringAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId]));
Assert.Equal(fileContent, data);
//create a temporary link to file
2024-01-18 01:47:39 +01:00
Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
2023-08-05 10:44:59 +02:00
new UIServerController.CreateTemporaryFileUrlViewModel
{
IsDownload = true,
TimeAmount = 1,
2022-01-07 04:32:00 +01:00
TimeType = UIServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes
}));
var statusMessageModel = controller.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessageModel);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);
var index = statusMessageModel.Html.IndexOf("target='_blank'>");
var url = statusMessageModel.Html.Substring(index)
.Replace("</a>", string.Empty)
.Replace("target='_blank'>", string.Empty);
//verify tmpfile is available and the same
2021-12-27 05:46:31 +01:00
data = await net.GetStringAsync(new Uri(url));
Assert.Equal(fileContent, data);
//delete file
Assert.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId));
statusMessageModel = controller.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessageModel);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanCreateReports()
{
using var tester = CreateServerTester();
tester.ActivateLightning();
tester.DeleteStore = false;
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var acc = tester.NewAccount();
await acc.GrantAccessAsync();
await acc.MakeAdmin();
acc.RegisterDerivationScheme("BTC", importKeysToNBX: true);
acc.RegisterLightningNode("BTC");
await acc.ReceiveUTXO(Money.Coins(1.0m));
var client = await acc.CreateClient();
var posController = acc.GetController<UIPointOfSaleController>();
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Static",
DefaultView = Client.Models.PosViewType.Static,
Template = new PointOfSaleSettings().Template
});
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
var invoiceId = GetInvoiceId(resp);
await acc.PayOnChain(invoiceId);
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Cart",
DefaultView = Client.Models.PosViewType.Cart,
Template = new PointOfSaleSettings().Template
});
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 2
},
new JObject()
{
["id"] = "black-tea",
["count"] = 1
},
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnBOLT11(invoiceId);
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 5
}
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnLNUrl(invoiceId);
await acc.CreateLNAddress();
await acc.PayOnLNAddress();
var report = await GetReport(acc, new() { ViewName = "Payments" });
// 1 payment on LN Address
// 1 payment on LNURL
// 1 payment on BOLT11
// 1 payment on chain
Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress");
var paymentTypeIndex = report.GetIndex("PaymentType");
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>())
.ToDictionary(d => d.Key);
Assert.Equal(3, paymentTypes["Lightning"].Count());
Assert.Single(paymentTypes["On-Chain"]);
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
2023-11-09 10:26:00 +01:00
report = await GetReport(acc, new() { ViewName = "Wallets" });
var txIdIndex = report.GetIndex("TransactionId");
var balanceIndex = report.GetIndex("BalanceChange");
Assert.Equal(2, report.Data.Count);
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
// Items sold
2023-11-09 10:26:00 +01:00
report = await GetReport(acc, new() { ViewName = "Sales" });
var itemIndex = report.GetIndex("Product");
var countIndex = report.GetIndex("Quantity");
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
Assert.Equal(8, itemsCount["green-tea"]);
Assert.Equal(1, itemsCount["black-tea"]);
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();
return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
.Value
.AssertType<StoreReportResponse>();
}
private static string GetInvoiceId(IActionResult resp)
{
var redirect = resp.AssertType<RedirectToActionResult>();
Assert.Equal("Checkout", redirect.ActionName);
return (string)redirect.RouteValues["invoiceId"];
}
}
2017-09-13 08:47:34 +02:00
}