using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.JsonConverters; using BTCPayServer.Lightning; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Services; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitcoin.OpenAsset; using NBitcoin.Payment; using NBitpayClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; using JsonReader = Newtonsoft.Json.JsonReader; namespace BTCPayServer.Tests { public class GreenfieldAPITests { public const int TestTimeout = TestUtils.TestTimeout; public GreenfieldAPITests(ITestOutputHelper helper) { Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.LogProvider = new XUnitLogProvider(helper); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ApiKeysControllerTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); var client = await user.CreateClient(Policies.CanViewProfile); var clientBasic = await user.CreateClient(); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(client.APIKey, apiKeyData.ApiKey); Assert.Single(apiKeyData.Permissions); //a client using Basic Auth has no business here await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); //revoke current api key await client.RevokeCurrentAPIKeyInfo(); await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); //a client using Basic Auth has no business here await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task SpecificCanModifyStoreCantCreateNewStore() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var acc = tester.NewAccount(); await acc.GrantAccessAsync(); var unrestricted = await acc.CreateClient(); var response = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "mystore" }); var apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings", response.Id) } })).ApiKey; var restricted = new BTCPayServerClient(unrestricted.Host, apiKey); // Unscoped permission should be required for create store await this.AssertHttpError(403, async () => await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" })); // Unrestricted should work fine await unrestricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); // Restricted but unscoped should work fine apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings") } })).ApiKey; restricted = new BTCPayServerClient(unrestricted.Host, apiKey); await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" }); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanCreateAndDeleteAPIKeyViaAPI() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var acc = tester.NewAccount(); await acc.GrantAccessAsync(); var unrestricted = await acc.CreateClient(); var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Label = "Hello world", Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } }); Assert.Equal("Hello world", apiKey.Label); var p = Assert.Single(apiKey.Permissions); Assert.Equal(Policies.CanViewProfile, p.Policy); var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey); await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() { Label = "Hello world2", Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } })); await unrestricted.RevokeAPIKey(apiKey.ApiKey); await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey)); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanCreateUsersViaAPI() { using (var tester = ServerTester.Create(newDb: true)) { tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); await AssertValidationError(new[] { "Email", "Password" }, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); await AssertValidationError(new[] { "Password" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test@gmail.com" })); // Pass too simple await AssertValidationError(new[] { "Password" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" })); // We have no admin, so it should work var user1 = await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); Assert.Empty(user1.Roles); // We have no admin, so it should work var user2 = await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); Assert.Empty(user2.Roles); // Duplicate email await AssertValidationError(new[] { "Email" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" })); // Let's make an admin var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true }); Assert.Contains("ServerAdmin", admin.Roles); Assert.NotNull(admin.Created); Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10); // Creating a new user without proper creds is now impossible (unauthorized) // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection await AssertHttpError(401, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); // But should be ok with subscriptions unlocked var settings = tester.PayTester.GetService(); await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); // But it should be forbidden to create an admin without being authenticated await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = true }); var adminAcc = tester.NewAccount(); adminAcc.UserId = admin.Id; adminAcc.IsAdmin = true; var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile); // We should be forbidden to create a new user without proper admin permissions await AssertHttpError(403, async () => await adminClient.CreateUser( new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); // However, should be ok with the unrestricted permissions of an admin adminClient = await adminAcc.CreateClient(Policies.Unrestricted); await adminClient.CreateUser( new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); // Even creating new admin should be ok await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); var user1Acc = tester.NewAccount(); user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); // User1 trying to get server management would still fail to create user await AssertHttpError(403, async () => await user1Client.CreateUser( new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); // User1 should be able to create user if subscription unlocked await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); await user1Client.CreateUser( new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); // But not an admin await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); // If we set DisableNonAdminCreateUserApi = true, it should always fail to create a user unless you are an admin await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false, DisableNonAdminCreateUserApi = true }); await AssertHttpError(403, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); await AssertHttpError(403, async () => await user1Client.CreateUser( new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" })); await adminClient.CreateUser( new CreateApplicationUserRequest() { Email = "test9@gmail.com", Password = "afewfoiewiou" }); } } [Fact] [Trait("Integration", "Integration")] public async Task CanUsePullPaymentViaAPI() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var acc = tester.NewAccount(); acc.Register(); acc.CreateStore(); var storeId = (await acc.RegisterDerivationSchemeAsync("BTC", importKeysToNBX: true)).StoreId; var client = await acc.CreateClient(); var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Test", Amount = 12.3m, Currency = "BTC", PaymentMethods = new[] { "BTC" } }); void VerifyResult() { Assert.Equal("Test", result.Name); Assert.Null(result.Period); // If it contains ? it means that we are resolving an unknown route with the link generator Assert.DoesNotContain("?", result.ViewLink); Assert.False(result.Archived); Assert.Equal("BTC", result.Currency); Assert.Equal(12.3m, result.Amount); } VerifyResult(); var unauthenticated = new BTCPayServerClient(tester.PayTester.ServerUri); result = await unauthenticated.GetPullPayment(result.Id); VerifyResult(); await AssertHttpError(404, async () => await unauthenticated.GetPullPayment("lol")); // Can't list pull payments unauthenticated await AssertHttpError(401, async () => await unauthenticated.GetPullPayments(storeId)); var pullPayments = await client.GetPullPayments(storeId); result = Assert.Single(pullPayments); VerifyResult(); Thread.Sleep(1000); var test2 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Test 2", Amount = 12.3m, Currency = "BTC", PaymentMethods = new[] { "BTC" } }); Logs.Tester.LogInformation("Can't archive without knowing the walletId"); await Assert.ThrowsAsync(async () => await client.ArchivePullPayment("lol", result.Id)); Logs.Tester.LogInformation("Can't archive without permission"); await Assert.ThrowsAsync(async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); await client.ArchivePullPayment(storeId, result.Id); result = await unauthenticated.GetPullPayment(result.Id); Assert.True(result.Archived); var pps = await client.GetPullPayments(storeId); result = Assert.Single(pps); Assert.Equal("Test 2", result.Name); pps = await client.GetPullPayments(storeId, true); Assert.Equal(2, pps.Length); Assert.Equal("Test 2", pps[0].Name); Assert.Equal("Test", pps[1].Name); var payouts = await unauthenticated.GetPayouts(pps[0].Id); Assert.Empty(payouts); var destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() { Destination = destination, Amount = 1_000_000m, PaymentMethod = "BTC", })); await this.AssertAPIError("archived", async () => await unauthenticated.CreatePayout(pps[1].Id, new CreatePayoutRequest() { Destination = destination, PaymentMethod = "BTC" })); var payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() { Destination = destination, PaymentMethod = "BTC" }); payouts = await unauthenticated.GetPayouts(pps[0].Id); var payout2 = Assert.Single(payouts); Assert.Equal(payout.Amount, payout2.Amount); Assert.Equal(payout.Id, payout2.Id); Assert.Equal(destination, payout2.Destination); Assert.Equal(PayoutState.AwaitingApproval, payout.State); Assert.Null(payout.PaymentMethodAmount); Logs.Tester.LogInformation("Can't overdraft"); await this.AssertAPIError("overdraft", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() { Destination = destination, Amount = 0.00001m, PaymentMethod = "BTC" })); Logs.Tester.LogInformation("Can't create too low payout"); await this.AssertAPIError("amount-too-low", async () => await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() { Destination = destination, PaymentMethod = "BTC" })); Logs.Tester.LogInformation("Can archive payout"); await client.CancelPayout(storeId, payout.Id); payouts = await unauthenticated.GetPayouts(pps[0].Id); Assert.Empty(payouts); payouts = await client.GetPayouts(pps[0].Id, true); payout = Assert.Single(payouts); Assert.Equal(PayoutState.Cancelled, payout.State); Logs.Tester.LogInformation("Can create payout after cancelling"); payout = await unauthenticated.CreatePayout(pps[0].Id, new CreatePayoutRequest() { Destination = destination, PaymentMethod = "BTC" }); var start = RoundSeconds(DateTimeOffset.Now + TimeSpan.FromDays(7.0)); var inFuture = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Starts in the future", Amount = 12.3m, StartsAt = start, Currency = "BTC", PaymentMethods = new[] { "BTC" } }); Assert.Equal(start, inFuture.StartsAt); Assert.Null(inFuture.ExpiresAt); await this.AssertAPIError("not-started", async () => await unauthenticated.CreatePayout(inFuture.Id, new CreatePayoutRequest() { Amount = 1.0m, Destination = destination, PaymentMethod = "BTC" })); var expires = RoundSeconds(DateTimeOffset.Now - TimeSpan.FromDays(7.0)); var inPast = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Will expires", Amount = 12.3m, ExpiresAt = expires, Currency = "BTC", PaymentMethods = new[] { "BTC" } }); await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest() { Amount = 1.0m, Destination = destination, PaymentMethod = "BTC" })); await this.AssertValidationError(new[] { "ExpiresAt" }, async () => await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Test 2", Amount = 12.3m, StartsAt = DateTimeOffset.UtcNow, ExpiresAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(1) })); Logs.Tester.LogInformation("Create a pull payment with USD"); var pp = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest() { Name = "Test USD", Amount = 5000m, Currency = "USD", PaymentMethods = new[] { "BTC" } }); destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(); Logs.Tester.LogInformation("Try to pay it in BTC"); payout = await unauthenticated.CreatePayout(pp.Id, new CreatePayoutRequest() { Destination = destination, PaymentMethod = "BTC" }); await this.AssertAPIError("old-revision", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() { Revision = -1 })); await this.AssertAPIError("rate-unavailable", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() { RateRule = "DONOTEXIST(BTC_USD)" })); payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() { Revision = payout.Revision }); Assert.Equal(PayoutState.AwaitingPayment, payout.State); Assert.NotNull(payout.PaymentMethodAmount); Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest() { Revision = payout.Revision })); } } private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset) { return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset); } private async Task AssertAPIError(string expectedError, Func act) { var err = await Assert.ThrowsAsync(async () => await act()); Assert.Equal(expectedError, err.APIError.Code); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task StoresControllerTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); var client = await user.CreateClient(Policies.Unrestricted); //create store var newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); //update store var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }); Assert.Equal("B", updatedStore.Name); Assert.Equal("B", (await client.GetStore(newStore.Id)).Name); //list stores var stores = await client.GetStores(); var storeIds = stores.Select(data => data.Id); var storeNames = stores.Select(data => data.Name); Assert.NotNull(stores); Assert.Equal(2, stores.Count()); Assert.Contains(newStore.Id, storeIds); Assert.Contains(user.StoreId, storeIds); //get store var store = await client.GetStore(user.StoreId); Assert.Equal(user.StoreId, store.Id); Assert.Contains(store.Name, storeNames); //remove store await client.RemoveStore(newStore.Id); await AssertHttpError(403, async () => { await client.GetStore(newStore.Id); }); Assert.Single(await client.GetStores()); newStore = await client.CreateStore(new CreateStoreRequest() { Name = "A" }); var scopedClient = await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString()); Assert.Single(await scopedClient.GetStores()); // We strip the user's Owner right, so the key should not work using var ctx = tester.PayTester.GetService().CreateContext(); var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id); storeEntity.Role = "Guest"; await ctx.SaveChangesAsync(); await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" })); } } private async Task AssertValidationError(string[] fields, Func act) { var remainingFields = fields.ToHashSet(); var ex = await Assert.ThrowsAsync(act); foreach (var field in fields) { Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray()); remainingFields.Remove(field); } Assert.Empty(remainingFields); return ex; } private async Task AssertHttpError(int code, Func act) { var ex = await Assert.ThrowsAsync(act); Assert.Contains(code.ToString(), ex.Message); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task UsersControllerTests() { using (var tester = ServerTester.Create(newDb: true)) { tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); var clientProfile = await user.CreateClient(Policies.CanModifyProfile); var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile); var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); var clientBasic = await user.CreateClient(); var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); Assert.NotNull(apiKeyProfileUserData); Assert.Equal(apiKeyProfileUserData.Id, user.UserId); Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles); await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); await clientProfile.GetCurrentUser(); await clientBasic.GetCurrentUser(); await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() })); var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() }); Assert.NotNull(newUser); var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() }); Assert.NotNull(newUser2); await AssertValidationError(new[] { "Email" }, async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString() })); await AssertValidationError(new[] { "Password" }, async () => await clientServer.CreateUser( new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", })); await AssertValidationError(new[] { "Email" }, async () => await clientServer.CreateUser( new CreateApplicationUserRequest() { Password = Guid.NewGuid().ToString() })); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanUseWebhooks() { void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook) { Assert.True(hook.Enabled); Assert.True(hook.AuthorizedEvents.Everything); Assert.False(hook.AutomaticRedelivery); Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url); } using var tester = ServerTester.Create(); using var fakeServer = new FakeServer(); await fakeServer.Start(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice); var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest() { Url = fakeServer.ServerUri.AbsoluteUri, AutomaticRedelivery = false }); Assert.NotNull(hook.Secret); AssertHook(fakeServer, hook); hook = await clientProfile.GetWebhook(user.StoreId, hook.Id); AssertHook(fakeServer, hook); var hooks = await clientProfile.GetWebhooks(user.StoreId); hook = Assert.Single(hooks); AssertHook(fakeServer, hook); await clientProfile.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 100 }); var req = await fakeServer.GetNextRequest(); req.Response.StatusCode = 200; fakeServer.Done(); hook = await clientProfile.UpdateWebhook(user.StoreId, hook.Id, new UpdateStoreWebhookRequest() { Url = hook.Url, Secret = "lol", AutomaticRedelivery = false }); Assert.Null(hook.Secret); AssertHook(fakeServer, hook); var deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id); var delivery = Assert.Single(deliveries); delivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, delivery.Id); Assert.NotNull(delivery); Assert.Equal(WebhookDeliveryStatus.HttpSuccess, delivery.Status); var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id); req = await fakeServer.GetNextRequest(); req.Response.StatusCode = 404; fakeServer.Done(); await TestUtils.EventuallyAsync(async () => { var newDelivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, newDeliveryId); Assert.NotNull(newDelivery); Assert.Equal(404, newDelivery.HttpCode); var req = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); Assert.Equal(delivery.Id, req.OriginalDeliveryId); Assert.True(req.IsRedelivery); Assert.Equal(WebhookDeliveryStatus.HttpError, newDelivery.Status); }); deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id); Assert.Equal(2, deliveries.Length); Assert.Equal(newDeliveryId, deliveries[0].Id); var jObj = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); Assert.NotNull(jObj); Logs.Tester.LogInformation("Should not be able to access webhook without proper auth"); var unauthorized = await user.CreateClient(Policies.CanCreateInvoice); await AssertHttpError(403, async () => { await unauthorized.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); }); Logs.Tester.LogInformation("Can use btcpay.store.canmodifystoresettings to query webhooks"); clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice); await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId); Logs.Tester.LogInformation("Testing corner cases"); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId)); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol")); Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", "lol")); Assert.Null(await clientProfile.GetWebhook(user.StoreId, "lol")); await AssertHttpError(404, async () => { await clientProfile.UpdateWebhook(user.StoreId, "lol", new UpdateStoreWebhookRequest() { Url = hook.Url }); }); Assert.True(await clientProfile.DeleteWebhook(user.StoreId, hook.Id)); Assert.False(await clientProfile.DeleteWebhook(user.StoreId, hook.Id)); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task HealthControllerTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); var apiHealthData = await unauthClient.GetHealth(); Assert.NotNull(apiHealthData); Assert.True(apiHealthData.Synchronized); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ServerInfoControllerTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); await AssertHttpError(401, async () => await unauthClient.GetServerInfo()); var user = tester.NewAccount(); user.GrantAccess(); var clientBasic = await user.CreateClient(); var serverInfoData = await clientBasic.GetServerInfo(); Assert.NotNull(serverInfoData); Assert.NotNull(serverInfoData.Version); Assert.NotNull(serverInfoData.Onion); Assert.True(serverInfoData.FullySynched); Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods); Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods); Assert.NotNull(serverInfoData.SyncStatus); Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC")); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task PaymentControllerTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); var client = await user.CreateClient(Policies.Unrestricted); var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests); //create payment request //validation errors await AssertValidationError(new[] { "Amount", "Currency" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A" }); }); await AssertValidationError(new[] { "Amount" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A", Currency = "BTC", Amount = 0 }); }); await AssertValidationError(new[] { "Currency" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); }); await AssertHttpError(403, async () => { await viewOnly.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 }); }); var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A", Currency = "USD", Amount = 1 }); //list payment request var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId); Assert.NotNull(paymentRequests); Assert.Single(paymentRequests); Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id); //get payment request var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); Assert.Equal(newPaymentRequest.Title, paymentRequest.Title); //update payment request var updateRequest = JObject.FromObject(paymentRequest).ToObject(); updateRequest.Title = "B"; await AssertHttpError(403, async () => { await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); }); await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); Assert.Equal(updateRequest.Title, paymentRequest.Title); //archive payment request await AssertHttpError(403, async () => { await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); }); await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); Assert.DoesNotContain(paymentRequest.Id, (await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id)); //let's test some payment stuff await user.RegisterDerivationSchemeAsync("BTC"); var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" }); var invoiceId = Assert.IsType(Assert.IsType(await user.GetController() .PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value); var invoice = user.BitPay.GetInvoice(invoiceId); await tester.WaitForEvent(async () => { await tester.ExplorerNode.SendToAddressAsync( BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue); }); await TestUtils.EventuallyAsync(async () => { Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status); Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status); }); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task InvoiceLegacyTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var user = tester.NewAccount(); await user.GrantAccessAsync(); user.RegisterDerivationScheme("BTC"); var client = await user.CreateClient(Policies.Unrestricted); var oldBitpay = user.BitPay; Logs.Tester.LogInformation("Let's create an invoice with bitpay API"); var oldInvoice = await oldBitpay.CreateInvoiceAsync(new Invoice() { Currency = "BTC", Price = 1000.19392922m, BuyerAddress1 = "blah", Buyer = new Buyer() { Address2 = "blah2" }, ItemCode = "code", ItemDesc = "desc", OrderId = "orderId", PosData = "posData" }); async Task AssertInvoiceMetadata() { Logs.Tester.LogInformation("Let's check if we can get invoice in the new format with the metadata"); var newInvoice = await client.GetInvoice(user.StoreId, oldInvoice.Id); Assert.Equal("posData", newInvoice.Metadata["posData"].Value()); Assert.Equal("code", newInvoice.Metadata["itemCode"].Value()); Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value()); Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value()); Assert.False(newInvoice.Metadata["physical"].Value()); Assert.Null(newInvoice.Metadata["buyerCountry"]); Assert.Equal(1000.19392922m, newInvoice.Amount); Assert.Equal("BTC", newInvoice.Currency); return newInvoice; } await AssertInvoiceMetadata(); Logs.Tester.LogInformation("Let's hack the Bitpay created invoice to be just like before this update. (Invoice V1)"); var invoiceV1 = "{\r\n \"version\": 1,\r\n \"id\": \"" + oldInvoice.Id + "\",\r\n \"storeId\": \"" + user.StoreId + "\",\r\n \"orderId\": \"orderId\",\r\n \"speedPolicy\": 1,\r\n \"rate\": 1.0,\r\n \"invoiceTime\": 1598329634,\r\n \"expirationTime\": 1598330534,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\",\r\n \"productInformation\": {\r\n \"itemDesc\": \"desc\",\r\n \"itemCode\": \"code\",\r\n \"physical\": false,\r\n \"price\": 1000.19392922,\r\n \"currency\": \"BTC\"\r\n },\r\n \"buyerInformation\": {\r\n \"buyerName\": null,\r\n \"buyerEmail\": null,\r\n \"buyerCountry\": null,\r\n \"buyerZip\": null,\r\n \"buyerState\": null,\r\n \"buyerCity\": null,\r\n \"buyerAddress2\": \"blah2\",\r\n \"buyerAddress1\": \"blah\",\r\n \"buyerPhone\": null\r\n },\r\n \"posData\": \"posData\",\r\n \"internalTags\": [],\r\n \"derivationStrategy\": null,\r\n \"derivationStrategies\": \"{\\\"BTC\\\":{\\\"signingKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\",\\\"source\\\":\\\"NBXplorer\\\",\\\"accountDerivation\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf-[legacy]\\\",\\\"accountOriginal\\\":null,\\\"accountKeySettings\\\":[{\\\"rootFingerprint\\\":\\\"54d5044d\\\",\\\"accountKeyPath\\\":\\\"44'/1'/0'\\\",\\\"accountKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\"}],\\\"label\\\":null}}\",\r\n \"status\": \"new\",\r\n \"exceptionStatus\": \"\",\r\n \"payments\": [],\r\n \"refundable\": false,\r\n \"refundMail\": null,\r\n \"redirectURL\": null,\r\n \"redirectAutomatically\": false,\r\n \"txFee\": 0,\r\n \"fullNotifications\": false,\r\n \"notificationEmail\": null,\r\n \"notificationURL\": null,\r\n \"serverUrl\": \"http://127.0.0.1:8001\",\r\n \"cryptoData\": {\r\n \"BTC\": {\r\n \"rate\": 1.0,\r\n \"paymentMethod\": {\r\n \"networkFeeMode\": 0,\r\n \"networkFeeRate\": 100.0,\r\n \"payjoinEnabled\": false\r\n },\r\n \"feeRate\": 100.0,\r\n \"txFee\": 0,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\"\r\n }\r\n },\r\n \"monitoringExpiration\": 1598416934,\r\n \"historicalAddresses\": null,\r\n \"availableAddressHashes\": null,\r\n \"extendedNotifications\": false,\r\n \"events\": null,\r\n \"paymentTolerance\": 0.0,\r\n \"archived\": false\r\n}"; var db = tester.PayTester.GetService(); using var ctx = db.CreateContext(); var dbInvoice = await ctx.Invoices.FindAsync(oldInvoice.Id); dbInvoice.Blob = ZipUtils.Zip(invoiceV1); await ctx.SaveChangesAsync(); var newInvoice = await AssertInvoiceMetadata(); Logs.Tester.LogInformation("Now, let's create an invoice with the new API but with the same metadata as Bitpay"); newInvoice.Metadata.Add("lol", "lol"); newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Metadata = newInvoice.Metadata, Amount = 1000.19392922m, Currency = "BTC" }); oldInvoice = await oldBitpay.GetInvoiceAsync(newInvoice.Id); await AssertInvoiceMetadata(); Assert.Equal("lol", newInvoice.Metadata["lol"].Value()); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task InvoiceTests() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); var user = tester.NewAccount(); await user.GrantAccessAsync(); await user.MakeAdmin(); await user.SetupWebhook(); var client = await user.CreateClient(Policies.Unrestricted); var viewOnly = await user.CreateClient(Policies.CanViewInvoices); //create //validation errors await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () => { await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } }); }); await AssertHttpError(403, async () => { await viewOnly.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 }); }); await user.RegisterDerivationSchemeAsync("BTC"); var newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"), Checkout = new CreateInvoiceRequest.CheckoutOptions() { RedirectAutomatically = true }}); Assert.True(newInvoice.Checkout.RedirectAutomatically); //list var invoices = await viewOnly.GetInvoices(user.StoreId); Assert.NotNull(invoices); Assert.Single(invoices); Assert.Equal(newInvoice.Id, invoices.First().Id); //list Filtered var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId, orderId: null, status: null, DateTimeOffset.Now.AddHours(-1), DateTimeOffset.Now.AddHours(1)); Assert.NotNull(invoicesFiltered); Assert.Single(invoicesFiltered); Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); //list Yesterday var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId, orderId: null, status: null, DateTimeOffset.Now.AddDays(-2), DateTimeOffset.Now.AddDays(-1)); Assert.NotNull(invoicesYesterday); Assert.Empty(invoicesYesterday); // Error, startDate and endDate inverted await AssertValidationError(new[] { "startDate", "endDate" }, () => viewOnly.GetInvoices(user.StoreId, orderId: null, status: null, DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddDays(-2))); await AssertValidationError(new[] { "startDate" }, () => viewOnly.SendHttpRequest($"api/v1/stores/{user.StoreId}/invoices", new Dictionary() { { "startDate", "blah" } })); //list Existing OrderId var invoicesExistingOrderId = await viewOnly.GetInvoices(user.StoreId, orderId: newInvoice.Metadata["orderId"].ToString()); Assert.NotNull(invoicesExistingOrderId); Assert.Single(invoicesFiltered); Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id); //list NonExisting OrderId var invoicesNonExistingOrderId = await viewOnly.GetInvoices(user.StoreId, orderId: "NonExistingOrderId"); Assert.NotNull(invoicesNonExistingOrderId); Assert.Empty(invoicesNonExistingOrderId); //list Existing Status var invoicesExistingStatus = await viewOnly.GetInvoices(user.StoreId, status:new []{newInvoice.Status}); Assert.NotNull(invoicesExistingStatus); Assert.Single(invoicesExistingStatus); Assert.Equal(newInvoice.Id, invoicesExistingStatus.First().Id); //list NonExisting Status var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId, status: new []{BTCPayServer.Client.Models.InvoiceStatus.Invalid}); Assert.NotNull(invoicesNonExistingStatus); Assert.Empty(invoicesNonExistingStatus); //get var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id); Assert.Equal(newInvoice.Metadata, invoice.Metadata); var paymentMethods = await viewOnly.GetInvoicePaymentMethods(user.StoreId, newInvoice.Id); Assert.Single(paymentMethods); var paymentMethod = paymentMethods.First(); Assert.Equal("BTC", paymentMethod.PaymentMethod); Assert.Empty(paymentMethod.Payments); //update newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 1 }); await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled }); newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 1 }); await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Invalid }); await AssertHttpError(403, async () => { await viewOnly.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() { Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}") }); }); invoice = await client.UpdateInvoice(user.StoreId, invoice.Id, new UpdateInvoiceRequest() { Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}") }); Assert.Equal("updated",invoice.Metadata["itemCode"].Value()); Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values().Sum()); //also test the the metadata actually got saved invoice = await client.GetInvoice(user.StoreId, invoice.Id); Assert.Equal("updated",invoice.Metadata["itemCode"].Value()); Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values().Sum()); //archive await AssertHttpError(403, async () => { await viewOnly.ArchiveInvoice(user.StoreId, invoice.Id); }); await client.ArchiveInvoice(user.StoreId, invoice.Id); Assert.DoesNotContain(invoice.Id, (await client.GetInvoices(user.StoreId)).Select(data => data.Id)); //unarchive await client.UnarchiveInvoice(user.StoreId, invoice.Id); Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id)); foreach (var marked in new[] { InvoiceStatus.Settled, InvoiceStatus.Invalid }) { var inv = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 100 }); await user.PayInvoice(inv.Id); await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = marked }); var result = await client.GetInvoice(user.StoreId, inv.Id); if (marked == InvoiceStatus.Settled) { Assert.Equal(InvoiceStatus.Settled, result.Status); user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, o => { Assert.Equal(inv.Id, o.InvoiceId); Assert.True(o.ManuallyMarked); }); } if (marked == InvoiceStatus.Invalid) { Assert.Equal(InvoiceStatus.Invalid, result.Status); var evt = user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, o => { Assert.Equal(inv.Id, o.InvoiceId); Assert.True(o.ManuallyMarked); }); Assert.NotNull(await client.GetWebhookDelivery(evt.StoreId, evt.WebhookId, evt.DeliveryId)); } } newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { DefaultLanguage = "it-it ", RedirectURL = "http://toto.com/lol" } }); Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink); var controller = tester.PayTester.GetController(user.UserId, user.StoreId); var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model; Assert.Equal("it-IT", model.DefaultLang); Assert.Equal("http://toto.com/lol", model.MerchantRefLink); var langs = tester.PayTester.GetService(); foreach (var match in new[] { "it", "it-IT", "it-LOL" }) { Assert.Equal("it-IT", langs.FindBestMatch(match).Code); } foreach (var match in new[] { "pt-BR" }) { Assert.Equal("pt-BR", langs.FindBestMatch(match).Code); } foreach (var match in new[] { "en", "en-US" }) { Assert.Equal("en", langs.FindBestMatch(match).Code); } foreach (var match in new[] { "pt", "pt-pt", "pt-PT" }) { Assert.Equal("pt-PT", langs.FindBestMatch(match).Code); } //payment method activation tests var store = await client.GetStore(user.StoreId); Assert.False(store.LazyPaymentMethods); store.LazyPaymentMethods = true; store = await client.UpdateStore(store.Id, JObject.FromObject(store).ToObject()); Assert.True(store.LazyPaymentMethods); invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() {Amount = 1, Currency = "USD"}); paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); Assert.Single(paymentMethods); Assert.False(paymentMethods.First().Activated); await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id, paymentMethods.First().PaymentMethod); paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); Assert.Single(paymentMethods); Assert.True(paymentMethods.First().Activated); } } [Fact(Timeout = 60 * 20 * 1000)] [Trait("Integration", "Integration")] [Trait("Lightning", "Lightning")] public async Task CanUseLightningAPI() { using (var tester = ServerTester.Create()) { tester.ActivateLightning(); await tester.StartAsync(); await tester.EnsureChannelsSetup(); var user = tester.NewAccount(); user.GrantAccess(true); user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); var merchant = tester.NewAccount(); merchant.GrantAccess(true); merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}"); var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60))); // The default client is using charge, so we should not be able to query channels var client = await user.CreateClient(Policies.CanUseInternalLightningNode); var info = await client.GetLightningNodeInfo("BTC"); Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); Assert.Contains("503", err.Message); // Not permission for the store! err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); Assert.Contains("403", err.Message); var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() { Amount = LightMoney.Satoshis(1000), Description = "lol", Expiry = TimeSpan.FromSeconds(400), PrivateRouteHints = false }); var chargeInvoice = invoiceData; Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); // Not permission for the server err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); Assert.Contains("403", err.Message); var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); Assert.Equal(2, data.Count()); BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() { Amount = LightMoney.Satoshis(1000), Description = "lol", Expiry = TimeSpan.FromSeconds(400), PrivateRouteHints = false }); Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() { BOLT11 = merchantInvoice.BOLT11 }); await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() { BOLT11 = "lol" })); var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() { Amount = -1, Expiry = TimeSpan.FromSeconds(-1), Description = null })); Assert.Equal(2, validationErr.ValidationErrors.Length); var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); Assert.NotNull(invoice.PaidAt); Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); // Amount received might be bigger because of internal implementation shit from lightning Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task NotificationAPITests() { using var tester = ServerTester.Create(); await tester.StartAsync(); var user = tester.NewAccount(); await user.GrantAccessAsync(true); var client = await user.CreateClient(Policies.CanManageNotificationsForUser); var viewOnlyClient = await user.CreateClient(Policies.CanViewNotificationsForUser); await tester.PayTester.GetService() .SendNotification(new UserScope(user.UserId), new NewVersionNotification()); Assert.Single(await viewOnlyClient.GetNotifications()); Assert.Single(await viewOnlyClient.GetNotifications(false)); Assert.Empty(await viewOnlyClient.GetNotifications(true)); Assert.Single(await client.GetNotifications()); Assert.Single(await client.GetNotifications(false)); Assert.Empty(await client.GetNotifications(true)); var notification = (await client.GetNotifications()).First(); notification = await client.GetNotification(notification.Id); Assert.False(notification.Seen); await AssertHttpError(403, async () => { await viewOnlyClient.UpdateNotification(notification.Id, true); }); await AssertHttpError(403, async () => { await viewOnlyClient.RemoveNotification(notification.Id); }); Assert.True((await client.UpdateNotification(notification.Id, true)).Seen); Assert.Single(await viewOnlyClient.GetNotifications(true)); Assert.Empty(await viewOnlyClient.GetNotifications(false)); await client.RemoveNotification(notification.Id); Assert.Empty(await viewOnlyClient.GetNotifications(true)); Assert.Empty(await viewOnlyClient.GetNotifications(false)); } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task OnChainPaymentMethodAPITests() { using var tester = ServerTester.Create(); await tester.StartAsync(); var user = tester.NewAccount(); await user.GrantAccessAsync(true); var client = await user.CreateClient(Policies.CanModifyStoreSettings); var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings); var store = await client.CreateStore(new CreateStoreRequest() {Name = "test store"}); Assert.Empty(await client.GetStoreOnChainPaymentMethods(store.Id)); await AssertHttpError(403, async () => { await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { }); }); var xpriv = new Mnemonic("all all all all all all all all all all all all").DeriveExtKey() .Derive(KeyPath.Parse("m/84'/0'/0'")); var xpub = xpriv.Neuter().ToString(Network.RegTest); var firstAddress = xpriv.Derive(KeyPath.Parse("0/0")).Neuter().GetPublicKey().GetAddress(ScriptPubKeyType.Segwit, Network.RegTest).ToString(); await AssertHttpError(404, async () => { await client.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC"); }); Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", new OnChainPaymentMethodData() {Enabled = true, DerivationScheme = xpub})).Addresses.First().Address); var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub}); Assert.Equal(xpub,method.DerivationScheme); method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") }); method = await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); Assert.Equal("lol", method.Label); Assert.Equal(RootedKeyPath.Parse("01020304/1/2/3"), method.AccountKeyPath); Assert.Equal(xpub,method.DerivationScheme); Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address); await AssertHttpError(403, async () => { await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); }); await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); await AssertHttpError(404, async () => { await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); }); } [Fact(Timeout = 60 * 2 * 1000)] [Trait("Lightning", "Lightning")] [Trait("Integration", "Integration")] public async Task LightningNetworkPaymentMethodAPITests() { using var tester = ServerTester.Create(); tester.ActivateLightning(); await tester.StartAsync(); await tester.EnsureChannelsSetup(); var admin = tester.NewAccount(); await admin.GrantAccessAsync(true); var admin2 = tester.NewAccount(); await admin2.GrantAccessAsync(true); var adminClient = await admin.CreateClient(Policies.CanModifyStoreSettings); var admin2Client = await admin2.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings); var viewOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings); var store = await adminClient.GetStore(admin.StoreId); Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id)); await AssertHttpError(403, async () => { await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { }); }); await AssertHttpError(404, async () => { await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC"); }); await admin.RegisterLightningNodeAsync("BTC", false); var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC"); await AssertHttpError(403, async () => { await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); }); await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); await AssertHttpError(404, async () => { await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC"); }); // Let's verify that the admin client can't change LN to unsafe connection strings without modify server settings rights foreach (var forbidden in new string[] { "type=clightning;server=tcp://127.0.0.1", "type=clightning;server=tcp://test", "type=clightning;server=tcp://test.lan", "type=clightning;server=tcp://test.local", "type=clightning;server=tcp://192.168.1.2", "type=clightning;server=unix://8.8.8.8", "type=clightning;server=unix://[::1]", "type=clightning;server=unix://[0:0:0:0:0:0:0:1]", }) { var ex = await AssertValidationError(new[] { "ConnectionString" }, async () => { await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { ConnectionString = forbidden, CryptoCode = "BTC", Enabled = true }); }); Assert.Contains("btcpay.server.canmodifyserversettings", ex.Message); // However, the other client should work because he has `btcpay.server.canmodifyserversettings` await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData() { ConnectionString = forbidden, CryptoCode = "BTC", Enabled = true }); } // Allowed ip should be ok await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { ConnectionString = "type=clightning;server=tcp://8.8.8.8", CryptoCode = "BTC", Enabled = true }); // If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid await admin2.MakeAdmin(false); await AssertValidationError(new[] { "ConnectionString" }, async () => { await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData() { ConnectionString = "type=clightning;server=tcp://127.0.0.1", CryptoCode = "BTC", Enabled = true }); }); var settings = (await tester.PayTester.GetService().GetSettingAsync())?? new PoliciesSettings(); settings.AllowLightningInternalNodeForAll = false; await tester.PayTester.GetService().UpdateSetting(settings); var nonAdminUser = tester.NewAccount(); await nonAdminUser.GrantAccessAsync(false); var nonAdminUserClient= await nonAdminUser.CreateClient(Policies.CanModifyStoreSettings); await AssertHttpError(404, async () => { await nonAdminUserClient.GetStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC"); }); await Assert.ThrowsAsync(async () => { await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method); }); settings = await tester.PayTester.GetService().GetSettingAsync(); settings.AllowLightningInternalNodeForAll = true; await tester.PayTester.GetService().UpdateSetting(settings); await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method); } [Fact(Timeout = 60 * 2 * 1000)] [Trait("Integration", "Integration")] public async Task WalletAPITests() { using var tester = ServerTester.Create(); await tester.StartAsync(); var user = tester.NewAccount(); await user.GrantAccessAsync(true); var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings); var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings); var walletId = await user.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true); //view only clients can't do jack shit with this API await AssertHttpError(403, async () => { await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); }); var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); Assert.Equal(0m, overview.Balance); var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode ); Assert.NotNull( fee.FeeRate); await AssertHttpError(403, async () => { await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); }); var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode ); var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true ); Assert.Equal(address.Address, address2.Address); Assert.NotEqual(address.Address, address3.Address); await AssertHttpError(403, async () => { await viewOnlyClient.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode); }); Assert.Empty(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode)); uint256 txhash = null; await tester.WaitForEvent(async () => { txhash = await tester.ExplorerNode.SendToAddressAsync( BitcoinAddress.Create(address3.Address, tester.ExplorerClient.Network.NBitcoinNetwork), new Money(0.01m, MoneyUnit.BTC)); }); await tester.ExplorerNode.GenerateAsync(1); var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false ); Assert.NotEqual(address3.Address, address4.Address); await client.UnReserveOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode); var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true ); Assert.Equal(address5.Address, address4.Address); var utxo = Assert.Single(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode)); Assert.Equal(0.01m, utxo.Amount); Assert.Equal(txhash, utxo.Outpoint.Hash); overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode ); Assert.Equal(0.01m, overview.Balance); //the simplest request: var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync(); var createTxRequest = new CreateOnChainTransactionRequest() { Destinations = new List() { new CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination() { Destination = nodeAddress.ToString(), Amount = 0.001m } }, FeeRate = new FeeRate(5m) //only because regtest may fail but not required }; await AssertHttpError(403, async () => { await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest ); }); await Assert.ThrowsAsync(async () => { await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); await Assert.ThrowsAsync(async () => { createTxRequest.ProceedWithBroadcast = false; await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest); }); Transaction tx; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); Assert.NotNull(tx); Assert.Contains(tx.Outputs, txout => txout.IsTo(nodeAddress) && txout.Value.ToDecimal(MoneyUnit.BTC) == 0.001m); Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed); // no change test createTxRequest.NoChange = true; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); Assert.NotNull(tx); Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress) ); Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed); createTxRequest.NoChange = false; //coin selection await AssertValidationError(new []{nameof(createTxRequest.SelectedInputs)}, async () => { createTxRequest.SelectedInputs = new List(); tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); createTxRequest.SelectedInputs = new List() { utxo.Outpoint }; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); createTxRequest.SelectedInputs = null; //destination testing await AssertValidationError(new []{ "Destinations"}, async () => { createTxRequest.Destinations[0].Amount = utxo.Amount; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); createTxRequest.Destinations[0].SubtractFromAmount = true; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); await AssertValidationError(new []{ "Destinations[0]"}, async () => { createTxRequest.Destinations[0].Amount = 0m; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); //dest can be a bip21 //cant use bip with subtractfromamount createTxRequest.Destinations[0].Amount = null; createTxRequest.Destinations[0].Destination = $"bitcoin:{nodeAddress}?amount=0.001"; await AssertValidationError(new []{ "Destinations[0]"}, async () => { tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); //if amt specified, it overrides bip21 amount createTxRequest.Destinations[0].Amount = 0.0001m; createTxRequest.Destinations[0].SubtractFromAmount = false; tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork("BTC")) ==0.0001m ); //fee rate test createTxRequest.FeeRate = FeeRate.Zero; await AssertValidationError(new []{ "FeeRate"}, async () => { tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); createTxRequest.FeeRate = new FeeRate(5.0m); createTxRequest.Destinations[0].Amount = 0.001m; createTxRequest.Destinations[0].Destination = nodeAddress.ToString(); createTxRequest.Destinations[0].SubtractFromAmount = false; await AssertHttpError(403, async () => { await viewOnlyClient.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode, createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork); }); createTxRequest.ProceedWithBroadcast = true; var txdata= await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest); Assert.Equal(TransactionStatus.Unconfirmed, txdata.Status); Assert.Null(txdata.BlockHeight); Assert.Null(txdata.BlockHash); Assert.NotNull(await tester.ExplorerClient.GetTransactionAsync(txdata.TransactionHash)); await AssertHttpError(403, async () => { await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString()); }); await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString()); await AssertHttpError(403, async () => { await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode); }); Assert.True(Assert.Single( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, new[] {TransactionStatus.Confirmed})).TransactionHash == utxo.Outpoint.Hash); Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, new[] {TransactionStatus.Unconfirmed}), data => data.TransactionHash == txdata.TransactionHash); Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash); await tester.WaitForEvent(async () => { await tester.ExplorerNode.GenerateAsync(1); }, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal)); Assert.Contains( await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, new[] {TransactionStatus.Confirmed}), data => data.TransactionHash == txdata.TransactionHash); } [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] public void NumericJsonConverterTests() { JsonReader Get(string val) { return new JsonTextReader(new StringReader(val)); } var jsonConverter = new NumericStringJsonConverter(); Assert.True(jsonConverter.CanConvert(typeof(decimal))); Assert.True(jsonConverter.CanConvert(typeof(decimal?))); Assert.True(jsonConverter.CanConvert(typeof(double))); Assert.True(jsonConverter.CanConvert(typeof(double?))); Assert.False(jsonConverter.CanConvert(typeof(float))); Assert.False(jsonConverter.CanConvert(typeof(string))); var numberJson = "1"; var numberDecimalJson = "1.2"; var stringJson = "\"1.2\""; Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null)); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null)); Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null)); Assert.Equal((double)1.0, jsonConverter.ReadJson(Get(numberJson), typeof(double), null, null)); Assert.Equal((double)1.2, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(double), null, null)); Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(double?), null, null)); Assert.Throws(() => { jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null); }); Assert.Throws(() => { jsonConverter.ReadJson(Get("null"), typeof(double), null, null); }); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null)); Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null)); Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null)); Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double?), null, null)); } } }