diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs index bdfa8573..bbeaaf90 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs @@ -11,6 +11,6 @@ public abstract class IntegrationTestBase { public const string Category = "Integration"; - private protected static PackageUploaderTestHost CreateHost( - Action? configureIngestion = null) => new(configureIngestion); + /// Creates a host wired to live WireMock.Net fakes of the Ingestion API and XFUS. + private protected static MockServerTestHost CreateMockServerHost() => new(); } diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs deleted file mode 100644 index 18defbf6..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace PackageUploader.IntegrationTest.Infrastructure; - -/// In-process HTTP handler that returns scripted responses and records requests, backing the mock integration suite. -internal sealed class MockHttpMessageHandler : HttpMessageHandler -{ - private readonly List _responders = []; - private readonly List _received = []; - private readonly Lock _receivedLock = new(); - - public IReadOnlyList ReceivedRequests - { - get - { - lock (_receivedLock) - { - return _received.ToArray(); - } - } - } - - public MockHttpMessageHandler When(HttpMethod method, string pathContains, - Func respond) - { - ArgumentNullException.ThrowIfNull(method); - ArgumentNullException.ThrowIfNull(pathContains); - ArgumentNullException.ThrowIfNull(respond); - - _responders.Add(new Responder(method, pathContains, respond)); - return this; - } - - public MockHttpMessageHandler WhenJson(HttpMethod method, string pathContains, string json, - HttpStatusCode status = HttpStatusCode.OK) => - When(method, pathContains, _ => new HttpResponseMessage(status) - { - Content = new StringContent(json, Encoding.UTF8, "application/json"), - }); - - protected override async Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) - { - string? body = request.Content is null - ? null - : await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - lock (_receivedLock) - { - _received.Add(new RecordedRequest( - request.Method, - request.RequestUri!, - CloneHeaders(request.Headers), - body)); - } - - var responder = _responders.FirstOrDefault(r => r.Matches(request)); - if (responder is null) - { - // Return a non-transient 4xx so a missing stub fails fast: the Ingestion pipeline's Polly - // policy retries on >=500, which would otherwise turn a missing stub into slow retries. - return new HttpResponseMessage(HttpStatusCode.BadRequest) - { - RequestMessage = request, - Content = new StringContent( - $"No mock responder registered for {request.Method} {request.RequestUri}", - Encoding.UTF8, "text/plain"), - }; - } - - var response = responder.Respond(request); - response.RequestMessage ??= request; - return response; - } - - private static IReadOnlyDictionary CloneHeaders(HttpRequestHeaders headers) - { - var clone = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var header in headers) - { - clone[header.Key] = string.Join(", ", header.Value); - } - return clone; - } - - private sealed class Responder(HttpMethod method, string pathContains, - Func respond) - { - public bool Matches(HttpRequestMessage request) => - request.Method == method && - request.RequestUri is not null && - request.RequestUri.PathAndQuery.Contains(pathContains, StringComparison.OrdinalIgnoreCase); - - public HttpResponseMessage Respond(HttpRequestMessage request) => respond(request); - } -} - -/// Snapshot of a request observed by . -internal sealed record RecordedRequest( - HttpMethod Method, - Uri Uri, - IReadOnlyDictionary Headers, - string? Body); diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs new file mode 100644 index 00000000..f7329f9c --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using PackageUploader.ClientApi; +using PackageUploader.ClientApi.Client.Ingestion.TokenProvider; +using PackageUploader.IntegrationTest.Infrastructure.Mocks; + +namespace PackageUploader.IntegrationTest.Infrastructure; + +/// +/// Composes the real wired to live WireMock.Net fakes of the +/// Ingestion API and XFUS. The Ingestion base address points at the server; +/// XFUS is reached via the upload domain returned in a stubbed package response (see +/// and ). Authentication uses +/// . Everything else (auth handler, Polly policies, +/// serialization, mappers) runs for real. Dispose to stop both servers and the provider. +/// +internal sealed class MockServerTestHost : IDisposable +{ + private readonly ServiceProvider _provider; + private readonly IServiceScope _scope; + + /// The fake Ingestion API. Configure stubs before exercising the service. + public IngestionMockServer Ingestion { get; } + + /// The fake XFUS upload service. Configure stubs before exercising the service. + public XfusMockServer Xfus { get; } + + /// The fully composed, public service under test, wired to the fakes. + public IPackageUploaderService Service { get; } + + public MockServerTestHost() + { + Ingestion = new IngestionMockServer(); + Xfus = new XfusMockServer(); + try + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. + ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", + ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", + ["IngestionConfig:RetryCount"] = "3", + }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + + services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); + + services.RemoveAll(); + services.AddScoped(); + + _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scope = _provider.CreateScope(); + Service = _scope.ServiceProvider.GetRequiredService(); + } + catch + { + // Avoid leaking the started WireMock servers if composition fails. + Xfus.Dispose(); + Ingestion.Dispose(); + throw; + } + } + + /// The XFUS server's base URL, for use as the upload domain in a stubbed package response. + public string XfusUploadDomain => Xfus.Url; + + public void Dispose() + { + _scope.Dispose(); + _provider.Dispose(); + Ingestion.Dispose(); + Xfus.Dispose(); + } +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs new file mode 100644 index 00000000..b8405de7 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// +/// A reusable WireMock.Net-backed fake of the Partner Center Ingestion API. Starts a real HTTP +/// server on a random local port and exposes fluent stubs for the endpoints PackageUploader uses, +/// with configurable success / error / retry / polling scenarios. Dispose to stop the server. +/// +internal sealed class IngestionMockServer : IDisposable +{ + private readonly WireMockServer _server; + private readonly string _id = Guid.NewGuid().ToString("N"); + + public IngestionMockServer() => _server = WireMockServer.Start(); + + /// Base URL of the running mock server (e.g. http://localhost:12345). + public string Url => _server.Url!; + + /// The underlying server, for advanced stubbing or request-log assertions. + public WireMockServer Server => _server; + + // ---- GetProduct: GET /products/{id} ---- + + public IngestionMockServer StubGetProduct(string productId, ResponseScenario scenario = ResponseScenario.Success) + { + var request = Request.Create().WithPath($"/products/{productId}").UsingGet(); + if (scenario != ResponseScenario.Success) + { + _server.Given(request).RespondWith(ErrorResponse(scenario)); + return this; + } + + _server.Given(request).RespondWith(JsonResponse(new + { + resourceType = "AzureGameProduct", + name = $"Test Product {productId}", + id = productId, + externalIds = new[] { new { type = "StoreId", value = "9TESTBIGID000" } }, + isModularPublishing = true, + })); + return this; + } + + // ---- GetBranches: GET /products/{id}/branches/getByModule(module=Package) ---- + + public IngestionMockServer StubGetPackageBranches( + string productId, + params (string FriendlyName, string CurrentDraftInstanceId)[] branches) + { + var values = branches.Select(b => new + { + resourceType = "Branch", + friendlyName = b.FriendlyName, + type = "Main", + module = "Package", + currentDraftInstanceId = b.CurrentDraftInstanceId, + }); + + _server + .Given(Request.Create().WithPath("/products/" + productId + "/branches/getByModule*").UsingGet()) + .RespondWith(JsonResponse(new { value = values })); + + _server + .Given(Request.Create().WithPath($"/products/{productId}/flights").UsingGet()) + .RespondWith(JsonResponse(new { value = Array.Empty() })); + return this; + } + + // ---- CreatePackageRequest: POST /products/{id}/packages ---- + + public IngestionMockServer StubCreatePackage( + string productId, + string packageId, + string fileName = "test.xvc", + string? xfusUploadDomain = null, + string? xfusId = null, + string xfusTenant = "DCE", + string xfusToken = "fake-xfus-token") + { + // When an XFUS upload domain is supplied, embed the upload info so the client uploads to the + // XFUS mock; xfusId must be a valid GUID because the client maps it to a Guid. When absent, + // use null (not new {}) so the field is omitted entirely — an empty object would deserialize + // into a non-null upload info with a null XfusId and crash the client's Guid mapping. + object? uploadInfo = xfusUploadDomain is null + ? null + : new + { + fileName, + xfusId = xfusId ?? Guid.NewGuid().ToString(), + token = xfusToken, + uploadDomain = xfusUploadDomain, + xfusTenant, + }; + + _server + .Given(Request.Create().WithPath($"/products/{productId}/packages").UsingPost()) + .RespondWith(JsonResponse(new + { + resourceType = "GamePackage", + id = packageId, + state = "PendingUpload", + fileName, + uploadInfo, + })); + return this; + } + + // ---- GetPackage processing poll: GET /products/{id}/packages/{packageId} ---- + // Returns each state in order across successive calls, then stays on the final state. + + public IngestionMockServer StubGetPackageProcessing( + string productId, + string packageId, + params string[] stateProgression) + { + var states = stateProgression.Length > 0 ? stateProgression : ["Processed"]; + var request = Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingGet(); + var scenario = $"package-{productId}-{packageId}-{_id}"; + + for (int i = 0; i < states.Length; i++) + { + bool isLast = i == states.Length - 1; + var body = JsonResponse(new + { + resourceType = "GamePackage", + id = packageId, + state = states[i], + }); + + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + + builder.RespondWith(body); + } + + return this; + } + + // ---- GetPackageConfig: GET /products/{id}/packageConfigurations/getByInstanceID(...) and GET/PUT by id ---- + + public IngestionMockServer StubPackageConfiguration( + string productId, + string instanceId, + string configId, + string marketGroupId = "default", + string marketGroupName = "default") + { + var body = JsonResponse(new + { + value = new[] { new { resourceType = "PackageConfiguration", id = configId } }, + }); + + _server + .Given(Request.Create().WithPath("/products/" + productId + "/packageConfigurations/getByInstanceID*").UsingGet()) + .RespondWith(body); + + var single = JsonResponse(new + { + resourceType = "PackageConfiguration", + id = configId, + marketGroupPackages = new[] + { + new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, + }, + }); + _server + .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingGet()) + .RespondWith(single); + _server + .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingPut()) + .RespondWith(single); + return this; + } + + // ---- ProcessPackage: PUT /products/{id}/packages/{packageId} ---- + + public IngestionMockServer StubProcessPackage(string productId, string packageId, string state = "Uploaded") + { + _server + .Given(Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingPut()) + .RespondWith(JsonResponse(new + { + resourceType = "GamePackage", + id = packageId, + state, + })); + return this; + } + + // ---- CreateSubmission: POST /products/{id}/submissions ---- + + public IngestionMockServer StubCreateSubmission(string productId, string submissionId) + { + _server + .Given(Request.Create().WithPath($"/products/{productId}/submissions").UsingPost()) + .RespondWith(JsonResponse(SubmissionBody(submissionId, "InProgress", "Submitted"))); + return this; + } + + // ---- GetSubmission poll: GET /products/{id}/submissions/{id} ---- + // Returns each (state, substate) in order across successive calls, then stays on the final one. + + public IngestionMockServer StubGetSubmission( + string productId, + string submissionId, + params (string State, string Substate)[] progression) + { + var steps = progression.Length > 0 ? progression : [("Published", "InStore")]; + var request = Request.Create().WithPath($"/products/{productId}/submissions/{submissionId}").UsingGet(); + var scenario = $"submission-{productId}-{submissionId}-{_id}"; + + for (int i = 0; i < steps.Length; i++) + { + bool isLast = i == steps.Length - 1; + var body = JsonResponse(SubmissionBody(submissionId, steps[i].State, steps[i].Substate)); + + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + + builder.RespondWith(body); + } + + return this; + } + + // ---- Generic scenario primitives ---- + + /// Always responds to the given method/path-wildcard with the supplied status code. + public IngestionMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) + { + _server + .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// + /// Fails the first calls with , then + /// succeeds with the given JSON body — used to exercise the client's Polly retry policy. + /// + public IngestionMockServer StubRetryThenSuccess( + string method, + string path, + object successBody, + int failures = 2, + HttpStatusCode failureStatus = HttpStatusCode.InternalServerError) + { + var request = Request.Create().WithPath(path).UsingMethod(method); + var scenario = $"retry-{method}-{path}-{_id}"; + + if (failures <= 0) + { + _server.Given(request).RespondWith(JsonResponse(successBody)); + return this; + } + + for (int i = 0; i < failures; i++) + { + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + builder.WillSetStateTo(StateName(i + 1)) + .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); + } + + _server.Given(request).InScenario(scenario) + .WhenStateIs(StateName(failures)) + .RespondWith(JsonResponse(successBody)); + return this; + } + + public void Dispose() => _server.Stop(); + + // ---- helpers ---- + + private static object SubmissionBody(string submissionId, string state, string substate) => new + { + resourceType = "Submission", + id = submissionId, + state, + substate, + pendingUpdateInfo = new { status = "Completed" }, + }; + + private static string StateName(int index) => $"step-{index}"; + + private static IResponseBuilder JsonResponse(object body) => + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(body); + + private static IResponseBuilder ErrorResponse(ResponseScenario scenario) => + Response.Create().WithStatusCode((int)scenario switch + { + _ when scenario == ResponseScenario.ServerError => HttpStatusCode.InternalServerError, + _ when scenario == ResponseScenario.Unauthorized => HttpStatusCode.Unauthorized, + _ when scenario == ResponseScenario.NotFound => HttpStatusCode.NotFound, + _ => HttpStatusCode.InternalServerError, + }); +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs new file mode 100644 index 00000000..4aaf2f24 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// How a stubbed endpoint should behave for a single, non-stateful response. +internal enum ResponseScenario +{ + /// Return a normal 2xx response with a valid body. + Success, + + /// Return a 500 Internal Server Error. + ServerError, + + /// Return a 401 Unauthorized. + Unauthorized, + + /// Return a 404 Not Found. + NotFound, +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs new file mode 100644 index 00000000..f848ffa0 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// +/// A reusable WireMock.Net-backed fake of the XFUS upload service. Serves the three-step chunked +/// upload conversation PackageUploader performs — initialize, per-block payload PUT, and continue — +/// for both the no-delta and delta upload paths, with configurable success / error / retry +/// scenarios. Dispose to stop the server. +/// +/// +/// The real client's base address is {UploadDomain}/api/v2/assets/, so all stubs are rooted +/// at /api/v2/assets/. Responses omit directUploadParameters.sasUri so the client +/// uploads blocks via the proxy PUT path (which this server serves) rather than to Azure blob +/// storage. The status field is emitted as a number because the client's serializer has no +/// string-enum converter (ReceivingBlocks=0, Busy=1, Completed=2). +/// +internal sealed class XfusMockServer : IDisposable +{ + private const string AssetsRoot = "/api/v2/assets"; + + private readonly WireMockServer _server; + private readonly string _id = Guid.NewGuid().ToString("N"); + + public XfusMockServer() => _server = WireMockServer.Start(); + + /// Base URL of the running mock server (e.g. http://localhost:12345). + public string Url => _server.Url!; + + /// The underlying server, for advanced stubbing or request-log assertions. + public WireMockServer Server => _server; + + /// + /// Configures a complete, successful no-delta upload: initialize returns the given blocks in the + /// ReceivingBlocks state, every block payload PUT succeeds, and continue reports Completed. + /// + public XfusMockServer StubNoDeltaUploadSuccess(params long[] blockSizes) + { + var sizes = blockSizes.Length > 0 ? blockSizes : [64L * 1024]; + StubInitialize(UploadProgress(sizes, XfusStatus.ReceivingBlocks)); + StubBlockUpload(); + StubContinue(UploadProgress([], XfusStatus.Completed)); + return this; + } + + /// Stubs POST .../{assetId}/initialize with the given JSON upload-progress body. + public XfusMockServer StubInitialize(object uploadProgressBody) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/initialize").UsingPost()) + .RespondWith(JsonResponse(uploadProgressBody)); + return this; + } + + /// Stubs PUT .../{assetId}/blocks/{blockId}/source/payload to accept block bytes. + public XfusMockServer StubBlockUpload(HttpStatusCode statusCode = HttpStatusCode.OK) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut()) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// Stubs POST .../{assetId}/continue with the given JSON upload-progress body. + public XfusMockServer StubContinue(object uploadProgressBody) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost()) + .RespondWith(JsonResponse(uploadProgressBody)); + return this; + } + + /// + /// Returns the given continue-progress bodies in order across successive calls, then stays on the + /// final one — used to simulate the server working through the upload (e.g. Busy then Completed) + /// or the multi-step delta plan. The progression is keyed per server instance, so it assumes a + /// single asset upload per (the usual one-upload-per-test pattern). + /// + public XfusMockServer StubContinueProgression(params object[] uploadProgressBodies) + { + var bodies = uploadProgressBodies.Length > 0 ? uploadProgressBodies : [UploadProgress([], XfusStatus.Completed)]; + var request = Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost(); + var scenario = $"xfus-continue-{_id}"; + + for (int i = 0; i < bodies.Length; i++) + { + bool isLast = i == bodies.Length - 1; + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + builder.RespondWith(JsonResponse(bodies[i])); + } + + return this; + } + + /// Always responds to the given method/path-wildcard with the supplied status code. + public XfusMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) + { + _server + .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// + /// Fails the first block payload PUTs with , + /// then accepts subsequent ones — used to exercise the uploader's block re-upload behaviour. + /// + public XfusMockServer StubBlockUploadRetryThenSuccess(int failures = 1, HttpStatusCode failureStatus = HttpStatusCode.ServiceUnavailable) + { + var request = Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut(); + var scenario = $"xfus-block-{_id}"; + + if (failures <= 0) + { + _server.Given(request).RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); + return this; + } + + for (int i = 0; i < failures; i++) + { + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + builder.WillSetStateTo(StateName(i + 1)) + .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); + } + + _server.Given(request).InScenario(scenario) + .WhenStateIs(StateName(failures)) + .WillSetStateTo(StateName(failures)) + .RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); + return this; + } + + public void Dispose() => _server.Stop(); + + // ---- helpers ---- + + private enum XfusStatus + { + ReceivingBlocks = 0, + Busy = 1, + Completed = 2, + } + + private static object UploadProgress(long[] blockSizes, XfusStatus status) + { + long offset = 0; + var blocks = new List(); + for (long i = 0; i < blockSizes.Length; i++) + { + blocks.Add(new + { + id = i, + blockIdBase64 = Convert.ToBase64String(BitConverter.GetBytes(i)), + offset, + size = blockSizes[i], + }); + offset += blockSizes[i]; + } + + return new + { + pendingBlocks = blocks, + status = (int)status, + }; + } + + private static string StateName(int index) => $"step-{index}"; + + private static IResponseBuilder JsonResponse(object body) => + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(body); +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs deleted file mode 100644 index 9a3bc6f3..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using PackageUploader.ClientApi; -using PackageUploader.ClientApi.Client.Ingestion.TokenProvider; - -namespace PackageUploader.IntegrationTest.Infrastructure; - -/// Composes the real with the Ingestion network handler and access-token provider replaced by test doubles. -internal sealed class PackageUploaderTestHost : IDisposable -{ - private const string IngestionHttpClientName = "IIngestionHttpClient"; - - private readonly ServiceProvider _provider; - private readonly IServiceScope _scope; - - public MockHttpMessageHandler IngestionHandler { get; } - - public IPackageUploaderService Service { get; } - - public PackageUploaderTestHost( - Action? configureIngestion = null, - string ingestionBaseAddress = "https://ingestion.test.local/") - { - IngestionHandler = new MockHttpMessageHandler(); - configureIngestion?.Invoke(IngestionHandler); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["IngestionConfig:BaseAddress"] = ingestionBaseAddress, - }) - .Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - - services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); - - services.RemoveAll(); - services.AddScoped(); - - services.AddHttpClient(IngestionHttpClientName) - .ConfigurePrimaryHttpMessageHandler(() => IngestionHandler); - - // IPackageUploaderService and the Ingestion auth handler are scoped; resolve them from an - // explicit scope (with scope validation on) rather than the root provider. - _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _scope = _provider.CreateScope(); - Service = _scope.ServiceProvider.GetRequiredService(); - } - - public void Dispose() - { - _scope.Dispose(); - _provider.Dispose(); - } -} diff --git a/src/PackageUploader.IntegrationTest/IngestionApiTests.cs b/src/PackageUploader.IntegrationTest/IngestionApiTests.cs new file mode 100644 index 00000000..bf296e44 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/IngestionApiTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Exceptions; +using PackageUploader.IntegrationTest.Infrastructure; +using PackageUploader.IntegrationTest.Infrastructure.Mocks; +using System.Net; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end integration tests for Ingestion API flows, exercising the real service against the +/// WireMock Ingestion fake: success, error mapping, transient-error retry, and paged collections. +/// +[TestClass] +public sealed class IngestionApiTests : IntegrationTestBase +{ + [TestMethod] + public async Task GetProductByProductId_Success_ReturnsMappedProduct() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("9P000TEST"); + + var product = await host.Service.GetProductByProductIdAsync("9P000TEST", CancellationToken.None); + + Assert.IsNotNull(product); + Assert.AreEqual("9P000TEST", product.ProductId); + Assert.AreEqual("Test Product 9P000TEST", product.ProductName); + } + + [TestMethod] + public async Task GetProductByProductId_NotFound_ThrowsProductNotFound() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("MISSING", ResponseScenario.NotFound); + + await Assert.ThrowsExactlyAsync( + () => host.Service.GetProductByProductIdAsync("MISSING", CancellationToken.None)); + } + + [TestMethod] + public async Task GetProductByProductId_RetriesTransientError_ThenSucceeds() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubRetryThenSuccess( + "GET", + "/products/RETRYME", + new { resourceType = "AzureGameProduct", id = "RETRYME", name = "Recovered" }, + failures: 2, + failureStatus: HttpStatusCode.InternalServerError); + + var product = await host.Service.GetProductByProductIdAsync("RETRYME", CancellationToken.None); + + Assert.IsNotNull(product); + Assert.AreEqual("RETRYME", product.ProductId); + } + + [TestMethod] + public async Task GetPackageBranches_ReturnsConfiguredBranches() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("PRODX"); + host.Ingestion.StubGetPackageBranches("PRODX", ("Main", "draft-1"), ("Beta", "draft-2")); + + var product = await host.Service.GetProductByProductIdAsync("PRODX", CancellationToken.None); + var branches = await host.Service.GetPackageBranchesAsync(product, CancellationToken.None); + + Assert.AreEqual(2, branches.Count); + CollectionAssert.AreEquivalent( + new[] { "Main", "Beta" }, + branches.Select(b => b.Name).ToArray()); + } +} diff --git a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj index 85e2569e..37972d8d 100644 --- a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj +++ b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/PackageUploader.IntegrationTest/PublishFlowTests.cs b/src/PackageUploader.IntegrationTest/PublishFlowTests.cs new file mode 100644 index 00000000..b4773989 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/PublishFlowTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Models; +using PackageUploader.IntegrationTest.Infrastructure; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end publish flow that exercises submission creation and polling against the Ingestion +/// fake: create a sandbox submission, then poll it until it reaches the Published state. +/// +[TestClass] +public sealed class PublishFlowTests : IntegrationTestBase +{ + [TestMethod] + public async Task PublishToSandbox_PollsSubmission_UntilPublished() + { + using var host = CreateMockServerHost(); + + const string productId = "PRODPUBLISH"; + const string submissionId = "sub-1"; + + host.Ingestion.StubGetProduct(productId); + host.Ingestion.StubGetPackageBranches(productId, ("Main", "draft-1")); + host.Ingestion.StubCreateSubmission(productId, submissionId); + host.Ingestion.StubGetSubmission(productId, submissionId, ("Published", "InStore")); + + var product = await host.Service.GetProductByProductIdAsync(productId, CancellationToken.None); + var branch = await host.Service.GetPackageBranchByFriendlyNameAsync(product, "Main", CancellationToken.None); + + var submission = await host.Service.PublishPackagesToSandboxAsync( + product, branch, "Sandbox.1", minutesToWaitForPublishing: 1, CancellationToken.None); + + Assert.IsNotNull(submission); + Assert.AreEqual(GameSubmissionState.Published, submission.GameSubmissionState); + } +} diff --git a/src/PackageUploader.IntegrationTest/SmokeTest.cs b/src/PackageUploader.IntegrationTest/SmokeTest.cs index 5a83a4fb..3a729410 100644 --- a/src/PackageUploader.IntegrationTest/SmokeTest.cs +++ b/src/PackageUploader.IntegrationTest/SmokeTest.cs @@ -3,31 +3,35 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using PackageUploader.IntegrationTest.Infrastructure; -using System.Net.Http; +using System.Linq; namespace PackageUploader.IntegrationTest; /// -/// Smoke test that validates the integration project is discovered, builds, and that the mock -/// harness routes a public service call through the real pipeline to the mock handler. +/// Smoke test that validates the integration project is discovered, builds, and that the mock-server +/// host routes a public service call over real HTTP to the WireMock Ingestion fake with the fake +/// auth token attached. /// [TestClass] public sealed class SmokeTest : IntegrationTestBase { [TestMethod] - public async Task TestHost_RoutesProductLookup_ThroughMockHandlerWithFakeAuth() + public async Task TestHost_RoutesProductLookup_ThroughWireMockWithFakeAuth() { - using var host = CreateHost(mock => - mock.WhenJson(HttpMethod.Get, "/products/", "{\"id\":\"smoke-test-product\"}")); + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("smoke-test-product"); var product = await host.Service.GetProductByProductIdAsync("smoke-test-product", TestContext.CancellationToken); Assert.IsNotNull(product); - Assert.AreEqual(1, host.IngestionHandler.ReceivedRequests.Count); + Assert.AreEqual("smoke-test-product", product.ProductId); - var request = host.IngestionHandler.ReceivedRequests[0]; - Assert.IsTrue(request.Headers.ContainsKey("Authorization")); - StringAssert.Contains(request.Headers["Authorization"], FakeAccessTokenProvider.FakeToken); + var logs = host.Ingestion.Server.LogEntries.ToList(); + Assert.AreEqual(1, logs.Count); + + var headers = logs[0].RequestMessage!.Headers!; + Assert.IsTrue(headers.ContainsKey("Authorization")); + StringAssert.Contains(string.Join(" ", headers["Authorization"]!), FakeAccessTokenProvider.FakeToken); } public TestContext TestContext { get; set; } = null!; diff --git a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs new file mode 100644 index 00000000..0d101b62 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Models; +using PackageUploader.IntegrationTest.Fixtures; +using PackageUploader.IntegrationTest.Infrastructure; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end upload flow that ties both fakes together: the real service creates a package against +/// the Ingestion fake, uploads the file to the XFUS fake (no-delta), then processes and polls the +/// package to completion. +/// +[TestClass] +public sealed class UploadFlowTests : IntegrationTestBase +{ + [TestMethod] + public async Task UploadGamePackage_NoDelta_CompletesThroughIngestionAndXfus() + { + using var host = CreateMockServerHost(); + using var packageFile = SyntheticPackageFile.Create(sizeInBytes: 4096, extension: ".xvc"); + + const string productId = "PRODUPLOAD"; + const string packageId = "pkg-1"; + + host.Ingestion.StubGetProduct(productId); + host.Ingestion.StubGetPackageBranches(productId, ("Main", "draft-1")); + host.Ingestion.StubPackageConfiguration(productId, "draft-1", "config-1", marketGroupId: "NA"); + host.Ingestion.StubCreatePackage(productId, packageId, xfusUploadDomain: host.XfusUploadDomain); + host.Ingestion.StubProcessPackage(productId, packageId, "Processed"); + host.Ingestion.StubGetPackageProcessing(productId, packageId, "Processed"); + host.Xfus.StubNoDeltaUploadSuccess(1024); + + var product = await host.Service.GetProductByProductIdAsync(productId, CancellationToken.None); + var branch = await host.Service.GetPackageBranchByFriendlyNameAsync(product, "Main", CancellationToken.None); + var config = await host.Service.GetPackageConfigurationAsync(product, branch, CancellationToken.None); + var marketGroupPackage = config.MarketGroupPackages[0]; + + var result = await host.Service.UploadGamePackageAsync( + product, + branch, + marketGroupPackage, + packageFile.Path, + gameAssets: null, + minutesToWaitForProcessing: 1, + deltaUpload: false, + isXvc: false, + CancellationToken.None); + + Assert.IsNotNull(result); + Assert.AreEqual(GamePackageState.Processed, result.State); + + // The file was actually uploaded to the XFUS fake: a block payload PUT must have occurred. + Assert.IsTrue( + host.Xfus.Server.LogEntries.Any(e => + e.RequestMessage!.Method == "PUT" && + e.RequestMessage.Path.Contains("/source/payload")), + "XFUS fake should have received a block payload PUT"); + } +} diff --git a/src/PackageUploader.IntegrationTest/packages.lock.json b/src/PackageUploader.IntegrationTest/packages.lock.json index 2bf55d70..1559dc72 100644 --- a/src/PackageUploader.IntegrationTest/packages.lock.json +++ b/src/PackageUploader.IntegrationTest/packages.lock.json @@ -58,6 +58,25 @@ "MSTest.Analyzers": "4.2.2" } }, + "WireMock.Net": { + "type": "Direct", + "requested": "[2.11.0, )", + "resolved": "2.11.0", + "contentHash": "gQgO9LPslgAPr0cHwHIx2OP3UYZX0Ex11UdAhFyiY2cHBjpxtw9A6GOcF79B56vMFOhYFBHWUgl6fcSFLin0yg==", + "dependencies": { + "WireMock.Net.GraphQL": "2.11.0", + "WireMock.Net.Matchers.SystemTextJsonPath": "2.11.0", + "WireMock.Net.MimePart": "2.11.0", + "WireMock.Net.Minimal": "2.11.0", + "WireMock.Net.OpenTelemetry": "2.11.0", + "WireMock.Net.ProtoBuf": "2.11.0" + } + }, + "AnyOf": { + "type": "Transitive", + "resolved": "0.5.0.1", + "contentHash": "WDQw5Qos3mhCumSCgKD70TM1dmqBAuJFGv1cFtNTwTaDLZR7kGy33M5C+L0vZV/bNRNwyi5ABvRGPWHL17rkNw==" + }, "Azure.Core": { "type": "Transitive", "resolved": "1.54.0", @@ -106,16 +125,699 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "Fare": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "21XZo/yuXK1k0EUhdLnjgRD4n0HQYmPFchV6uaORcRc65rasZ1vdm2dmJXPBKZiIBztRRYRmmg/B76W721VWkA==" + }, + "GraphQL": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "BZkfH7GVacTZEkyqa4XN9mW12UA/0XYrpEkkrJnNBf0Pqw8CZWQfItLaWgG2C1Ju9YHH1UUG+LmIpo8iMP6pKA==", + "dependencies": { + "GraphQL-Parser": "9.5.0", + "GraphQL.Analyzers": "8.5.0" + } + }, + "GraphQL-Parser": { + "type": "Transitive", + "resolved": "9.5.0", + "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" + }, + "GraphQL.Analyzers": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "jwfvZD5agmw9J8iZEe6BUfKAY+/lC7EqDQg+6JRwXaQ6G/MCLy8jyBc2SHF/2JdAtrkygc1bVyCUP5mR2PHzVA==" + }, + "GraphQL.NewtonsoftJson": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "tAeUoUhJih5fdZRCV0ue3G/gsu8YBiyNZkgLVFyk0wTk8vJGLTBDJaP5o5LVo1edVnk0bR+0/PaNXAEJxkVrTw==", + "dependencies": { + "GraphQL": "[8.5.0, 9.0.0)", + "Newtonsoft.Json": "13.0.3" + } + }, + "Handlebars.Net": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q==" + }, + "Handlebars.Net.Helpers": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "MZ0/Nvy3XdEy/igZD4fJy5HiUKcKUA170Sq+2RmJFsGiWSqlroXzp8TQqJTDCi1aBtETfQOVdBG0YNvuDs9+uQ==", + "dependencies": { + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Handlebars.Net.Helpers.Core": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "vLTL6UrLUPPiWDCKig8FLhSU+i9J4n/8RfrhadvnvxqziyK0ArxKMT2gLqQ+X/8vJaRcI9zvD5HxA8KjWbq3Dw==", + "dependencies": { + "Handlebars.Net": "2.1.6", + "Stef.Validation": "0.1.1" + } + }, + "Handlebars.Net.Helpers.Humanizer": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "A7TmfLtv7x8HiVckXBmKmOAsO5GKxjSOjxymXS70upqzLLH8BjrhFl+QIGFCdVIWQRx3+yNjGcsz/JXNwt9YZg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "Humanizer": "[2.14.1, 4.0.0)" + } + }, + "Handlebars.Net.Helpers.Json": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "iRBo/ik0M8M6ezJt4QzZm5KQptEdeh6bVtnDbieuxh5YPTUsPMFvtoq0gg426PwrahE+5rXoFZmIM11Oy5GwTg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "Newtonsoft.Json": "13.0.3" + } + }, + "Handlebars.Net.Helpers.Random": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "zKcfFDN4QxgEjk4Em9yz/PQu0mBpIgEaqjhacg2Fl6M0oSsF7VBVflae2WRM9MtiVeRTwLkVwcy7TvJ6iqFuVQ==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "RandomDataGenerator.Net": "1.0.19" + } + }, + "Handlebars.Net.Helpers.Xeger": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "J+w9KalIuYlTKMeIv8eoisdoMEz44elri0UOLtfTAuDbADwnBBsJGp4kAQI107+hBcqery9OCRCXm8fvH4eCxQ==", + "dependencies": { + "Fare": "2.2.1", + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Handlebars.Net.Helpers.XPath": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "uUGzjR5w5YCv+BdWQ4RpWAho0tUG0zfAKG5v+abXS6+E+fjbfSshOg7LyoWTVcGTWO0PouukhSMUFaumB2K4tg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "XPath2.Extensions": "1.1.5" + } + }, + "Handlebars.Net.Helpers.Xslt": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "bOaX47avO4Uja6jTZcBAgS5KjL/2ZaewCpB0Oy7cVegctPyxiiRx/T44XGSt0133hHry9f5nJVsjFKNLrYq0Pg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Humanizer": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", + "dependencies": { + "Humanizer.Core.af": "2.14.1", + "Humanizer.Core.ar": "2.14.1", + "Humanizer.Core.az": "2.14.1", + "Humanizer.Core.bg": "2.14.1", + "Humanizer.Core.bn-BD": "2.14.1", + "Humanizer.Core.cs": "2.14.1", + "Humanizer.Core.da": "2.14.1", + "Humanizer.Core.de": "2.14.1", + "Humanizer.Core.el": "2.14.1", + "Humanizer.Core.es": "2.14.1", + "Humanizer.Core.fa": "2.14.1", + "Humanizer.Core.fi-FI": "2.14.1", + "Humanizer.Core.fr": "2.14.1", + "Humanizer.Core.fr-BE": "2.14.1", + "Humanizer.Core.he": "2.14.1", + "Humanizer.Core.hr": "2.14.1", + "Humanizer.Core.hu": "2.14.1", + "Humanizer.Core.hy": "2.14.1", + "Humanizer.Core.id": "2.14.1", + "Humanizer.Core.is": "2.14.1", + "Humanizer.Core.it": "2.14.1", + "Humanizer.Core.ja": "2.14.1", + "Humanizer.Core.ko-KR": "2.14.1", + "Humanizer.Core.ku": "2.14.1", + "Humanizer.Core.lv": "2.14.1", + "Humanizer.Core.ms-MY": "2.14.1", + "Humanizer.Core.mt": "2.14.1", + "Humanizer.Core.nb": "2.14.1", + "Humanizer.Core.nb-NO": "2.14.1", + "Humanizer.Core.nl": "2.14.1", + "Humanizer.Core.pl": "2.14.1", + "Humanizer.Core.pt": "2.14.1", + "Humanizer.Core.ro": "2.14.1", + "Humanizer.Core.ru": "2.14.1", + "Humanizer.Core.sk": "2.14.1", + "Humanizer.Core.sl": "2.14.1", + "Humanizer.Core.sr": "2.14.1", + "Humanizer.Core.sr-Latn": "2.14.1", + "Humanizer.Core.sv": "2.14.1", + "Humanizer.Core.th-TH": "2.14.1", + "Humanizer.Core.tr": "2.14.1", + "Humanizer.Core.uk": "2.14.1", + "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", + "Humanizer.Core.uz-Latn-UZ": "2.14.1", + "Humanizer.Core.vi": "2.14.1", + "Humanizer.Core.zh-CN": "2.14.1", + "Humanizer.Core.zh-Hans": "2.14.1", + "Humanizer.Core.zh-Hant": "2.14.1" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Humanizer.Core.af": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ar": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.az": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bg": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bn-BD": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.cs": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.da": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.de": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.el": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.es": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fa": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fi-FI": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr-BE": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.he": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hu": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hy": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.id": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.is": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.it": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ja": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ko-KR": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ku": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.lv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ms-MY": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.mt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb-NO": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ro": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ru": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr-Latn": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.th-TH": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.tr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Cyrl-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Latn-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.vi": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-CN": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hans": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hant": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "JmesPath.Net": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "sL1LeqBm+BWSKvgZN/T470IqkXcKQXmOYsRUZU18jDZeiIBmvUfIe9m3VhiII/jOK/6WmrQ+W8Pqwz3k28WX9g==", + "dependencies": { + "JmesPath.Net.Parser": "1.1.0", + "Newtonsoft.Json": "13.0.4" + } + }, + "JmesPath.Net.Parser": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "NLTE/dPy8lMcZO6E7SL5Jw3fay8Vesll7+hkeRVSRaVNg1RRyPBV3/u6CM7QNgtnzhvyFPDyxUHUuRdh0vzCSg==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "fRctF2J2SILYG6wqP21drmeEODmCVkVQ/b3MndDu2fT1swfySyUgq7ePCk+aENGlDcIm05fyfjh9vcuqDEfv3w==" + }, + "JsonConverter.Abstractions": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "Ci3nuKx3GgMDfW9JA4dJpU+hJV5G1ve72mploQP8ivSDpOmo2QbfVAQkxsVzb3UQJCgwxxM6rdE/fPXwM0yj0g==" + }, + "JsonConverter.Newtonsoft.Json": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "K6doeW12emLiJV4laUf58y3kkjng6/IARRtC7+20qIOBrP1pBxGRsjz2IfxCgSBkTML3q6FWe7O+/UE374lsaA==", + "dependencies": { + "JsonConverter.Abstractions": "0.13.0", + "Newtonsoft.Json": "13.0.4", + "Stef.Validation": "0.1.1" + } + }, + "JsonConverter.System.Text.Json": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "UtRbkZT16Z0OVZ9n/h60E0GlUDTul/DjpuuajdsCNvefsmTxf6WNB8Cq9Hjwu9pGjfqOaFOmaz8k54DaHBmc0g==", + "dependencies": { + "JsonConverter.Abstractions": "0.13.0", + "Stef.Validation": "0.1.1" + } + }, + "JsonPath.Net": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "Cmt2mvPYOLljjqSfM1xUYZYTPf8MPbwv2XpCpPxxq9u23/CGrz/fljgd1fJUNujd3+E1adNOyF1TLwppbyQwxg==", + "dependencies": { + "Json.More.Net": "3.0.1" + } + }, + "MetadataReferenceService.Abstractions": { + "type": "Transitive", + "resolved": "0.0.1", + "contentHash": "Sf5ip58vlqWkQIAULIOKFIIFuhtRd8lChsJRZdFo746NVApEp/qgxNf/zCLjbB/RA/8TQGXWrFPKpqjyeh3EMg==", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Stef.Validation": "0.1.1" + } + }, + "MetadataReferenceService.Default": { + "type": "Transitive", + "resolved": "0.0.1", + "contentHash": "ihrchqYobpQMA9tn0W+MGD3oe5onqCttbR3lQfEiVzwF0V9/DS+K4YtvsUPGDC9XIie2Xw3lugSSk97k+OUwnQ==", + "dependencies": { + "MetadataReferenceService.Abstractions": "0.0.1" + } + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.9", + "contentHash": "+CcfWi1LoKYbcxt+3toO4xbBG+qSSMbPuuow+cbZKIrITXuu1geN1traamL4jG8QaHdHGm3M0eCh+EOgdMgNPA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.8" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "10.0.3", "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, "Microsoft.DiaSymReader": { "type": "Transitive", "resolved": "2.2.3", @@ -402,6 +1104,11 @@ "Microsoft.Extensions.Primitives": "10.0.7" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.7", @@ -459,6 +1166,56 @@ "resolved": "8.14.0", "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "CZMom/ZoWcgjxLMxmCmcEkuoA0OA4swN1CGeMBQyxF/hEZgRbWK9EnWVJ9/oMUq3D1+OGJjnbN+W6gFq9kZcEg==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "E0AbluNkI30/VKa96PxJhhFZDx/NGYIXFrRIRq1N5/V0TToaiuc3hM90QLFszT2BBQefnp/wjm12ilSudmt9bg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "6.34.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "xrqYK+V3FW+fMQ5oI7cwku2wj1RHz8qym3kh+rD+BTgCw1RmfFyWrLQ8/rVEqTl2nn4NcC0N+sHk0Q4qQ8dK9A==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "6.34.0", + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "SN3eZtssgpfnTCUlKsTJn9/0UiSc/HsbGLFl5Xp8vXFLXBeweWiDu54jFngSirjtJd6lSw3GgZhK5LZvVXGGLQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "6.34.0", + "System.IdentityModel.Tokens.Jwt": "6.34.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "PEPcGMqbEwEwbpQ6nTld9Nqq6V5BPZSOfk71qXZ7h7DuGuxa13bWvjImhJba5Ko88YvIuZuOBJWFZmjLfwbNXA==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "6.34.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.8", + "contentHash": "JO60u/VVUdaZfv4XQ//zgcH54y8rnxdpcvXnsDqWLKB4adDKaCiaozixDfQ/6H+PKYfkNV2CL8b8U+F9mciE3Q==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.2", @@ -510,6 +1267,91 @@ "resolved": "4.2.2", "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "10.9.0", + "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==", + "dependencies": { + "Namotion.Reflection": "2.1.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "NJsonSchema.Extensions": { + "type": "Transitive", + "resolved": "0.2.0", + "contentHash": "zLHUfuCmnaaQbKxqvTALrxhXV6Pbdy4G3ZlAI+7oaXdJmSyQPpMHGcxDBmw0+qHziT7jVImxU1BjcidKJHeprg==", + "dependencies": { + "NJsonSchema": "10.9.0" + } + }, + "NSwag.Core": { + "type": "Transitive", + "resolved": "13.16.1", + "contentHash": "xiX+H3Bv6zxrqJExPepO5WQVutkDUMdlUA3NqQ8VguwsYwJlkV05eF8XvmbJn/yGJWUag7vLImuXAoj0/327Bg==", + "dependencies": { + "NJsonSchema": "10.7.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==", + "dependencies": { + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "u8n/W8yIlqv0BXZmvId1iVaeWXG42tGKdTkuLYg5g57Y/r9CeUNzqtrSHNdG5IoO8iPX79w3v+WsbAHgUQbfeg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "2nPd7r0ug/gd6/CNFL6Rlu+RSQ9WYGSGHAYQ1ssbSqyzKJpqTunfx2I/1O0WB5k+L0cyXbG4XVZpoSoUc3M7wg==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" + } + }, "Polly": { "type": "Transitive", "resolved": "7.2.4", @@ -528,6 +1370,65 @@ "Polly": "7.1.0" } }, + "protobuf-net": { + "type": "Transitive", + "resolved": "3.2.52", + "contentHash": "XbZurNU3B/VaL/5OJ0kshO+AWxsZroI1saKuLfZpDwH2ngb2K9bdF1nIW6elFOViZw7TQCmfVZapxrMKCDqecQ==", + "dependencies": { + "protobuf-net.Core": "3.2.52" + } + }, + "protobuf-net.Core": { + "type": "Transitive", + "resolved": "3.2.52", + "contentHash": "zOpGtUo2QTgbsiI0D0yCe8aUTgDPov6kqIu1CDHI6isqhYcAHdirRrdnfsQXmAUfAWx1LwVYGgC6xe6fNS4UAg==" + }, + "ProtoBufJsonConverter": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "lxvcZQlCtgYZpfm9hhAJVZ1jsPkb9g3fyaAOnQLyEu8MiAowEIdED6jzwfjqLqOhY4AahqnbfRvZ904Ud43X7w==", + "dependencies": { + "MetadataReferenceService.Default": "0.0.1", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Newtonsoft.Json": "13.0.3", + "Stef.Validation": "0.1.1", + "protobuf-net": "3.2.52" + } + }, + "RamlToOpenApiConverter.SourceOnly": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "uYuENc27+ly789oJITPXGDEjvq/gzXrIZ0STCaN2Gkg6rmYTLPkcQyqXuia6HFzMaNBDEI1+WT5V+G4mYYq6bQ==" + }, + "RandomDataGenerator.Net": { + "type": "Transitive", + "resolved": "1.0.19.1", + "contentHash": "OkAqBA69VbYYg+biX2DYWcucOI/yEivkdJ/XPqife/mQAC0r/NArcAU/EI3i/1oRFUUCjibV0b7qEjK+TYTqDw==", + "dependencies": { + "Fare": "2.2.1", + "Stef.Validation": "0.1.1" + } + }, + "Scriban.Signed": { + "type": "Transitive", + "resolved": "7.2.0", + "contentHash": "AGI7QURe/UR9lsfM53yylQngbe77PngBnpzZQ+QV3wcevP4cydyCT+YAMR4rntWEle7v++xUjmfmtXuAvp6Rtw==" + }, + "SharpYaml": { + "type": "Transitive", + "resolved": "2.1.3", + "contentHash": "qd6kDo2uZPG/AvQ3NS2CXr7fDtP+rjiNQ81No3mFGpn41DcG0FEklgnj/j5hVhgQ6YTgTYbvyzkdDnvT+RtuLQ==" + }, + "SimMetrics.Net": { + "type": "Transitive", + "resolved": "1.0.5", + "contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q==" + }, + "Stef.Validation": { + "type": "Transitive", + "resolved": "0.3.0", + "contentHash": "OfzmxQMK4eBzmobph43p1NsLTgVAC3XGTcvQS0odhsdL6uS7UsFzBMK6S9mAfIijZdLW4q98aZ3dtTOeTaQo6Q==" + }, "System.ClientModel": { "type": "Transitive", "resolved": "1.10.0", @@ -549,6 +1450,15 @@ "resolved": "10.0.7", "contentHash": "WbmDLeTPYhEzXhvYVioTVn/D1XX6bovyny9n5p8Zxtf03+eY385RB818teZm6n+fA63iZNvng0/Np4tLuhkMhQ==" }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "c0misfmFT3QxKY+a16PGlj+DtiUzoPaf26m2avyPZaLRc9vlIdLtmovfRY5MqN+y/SEoBSRXrgVaeZGPgFQQ6w==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.34.0", + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, "System.Interactive.Async": { "type": "Transitive", "resolved": "7.0.1", @@ -577,6 +1487,139 @@ "resolved": "4.5.0", "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==" }, + "TinyMapper.Signed": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "W5uc9QXp8PUgP3VQ1Qyt3vK8ptyjj38tJ7nEAtRKA6R/4e6+2gsgYrAmRg9fCK1hhe3E0yeAm5acC14qx2CINg==" + }, + "WireMock.Net.Abstractions": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "/iy5Jn1QHW1CDmyVj6e+/rhUQpuNHk5Y6Qy6MQSFv48R86YovqZ2Au0VwtL2nVvwbRY0TeMH3XkhARHcynRUPA==" + }, + "WireMock.Net.GraphQL": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "QVfnWZrjI9P9v3pOzfDByZ0IN4JmV91jo+H6jz5heJ2dWPFFmmqLpxIc2Y2ExuJGYQAJmvNYAMC3/2YSXK7+wQ==", + "dependencies": { + "GraphQL.NewtonsoftJson": "8.5.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Matchers.SystemTextJsonPath": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "B8qofF1bdEgtwHRgswtOJkZxHmLmmf8RIH4Jk6KJSHn4QKwwMrtPdEOD632/03P8n93O2K2mfd8PDsovLQ7gcQ==", + "dependencies": { + "JsonPath.Net": "3.0.2", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.MimePart": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "+aCR6r3cx+ZvS0hhu5CUjNAstub5Sy1JmqFC7q3QQK82yazlH7OB4Dzh1rTJH+BXcxoWKC91nqXgt6xfD8Uv8g==", + "dependencies": { + "Stef.Validation": "0.3.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Minimal": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "NmQrlaNiZRx535OFUJYCG3+HZRXEi0rom4mN+09qas6RVaZgnh+ChxnWFIdOS8rXoTRlpDmFHG1fdUVHHX4mhA==", + "dependencies": { + "JmesPath.Net": "1.1.0", + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.34.0", + "NJsonSchema.Extensions": "0.2.0", + "NSwag.Core": "13.16.1", + "Scriban.Signed": "7.2.0", + "SimMetrics.Net": "1.0.5", + "TinyMapper.Signed": "4.0.0", + "WireMock.Net.OpenApiParser": "2.11.0", + "WireMock.Net.Shared": "2.11.0", + "WireMock.Org.Abstractions": "2.11.0" + } + }, + "WireMock.Net.OpenApiParser": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "b3awrEjgWG6iPCT2wO5Jdye6JaGlCeGoCrgtaeucP0GVYn/BmeNGqKU6BGpIsx7J0+W6YBj0QgpNQp7/nRgeaw==", + "dependencies": { + "Newtonsoft.Json": "13.0.4", + "RamlToOpenApiConverter.SourceOnly": "0.11.0", + "RandomDataGenerator.Net": "1.0.19.1", + "SharpYaml": "2.1.3", + "Stef.Validation": "0.3.0", + "WireMock.Net.Abstractions": "2.11.0", + "YamlDotNet": "16.3.0" + } + }, + "WireMock.Net.OpenTelemetry": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "T+ShaY7mI82RhQmCUrs5n5ZX67xlTyIfC1/gNiftca/wiaXCctj+b8gt7tr7z+LuKroTnMRQDuOFdHhBKVjUQQ==", + "dependencies": { + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.Instrumentation.AspNetCore": "1.15.2", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.ProtoBuf": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "cWJTxP4oG9PZoOQDTzRNbuJX8mY/MtmjMCiwnSx2h6Vm33HxHFngdmHdgK4ZjbZfnqq6gKIXPel+DvEExXa6lQ==", + "dependencies": { + "ProtoBufJsonConverter": "0.11.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Shared": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "WvVfCUHP6noNmNdpCWuWeBok44QFjgwQITGoTiMd0v40fCB85SKJU/1uocP8ZgRABELBMbbLALsIuz31KdeNcA==", + "dependencies": { + "AnyOf": "0.5.0.1", + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Humanizer": "2.5.5", + "Handlebars.Net.Helpers.Json": "2.5.5", + "Handlebars.Net.Helpers.Random": "2.5.5", + "Handlebars.Net.Helpers.XPath": "2.5.5", + "Handlebars.Net.Helpers.Xeger": "2.5.5", + "Handlebars.Net.Helpers.Xslt": "2.5.5", + "JsonConverter.Newtonsoft.Json": "0.13.0", + "JsonConverter.System.Text.Json": "0.13.0", + "Microsoft.AspNetCore.Http": "2.3.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Stef.Validation": "0.3.0", + "WireMock.Net.Abstractions": "2.11.0" + } + }, + "WireMock.Org.Abstractions": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "ERlcOKHCgOblIaph2QZYpj9wrIwHRirwHFRqzfo4qDUVxH3QB8PoxuVEaL6LGFUM9gKBilrdvs4ulsxKiwjqwQ==" + }, + "XPath2": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "LQg7kZyAmmb+qvv5TiOuuijxN97rRbR05qbMkVIH+i+sx9CA2UNUKGNtdVxWEXOabS8BIwlXm6ox1OOTjvZ6jw==" + }, + "XPath2.Extensions": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "oEbdGUJsF25QL3Vj1GgSlT2xdbxnka5dKcjuA9CouWCV/l9ecSfypOv78B1+YUD8a8w47prLNw2i3ofLNcrbGA==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "XPath2": "1.1.5" + } + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "16.3.0", + "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" + }, "PackageUploader": { "type": "Project", "dependencies": {