From 5cfe6d537cb1715880db84a25bd19de67f783769 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Tue, 10 Sep 2024 05:15:58 -0500 Subject: [PATCH 1/4] Added property UnknownCapabilities to the Allowed.cs class. Updated DebuggerDisplay to represent unknown capabilities in the returned string. Made a copy of the Allowed class called AllowedRaw.cs in Bytewizer.Backblaze.Client.Internal that represents capabilities as a list of plain strings, with method ParseCapabilities to transform instances into the original Allowed class. Updated LangVersion in Backblaze.Client.csproj to 8.0 to allow the use of the coalescing assignment operator. Made a copy of AuthorizedAccountResponse.cs in Bytewizer.Backblaze.Client.Internal that uses AllowedRaw instead of Allowed. Updated AuthorizeAccountAsync to use AuthorizeAccountResponseRaw for the actual request and then convert the result to the regular AuthorizedAccountResponse structure, with ParseCapabilities splitting off unknown capabilities. --- src/Client/Backblaze.Client.csproj | 2 +- src/Client/Client/ApiRest.Endpoints.cs | 6 +- src/Client/Client/Internal/AllowedRaw.cs | 79 ++++++++++++++++ .../Internal/AuthorizeAccountResponseRaw.cs | 94 +++++++++++++++++++ src/Client/Models/Allowed.cs | 20 +++- 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 src/Client/Client/Internal/AllowedRaw.cs create mode 100644 src/Client/Client/Internal/AuthorizeAccountResponseRaw.cs diff --git a/src/Client/Backblaze.Client.csproj b/src/Client/Backblaze.Client.csproj index 640a322..e4f968c 100644 --- a/src/Client/Backblaze.Client.csproj +++ b/src/Client/Backblaze.Client.csproj @@ -10,7 +10,7 @@ Microcompiler Bytewizer Inc. Backblaze B2 Cloud Storage - 7.1 + 8.0 Bytewizer.Backblaze 1.1.0 $(VersionPrefix)-$(VersionSuffix) diff --git a/src/Client/Client/ApiRest.Endpoints.cs b/src/Client/Client/ApiRest.Endpoints.cs index e7a91a8..d82c08c 100644 --- a/src/Client/Client/ApiRest.Endpoints.cs +++ b/src/Client/Client/ApiRest.Endpoints.cs @@ -13,6 +13,8 @@ using Bytewizer.Backblaze.Models; using Bytewizer.Backblaze.Extensions; +using Bytewizer.Backblaze.Client.Internal; + namespace Bytewizer.Backblaze.Client { public abstract partial class ApiRest : DisposableObject @@ -47,7 +49,9 @@ public async Task> AuthorizeAccountAync using (var results = await _policy.InvokeClient.ExecuteAsync(async () => { return await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); })) { - return await HandleResponseAsync(results).ConfigureAwait(false); + var rawResult = await HandleResponseAsync(results).ConfigureAwait(false); + + return new ApiResults(rawResult.HttpResponse, rawResult.Response.ToAuthorizedAccountResponse()); } } diff --git a/src/Client/Client/Internal/AllowedRaw.cs b/src/Client/Client/Internal/AllowedRaw.cs new file mode 100644 index 0000000..5901d64 --- /dev/null +++ b/src/Client/Client/Internal/AllowedRaw.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using Newtonsoft.Json; + +using Bytewizer.Backblaze.Models; + +namespace Bytewizer.Backblaze.Client.Internal +{ + /// + /// Represents information related to an allowed authorization. + /// + [DebuggerDisplay("{DebuggerDisplay, nq}")] + internal class AllowedRaw + { + /// + /// Gets or sets a list of allowed. + /// + [JsonProperty(Required = Required.Always)] + public List Capabilities { get; set; } + + /// + /// Gets or sets restricted access only to this bucket id. + /// + public string BucketId { get; set; } + + /// + /// When bucket id is set and it is a valid bucket that has not been deleted this field is set to the name of the + /// bucket. It's possible that bucket id is set to a bucket that no longer exists in which case this field will be + /// null. It's also null when bucket id is null. + /// + public string BucketName { get; set; } + + /// + /// Gets or sets restricted access to files whose names start with the prefix. + /// + public string NamePrefix { get; set; } + + /// + /// Debugger display for this object. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get { return $"{{{nameof(Capabilities)}: {string.Join(", ", Capabilities)}, {nameof(NamePrefix)}: {NamePrefix}}}"; } + } + + /// + /// Translates this instance to an instance of , + /// parsing capabilities into the and + /// properties. + /// + /// An instance of . + public Allowed ParseCapabilities() + { + Allowed parsed = new Allowed(); + + parsed.BucketId = this.BucketId; + parsed.BucketName = this.BucketName; + parsed.NamePrefix = this.NamePrefix; + + foreach (string capabilityName in this.Capabilities) + { + if (Enum.TryParse(capabilityName, out var parsedCapability)) + { + parsed.Capabilities.Add(parsedCapability); + } + else + { + parsed.UnknownCapabilities ??= new List(); + parsed.UnknownCapabilities.Add(capabilityName); + } + } + + return parsed; + } + } +} diff --git a/src/Client/Client/Internal/AuthorizeAccountResponseRaw.cs b/src/Client/Client/Internal/AuthorizeAccountResponseRaw.cs new file mode 100644 index 0000000..550909f --- /dev/null +++ b/src/Client/Client/Internal/AuthorizeAccountResponseRaw.cs @@ -0,0 +1,94 @@ +using System; +using System.Diagnostics; + +using Newtonsoft.Json; + +using Bytewizer.Backblaze.Models; + +namespace Bytewizer.Backblaze.Client.Internal +{ + /// + /// Contains the results of authorize account request operation. + /// + [DebuggerDisplay("{DebuggerDisplay, nq}")] + internal class AuthorizeAccountResponseRaw : IResponse + { + /// + /// The identifier for the account. + /// + [JsonProperty(Required = Required.Always)] + public string AccountId { get; internal set; } + + /// + /// An authorization token to use with all calls other than authorize account that need an authorization header. + /// This authorization token is valid for at most 24 hours. + /// + [JsonProperty(Required = Required.Always)] + public string AuthorizationToken { get; internal set; } + + /// + /// The capabilities of this auth token and any restrictions on using it. + /// + [JsonProperty(Required = Required.Always)] + public AllowedRaw Allowed { get; internal set; } + + /// + /// The base url to use for all API calls except for uploading and downloading files. + /// + [JsonProperty(Required = Required.Always)] + public Uri ApiUrl { get; internal set; } + + /// + /// The base url to use for downloading files. + /// + [JsonProperty(Required = Required.Always)] + public Uri DownloadUrl { get; internal set; } + + /// + /// The recommended size for each part of a large file. We recommend using this part size for optimal upload performance. + /// + [JsonProperty(Required = Required.Always)] + public long RecommendedPartSize { get; internal set; } + + /// + /// The smallest possible size of a part of a large file (except the last one). This is smaller than the . + /// If you use it you may find that it takes longer overall to upload a large file. + /// + [JsonProperty(Required = Required.Always)] + public long AbsoluteMinimumPartSize { get; internal set; } + + /// + /// OBSOLETE: This field will always have the same value as . + /// + [Obsolete("This field will always have the same value as 'RecommendedPartSize'.")] + public long MinimumPartSize { get; internal set; } + + /// + /// Debugger display for this object. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get { return $"{{{nameof(AccountId)}: {AccountId}, {nameof(AuthorizationToken)}: {AuthorizationToken}}}"; } + } + + internal AuthorizeAccountResponse ToAuthorizedAccountResponse() + { + var response = new AuthorizeAccountResponse(); + + response.AccountId = this.AccountId; + response.AuthorizationToken = this.AuthorizationToken; + response.Allowed = this.Allowed.ParseCapabilities(); + response.ApiUrl = this.ApiUrl; + response.DownloadUrl = this.DownloadUrl; + response.RecommendedPartSize = this.RecommendedPartSize; + response.AbsoluteMinimumPartSize = this.AbsoluteMinimumPartSize; + + #pragma warning disable 618 + response.MinimumPartSize = this.MinimumPartSize; + #pragma warning restore 618 + + return response; + } + } +} diff --git a/src/Client/Models/Allowed.cs b/src/Client/Models/Allowed.cs index 3caa1c5..f077983 100644 --- a/src/Client/Models/Allowed.cs +++ b/src/Client/Models/Allowed.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.Generic; +using System.Diagnostics; using Newtonsoft.Json; @@ -16,6 +17,11 @@ public class Allowed [JsonProperty(Required = Required.Always)] public Capabilities Capabilities { get; set; } + /// + /// Gets or sets a list of capabilities that couldn't be parsed into values. + /// + public List UnknownCapabilities { get; set; } + /// /// Gets or sets restricted access only to this bucket id. /// @@ -39,7 +45,17 @@ public class Allowed [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { - get { return $"{{{nameof(Capabilities)}: {string.Join(", ", Capabilities)}, {nameof(NamePrefix)}: {NamePrefix}}}"; } + get + { + string unknownCapabilitiesString = ""; + + if ((this.UnknownCapabilities != null) && (this.UnknownCapabilities.Count > 0)) + { + unknownCapabilitiesString = " (+ " + string.Join(", ", UnknownCapabilities) + ")"; + } + + return $"{{{nameof(Capabilities)}: {string.Join(", ", Capabilities)}{unknownCapabilitiesString}, {nameof(NamePrefix)}: {NamePrefix}}}"; + } } } } From d1bca465a03d5260eb59d725a956abf7709834b1 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Thu, 10 Oct 2024 11:31:35 -0500 Subject: [PATCH 2/4] Added automated testing of Capabilities Resiliency: - Added an AssemblyInfo class to Backblaze.Client.csproj with an InternalsVisibleTo attribute allowing Backblaze.Tests.Unit access to internal members. - Added references from Backblaze.Tests.Unit.csproj to NSubstitute, Bogus, FluentAssertions and ASP.NET Core (in order to use Kestrel). - Added class TestServer.cs to Backblaze.Tests.Unit to create an in-process hosted web server for testing purposes. - Added text fixture class ApiClientFixture.cs to Backblaze.Tests.Unit with tests of the AuthorizeAccountAync method, including specifically Capabilities Resiliency. Moved the initialization of Allowed.Capabilities and Allowed.UnknownCapabilities out of the loop in AllowedRaw.ParseCapabilities and added explicit initialization for Capabilities. --- src/Client/Client/Internal/AllowedRaw.cs | 8 +- src/Client/Properties/AssemblyInfo.cs | 3 + test/Unit/ApiClientFixture.cs | 157 +++++++++++++++++++++++ test/Unit/Backblaze.Tests.Unit.csproj | 7 + test/Unit/TestServer.cs | 77 +++++++++++ 5 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 src/Client/Properties/AssemblyInfo.cs create mode 100644 test/Unit/ApiClientFixture.cs create mode 100644 test/Unit/TestServer.cs diff --git a/src/Client/Client/Internal/AllowedRaw.cs b/src/Client/Client/Internal/AllowedRaw.cs index 5901d64..d96d3d4 100644 --- a/src/Client/Client/Internal/AllowedRaw.cs +++ b/src/Client/Client/Internal/AllowedRaw.cs @@ -60,17 +60,15 @@ public Allowed ParseCapabilities() parsed.BucketName = this.BucketName; parsed.NamePrefix = this.NamePrefix; + parsed.Capabilities = new Capabilities(); + parsed.UnknownCapabilities = new List(); + foreach (string capabilityName in this.Capabilities) { if (Enum.TryParse(capabilityName, out var parsedCapability)) - { parsed.Capabilities.Add(parsedCapability); - } else - { - parsed.UnknownCapabilities ??= new List(); parsed.UnknownCapabilities.Add(capabilityName); - } } return parsed; diff --git a/src/Client/Properties/AssemblyInfo.cs b/src/Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ee0cd3a --- /dev/null +++ b/src/Client/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Backblaze.Tests.Unit")] diff --git a/test/Unit/ApiClientFixture.cs b/test/Unit/ApiClientFixture.cs new file mode 100644 index 0000000..7de8b55 --- /dev/null +++ b/test/Unit/ApiClientFixture.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +using Bytewizer.Backblaze.Client; +using Bytewizer.Backblaze.Client.Internal; + +using Xunit; + +using Bogus; + +using NSubstitute; + +using FluentAssertions; + +using Bytewizer.Backblaze.Models; + +public class ApiClientFixture +{ + [Fact] + public async Task AuthorizeAccountAync_should_parse_capabilities() + { + // Arrange + var faker = new Faker(); + + var dummyKeyId = faker.Random.Hash(); + var dummyApplicationKey = faker.Random.Hash(); + var dummyAccountId = faker.Random.Hash(); + var dummyAuthorizationToken = faker.Random.Hash(); + var dummyApiUrl = new Uri(faker.Internet.Url()); + var dummyDownloadUrl = new Uri(faker.Internet.Url()); + var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); + var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); + + var mockHttpClient = new HttpClient(); + + var clientOptions = new ClientOptions(); + + clientOptions.RequestMaxParallel = 1; + clientOptions.DownloadMaxParallel = 1; + clientOptions.UploadMaxParallel = 1; + clientOptions.TestMode = ""; + + var mockLogger = Substitute.For>(); + + var mockMemoryCache = Substitute.For(); + + using (var testServer = new TestServer()) + { + testServer + .When("/b2_authorize_account") + .Then( + new AuthorizeAccountResponseRaw() + { + AccountId = dummyAccountId, + AuthorizationToken = dummyAuthorizationToken, + ApiUrl = dummyApiUrl, + DownloadUrl = dummyDownloadUrl, + Allowed = + new AllowedRaw() + { + Capabilities = dummyCapabilityStrings + } + }); + + var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); + + sut.AccountInfo.AuthUrl = testServer.BaseUrl; + + // Act + var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); + + // Assert + result.IsSuccessStatusCode.Should().BeTrue(); + result.Response.Should().NotBeNull(); + result.Response.Allowed.Should().NotBeNull(); + result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); + result.Response.Allowed.UnknownCapabilities.Should().BeEmpty(); + } + } + + [Fact] + public async Task AuthorizeAccountAync_should_parse_unknown_capabilities() + { + // Arrange + var faker = new Faker(); + + var dummyKeyId = faker.Random.Hash(); + var dummyApplicationKey = faker.Random.Hash(); + var dummyAccountId = faker.Random.Hash(); + var dummyAuthorizationToken = faker.Random.Hash(); + var dummyApiUrl = new Uri(faker.Internet.Url()); + var dummyDownloadUrl = new Uri(faker.Internet.Url()); + var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); + var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); + + var unknownDummyCapabilities = + new[] + { + faker.Random.Words(3), + faker.Random.Words(3), + }; + + dummyCapabilityStrings.AddRange(unknownDummyCapabilities); + + var mockHttpClient = new HttpClient(); + + var clientOptions = new ClientOptions(); + + clientOptions.RequestMaxParallel = 1; + clientOptions.DownloadMaxParallel = 1; + clientOptions.UploadMaxParallel = 1; + clientOptions.TestMode = ""; + + var mockLogger = Substitute.For>(); + + var mockMemoryCache = Substitute.For(); + + using (var testServer = new TestServer()) + { + testServer + .When("/b2_authorize_account") + .Then( + new AuthorizeAccountResponseRaw() + { + AccountId = dummyAccountId, + AuthorizationToken = dummyAuthorizationToken, + ApiUrl = dummyApiUrl, + DownloadUrl = dummyDownloadUrl, + Allowed = + new AllowedRaw() + { + Capabilities = dummyCapabilityStrings + } + }); + + var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); + + sut.AccountInfo.AuthUrl = testServer.BaseUrl; + + // Act + var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); + + // Assert + result.IsSuccessStatusCode.Should().BeTrue(); + result.Response.Should().NotBeNull(); + result.Response.Allowed.Should().NotBeNull(); + result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); + result.Response.Allowed.UnknownCapabilities.Should().BeEquivalentTo(unknownDummyCapabilities); + } + } +} diff --git a/test/Unit/Backblaze.Tests.Unit.csproj b/test/Unit/Backblaze.Tests.Unit.csproj index 321dd10..d044dc4 100644 --- a/test/Unit/Backblaze.Tests.Unit.csproj +++ b/test/Unit/Backblaze.Tests.Unit.csproj @@ -25,7 +25,14 @@ + + + + + + + all diff --git a/test/Unit/TestServer.cs b/test/Unit/TestServer.cs new file mode 100644 index 0000000..9fd802d --- /dev/null +++ b/test/Unit/TestServer.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +using Microsoft.Extensions.Hosting; + +class TestServer : IDisposable +{ + WebApplication _application; + + public WebApplication Application => _application; + + public Uri BaseUrl => new Uri(_application.Urls.First()); + + public TestServer() + { + var builder = WebApplication.CreateBuilder(); + + _application = builder.Build(); + + // Without this route on the root, routes on subpaths seem to always return 404. + _application.MapGet("/", () => "Test server"); + + var lifetime = (IHostApplicationLifetime)_application.Services.GetService(typeof(IHostApplicationLifetime)); + + var started = new ManualResetEvent(initialState: false); + + lifetime.ApplicationStarted.Register(() => started.Set()); + + Task.Run(() => _application.Run()); + + started.WaitOne(); + } + + public RouteBuilder When(string route) => When(HttpMethod.Get, route); + public RouteBuilder When(HttpMethod method, string route) => new RouteBuilder(this, method, route); + + public class RouteBuilder + { + TestServer _server; + HttpMethod _method; + string _route; + + public RouteBuilder(TestServer server, HttpMethod method, string route) + { + _server = server; + _method = method; + _route = route; + } + + public void Then(object result) + => Then(() => Results.Json(result)); + + public void Then(Delegate action) + { + if (_method == HttpMethod.Get) + _server.Application.MapGet(_route, action); + else if (_method == HttpMethod.Post) + _server.Application.MapPost(_route, action); + else + throw new NotSupportedException("Not supported in TestServer: HTTP method " + _method); + } + } + + public void Dispose() + { + var task = _application.StopAsync(); + + task.ConfigureAwait(false); + task.Wait(); + } +} From 684535f51aee2e989b8ed19d60377a133665dad3 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Thu, 10 Oct 2024 11:33:55 -0500 Subject: [PATCH 3/4] Changed test AuthorizeAccountAync_should_parse_unknown_capabilities to use explicit strings instead of Faker-generated strings for unknown capability names, since there is a tiny change of faker.Random.Words(3) returning the same string twice. --- test/Unit/ApiClientFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Unit/ApiClientFixture.cs b/test/Unit/ApiClientFixture.cs index 7de8b55..d6b874c 100644 --- a/test/Unit/ApiClientFixture.cs +++ b/test/Unit/ApiClientFixture.cs @@ -102,8 +102,8 @@ public async Task AuthorizeAccountAync_should_parse_unknown_capabilities() var unknownDummyCapabilities = new[] { - faker.Random.Words(3), - faker.Random.Words(3), + "UnknownCapability1", + "UnknownCapability2", }; dummyCapabilityStrings.AddRange(unknownDummyCapabilities); From 9743d7047978d5bdb97128259a0267e1ca98a8bf Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Thu, 10 Oct 2024 11:38:16 -0500 Subject: [PATCH 4/4] Moved TestServer.cs and ApiClientFixture.cs into the correct namespace for the Backblaze.Tests.Unit project. --- test/Unit/ApiClientFixture.cs | 233 +++++++++++++++++----------------- test/Unit/TestServer.cs | 95 +++++++------- 2 files changed, 167 insertions(+), 161 deletions(-) diff --git a/test/Unit/ApiClientFixture.cs b/test/Unit/ApiClientFixture.cs index d6b874c..a8e60db 100644 --- a/test/Unit/ApiClientFixture.cs +++ b/test/Unit/ApiClientFixture.cs @@ -20,138 +20,141 @@ using Bytewizer.Backblaze.Models; -public class ApiClientFixture +namespace Backblaze.Tests.Unit { - [Fact] - public async Task AuthorizeAccountAync_should_parse_capabilities() + public class ApiClientFixture { - // Arrange - var faker = new Faker(); + [Fact] + public async Task AuthorizeAccountAync_should_parse_capabilities() + { + // Arrange + var faker = new Faker(); - var dummyKeyId = faker.Random.Hash(); - var dummyApplicationKey = faker.Random.Hash(); - var dummyAccountId = faker.Random.Hash(); - var dummyAuthorizationToken = faker.Random.Hash(); - var dummyApiUrl = new Uri(faker.Internet.Url()); - var dummyDownloadUrl = new Uri(faker.Internet.Url()); - var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); - var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); + var dummyKeyId = faker.Random.Hash(); + var dummyApplicationKey = faker.Random.Hash(); + var dummyAccountId = faker.Random.Hash(); + var dummyAuthorizationToken = faker.Random.Hash(); + var dummyApiUrl = new Uri(faker.Internet.Url()); + var dummyDownloadUrl = new Uri(faker.Internet.Url()); + var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); + var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); - var mockHttpClient = new HttpClient(); + var mockHttpClient = new HttpClient(); - var clientOptions = new ClientOptions(); + var clientOptions = new ClientOptions(); - clientOptions.RequestMaxParallel = 1; - clientOptions.DownloadMaxParallel = 1; - clientOptions.UploadMaxParallel = 1; - clientOptions.TestMode = ""; + clientOptions.RequestMaxParallel = 1; + clientOptions.DownloadMaxParallel = 1; + clientOptions.UploadMaxParallel = 1; + clientOptions.TestMode = ""; - var mockLogger = Substitute.For>(); + var mockLogger = Substitute.For>(); - var mockMemoryCache = Substitute.For(); + var mockMemoryCache = Substitute.For(); - using (var testServer = new TestServer()) - { - testServer - .When("/b2_authorize_account") - .Then( - new AuthorizeAccountResponseRaw() - { - AccountId = dummyAccountId, - AuthorizationToken = dummyAuthorizationToken, - ApiUrl = dummyApiUrl, - DownloadUrl = dummyDownloadUrl, - Allowed = - new AllowedRaw() - { - Capabilities = dummyCapabilityStrings - } - }); - - var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); - - sut.AccountInfo.AuthUrl = testServer.BaseUrl; - - // Act - var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); - - // Assert - result.IsSuccessStatusCode.Should().BeTrue(); - result.Response.Should().NotBeNull(); - result.Response.Allowed.Should().NotBeNull(); - result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); - result.Response.Allowed.UnknownCapabilities.Should().BeEmpty(); + using (var testServer = new TestServer()) + { + testServer + .When("/b2_authorize_account") + .Then( + new AuthorizeAccountResponseRaw() + { + AccountId = dummyAccountId, + AuthorizationToken = dummyAuthorizationToken, + ApiUrl = dummyApiUrl, + DownloadUrl = dummyDownloadUrl, + Allowed = + new AllowedRaw() + { + Capabilities = dummyCapabilityStrings + } + }); + + var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); + + sut.AccountInfo.AuthUrl = testServer.BaseUrl; + + // Act + var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); + + // Assert + result.IsSuccessStatusCode.Should().BeTrue(); + result.Response.Should().NotBeNull(); + result.Response.Allowed.Should().NotBeNull(); + result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); + result.Response.Allowed.UnknownCapabilities.Should().BeEmpty(); + } } - } - [Fact] - public async Task AuthorizeAccountAync_should_parse_unknown_capabilities() - { - // Arrange - var faker = new Faker(); - - var dummyKeyId = faker.Random.Hash(); - var dummyApplicationKey = faker.Random.Hash(); - var dummyAccountId = faker.Random.Hash(); - var dummyAuthorizationToken = faker.Random.Hash(); - var dummyApiUrl = new Uri(faker.Internet.Url()); - var dummyDownloadUrl = new Uri(faker.Internet.Url()); - var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); - var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); - - var unknownDummyCapabilities = - new[] - { - "UnknownCapability1", - "UnknownCapability2", - }; + [Fact] + public async Task AuthorizeAccountAync_should_parse_unknown_capabilities() + { + // Arrange + var faker = new Faker(); - dummyCapabilityStrings.AddRange(unknownDummyCapabilities); + var dummyKeyId = faker.Random.Hash(); + var dummyApplicationKey = faker.Random.Hash(); + var dummyAccountId = faker.Random.Hash(); + var dummyAuthorizationToken = faker.Random.Hash(); + var dummyApiUrl = new Uri(faker.Internet.Url()); + var dummyDownloadUrl = new Uri(faker.Internet.Url()); + var dummyCapabilities = faker.PickRandom(Enum.GetValues(typeof(Capability)).OfType(), 5).ToList(); + var dummyCapabilityStrings = dummyCapabilities.Select(c => c.ToString()).ToList(); - var mockHttpClient = new HttpClient(); + var unknownDummyCapabilities = + new[] + { + "UnknownCapability1", + "UnknownCapability2", + }; - var clientOptions = new ClientOptions(); + dummyCapabilityStrings.AddRange(unknownDummyCapabilities); - clientOptions.RequestMaxParallel = 1; - clientOptions.DownloadMaxParallel = 1; - clientOptions.UploadMaxParallel = 1; - clientOptions.TestMode = ""; + var mockHttpClient = new HttpClient(); - var mockLogger = Substitute.For>(); + var clientOptions = new ClientOptions(); - var mockMemoryCache = Substitute.For(); + clientOptions.RequestMaxParallel = 1; + clientOptions.DownloadMaxParallel = 1; + clientOptions.UploadMaxParallel = 1; + clientOptions.TestMode = ""; - using (var testServer = new TestServer()) - { - testServer - .When("/b2_authorize_account") - .Then( - new AuthorizeAccountResponseRaw() - { - AccountId = dummyAccountId, - AuthorizationToken = dummyAuthorizationToken, - ApiUrl = dummyApiUrl, - DownloadUrl = dummyDownloadUrl, - Allowed = - new AllowedRaw() - { - Capabilities = dummyCapabilityStrings - } - }); - - var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); - - sut.AccountInfo.AuthUrl = testServer.BaseUrl; - - // Act - var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); - - // Assert - result.IsSuccessStatusCode.Should().BeTrue(); - result.Response.Should().NotBeNull(); - result.Response.Allowed.Should().NotBeNull(); - result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); - result.Response.Allowed.UnknownCapabilities.Should().BeEquivalentTo(unknownDummyCapabilities); + var mockLogger = Substitute.For>(); + + var mockMemoryCache = Substitute.For(); + + using (var testServer = new TestServer()) + { + testServer + .When("/b2_authorize_account") + .Then( + new AuthorizeAccountResponseRaw() + { + AccountId = dummyAccountId, + AuthorizationToken = dummyAuthorizationToken, + ApiUrl = dummyApiUrl, + DownloadUrl = dummyDownloadUrl, + Allowed = + new AllowedRaw() + { + Capabilities = dummyCapabilityStrings + } + }); + + var sut = new ApiClient(mockHttpClient, clientOptions, mockLogger, mockMemoryCache); + + sut.AccountInfo.AuthUrl = testServer.BaseUrl; + + // Act + var result = await sut.AuthorizeAccountAync(dummyKeyId, dummyApplicationKey, CancellationToken.None); + + // Assert + result.IsSuccessStatusCode.Should().BeTrue(); + result.Response.Should().NotBeNull(); + result.Response.Allowed.Should().NotBeNull(); + result.Response.Allowed.Capabilities.Should().BeEquivalentTo(dummyCapabilities); + result.Response.Allowed.UnknownCapabilities.Should().BeEquivalentTo(unknownDummyCapabilities); + } } } -} +} \ No newline at end of file diff --git a/test/Unit/TestServer.cs b/test/Unit/TestServer.cs index 9fd802d..4168b94 100644 --- a/test/Unit/TestServer.cs +++ b/test/Unit/TestServer.cs @@ -9,69 +9,72 @@ using Microsoft.Extensions.Hosting; -class TestServer : IDisposable +namespace Backblaze.Tests.Unit { - WebApplication _application; - - public WebApplication Application => _application; - - public Uri BaseUrl => new Uri(_application.Urls.First()); - - public TestServer() + class TestServer : IDisposable { - var builder = WebApplication.CreateBuilder(); + WebApplication _application; - _application = builder.Build(); + public WebApplication Application => _application; - // Without this route on the root, routes on subpaths seem to always return 404. - _application.MapGet("/", () => "Test server"); + public Uri BaseUrl => new Uri(_application.Urls.First()); - var lifetime = (IHostApplicationLifetime)_application.Services.GetService(typeof(IHostApplicationLifetime)); + public TestServer() + { + var builder = WebApplication.CreateBuilder(); - var started = new ManualResetEvent(initialState: false); + _application = builder.Build(); - lifetime.ApplicationStarted.Register(() => started.Set()); + // Without this route on the root, routes on subpaths seem to always return 404. + _application.MapGet("/", () => "Test server"); - Task.Run(() => _application.Run()); + var lifetime = (IHostApplicationLifetime)_application.Services.GetService(typeof(IHostApplicationLifetime)); - started.WaitOne(); - } + var started = new ManualResetEvent(initialState: false); - public RouteBuilder When(string route) => When(HttpMethod.Get, route); - public RouteBuilder When(HttpMethod method, string route) => new RouteBuilder(this, method, route); + lifetime.ApplicationStarted.Register(() => started.Set()); - public class RouteBuilder - { - TestServer _server; - HttpMethod _method; - string _route; + Task.Run(() => _application.Run()); - public RouteBuilder(TestServer server, HttpMethod method, string route) - { - _server = server; - _method = method; - _route = route; + started.WaitOne(); } - public void Then(object result) - => Then(() => Results.Json(result)); + public RouteBuilder When(string route) => When(HttpMethod.Get, route); + public RouteBuilder When(HttpMethod method, string route) => new RouteBuilder(this, method, route); - public void Then(Delegate action) + public class RouteBuilder { - if (_method == HttpMethod.Get) - _server.Application.MapGet(_route, action); - else if (_method == HttpMethod.Post) - _server.Application.MapPost(_route, action); - else - throw new NotSupportedException("Not supported in TestServer: HTTP method " + _method); + TestServer _server; + HttpMethod _method; + string _route; + + public RouteBuilder(TestServer server, HttpMethod method, string route) + { + _server = server; + _method = method; + _route = route; + } + + public void Then(object result) + => Then(() => Results.Json(result)); + + public void Then(Delegate action) + { + if (_method == HttpMethod.Get) + _server.Application.MapGet(_route, action); + else if (_method == HttpMethod.Post) + _server.Application.MapPost(_route, action); + else + throw new NotSupportedException("Not supported in TestServer: HTTP method " + _method); + } } - } - public void Dispose() - { - var task = _application.StopAsync(); + public void Dispose() + { + var task = _application.StopAsync(); - task.ConfigureAwait(false); - task.Wait(); + task.ConfigureAwait(false); + task.Wait(); + } } -} +} \ No newline at end of file