btcpayserver/BTCPayServer.Tests/GreenfieldAPITests.cs
Andrew Camilleri cd9feccf6e
Mark Payouts as Paid (#2539)
* Mark Payouts as Paid

This PR allows users to mark payouts as paid manually through the UI  and through the API. It also sets up the payout proof system to be able store a manual proof that will in a later PR allow you to specify a proof of payment (link or text)

* add docs, test and greenfield client

* remove extra docs stuff

* Update BTCPayServer.Tests/GreenfieldAPITests.cs

Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>

* clean up pull payment/payouts fetch code

* Ensure payoutis are retrieved with pull payment

Co-authored-by: britttttk <39231115+britttttk@users.noreply.github.com>
2021-06-10 18:43:45 +09:00

1796 lines
93 KiB
C#

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 CanDeleteUsersViaApi()
{
using var tester = ServerTester.Create(newDb: true);
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
// Should not be authorized to perform this action
await AssertHttpError(401,
async () => await unauthClient.DeleteUser("lol user id"));
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin();
var adminClient = await user.CreateClient(Policies.Unrestricted);
//can't delete if the only admin
await AssertHttpError(403,
async () => await adminClient.DeleteCurrentUser());
// Should 404 if user doesn't exist
await AssertHttpError(404,
async () => await adminClient.DeleteUser("lol user id"));
user = tester.NewAccount();
await user.GrantAccessAsync();
var badClient = await user.CreateClient(Policies.CanCreateInvoice);
await AssertHttpError(403,
async () => await badClient.DeleteCurrentUser());
var goodClient = await user.CreateClient(Policies.CanDeleteUser, Policies.CanViewProfile);
await goodClient.DeleteCurrentUser();
await AssertHttpError(404,
async () => await adminClient.DeleteUser(user.UserId));
tester.Stores.Remove(user.StoreId);
}
[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<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(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<PoliciesSettings>(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<PoliciesSettings>(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<HttpRequestException>(async () => await client.ArchivePullPayment("lol", result.Id));
Logs.Tester.LogInformation("Can't archive without permission");
await Assert.ThrowsAsync<HttpRequestException>(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
}));
// Create one pull payment with an amount of 9 decimals
var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test 2",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
});
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
});
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
// The payout should round the value of the payment down to the network of the payment method
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
Assert.Equal(12.303228134m, payout.Amount);
await client.MarkPayoutPaid(storeId, payout.Id);
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payout.State);
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
}
}
private DateTimeOffset RoundSeconds(DateTimeOffset dateTimeOffset)
{
return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset);
}
private async Task AssertAPIError(string expectedError, Func<Task> act)
{
var err = await Assert.ThrowsAsync<GreenFieldAPIException>(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<Data.ApplicationDbContextFactory>().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<GreenFieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(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<Task> act)
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(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<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
await clientServer.GetCurrentUser();
await clientProfile.GetCurrentUser();
await clientBasic.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(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<UpdatePaymentRequestRequest>();
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<string>(Assert.IsType<OkObjectResult>(await user.GetController<PaymentRequestController>()
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
var invoice = user.BitPay.GetInvoice(invoiceId);
await tester.WaitForEvent<InvoiceDataChangedEvent>(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<Client.Models.InvoiceData> 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<string>());
Assert.Equal("code", newInvoice.Metadata["itemCode"].Value<string>());
Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value<string>());
Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value<string>());
Assert.False(newInvoice.Metadata["physical"].Value<bool>());
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<Data.ApplicationDbContextFactory>();
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<string>());
}
}
[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<Client.Models.InvoiceData[]>($"api/v1/stores/{user.StoreId}/invoices", new Dictionary<string, object>()
{
{ "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<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
//also test the the metadata actually got saved
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().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<WebhookInvoiceSettledEvent>(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<WebhookInvoiceInvalidEvent>(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<InvoiceController>(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<LanguageService>();
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<UpdateStoreRequest>());
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<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
Assert.Contains("503", err.Message);
// Not permission for the store!
err = await Assert.ThrowsAsync<HttpRequestException>(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<HttpRequestException>(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<GreenFieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
{
BOLT11 = "lol"
}));
var validationErr = await Assert.ThrowsAsync<GreenFieldValidationException>(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<NotificationSender>()
.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<SettingsRepository>().GetSettingAsync<PoliciesSettings>())?? new PoliciesSettings();
settings.AllowLightningInternalNodeForAll = false;
await tester.PayTester.GetService<SettingsRepository>().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<GreenFieldValidationException>(async () =>
{
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
});
settings = await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>();
settings.AllowLightningInternalNodeForAll = true;
await tester.PayTester.GetService<SettingsRepository>().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<NewOnChainTransactionEvent>(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<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
{
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<ArgumentOutOfRangeException>(async () =>
{
await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(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<OutPoint>();
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.SelectedInputs = new List<OutPoint>()
{
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<BTCPayNetwork>("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<NewBlockEvent>(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<JsonSerializationException>(() =>
{
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
});
Assert.Throws<JsonSerializationException>(() =>
{
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));
}
}
}