Add unhappy scenario integration test with razor and context fixes

This commit is contained in:
napoly 2025-11-11 06:40:44 +01:00
parent a3d7dd795f
commit 2aad169dce
8 changed files with 175 additions and 18 deletions

View file

@ -31,7 +31,7 @@ jobs:
run: dotnet clean
- name: Build projects
run: dotnet build -c Release --no-restore
run: dotnet build -maxcpucount:1 -c Release --no-restore
- name: Deterministic build check
run: |

View file

@ -8,9 +8,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="Microsoft.Playwright" Version="1.52.0"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.Playwright" Version="1.52.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -26,8 +27,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj"/>
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Tests\BTCPayServer.Tests.csproj"/>
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
<ProjectReference Include="..\submodules\btcpayserver\BTCPayServer.Tests\BTCPayServer.Tests.csproj" />
<ProjectReference Include="..\Plugins\Monero\BTCPayServer.Plugins.Monero.csproj" />
</ItemGroup>

View file

@ -1,5 +1,9 @@
using System.Diagnostics;
using BTCPayServer.Plugins.Monero.Services;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests;
using BTCPayServer.Tests.Mocks;
using Xunit;
@ -36,20 +40,25 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await s.RegisterNewUser(true);
await s.CreateNewStore(preferredExchange: "Kraken");
await s.Page.Locator("a.nav-link[href*='monerolike/XMR']").ClickAsync();
await s.Page.Locator("input#PrimaryAddress").FillAsync("43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L");
await s.Page.Locator("input#PrivateViewKey").FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#PrimaryAddress")
.FillAsync(
"43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L");
await s.Page.Locator("input#PrivateViewKey")
.FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#RestoreHeight").FillAsync("0");
await s.Page.Locator("input#WalletPassword").FillAsync("pass123");
await s.Page.ClickAsync("button[name='command'][value='set-wallet-details']");
await s.Page.CheckAsync("#Enabled");
await s.Page.SelectOptionAsync("#SettlementConfirmationThresholdChoice", "2");
await s.Page.ClickAsync("#SaveButton");
var classList = await s.Page.Locator("svg.icon-checkmark").GetAttributeAsync("class");
var classList = await s.Page
.Locator("svg.icon-checkmark")
.GetAttributeAsync("class");
Assert.Contains("text-success", classList);
// Set rate provider
await s.Page.Locator("#StoreNav-General").ClickAsync();
await s.Page.Locator("#mainNav #StoreNav-Rates").ClickAsync();
await s.Page.Locator("#menu-item-General").ClickAsync();
await s.Page.Locator("#menu-item-Rates").ClickAsync();
await s.Page.FillAsync("#DefaultCurrencyPairs", "BTC_USD,XMR_USD,XMR_BTC");
await s.Page.SelectOptionAsync("#PrimarySource_PreferredExchange", "kraken");
await s.Page.Locator("#page-primary").ClickAsync();
@ -69,7 +78,9 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await s.Page.ClickAsync("#DetailsToggle");
// Verify the total fiat amount is $4.20
var totalFiat = await s.Page.Locator("#PaymentDetails-TotalFiat dd.clipboard-button").InnerTextAsync();
var totalFiat = await s.Page
.Locator("#PaymentDetails-TotalFiat dd.clipboard-button")
.InnerTextAsync();
Assert.Equal("$4.20", totalFiat);
await s.Page.GoBackAsync();
@ -92,5 +103,90 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
// Select confirmation time to 0
await s.Page.SelectOptionAsync("#SettlementConfirmationThresholdChoice", "3");
await s.Page.ClickAsync("#SaveButton");
await CleanUp(s);
}
[Fact]
public async Task ShouldFailWhenWrongPrimaryAddress()
{
await using var s = CreatePlaywrightTester();
await s.StartAsync();
await s.RegisterNewUser(true);
await s.CreateNewStore();
await s.Page.Locator("a.nav-link[href*='monerolike/XMR']").ClickAsync();
await s.Page.Locator("input#PrimaryAddress")
.FillAsync("wrongprimaryaddressfSF6ZKGFT7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L");
await s.Page.Locator("input#PrivateViewKey")
.FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#RestoreHeight").FillAsync("0");
await s.Page.Locator("input#WalletPassword").FillAsync("pass123");
await s.Page.ClickAsync("button[name='command'][value='set-wallet-details']");
var errorText = await s.Page
.Locator("div.validation-summary-errors li")
.InnerTextAsync();
Assert.Equal("Could not generate view wallet from keys: Failed to parse public address", errorText);
await CleanUp(s);
}
private static async Task CleanUp(PlaywrightTester playwrightTester)
{
MoneroRPCProvider moneroRpcProvider = playwrightTester.Server.PayTester.GetService<MoneroRPCProvider>();
if (moneroRpcProvider.IsAvailable("XMR"))
{
await moneroRpcProvider.CloseWallet("XMR");
await moneroRpcProvider.UpdateSummary("XMR");
}
if (playwrightTester.Server.PayTester.InContainer)
{
moneroRpcProvider.DeleteWallet();
}
else
{
await RemoveWalletFromLocalDocker();
}
}
static async Task RemoveWalletFromLocalDocker()
{
try
{
var removeWalletFromDocker = new ProcessStartInfo
{
FileName = "docker",
Arguments = "exec xmr_wallet sh -c \"rm -rf /wallet/*\"",
RedirectStandardOutput = true,
RedirectStandardError = true
};
using var process = Process.Start(removeWalletFromDocker);
if (process is null)
{
return;
}
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (!string.IsNullOrWhiteSpace(stdout))
{
Console.WriteLine(stdout);
}
if (!string.IsNullOrWhiteSpace(stderr))
{
Console.WriteLine(stderr);
}
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup failed: {ex}");
}
}
}

View file

@ -22,6 +22,7 @@ services:
- "tests:127.0.0.1"
volumes:
- ../coverage:/coverage
- xmr_wallet:/wallet
# The dev container is not used, it is just handy to run `docker-compose up dev` to start all services
dev:
@ -89,7 +90,7 @@ services:
- "bitcoin_datadir:/data"
monerod:
image: btcpayserver/monero:0.18.4.2
image: btcpayserver/monero:0.18.4.3
restart: unless-stopped
container_name: monerod
command: monerod --fixed-difficulty 1 --log-level=2 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --regtest --no-igd --hide-my-port --offline --non-interactive
@ -99,7 +100,7 @@ services:
- "18081:18081"
xmr_wallet:
image: btcpayserver/monero:0.18.4.2
image: btcpayserver/monero:0.18.4.3
restart: unless-stopped
container_name: xmr_wallet
command: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"

View file

@ -39,10 +39,7 @@
<!-- Deterministic build -->
<ItemGroup>
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.25" PrivateAssets="All"/>
<PackageReference Include="DotNet.ReproducibleBuilds.Isolated" Version="1.2.25" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
</ItemGroup>
<!-- If you need Entity Framework, you can uncomment this. This will make it usable in your project without publishing assemblies

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
@ -8,6 +9,8 @@ using BTCPayServer.Plugins.Monero.Configuration;
using BTCPayServer.Plugins.Monero.RPC;
using BTCPayServer.Plugins.Monero.RPC.Models;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Plugins.Monero.Services
@ -18,6 +21,7 @@ namespace BTCPayServer.Plugins.Monero.Services
private readonly EventAggregator _eventAggregator;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
private readonly ILogger<MoneroRPCProvider> _logger;
private readonly ConcurrentDictionary<string, MoneroLikeSummary> _summaries = new();
@ -25,10 +29,12 @@ namespace BTCPayServer.Plugins.Monero.Services
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration,
EventAggregator eventAggregator,
ILogger<MoneroRPCProvider> logger,
IHttpClientFactory httpClientFactory)
{
_moneroLikeConfiguration = moneroLikeConfiguration;
_eventAggregator = eventAggregator;
_logger = logger;
DaemonRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password,
@ -52,6 +58,56 @@ namespace BTCPayServer.Plugins.Monero.Services
summary.WalletAvailable;
}
public async Task CloseWallet(string cryptoCode)
{
if (!WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient))
{
throw new InvalidOperationException($"Wallet RPC client not found for {cryptoCode}");
}
await walletRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, object>(
"close_wallet", JsonRpcClient.NoRequestModel.Instance);
}
public void DeleteWallet()
{
if (!_moneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var configItem))
{
_logger.LogWarning("DeleteWallet: No XMR configuration found.");
return;
}
if (string.IsNullOrEmpty(configItem.WalletDirectory))
{
_logger.LogWarning("DeleteWallet: WalletDirectory is null or empty for XMR configuration.");
return;
}
try
{
var walletFile = Path.Combine(configItem.WalletDirectory, "view_wallet");
var keysFile = walletFile + ".keys";
var passwordFile = Path.Combine(configItem.WalletDirectory, "password");
if (File.Exists(walletFile))
{
File.Delete(walletFile);
}
if (File.Exists(keysFile))
{
File.Delete(keysFile);
}
if (File.Exists(passwordFile))
{
File.Delete(passwordFile);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete wallet files in directory {Dir}",
configItem.WalletDirectory);
}
}
public async Task<MoneroLikeSummary> UpdateSummary(string cryptoCode)
{
if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) ||

6
global.json Normal file
View file

@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.416",
"rollForward": "latestFeature"
}
}

@ -1 +1 @@
Subproject commit 22f06893b9e11e88042f253099b76295c1f99fef
Subproject commit 5a487985f4c1b6f4b80a5618fa4d8e8a82069adb