diff --git a/.circleci/config.yml b/.circleci/config.yml index f6c8dd4bf..c403da01b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: - checkout - run: command: | - cd .circleci && ./run-tests.sh "Fast=Fast" && ./can-build.sh + cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh selenium_tests: machine: enabled: true @@ -24,18 +24,6 @@ jobs: - run: command: | cd .circleci && ./run-tests.sh "Integration=Integration" - external_tests: - machine: - enabled: true - steps: - - checkout - - run: - command: | - if [ "$CIRCLE_PROJECT_USERNAME" == "btcpayserver" ] && [ "$CIRCLE_PROJECT_REPONAME" == "btcpayserver" ]; then - cd .circleci && ./run-tests.sh "ExternalIntegration=ExternalIntegration" - else - echo "Skipping running ExternalIntegration tests outside of context of main user and repository that have access to secrets" - fi trigger_docs_build: machine: enabled: true @@ -127,10 +115,6 @@ workflows: - fast_tests - selenium_tests - integration_tests - - external_tests: - filters: - branches: - only: master publish: jobs: - trigger_docs_build: diff --git a/BTCPayServer.Tests/FactWithSecretAttribute.cs b/BTCPayServer.Tests/FactWithSecretAttribute.cs new file mode 100644 index 000000000..a51ad89e4 --- /dev/null +++ b/BTCPayServer.Tests/FactWithSecretAttribute.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Xunit; +using Xunit.Sdk; + +namespace BTCPayServer.Tests +{ + public class FactWithSecretAttribute : FactAttribute + { + public FactWithSecretAttribute(string secret) + { + try + { + GetFromSecrets(secret); + } + catch (XunitException ex) + { + Skip = ex.Message; + } + } + public static string GetFromSecrets(string key) + { + var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}"); + if (!string.IsNullOrEmpty(connStr) && connStr != "none") + return connStr; + var builder = new ConfigurationBuilder(); + builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117"); + var config = builder.Build(); + var token = config[key]; + Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} \""); + return token; + } + } +} diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 60a98d0be..5924100cf 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -162,23 +162,6 @@ namespace BTCPayServer.Tests #endif } - [Fact] - public async Task CheckNoDeadLink() - { - var views = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "Views"); - var viewFiles = Directory.EnumerateFiles(views, "*.cshtml", SearchOption.AllDirectories).ToArray(); - Assert.NotEmpty(viewFiles); - Regex regex = new Regex("href=\"(http.*?)\""); - var httpClient = new HttpClient(); - List checkLinks = new List(); - foreach (var file in viewFiles) - { - checkLinks.Add(CheckDeadLinks(regex, httpClient, file)); - } - - await Task.WhenAll(checkLinks); - } - [Fact] public async Task CheckExternalNoReferrerLinks() { @@ -243,73 +226,6 @@ namespace BTCPayServer.Tests } } - private async Task CheckDeadLinks(Regex regex, HttpClient httpClient, string file) - { - List checkLinks = new List(); - var text = await File.ReadAllTextAsync(file); - - var urlBlacklist = new string[] - { - "https://www.btse.com", // not allowing to be hit from circleci - "https://www.bitpay.com", // not allowing to be hit from circleci - "https://support.bitpay.com", - "https://www.pnxbet.com" //has geo blocking - }; - - foreach (var match in regex.Matches(text).OfType()) - { - var url = match.Groups[1].Value; - if (urlBlacklist.Any(a => url.StartsWith(a.ToLowerInvariant()))) - continue; - checkLinks.Add(AssertLinkNotDead(httpClient, url, file)); - } - - await Task.WhenAll(checkLinks); - } - - private async Task AssertLinkNotDead(HttpClient httpClient, string url, string file) - { - var uri = new Uri(url); - - try - { - using var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.TryAddWithoutValidation("Accept", - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); - request.Headers.TryAddWithoutValidation("User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0"); - var response = await httpClient.SendAsync(request); - if (response.StatusCode == HttpStatusCode.ServiceUnavailable) // Temporary issue - { - TestLogs.LogInformation($"Unavailable: {url} ({file})"); - return; - } - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - if (uri.Fragment.Length != 0) - { - var fragment = uri.Fragment.Substring(1); - var contents = await response.Content.ReadAsStringAsync(); - Assert.Matches($"id=\"{fragment}\"", contents); - } - - TestLogs.LogInformation($"OK: {url} ({file})"); - } - catch (Exception ex) when (ex is MatchesException) - { - var details = ex.Message; - TestLogs.LogInformation($"FAILED: {url} ({file}) – anchor not found: {uri.Fragment}"); - - throw; - } - catch (Exception ex) - { - var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message; - TestLogs.LogInformation($"FAILED: {url} ({file}) {details}"); - - throw; - } - } - [Fact] public void CanHandleUriValidation() { @@ -535,25 +451,6 @@ namespace BTCPayServer.Tests } #endif - [Fact] - public async Task CheckJsContent() - { - // This test verify that no malicious js is added in the minified files. - // We should extend the tests to other js files, but we can do as we go... - - using HttpClient client = new HttpClient(); - var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js"); - var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; - var expected = await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync(); - Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase)); - } - string GetFileContent(params string[] path) - { - var l = path.ToList(); - l.Insert(0, TestUtils.TryGetSolutionDirectoryInfo().FullName); - return File.ReadAllText(Path.Combine(l.ToArray())); - } - [Fact] public void CanParseLegacyLabels() { @@ -919,166 +816,6 @@ namespace BTCPayServer.Tests } } - [Fact()] - public void CanSolveTheDogesRatesOnKraken() - { - var provider = new BTCPayNetworkProvider(ChainName.Mainnet); - var factory = CreateBTCPayRateFactory(); - var fetcher = new RateFetcher(factory); - - Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule)); - foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" }) - { - var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult(); - Assert.NotNull(result.BidAsk); - Assert.Empty(result.Errors); - } - } - - [Fact] - public void CanGetRateCryptoCurrenciesByDefault() - { - var provider = new BTCPayNetworkProvider(ChainName.Mainnet); - var factory = CreateBTCPayRateFactory(); - var fetcher = new RateFetcher(factory); - var pairs = - provider.GetAll() - .Select(c => new CurrencyPair(c.CryptoCode, "USD")) - .ToHashSet(); - - var rules = new StoreBlob().GetDefaultRateRules(provider); - var result = fetcher.FetchRates(pairs, rules, default); - foreach (var value in result) - { - var rateResult = value.Value.GetAwaiter().GetResult(); - TestLogs.LogInformation($"Testing {value.Key.ToString()}"); - if (value.Key.ToString() == "BTX_USD") // Broken shitcoin - continue; - Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}"); - } - } - - [Fact] - public void CanQueryDirectProviders() - { - var factory = CreateBTCPayRateFactory(); - var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct) - .Select(s => s.Id).ToHashSet(); - var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray()); - foreach (var result in factory - .Providers - .Where(p => p.Value is BackgroundFetcherRateProvider bf && - !(bf.Inner is CoinGeckoRateProvider cg && cg.UnderlyingExchange != null)) - .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), - Fetcher: (BackgroundFetcherRateProvider)p.Value)) - .ToList()) - { - TestLogs.LogInformation($"Testing {result.ExpectedName}"); - if (result.ExpectedName == "ndax") - { - TestLogs.LogInformation($"Skipping (currently crashing)"); - continue; - } - - result.Fetcher.InvalidateCache(); - var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result); - result.Fetcher.InvalidateCache(); - Assert.NotNull(exchangeRates); - Assert.NotEmpty(exchangeRates); - Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); - if (result.ExpectedName == "bitbank" || result.ExpectedName == "bitflyer") - { - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && - e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY - } - else if (result.ExpectedName == "polispay") - { - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && - e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS - } - else if (result.ExpectedName == "argoneum") - { - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => e.CurrencyPair == new CurrencyPair("BTC", "AGM") && - e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 AGM - } - else if (result.ExpectedName == "ripio") - { - // Ripio keeps changing their pair, so anything is fine... - Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); - } - else if (result.ExpectedName == "cryptomarket") - { - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => e.CurrencyPair == new CurrencyPair("BTC", "CLP") && - e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 CLP - } - else - { - // This check if the currency pair is using right currency pair - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") || - e.CurrencyPair == new CurrencyPair("BTC", "EUR") || - e.CurrencyPair == new CurrencyPair("BTC", "USDT") || - e.CurrencyPair == new CurrencyPair("BTC", "USDC") || - e.CurrencyPair == new CurrencyPair("BTC", "CAD")) - && e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD - ); - } - // We are not showing a directly implemented exchange as directly implemented in the UI - // we need to modify the AvailableRateProvider - - // There are some exception we stopped supporting but don't want to break backward compat - if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax") - Assert.Contains(result.ExpectedName, directlySupported); - } - - // Kraken emit one request only after first GetRates - factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult(); - } - - [Fact] - public async Task CanExportBackgroundFetcherState() - { - var factory = CreateBTCPayRateFactory(); - var provider = (BackgroundFetcherRateProvider)factory.Providers["kraken"]; - await provider.GetRatesAsync(default); - var state = provider.GetState(); - Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR")); - var provider2 = new BackgroundFetcherRateProvider(provider.Inner) - { - RefreshRate = provider.RefreshRate, - ValidatyTime = provider.ValidatyTime - }; - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); - // Should throw - await Assert.ThrowsAsync(async () => - await provider2.GetRatesAsync(cts.Token)); - } - - provider2.LoadState(state); - Assert.Equal(provider.LastRequested, provider2.LastRequested); - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); - // Should not throw, as things should be cached - await provider2.GetRatesAsync(cts.Token); - } - - Assert.Equal(provider.NextUpdate, provider2.NextUpdate); - Assert.NotEqual(provider.LastRequested, provider2.LastRequested); - Assert.Equal(provider.Expiration, provider2.Expiration); - - var str = JsonConvert.SerializeObject(state); - var state2 = JsonConvert.DeserializeObject(str); - var str2 = JsonConvert.SerializeObject(state2); - Assert.Equal(str, str2); - } - [Fact] public async Task CanExpandExternalConnectionString() { diff --git a/BTCPayServer.Tests/StorageTests.cs b/BTCPayServer.Tests/StorageTests.cs deleted file mode 100644 index dc66050b0..000000000 --- a/BTCPayServer.Tests/StorageTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Models; -using BTCPayServer.Controllers; -using BTCPayServer.Models; -using BTCPayServer.Models.ServerViewModels; -using BTCPayServer.Storage.Models; -using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; -using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; -using BTCPayServer.Storage.ViewModels; -using BTCPayServer.Tests.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Xunit; -using Xunit.Abstractions; - -namespace BTCPayServer.Tests -{ - public class StorageTests - { - public StorageTests(ITestOutputHelper helper) - { - Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; - Logs.LogProvider = new XUnitLogProvider(helper); - } - - [Fact(Timeout = TestUtils.TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CanConfigureStorage() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - - - //Once we select a provider, redirect to its view - var localResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.FileSystem - })); - Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName); - Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]); - - - var AmazonS3result = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.AmazonS3 - })); - Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName); - Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]); - - var GoogleResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.GoogleCloudStorage - })); - Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName); - Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]); - - - var AzureResult = Assert - .IsType(controller.Storage(new StorageSettings() - { - Provider = StorageProvider.AzureBlobStorage - })); - Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName); - Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]); - - //Cool, we get redirected to the config pages - //Let's configure this stuff - - //Let's try and cheat and go to an invalid storage provider config - Assert.Equal(nameof(Storage), (Assert - .IsType(await controller.StorageProvider("I am not a real provider")) - .ActionName)); - - //ok no more messing around, let's configure this shit. - var fileSystemStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) - .Model); - - //local file system does not need config, easy days! - Assert.IsType( - await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); - - //ok cool, let's see if this got set right - var shouldBeRedirectingToLocalStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.FileSystem, - shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); - - - //if we tell the settings page to force, it should allow us to select a new provider - Assert.IsType(Assert.IsType(await controller.Storage(true)).Model); - - //awesome, now let's see if the files result says we're all set up - var viewFilesViewModel = - Assert.IsType(Assert.IsType(await controller.Files()).Model); - Assert.True(viewFilesViewModel.StorageConfigured); - Assert.Empty(viewFilesViewModel.Files); - } - } - - [Fact] - [Trait("Integration", "Integration")] - public async void CanUseLocalProviderFiles() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - - var fileSystemStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) - .Model); - Assert.IsType( - await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); - - var shouldBeRedirectingToLocalStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.FileSystem, - shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); - - - await CanUploadRemoveFiles(controller); - } - } - - [Fact(Timeout = TestUtils.TestTimeout)] - [Trait("ExternalIntegration", "ExternalIntegration")] - public async Task CanUseAzureBlobStorage() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - var controller = tester.PayTester.GetController(user.UserId, user.StoreId); - var azureBlobStorageConfiguration = Assert.IsType(Assert - .IsType(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) - .Model); - - azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString"); - azureBlobStorageConfiguration.ContainerName = "testscontainer"; - Assert.IsType( - await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration)); - - - var shouldBeRedirectingToAzureStorageConfigPage = - Assert.IsType(await controller.Storage()); - Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName); - Assert.Equal(StorageProvider.AzureBlobStorage, - shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]); - - //seems like azure config worked, let's see if the conn string was actually saved - - Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert - .IsType(Assert - .IsType( - await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) - .Model).ConnectionString); - - - - await CanUploadRemoveFiles(controller); - } - } - - - private async Task CanUploadRemoveFiles(ServerController controller) - { - var fileContent = "content"; - List fileList = new List(); - fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent)); - - var uploadFormFileResult = Assert.IsType(await controller.CreateFiles(fileList)); - Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds")); - string[] uploadFileList = (string[])uploadFormFileResult.RouteValues["fileIds"]; - var fileId = uploadFileList[0]; - Assert.Equal("Files", uploadFormFileResult.ActionName); - - //check if file was uploaded and saved in db - var viewFilesViewModel = - Assert.IsType(Assert.IsType(await controller.Files(new string[] { fileId })).Model); - - Assert.NotEmpty(viewFilesViewModel.Files); - Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId)); - Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]); - - - //verify file is available and the same - var net = new System.Net.WebClient(); - var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId])); - Assert.Equal(fileContent, data); - - //create a temporary link to file - var tmpLinkGenerate = Assert.IsType(await controller.CreateTemporaryFileUrl(fileId, - new ServerController.CreateTemporaryFileUrlViewModel() - { - IsDownload = true, - TimeAmount = 1, - TimeType = ServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes - })); - var statusMessageModel = controller.TempData.GetStatusMessageModel(); - Assert.NotNull(statusMessageModel); - Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity); - var index = statusMessageModel.Html.IndexOf("target='_blank'>"); - var url = statusMessageModel.Html.Substring(index) - .Replace("", string.Empty) - .Replace("target='_blank'>", string.Empty); - //verify tmpfile is available and the same - data = await net.DownloadStringTaskAsync(new Uri(url)); - Assert.Equal(fileContent, data); - - - //delete file - Assert.IsType(await controller.DeleteFile(fileId)); - statusMessageModel = controller.TempData.GetStatusMessageModel(); - Assert.NotNull(statusMessageModel); - - Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity); - - //attempt to fetch deleted file - viewFilesViewModel = - Assert.IsType(Assert.IsType(await controller.Files(new string[] { fileId })).Model); - Assert.Null(viewFilesViewModel.DirectUrlByFiles); - } - - - - - - - private static string GetFromSecrets(string key) - { - var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}"); - if (!string.IsNullOrEmpty(connStr) && connStr != "none") - return connStr; - var builder = new ConfigurationBuilder(); - builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117"); - var config = builder.Build(); - var token = config[key]; - Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} \""); - return token; - } - } -} diff --git a/BTCPayServer.Tests/TestData/OpenAPI-Specification-schema.json b/BTCPayServer.Tests/TestData/OpenAPI-Specification-schema.json new file mode 100644 index 000000000..dadd2837e --- /dev/null +++ b/BTCPayServer.Tests/TestData/OpenAPI-Specification-schema.json @@ -0,0 +1,1662 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/schema/2021-09-28", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Validation schema for OpenAPI Specification 3.0.X.", + "type": "object", + "required": [ + "openapi", + "info", + "paths" + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "type": "object", + "required": [ + "title", + "version" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": [ + "default" + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": { + }, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": { + }, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": { + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": { + }, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": [ + "simple" + ], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/definitions/Operation" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": [ + "responses" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": { + } + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": [ + "schema", + "content" + ] + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": [ + "style" + ] + } + }, + { + "not": { + "required": [ + "explode" + ] + } + }, + { + "not": { + "required": [ + "allowReserved" + ] + } + }, + { + "not": { + "required": [ + "example" + ] + } + }, + { + "not": { + "required": [ + "examples" + ] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "required": [ + "name", + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + }, + { + "$ref": "#/definitions/ParameterLocation" + } + ] + }, + "ParameterLocation": { + "description": "Parameter location", + "oneOf": [ + { + "description": "Parameter in path", + "required": [ + "required" + ], + "properties": { + "in": { + "enum": [ + "path" + ] + }, + "style": { + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + }, + "required": { + "enum": [ + true + ] + } + } + }, + { + "description": "Parameter in query", + "properties": { + "in": { + "enum": [ + "query" + ] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + { + "description": "Parameter in header", + "properties": { + "in": { + "enum": [ + "header" + ] + }, + "style": { + "enum": [ + "simple" + ], + "default": "simple" + } + } + }, + { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": [ + "cookie" + ] + }, + "style": { + "enum": [ + "form" + ], + "default": "form" + } + } + } + ] + }, + "RequestBody": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": [ + "bearerFormat" + ] + }, + "properties": { + "scheme": { + "not": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": { + } + }, + "requestBody": { + }, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": [ + "operationId", + "operationRef" + ] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": { + } + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + } +} diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs new file mode 100644 index 000000000..5d4240a38 --- /dev/null +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Controllers; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using NBitcoin; +using NBitpayClient; +using Newtonsoft.Json; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace BTCPayServer.Tests +{ + [Trait("ThirdParty", "ThirdParty")] + public class ThirdPartyTests : UnitTestBase + { + + public ThirdPartyTests(ITestOutputHelper helper) : base(helper) + { + + } + + [FactWithSecret("AzureBlobStorageConnectionString")] + public async Task CanUseAzureBlobStorage() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + var azureBlobStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model); + + azureBlobStorageConfiguration.ConnectionString = FactWithSecretAttribute.GetFromSecrets("AzureBlobStorageConnectionString"); + azureBlobStorageConfiguration.ContainerName = "testscontainer"; + Assert.IsType( + await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration)); + + + var shouldBeRedirectingToAzureStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage, + shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]); + + //seems like azure config worked, let's see if the conn string was actually saved + + Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert + .IsType(Assert + .IsType( + await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString())) + .Model).ConnectionString); + + + + await UnitTest1.CanUploadRemoveFiles(controller); + } + } + + [Fact] + public void CanQueryDirectProviders() + { + var factory = FastTests.CreateBTCPayRateFactory(); + var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct) + .Select(s => s.Id).ToHashSet(); + var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray()); + foreach (var result in factory + .Providers + .Where(p => p.Value is BackgroundFetcherRateProvider bf && + !(bf.Inner is CoinGeckoRateProvider cg && cg.UnderlyingExchange != null)) + .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), + Fetcher: (BackgroundFetcherRateProvider)p.Value)) + .ToList()) + { + TestLogs.LogInformation($"Testing {result.ExpectedName}"); + if (result.ExpectedName == "ndax") + { + TestLogs.LogInformation($"Skipping (currently crashing)"); + continue; + } + + result.Fetcher.InvalidateCache(); + var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result); + result.Fetcher.InvalidateCache(); + Assert.NotNull(exchangeRates); + Assert.NotEmpty(exchangeRates); + Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); + if (result.ExpectedName == "bitbank" || result.ExpectedName == "bitflyer") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && + e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY + } + else if (result.ExpectedName == "polispay") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && + e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS + } + else if (result.ExpectedName == "argoneum") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("BTC", "AGM") && + e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 AGM + } + else if (result.ExpectedName == "ripio") + { + // Ripio keeps changing their pair, so anything is fine... + Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); + } + else if (result.ExpectedName == "cryptomarket") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("BTC", "CLP") && + e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 CLP + } + else + { + // This check if the currency pair is using right currency pair + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") || + e.CurrencyPair == new CurrencyPair("BTC", "EUR") || + e.CurrencyPair == new CurrencyPair("BTC", "USDT") || + e.CurrencyPair == new CurrencyPair("BTC", "USDC") || + e.CurrencyPair == new CurrencyPair("BTC", "CAD")) + && e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD + ); + } + // We are not showing a directly implemented exchange as directly implemented in the UI + // we need to modify the AvailableRateProvider + + // There are some exception we stopped supporting but don't want to break backward compat + if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax") + Assert.Contains(result.ExpectedName, directlySupported); + } + + // Kraken emit one request only after first GetRates + factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult(); + } + + [Fact] + public async Task CheckNoDeadLink() + { + var views = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "Views"); + var viewFiles = Directory.EnumerateFiles(views, "*.cshtml", SearchOption.AllDirectories).ToArray(); + Assert.NotEmpty(viewFiles); + Regex regex = new Regex("href=\"(http.*?)\""); + var httpClient = new HttpClient(); + List checkLinks = new List(); + foreach (var file in viewFiles) + { + checkLinks.Add(CheckDeadLinks(regex, httpClient, file)); + } + + await Task.WhenAll(checkLinks); + } + + private async Task CheckDeadLinks(Regex regex, HttpClient httpClient, string file) + { + List checkLinks = new List(); + var text = await File.ReadAllTextAsync(file); + + var urlBlacklist = new string[] + { + "https://www.btse.com", // not allowing to be hit from circleci + "https://www.bitpay.com", // not allowing to be hit from circleci + "https://support.bitpay.com", + "https://www.pnxbet.com" //has geo blocking + }; + + foreach (var match in regex.Matches(text).OfType()) + { + var url = match.Groups[1].Value; + if (urlBlacklist.Any(a => url.StartsWith(a.ToLowerInvariant()))) + continue; + checkLinks.Add(AssertLinkNotDead(httpClient, url, file)); + } + + await Task.WhenAll(checkLinks); + } + + private async Task AssertLinkNotDead(HttpClient httpClient, string url, string file) + { + var uri = new Uri(url); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.TryAddWithoutValidation("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + request.Headers.TryAddWithoutValidation("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0"); + var response = await httpClient.SendAsync(request); + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) // Temporary issue + { + TestLogs.LogInformation($"Unavailable: {url} ({file})"); + return; + } + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + if (uri.Fragment.Length != 0) + { + var fragment = uri.Fragment.Substring(1); + var contents = await response.Content.ReadAsStringAsync(); + Assert.Matches($"id=\"{fragment}\"", contents); + } + + TestLogs.LogInformation($"OK: {url} ({file})"); + } + catch (Exception ex) when (ex is MatchesException) + { + var details = ex.Message; + TestLogs.LogInformation($"FAILED: {url} ({file}) – anchor not found: {uri.Fragment}"); + + throw; + } + catch (Exception ex) + { + var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message; + TestLogs.LogInformation($"FAILED: {url} ({file}) {details}"); + + throw; + } + } + + [Fact()] + public void CanSolveTheDogesRatesOnKraken() + { + var provider = new BTCPayNetworkProvider(ChainName.Mainnet); + var factory = FastTests.CreateBTCPayRateFactory(); + var fetcher = new RateFetcher(factory); + + Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule)); + foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" }) + { + var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult(); + Assert.NotNull(result.BidAsk); + Assert.Empty(result.Errors); + } + } + + [Fact] + public void CanGetRateCryptoCurrenciesByDefault() + { + var provider = new BTCPayNetworkProvider(ChainName.Mainnet); + var factory = FastTests.CreateBTCPayRateFactory(); + var fetcher = new RateFetcher(factory); + var pairs = + provider.GetAll() + .Select(c => new CurrencyPair(c.CryptoCode, "USD")) + .ToHashSet(); + + var rules = new StoreBlob().GetDefaultRateRules(provider); + var result = fetcher.FetchRates(pairs, rules, default); + foreach (var value in result) + { + var rateResult = value.Value.GetAwaiter().GetResult(); + TestLogs.LogInformation($"Testing {value.Key.ToString()}"); + if (value.Key.ToString() == "BTX_USD") // Broken shitcoin + continue; + Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}"); + } + } + + [Fact] + public async Task CheckJsContent() + { + // This test verify that no malicious js is added in the minified files. + // We should extend the tests to other js files, but we can do as we go... + + using HttpClient client = new HttpClient(); + var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js"); + var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; + var expected = await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync(); + Assert.Equal(expected, actual.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase)); + } + string GetFileContent(params string[] path) + { + var l = path.ToList(); + l.Insert(0, TestUtils.TryGetSolutionDirectoryInfo().FullName); + return File.ReadAllText(Path.Combine(l.ToArray())); + } + + [Fact] + public async Task CanExportBackgroundFetcherState() + { + var factory = FastTests.CreateBTCPayRateFactory(); + var provider = (BackgroundFetcherRateProvider)factory.Providers["kraken"]; + await provider.GetRatesAsync(default); + var state = provider.GetState(); + Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR")); + var provider2 = new BackgroundFetcherRateProvider(provider.Inner) + { + RefreshRate = provider.RefreshRate, + ValidatyTime = provider.ValidatyTime + }; + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); + // Should throw + await Assert.ThrowsAsync(async () => + await provider2.GetRatesAsync(cts.Token)); + } + + provider2.LoadState(state); + Assert.Equal(provider.LastRequested, provider2.LastRequested); + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); + // Should not throw, as things should be cached + await provider2.GetRatesAsync(cts.Token); + } + + Assert.Equal(provider.NextUpdate, provider2.NextUpdate); + Assert.NotEqual(provider.LastRequested, provider2.LastRequested); + Assert.Equal(provider.Expiration, provider2.Expiration); + + var str = JsonConvert.SerializeObject(state); + var state2 = JsonConvert.DeserializeObject(str); + var str2 = JsonConvert.SerializeObject(state2); + Assert.Equal(str, str2); + } + + [Fact] + public async Task CanUseExchangeSpecificRate() + { + using (var tester = ServerTester.Create()) + { + tester.PayTester.MockRates = false; + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + List rates = new List(); + rates.Add(await CreateInvoice(tester, user, "coingecko")); + var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY"); + var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY"); + Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache + rates.Add(bitflyer); + + foreach (var rate in rates) + { + Assert.Single(rates.Where(r => r == rate)); + } + } + } + + private static async Task CreateInvoice(ServerTester tester, TestAccount user, string exchange, + string currency = "USD") + { + var storeController = user.GetController(); + var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; + vm.PreferredExchange = exchange; + await storeController.Rates(vm); + var invoice2 = await user.BitPay.CreateInvoiceAsync( + new Invoice() + { + Price = 5000.0m, + Currency = currency, + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + return invoice2.CryptoInfo[0].Rate; + } + } +} diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 89eadc2ea..ee479660c 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -45,10 +45,14 @@ using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Labels; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Rates; +using BTCPayServer.Storage.Models; +using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using BTCPayServer.Storage.ViewModels; using BTCPayServer.Tests.Logging; using BTCPayServer.Validation; using ExchangeSharp; using Fido2NetLib; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -138,12 +142,8 @@ namespace BTCPayServer.Tests var sresp = Assert .IsType(await tester.PayTester.GetController(acc.UserId, acc.StoreId) .Swagger()).Value.ToJson(); - JObject swagger = JObject.Parse(sresp); - using HttpClient client = new HttpClient(); - var resp = await client.GetAsync( - "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json"); - var schema = JSchema.Parse(await resp.Content.ReadAsStringAsync()); + var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json"))); IList errors; bool valid = swagger.IsValid(schema, out errors); //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. @@ -1265,51 +1265,6 @@ namespace BTCPayServer.Tests } } - [Fact(Timeout = LongRunningTestTimeout)] - [Trait("Integration", "Integration")] - public async Task CanUseExchangeSpecificRate() - { - using (var tester = ServerTester.Create()) - { - tester.PayTester.MockRates = false; - await tester.StartAsync(); - var user = tester.NewAccount(); - user.GrantAccess(); - user.RegisterDerivationScheme("BTC"); - List rates = new List(); - rates.Add(await CreateInvoice(tester, user, "coingecko")); - var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY"); - var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY"); - Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache - rates.Add(bitflyer); - - foreach (var rate in rates) - { - Assert.Single(rates.Where(r => r == rate)); - } - } - } - - private static async Task CreateInvoice(ServerTester tester, TestAccount user, string exchange, - string currency = "USD") - { - var storeController = user.GetController(); - var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; - vm.PreferredExchange = exchange; - await storeController.Rates(vm); - var invoice2 = await user.BitPay.CreateInvoiceAsync( - new Invoice() - { - Price = 5000.0m, - Currency = currency, - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true - }, Facade.Merchant); - return invoice2.CryptoInfo[0].Rate; - } - [Fact(Timeout = LongRunningTestTimeout)] [Trait("Integration", "Integration")] public async Task CanUseAnyoneCanCreateInvoice() @@ -2794,7 +2749,175 @@ namespace BTCPayServer.Tests } } - - + + [Fact(Timeout = TestUtils.TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanConfigureStorage() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + + //Once we select a provider, redirect to its view + var localResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.FileSystem + })); + Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName); + Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]); + + + var AmazonS3result = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.AmazonS3 + })); + Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName); + Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]); + + var GoogleResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.GoogleCloudStorage + })); + Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName); + Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]); + + + var AzureResult = Assert + .IsType(controller.Storage(new StorageSettings() + { + Provider = StorageProvider.AzureBlobStorage + })); + Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName); + Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]); + + //Cool, we get redirected to the config pages + //Let's configure this stuff + + //Let's try and cheat and go to an invalid storage provider config + Assert.Equal(nameof(Storage), (Assert + .IsType(await controller.StorageProvider("I am not a real provider")) + .ActionName)); + + //ok no more messing around, let's configure this shit. + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); + + //local file system does not need config, easy days! + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + + //ok cool, let's see if this got set right + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + + + //if we tell the settings page to force, it should allow us to select a new provider + Assert.IsType(Assert.IsType(await controller.Storage(true)).Model); + + //awesome, now let's see if the files result says we're all set up + var viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files()).Model); + Assert.True(viewFilesViewModel.StorageConfigured); + Assert.Empty(viewFilesViewModel.Files); + } + } + + [Fact] + [Trait("Integration", "Integration")] + public async void CanUseLocalProviderFiles() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var controller = tester.PayTester.GetController(user.UserId, user.StoreId); + + var fileSystemStorageConfiguration = Assert.IsType(Assert + .IsType(await controller.StorageProvider(StorageProvider.FileSystem.ToString())) + .Model); + Assert.IsType( + await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration)); + + var shouldBeRedirectingToLocalStorageConfigPage = + Assert.IsType(await controller.Storage()); + Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName); + Assert.Equal(StorageProvider.FileSystem, + shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]); + + + await CanUploadRemoveFiles(controller); + } + } + + internal static async Task CanUploadRemoveFiles(ServerController controller) + { + var fileContent = "content"; + List fileList = new List(); + fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent)); + + var uploadFormFileResult = Assert.IsType(await controller.CreateFiles(fileList)); + Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds")); + string[] uploadFileList = (string[])uploadFormFileResult.RouteValues["fileIds"]; + var fileId = uploadFileList[0]; + Assert.Equal("Files", uploadFormFileResult.ActionName); + + //check if file was uploaded and saved in db + var viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files(new string[] { fileId })).Model); + + Assert.NotEmpty(viewFilesViewModel.Files); + Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId)); + Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]); + + + //verify file is available and the same + var net = new System.Net.WebClient(); + var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId])); + Assert.Equal(fileContent, data); + + //create a temporary link to file + var tmpLinkGenerate = Assert.IsType(await controller.CreateTemporaryFileUrl(fileId, + new ServerController.CreateTemporaryFileUrlViewModel() + { + IsDownload = true, + TimeAmount = 1, + TimeType = ServerController.CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes + })); + var statusMessageModel = controller.TempData.GetStatusMessageModel(); + Assert.NotNull(statusMessageModel); + Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity); + var index = statusMessageModel.Html.IndexOf("target='_blank'>"); + var url = statusMessageModel.Html.Substring(index) + .Replace("", string.Empty) + .Replace("target='_blank'>", string.Empty); + //verify tmpfile is available and the same + data = await net.DownloadStringTaskAsync(new Uri(url)); + Assert.Equal(fileContent, data); + + + //delete file + Assert.IsType(await controller.DeleteFile(fileId)); + statusMessageModel = controller.TempData.GetStatusMessageModel(); + Assert.NotNull(statusMessageModel); + + Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity); + + //attempt to fetch deleted file + viewFilesViewModel = + Assert.IsType(Assert.IsType(await controller.Files(new string[] { fileId })).Model); + Assert.Null(viewFilesViewModel.DirectUrlByFiles); + } } } diff --git a/BTCPayServer.Tests/UnitTestBase.cs b/BTCPayServer.Tests/UnitTestBase.cs index 653d01a0f..09fd20140 100644 --- a/BTCPayServer.Tests/UnitTestBase.cs +++ b/BTCPayServer.Tests/UnitTestBase.cs @@ -14,6 +14,8 @@ namespace BTCPayServer.Tests { TestLogs = new XUnitLog(helper) { Name = "Tests" }; TestLogProvider = new XUnitLogProvider(helper); + Logs.Tester = TestLogs; + Logs.LogProvider = TestLogProvider; } public ILog TestLogs { diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs index b473e368e..84f23ad5a 100644 --- a/BTCPayServer.Tests/UtilitiesTests.cs +++ b/BTCPayServer.Tests/UtilitiesTests.cs @@ -18,13 +18,13 @@ namespace BTCPayServer.Tests /// /// Download transifex transactions and put them in BTCPayServer\wwwroot\locales /// + [FactWithSecret("TransifexAPIToken")] [Trait("Utilities", "Utilities")] - [Fact] public async Task PullTransifexTranslations() { // 1. Generate an API Token on https://www.transifex.com/user/settings/api/ // 2. Run "dotnet user-secrets set TransifexAPIToken " - var client = new TransifexClient(GetTransifexAPIToken()); + var client = new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken")); var json = await client.GetTransifexAsync("https://api.transifex.com/organizations/btcpayserver/projects/btcpayserver/resources/enjson/"); var langs = new[] { "en" }.Concat(((JObject)json["stats"]).Properties().Select(n => n.Name)).ToArray(); @@ -74,16 +74,6 @@ namespace BTCPayServer.Tests File.WriteAllText(Path.Combine(langsDir, langFile), content); }).ToArray()); } - - private static string GetTransifexAPIToken() - { - var builder = new ConfigurationBuilder(); - builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117"); - var config = builder.Build(); - var token = config["TransifexAPIToken"]; - Assert.False(token == null, "TransifexAPIToken is not set.\n 1.Generate an API Token on https://www.transifex.com/user/settings/api/ \n 2.Run \"dotnet user-secrets set TransifexAPIToken \""); - return token; - } } public class TransifexClient