using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Configuration; using BTCPayServer.Hosting; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Rating; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Mocks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Session; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; namespace BTCPayServer.Tests { public class BTCPayServerTester : IDisposable { internal readonly string _Directory; public ILoggerProvider LoggerProvider { get; } public bool? BindAllInterfaces { get; set; } ILog TestLogs; public BTCPayServerTester(ILog testLogs, ILoggerProvider loggerProvider, string scope) { this.LoggerProvider = loggerProvider; this.TestLogs = testLogs; this._Directory = scope ?? throw new ArgumentNullException(nameof(scope)); } public Uri NBXplorerUri { get; set; } public Uri LTCNBXplorerUri { get; set; } public Uri LBTCNBXplorerUri { get; set; } public Uri ServerUri { get; set; } public Uri ServerUriWithIP { get; set; } public string Postgres { get; set; } public string ExplorerPostgres { get; set; } IHost _Host; public int Port { get; set; } public async Task RestartStartupTask() { var startupTask = GetService().GetServices() .Single(task => task is T); await startupTask.ExecuteAsync(); } public bool MockRates { get; set; } = true; public string SocksEndpoint { get; set; } /// /// This helps testing plugins. /// See https://github.com/btcpayserver/btcpayserver/pull/7008 /// public bool LoadPluginsInDefaultAssemblyContext { get; set; } = true; public HashSet Chains { get; set; } = new HashSet() { "BTC" }; public bool UseLightning { get; set; } public bool CheatMode { get; set; } = true; public bool DisableRegistration { get; set; } = false; public async Task StartAsync() { if (_Host is not null) return; if (!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); string chain = NBXplorerDefaultSettings.GetFolderName(ChainName.Regtest); string chainDirectory = Path.Combine(_Directory, chain); if (!Directory.Exists(chainDirectory)) Directory.CreateDirectory(chainDirectory); StringBuilder config = new StringBuilder(); if (string.Equals(Environment.GetEnvironmentVariable("BTCPAY_NODEFAULTCHAIN"), "true", StringComparison.OrdinalIgnoreCase)) { Chains = new HashSet { "none" }; config.AppendLine("nodefaultchain=true"); } config.AppendLine($"{chain.ToLowerInvariant()}=1"); if (BindAllInterfaces ?? InContainer) { config.AppendLine("bind=0.0.0.0"); } config.AppendLine($"port={Port}"); config.AppendLine($"chains={string.Join(',', Chains)}"); if (Chains.Contains("BTC", StringComparer.OrdinalIgnoreCase)) { config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}"); config.AppendLine($"btc.explorer.cookiefile=0"); } var btcpayserverTestDataDir = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName,"BTCPayServer.Tests", "TestData"); if (UseLightning) { config.AppendLine($"btc.lightning={IntegratedLightning}"); var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json"); File.Copy(Path.Combine(btcpayserverTestDataDir, "LndSeedBackup/walletunlock.json"), localLndBackupFile, true); config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}"); } if (Chains.Contains("LTC", StringComparer.OrdinalIgnoreCase)) { config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}"); config.AppendLine($"ltc.explorer.cookiefile=0"); } if (Chains.Contains("LBTC", StringComparer.OrdinalIgnoreCase)) { config.AppendLine($"lbtc.explorer.url={LBTCNBXplorerUri.AbsoluteUri}"); config.AppendLine($"lbtc.explorer.cookiefile=0"); } if (CheatMode) config.AppendLine("cheatmode=1"); config.AppendLine($"torrcfile={Path.Combine(btcpayserverTestDataDir, "Tor/torrc")}"); config.AppendLine($"socksendpoint={SocksEndpoint}"); config.AppendLine($"debuglog=debug.log"); config.AppendLine($"nocsp={NoCSP.ToString().ToLowerInvariant()}"); if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile)) config.AppendLine($"sshpassword={SSHPassword}"); if (!string.IsNullOrEmpty(SSHKeyFile)) config.AppendLine($"sshkeyfile={SSHKeyFile}"); if (!string.IsNullOrEmpty(SSHConnection)) config.AppendLine($"sshconnection={SSHConnection}"); if (!String.IsNullOrEmpty(Postgres)) config.AppendLine($"postgres=" + Postgres); if (!string.IsNullOrEmpty(ExplorerPostgres)) config.AppendLine($"explorer.postgres=" + ExplorerPostgres); var confPath = Path.Combine(chainDirectory, "settings.config"); await File.WriteAllTextAsync(confPath, config.ToString()); ServerUri = new Uri("http://" + HostName + ":" + Port + "/"); ServerUriWithIP = new Uri("http://127.0.0.1:" + Port + "/"); HttpClient = new HttpClient(); HttpClient.BaseAddress = ServerUri; Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); var confBuilder = new DefaultConfiguration() { Logger = LoggerProvider.CreateLogger("Console") }.CreateConfigurationBuilder(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" }); // This make sure that tests work outside of this assembly (ie, test project it a plugin) confBuilder.SetBasePath(TestUtils.TestDirectory); #if DEBUG confBuilder.AddJsonFile("appsettings.dev.json", true, false); #endif if (LoadPluginsInDefaultAssemblyContext) confBuilder.AddInMemoryCollection([new("TEST_RUNNER_ENABLED", "true")]); var conf = confBuilder.Build(); _Host = Host.CreateDefaultBuilder() .UseDefaultServiceProvider(options => { options.ValidateScopes = true; }) .ConfigureAppConfiguration((hostingContext, configBuilder) => { configBuilder.AddConfiguration(conf); }) .ConfigureLogging(logging => { // This matches your logging setup more directly than services.AddLogging(...) logging.ClearProviders(); // optional: keep if you want full control logging.SetMinimumLevel(LogLevel.Information); logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical); logging.AddFilter("Microsoft", LogLevel.Error); logging.AddFilter("Microsoft.EntityFrameworkCore.Migrations", LogLevel.Information); logging.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error); logging.AddFilter("BTCPayServer.Security", LogLevel.Warning); // If LoggerProvider is an ILoggerProvider instance: logging.ClearProviders(); logging.AddProvider(LoggerProvider); // If you removed ClearProviders(), also add back defaults you want (Console/EventSource/etc.) // logging.AddConsole(); // logging.AddDebug(); }) .ConfigureSerilog(conf) .ConfigureWebHostDefaults(webBuilder => { var root = FindBTCPayServerDirectory(); webBuilder .UseContentRoot(root) .UseWebRoot(Path.Combine(root, "wwwroot")) .UseKestrel() .UseStartup() .ConfigureServices(services => { if (RuntimeCompilation) services.AddMvcCore().AddRazorRuntimeCompilation(); services.AddMvcCore().ConfigureApplicationPartManager(apm => { var assembly = typeof(BTCPayServerTester).Assembly; apm.ApplicationParts.Add(new AssemblyPart(assembly)); }).AddControllersAsServices(); services.TryAddSingleton( new BTCPayServer.Services.Fees.FixedFeeProvider(new FeeRate(100L, 1))); }); }) .UseEnvironment(HostEnvironment) .Build(); await _Host.StartWithTasksAsync(); var urls = _Host.GetServerFeatures().Addresses; foreach (var url in urls) { TestLogs.LogInformation("Listening on " + url); } TestLogs.LogInformation("Server URI " + ServerUri); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository)); Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider)); if (MockRates) { var rateProvider = (RateProviderFactory)_Host.Services.GetRequiredService(typeof(RateProviderFactory)); var realKraken = rateProvider.Providers["kraken"]; var realBitflyer = rateProvider.Providers["bitflyer"]; var realNDax = rateProvider.Providers["ndax"]; var realBitfinex = rateProvider.Providers["bitfinex"]; var realBitpay = rateProvider.Providers["bitpay"]; rateProvider.Providers.Clear(); rateProvider.AvailableRateProviders.Clear(); coinAverageMock = new MockRateProvider(new("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/")); coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m))); coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_EUR"), new BidAsk(4000m))); coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m))); coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m))); coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m))); rateProvider.Providers.Add("coingecko", coinAverageMock); var bitflyerMock = new MockRateProvider(realBitflyer.RateSourceInfo); bitflyerMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_JPY"), new BidAsk(700000m))); rateProvider.Providers.Add("bitflyer", bitflyerMock); var ndax = new MockRateProvider(realNDax.RateSourceInfo); ndax.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(6000m))); rateProvider.Providers.Add("ndax", ndax); var bitfinex = new MockRateProvider(realBitfinex.RateSourceInfo); bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m))); rateProvider.Providers.Add("bitfinex", bitfinex); var bitpay = new MockRateProvider(realBitpay.RateSourceInfo); bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETB_BTC"), new BidAsk(0.1m))); bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("DOGE_BTC"), new BidAsk(0.004m))); rateProvider.Providers.Add("bitpay", bitpay); var kraken = new MockRateProvider(realKraken.RateSourceInfo); kraken.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETH_BTC"), new BidAsk(0.1m))); kraken.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m))); kraken.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m))); rateProvider.Providers.Add("kraken", kraken); foreach (var prov in rateProvider.Providers) rateProvider.AvailableRateProviders.Add(prov.Value.RateSourceInfo); } // reset test server policies var settings = GetService(); await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false }); TestLogs.LogInformation("Waiting site is operational..."); await WaitSiteIsOperational(); TestLogs.LogInformation("Site is now operational"); } MockRateProvider coinAverageMock; private async Task WaitSiteIsOperational() { // Opportunistic call to wake up view compilation in debug mode, we don't need to await. _ = HttpClient.GetAsync("/").ConfigureAwait(false); try { using var cts = new CancellationTokenSource(20_000); await WaitIsFullySynched(cts.Token); } catch (OperationCanceledException) { // Sometimes NBX needs a kick... await MineOneBlock(); using var cts = new CancellationTokenSource(20_000); await WaitIsFullySynched(cts.Token); } } private async Task MineOneBlock() { var cheatModes = GetService>(); foreach (var cheatMode in cheatModes.OfType()) { try { await cheatMode.MineBlock(new ICheckoutCheatModeExtension.MineBlockContext() { BlockCount = 1 }); } catch { } } } private async Task WaitIsFullySynched(CancellationToken cancellationToken) { var o = GetService>().ToArray(); while (!o.All(d => d.AllAvailable())) { await Task.Delay(10, cancellationToken).ConfigureAwait(false); } } private string FindBTCPayServerDirectory() { var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo(); return Path.Combine(solutionDirectory.FullName, "BTCPayServer"); } public HttpClient HttpClient { get; set; } public string HostName { get; internal set; } public InvoiceRepository InvoiceRepository { get; private set; } public StoreRepository StoreRepository { get; private set; } public BTCPayNetworkProvider Networks { get; private set; } public string IntegratedLightning { get; internal set; } public bool InContainer { get; internal set; } public T GetService() => _Host.Services.GetRequiredService(); public IServiceProvider ServiceProvider => _Host.Services; public string SSHPassword { get; internal set; } public string SSHKeyFile { get; internal set; } public string SSHConnection { get; set; } public bool NoCSP { get; set; } public string HostEnvironment { get; set; } = Environments.Development; public bool RuntimeCompilation { get; set; } public T GetController(string userId = null, string storeId = null, bool isAdmin = false) where T : ControllerBase { var context = new DefaultHttpContext(); context.Request.Host = new HostString("127.0.0.1", Port); context.Request.Scheme = "http"; context.Request.Protocol = "http"; if (userId != null) { List claims = new List(); claims.Add(new Claim(ClaimTypes.NameIdentifier, userId)); if (isAdmin) claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin)); claims.Add(new Claim(GreenfieldConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString())); context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie)); } if (storeId != null) { context.SetStoreData(GetService().FindStore(storeId, userId).GetAwaiter().GetResult()); } var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var provider = scope.CreateScope().ServiceProvider; context.RequestServices = provider; var httpAccessor = provider.GetRequiredService(); httpAccessor.HttpContext = context; context.Session = provider.GetRequiredService().Create("Dummy", TimeSpan.MaxValue, TimeSpan.MaxValue, () => true, true); var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T)); controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/")); controller.ControllerContext = new ControllerContext() { HttpContext = context }; return controller; } public void Dispose() { if (_Host != null) { var app = _Host.Services.GetService(); app.StopApplication(); _Host.WaitForShutdown(); _Host.Dispose(); } } public void ChangeRate(string pair, BidAsk bidAsk) { var p = CurrencyPair.Parse(pair); var index = coinAverageMock.ExchangeRates.FindIndex(o => o.CurrencyPair == p); coinAverageMock.ExchangeRates[index] = new PairRate(p, bidAsk); } public async Task EnableExperimental() { var r = GetService(); var p = await r.GetSettingAsync() ?? new PoliciesSettings(); p.Experimental = true; await r.UpdateSetting(p); } } }