diff --git a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs index 68763dbc0..83499f51a 100644 --- a/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs +++ b/Solutions/Menes.Abstractions/Menes/Exceptions/OpenApiAccessControlPolicyEvaluationFailedException.cs @@ -100,7 +100,7 @@ private void AddProblemDetails() this.AddProblemDetailsExtension("Policy Name", this.PolicyName); } - if (this.Requests != null && this.Requests.Length > 0) + if (this.Requests?.Length > 0) { this.AddProblemDetailsExtension("Requests", string.Join(Environment.NewLine, this.Requests.Select(r => $"{r.OperationId}: {r.Method} {r.Path}"))); } diff --git a/Solutions/Menes.Abstractions/Menes/Hal/HalDocument.cs b/Solutions/Menes.Abstractions/Menes/Hal/HalDocument.cs index 30022b09b..1039045d2 100644 --- a/Solutions/Menes.Abstractions/Menes/Hal/HalDocument.cs +++ b/Solutions/Menes.Abstractions/Menes/Hal/HalDocument.cs @@ -34,7 +34,7 @@ public HalDocument(IJsonSerializerSettingsProvider serializerSettingsProvider) /// /// Gets the serializer settings for the HAL document. /// - public JsonSerializerSettings SerializerSettings { get; private set; } + public JsonSerializerSettings SerializerSettings { get; } /// /// Gets the properites for the HalDocument. diff --git a/Solutions/Menes.Abstractions/Menes/Hal/IHalDocumentMapper.cs b/Solutions/Menes.Abstractions/Menes/Hal/IHalDocumentMapper.cs index bf59d8c9c..b3afa7927 100644 --- a/Solutions/Menes.Abstractions/Menes/Hal/IHalDocumentMapper.cs +++ b/Solutions/Menes.Abstractions/Menes/Hal/IHalDocumentMapper.cs @@ -29,4 +29,20 @@ public interface IHalDocumentMapper : IHalDocumentMapper /// The for the resource. HalDocument Map(T resource); } + + /// + /// Implemented by types which can map a resource to a HAL document and require additional context for the mapping. + /// + /// The type of the resource to map. + /// The type of the object that provides additional context to the mapping. + public interface IHalDocumentMapper : IHalDocumentMapper + { + /// + /// Map a resource to a HAL document. + /// + /// The resource to map. + /// The additional context information. + /// The for the resource. + HalDocument Map(TResource resource, TContext context); + } } diff --git a/Solutions/Menes.Abstractions/Menes/Links/OpenApiAccessCheckerExtensions.cs b/Solutions/Menes.Abstractions/Menes/Links/OpenApiAccessCheckerExtensions.cs index c06189ff1..9f8af5d4f 100644 --- a/Solutions/Menes.Abstractions/Menes/Links/OpenApiAccessCheckerExtensions.cs +++ b/Solutions/Menes.Abstractions/Menes/Links/OpenApiAccessCheckerExtensions.cs @@ -42,8 +42,8 @@ public static async Task RemoveForbiddenLinksAsync(this IOpenApiAccessChecker th AddHalDocumentLinksToMap( target, linkMap, - !options.HasFlag(HalDocumentLinkRemovalOptions.NonRecursive), - options.HasFlag(HalDocumentLinkRemovalOptions.Unsafe)); + (options & HalDocumentLinkRemovalOptions.NonRecursive) == 0, + (options & HalDocumentLinkRemovalOptions.Unsafe) != 0); // Build a second map of operation descriptors (needed to invoke the access policy check) to our OpenApiWebLinks. var operationDescriptorMap = linkMap diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs index 2b66c3494..d1cf3e87b 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/HttpRequestParameterBuilder.cs @@ -519,7 +519,8 @@ private object ConvertValue(OpenApiSchema schema, string value) this.logger.LogError( "Failed to convert value with [{schema}]", schema.GetLoggingInformation()); - throw new NotImplementedException(); + + throw new OpenApiServiceMismatchException($"Unable to convert value to match [{schema.GetLoggingInformation()}]"); } } } diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs index 035378cb0..c10d092da 100644 --- a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs +++ b/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiActionResult.cs @@ -263,7 +263,8 @@ private string ConvertValue(OpenApiSchema schema, object value) this.logger.LogError( "Failed to convert value with [{schema}]", schema.GetLoggingInformation()); - throw new NotImplementedException(); + + throw new OpenApiServiceMismatchException($"Failed to convert value to match [{schema.GetLoggingInformation()}]"); } private void BuildHeaders(HttpResponse httpResponse, OpenApiResponse response) diff --git a/Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpRequestHostServiceCollectionExtensions.cs b/Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs similarity index 100% rename from Solutions/Menes.Hosting.AspNetCore/Menes/Internal/OpenApiHttpRequestHostServiceCollectionExtensions.cs rename to Solutions/Menes.Hosting.AspNetCore/Microsoft/Extensions/DependencyInjection/OpenApiHttpRequestHostServiceCollectionExtensions.cs diff --git a/Solutions/Menes.Hosting/Microsoft/Extensions/DependencyInjection/OpenApiHostingServiceCollectionExtensions.cs b/Solutions/Menes.Hosting/Microsoft/Extensions/DependencyInjection/OpenApiHostingServiceCollectionExtensions.cs index f3fd4cf09..dd449e064 100644 --- a/Solutions/Menes.Hosting/Microsoft/Extensions/DependencyInjection/OpenApiHostingServiceCollectionExtensions.cs +++ b/Solutions/Menes.Hosting/Microsoft/Extensions/DependencyInjection/OpenApiHostingServiceCollectionExtensions.cs @@ -153,7 +153,7 @@ public static IServiceCollection AddOpenApiHosting(this ISe /// Add an to the service collection. /// /// The type of the resource mapped by the HAL document mapper. - /// The type fo the mapper. + /// The type of the mapper. /// The service collection to which to add the mapper. /// The service collection, configured with the HAL document mapper. public static IServiceCollection AddHalDocumentMapper(this IServiceCollection services) @@ -165,6 +165,23 @@ public static IServiceCollection AddHalDocumentMapper(this I return services; } + /// + /// Add an to the service collection. + /// + /// The type of the resource mapped by the HAL document mapper. + /// The type of the additional context required by the HAL document mapper. + /// The type of the mapper. + /// The service collection to which to add the mapper. + /// The service collection, configured with the HAL document mapper. + public static IServiceCollection AddHalDocumentMapper(this IServiceCollection services) + where TMapper : class, IHalDocumentMapper + { + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + services.AddSingleton>(s => s.GetRequiredService()); + return services; + } + /// /// Adds the /swagger endpoint to your host. /// diff --git a/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs b/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs index 5630039a0..58ec6c62e 100644 --- a/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs +++ b/Solutions/Menes.PetStore.Hosting/Menes/PetStore/Hosting/Startup.cs @@ -31,10 +31,7 @@ public void Configure(IWebJobsBuilder builder) services.AddHalDocumentMapper(); services.AddHalDocumentMapper(); - services.AddOpenApiHttpRequestHosting(hostConfig => - { - LoadDocuments(hostConfig); - }); + _ = services.AddOpenApiHttpRequestHosting(LoadDocuments); // We can add all the services here // We will only actually *provide* services that are in the YAML file(s) we load below diff --git a/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature b/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature new file mode 100644 index 000000000..9f01c4ba4 --- /dev/null +++ b/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature @@ -0,0 +1,56 @@ +Feature: OpenApi Hosting Initialisation + In order to use Menes in my application + As a developer + I want to be able to add Menes services and related components to my service collection + +Background: + Given I have created a service collection to register my services against + +Scenario: Adding AspNetCore OpenApi hosting adds the IOpenApiHost for HttpRequest and IActionResult + When I add AspNetCore OpenApi hosting to the service collection + And I build the service provider from the service collection + Then a service is available as a Singleton for type IOpenApiHost{HttpRequest, IActionResult} + +Scenario: Adding OpenApi hosting enables auditing to console by default + Given I have added AspNetCore OpenApi hosting to the service collection + And I have built the service provider from the service collection + Then an audit log builder service is available for auditing operations which return OpenApiResults + And an audit log builder service is available for auditing operations which return a POCO + And an audit log sink service is available for console logging + And auditing is enabled + +Scenario Outline: OpenApi host initialisation maps standard Menes exception types to their corresponding HTTP status codes + Given I have added AspNetCore OpenApi hosting to the service collection + And I have built the service provider from the service collection + When I request an instance of the OpenApi host + Then the exception of type '' is mapped to response code '' + + Examples: + | Exception Type | Mapped Response Code | + | Menes.Exceptions.OpenApiBadRequestException, Menes.Abstractions | 400 | + | Menes.Exceptions.OpenApiUnauthorizedException, Menes.Abstractions | 401 | + | Menes.Exceptions.OpenApiForbiddenException, Menes.Abstractions | 403 | + | Menes.Exceptions.OpenApiNotFoundException, Menes.Abstractions | 404 | + +Scenario: OpenApi host initialisation adds link maps from registered IHalDocumentMapper types + Given I have added AspNetCore OpenApi hosting to the service collection + And I have registered a HalDocumentMapper for a resource type to the service collection + And I have registered a HalDocumentMapper for a resource and context type to the service collection + And I have built the service provider from the service collection + When I request an instance of the OpenApi host + Then the HalDocumentMapper for resource type has configured its links + And the HalDocumentMapper for resource and context types has configured its links + +Scenario: Registering HAL document mappers with resource type parameters adds them to the container with the concrete type, the IHalDocumentMapper interface and the generic IHalDocumentMapper interface + When I register a HalDocumentMapper for a resource type to the service collection + And I build the service provider from the service collection + Then it should be available as a Singleton with the service type matching the concrete type of the mapper + And It should be available as a Singleton with a service type of IHalDocumentMapper + And it should be available as a Singleton with a service type of IHalDocumentMapper{TResource} + +Scenario: Registering HAL document mappers with resource and context type parameters adds them to the container with the concrete type, the IHalDocumentMapper interface and the generic IHalDocumentMapper interface + When I register a HalDocumentMapper for a resource and context type to the service collection + And I build the service provider from the service collection + Then it should be available as a Singleton with the service type matching the concrete type of the mapper with context + And It should be available as a Singleton with a service type of IHalDocumentMapper + And it should be available as a Singleton with a service type of IHalDocumentMapper{TResource, TContext} diff --git a/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature.cs b/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature.cs new file mode 100644 index 000000000..2dd59cf54 --- /dev/null +++ b/Solutions/Menes.Specs/Features/OpenApiHostingInitialisation.feature.cs @@ -0,0 +1,255 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (http://www.specflow.org/). +// SpecFlow Version:3.0.0.0 +// SpecFlow Generator Version:3.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace Menes.Specs.Features +{ + using TechTalk.SpecFlow; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.0.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [NUnit.Framework.TestFixtureAttribute()] + [NUnit.Framework.DescriptionAttribute("OpenApi Hosting Initialisation")] + public partial class OpenApiHostingInitialisationFeature + { + + private TechTalk.SpecFlow.ITestRunner testRunner; + +#line 1 "OpenApiHostingInitialisation.feature" +#line hidden + + [NUnit.Framework.OneTimeSetUpAttribute()] + public virtual void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "OpenApi Hosting Initialisation", "\tIn order to use Menes in my application\r\n\tAs a developer\r\n\tI want to be able to " + + "add Menes services and related components to my service collection", ProgrammingLanguage.CSharp, ((string[])(null))); + testRunner.OnFeatureStart(featureInfo); + } + + [NUnit.Framework.OneTimeTearDownAttribute()] + public virtual void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [NUnit.Framework.SetUpAttribute()] + public virtual void TestInitialize() + { + } + + [NUnit.Framework.TearDownAttribute()] + public virtual void ScenarioTearDown() + { + testRunner.OnScenarioEnd(); + } + + public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); + } + + public virtual void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public virtual void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + public virtual void FeatureBackground() + { +#line 6 +#line 7 + testRunner.Given("I have created a service collection to register my services against", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Adding AspNetCore OpenApi hosting adds the IOpenApiHost for HttpRequest and IActi" + + "onResult")] + public virtual void AddingAspNetCoreOpenApiHostingAddsTheIOpenApiHostForHttpRequestAndIActionResult() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Adding AspNetCore OpenApi hosting adds the IOpenApiHost for HttpRequest and IActi" + + "onResult", null, ((string[])(null))); +#line 9 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 10 + testRunner.When("I add AspNetCore OpenApi hosting to the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 11 + testRunner.And("I build the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 12 + testRunner.Then("a service is available as a Singleton for type IOpenApiHost{HttpRequest, IActionR" + + "esult}", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + this.ScenarioCleanup(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Adding OpenApi hosting enables auditing to console by default")] + public virtual void AddingOpenApiHostingEnablesAuditingToConsoleByDefault() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Adding OpenApi hosting enables auditing to console by default", null, ((string[])(null))); +#line 14 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 15 + testRunner.Given("I have added AspNetCore OpenApi hosting to the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line 16 + testRunner.And("I have built the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 17 + testRunner.Then("an audit log builder service is available for auditing operations which return Op" + + "enApiResults", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line 18 + testRunner.And("an audit log builder service is available for auditing operations which return a " + + "POCO", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 19 + testRunner.And("an audit log sink service is available for console logging", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 20 + testRunner.And("auditing is enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + this.ScenarioCleanup(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("OpenApi host initialisation maps standard Menes exception types to their correspo" + + "nding HTTP status codes")] + [NUnit.Framework.TestCaseAttribute("Menes.Exceptions.OpenApiBadRequestException, Menes.Abstractions", "400", null)] + [NUnit.Framework.TestCaseAttribute("Menes.Exceptions.OpenApiUnauthorizedException, Menes.Abstractions", "401", null)] + [NUnit.Framework.TestCaseAttribute("Menes.Exceptions.OpenApiForbiddenException, Menes.Abstractions", "403", null)] + [NUnit.Framework.TestCaseAttribute("Menes.Exceptions.OpenApiNotFoundException, Menes.Abstractions", "404", null)] + public virtual void OpenApiHostInitialisationMapsStandardMenesExceptionTypesToTheirCorrespondingHTTPStatusCodes(string exceptionType, string mappedResponseCode, string[] exampleTags) + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("OpenApi host initialisation maps standard Menes exception types to their correspo" + + "nding HTTP status codes", null, exampleTags); +#line 22 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 23 + testRunner.Given("I have added AspNetCore OpenApi hosting to the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line 24 + testRunner.And("I have built the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 25 + testRunner.When("I request an instance of the OpenApi host", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 26 + testRunner.Then(string.Format("the exception of type \'{0}\' is mapped to response code \'{1}\'", exceptionType, mappedResponseCode), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + this.ScenarioCleanup(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("OpenApi host initialisation adds link maps from registered IHalDocumentMapper typ" + + "es")] + public virtual void OpenApiHostInitialisationAddsLinkMapsFromRegisteredIHalDocumentMapperTypes() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("OpenApi host initialisation adds link maps from registered IHalDocumentMapper typ" + + "es", null, ((string[])(null))); +#line 35 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 36 + testRunner.Given("I have added AspNetCore OpenApi hosting to the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line 37 + testRunner.And("I have registered a HalDocumentMapper for a resource type to the service collecti" + + "on", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 38 + testRunner.And("I have registered a HalDocumentMapper for a resource and context type to the serv" + + "ice collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 39 + testRunner.And("I have built the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 40 + testRunner.When("I request an instance of the OpenApi host", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 41 + testRunner.Then("the HalDocumentMapper for resource type has configured its links", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line 42 + testRunner.And("the HalDocumentMapper for resource and context types has configured its links", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + this.ScenarioCleanup(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Registering HAL document mappers with resource type parameters adds them to the c" + + "ontainer with the concrete type, the IHalDocumentMapper interface and the generi" + + "c IHalDocumentMapper interface")] + public virtual void RegisteringHALDocumentMappersWithResourceTypeParametersAddsThemToTheContainerWithTheConcreteTypeTheIHalDocumentMapperInterfaceAndTheGenericIHalDocumentMapperInterface() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Registering HAL document mappers with resource type parameters adds them to the c" + + "ontainer with the concrete type, the IHalDocumentMapper interface and the generi" + + "c IHalDocumentMapper interface", null, ((string[])(null))); +#line 44 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 45 + testRunner.When("I register a HalDocumentMapper for a resource type to the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 46 + testRunner.And("I build the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 47 + testRunner.Then("it should be available as a Singleton with the service type matching the concrete" + + " type of the mapper", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line 48 + testRunner.And("It should be available as a Singleton with a service type of IHalDocumentMapper", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 49 + testRunner.And("it should be available as a Singleton with a service type of IHalDocumentMapper{T" + + "Resource}", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + this.ScenarioCleanup(); + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Registering HAL document mappers with resource and context type parameters adds t" + + "hem to the container with the concrete type, the IHalDocumentMapper interface an" + + "d the generic IHalDocumentMapper interface")] + public virtual void RegisteringHALDocumentMappersWithResourceAndContextTypeParametersAddsThemToTheContainerWithTheConcreteTypeTheIHalDocumentMapperInterfaceAndTheGenericIHalDocumentMapperInterface() + { + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Registering HAL document mappers with resource and context type parameters adds t" + + "hem to the container with the concrete type, the IHalDocumentMapper interface an" + + "d the generic IHalDocumentMapper interface", null, ((string[])(null))); +#line 51 +this.ScenarioInitialize(scenarioInfo); + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line 52 + testRunner.When("I register a HalDocumentMapper for a resource and context type to the service col" + + "lection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line 53 + testRunner.And("I build the service provider from the service collection", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 54 + testRunner.Then("it should be available as a Singleton with the service type matching the concrete" + + " type of the mapper with context", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line 55 + testRunner.And("It should be available as a Singleton with a service type of IHalDocumentMapper", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line 56 + testRunner.And("it should be available as a Singleton with a service type of IHalDocumentMapper{T" + + "Resource, TContext}", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/Solutions/Menes.Specs/Steps/AccessControlPolicySteps.cs b/Solutions/Menes.Specs/Steps/AccessControlPolicySteps.cs index 0a7c8f2c2..31b2932ac 100644 --- a/Solutions/Menes.Specs/Steps/AccessControlPolicySteps.cs +++ b/Solutions/Menes.Specs/Steps/AccessControlPolicySteps.cs @@ -102,7 +102,7 @@ public void ThenEachPolicyShouldReceiveAPathOf(string path) { foreach ((Mock policy, CompletionSourceWithArgs> completion) in this.policies) { - Assert.AreEqual(path, completion.Arguments[0].Requests.First().Path); + Assert.AreEqual(path, completion.Arguments[0].Requests[0].Path); } } @@ -111,7 +111,7 @@ public void ThenEachPolicyShouldReceiveAnOperationIdOf(string operationId) { foreach ((Mock policy, CompletionSourceWithArgs> completion) in this.policies) { - Assert.AreEqual(operationId, completion.Arguments[0].Requests.First().OperationId); + Assert.AreEqual(operationId, completion.Arguments[0].Requests[0].OperationId); } } @@ -120,7 +120,7 @@ public void ThenEachPolicyShouldReceiveAnHttpMethodOf(string method) { foreach ((Mock policy, CompletionSourceWithArgs> completion) in this.policies) { - Assert.AreEqual(method, completion.Arguments[0].Requests.First().Method); + Assert.AreEqual(method, completion.Arguments[0].Requests[0].Method); } } diff --git a/Solutions/Menes.Specs/Steps/HalDocumentSteps.cs b/Solutions/Menes.Specs/Steps/HalDocumentSteps.cs index 974d19d43..892c4e26b 100644 --- a/Solutions/Menes.Specs/Steps/HalDocumentSteps.cs +++ b/Solutions/Menes.Specs/Steps/HalDocumentSteps.cs @@ -79,7 +79,6 @@ public void GivenIAddAnEmbeddedResourceToTheHalDocumentT() document.AddEmbeddedResource("somerel", halDocumentFactory.CreateHalDocument()); } - [Then("the properties of the domain class should be serialized as top level properties in the JSON")] public void ThenThePropertiesOfTheDomainClassShouldBeSerializedAsTopLevelPropertiesInTheJSON() { diff --git a/Solutions/Menes.Specs/Steps/OpenApiAccessCheckerExtensionsSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiAccessCheckerExtensionsSteps.cs index 045a167e9..deeaf821a 100644 --- a/Solutions/Menes.Specs/Steps/OpenApiAccessCheckerExtensionsSteps.cs +++ b/Solutions/Menes.Specs/Steps/OpenApiAccessCheckerExtensionsSteps.cs @@ -47,18 +47,23 @@ public void GivenTheHalDocumentCalledHasInternalLinks(string halDocumentName, Ta { HalDocument doc = this.scenarioContext.Get(halDocumentName); - openApiWebLinkTable.Rows.ForEach(row => - { - doc.AddLink(row["Rel"], new OpenApiWebLink(row["OperationId"], row["Href"], Enum.Parse(row["OperationType"], true))); - }); + openApiWebLinkTable.Rows.ForEach( + row => doc.AddLink( + row["Rel"], + new OpenApiWebLink( + row["OperationId"], + row["Href"], + Enum.Parse(row["OperationType"], true)))); } [Given("the HalDocument called '(.*)' has embedded resources")] public void GivenTheHalDocumentCalledHasEmbeddedResources(string halDocumentName, Table embeddedResourceTable) { - IHalDocumentFactory halDocumentFactory = ContainerBindings.GetServiceProvider(this.featureContext).GetService(); + IHalDocumentFactory halDocumentFactory = + ContainerBindings.GetServiceProvider(this.featureContext).GetService(); - IEnumerable<(string Rel, object Object)> embeddedResources = embeddedResourceTable.CreateSet<(string Rel, object Object)>(); + IEnumerable<(string Rel, object Object)> embeddedResources = + embeddedResourceTable.CreateSet<(string Rel, object Object)>(); HalDocument doc = this.scenarioContext.Get(halDocumentName); embeddedResources.ForEach(x => doc.AddEmbeddedResource(x.Rel, CreateHalDocument(x, halDocumentFactory))); } @@ -86,12 +91,14 @@ public Task WhenIRequestAnAccessCheckOnTheHalDocumentCalledWithTheFollowingOptio { HalDocument doc = this.scenarioContext.Get(halDocumentName); var mock = new Mock(); - mock.Setup(x => x.CheckAccessPoliciesAsync(It.IsAny(), It.IsAny())).Returns((IOpenApiContext context, AccessCheckOperationDescriptor[] descriptors) => this.MockCheckAccessPoliciesAsync(descriptors)); + mock.Setup(x => x.CheckAccessPoliciesAsync(It.IsAny(), It.IsAny())).Returns((IOpenApiContext _, AccessCheckOperationDescriptor[] descriptors) => this.MockCheckAccessPoliciesAsync(descriptors)); // Normally this would be invoked as an extension method on mock.Object, // but it's invoked as a static method here as otherwise it looks like we're // running our test against a mock, which is misleading. +#pragma warning disable RCS1196 // Call extension method as instance method. return OpenApiAccessCheckerExtensions.RemoveForbiddenLinksAsync(mock.Object, doc, new SimpleOpenApiContext(), Enum.Parse(optionsTable.Rows[0][0])); +#pragma warning restore RCS1196 // Call extension method as instance method. } [Then("the HalDocument called '(.*)' should contain only the following link relations")] diff --git a/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs new file mode 100644 index 000000000..ee7a8e1f2 --- /dev/null +++ b/Solutions/Menes.Specs/Steps/OpenApiHostingServiceCollectionExtensionsSteps.cs @@ -0,0 +1,220 @@ +// +// Copyright (c) Endjin. All rights reserved. +// + +namespace Menes.Specs.Steps +{ + using System; + using System.Linq; + using System.Reflection; + using Menes.Auditing; + using Menes.Auditing.AuditLogSinks.Development; + using Menes.Auditing.Internal; + using Menes.Hal; + using Menes.Specs.Steps.TestClasses; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TechTalk.SpecFlow; + + [Binding] + public class OpenApiHostingServiceCollectionExtensionsSteps + { + private readonly ScenarioContext scenarioContext; + + public OpenApiHostingServiceCollectionExtensionsSteps(ScenarioContext scenarioContext) + { + this.scenarioContext = scenarioContext; + } + + [Given("I have created a service collection to register my services against")] + public void GivenIHaveCreatedAServiceCollectionToRegisterMyServicesAgainst() + { + this.scenarioContext.Set(new ServiceCollection()); + } + + [Given("I have added AspNetCore OpenApi hosting to the service collection")] + [When("I add AspNetCore OpenApi hosting to the service collection")] + public void WhenIAddOpenApiHostingToTheServiceCollection() + { + ServiceCollection collection = this.scenarioContext.Get(); + + collection.AddLogging(); + collection.AddOpenApiHttpRequestHosting(_ => { }, null); + } + + [Given("I have built the service provider from the service collection")] + [When("I build the service provider from the service collection")] + public void GivenIHaveBuiltTheServiceCollection() + { + ServiceProvider provider = this.scenarioContext.Get().BuildServiceProvider(); + this.scenarioContext.Set(provider); + } + + [Given("I have registered a HalDocumentMapper for a resource type to the service collection")] + [When("I register a HalDocumentMapper for a resource type to the service collection")] + public void WhenIRegisterAHalDocumentMapperForAResourceTypeToTheServiceCollection() + { + this.scenarioContext.Get().AddHalDocumentMapper(); + } + + [Given("I have registered a HalDocumentMapper for a resource and context type to the service collection")] + [When("I register a HalDocumentMapper for a resource and context type to the service collection")] + public void WhenIAddAHalDocumentMapperForAResourceAndContextTypeToTheServiceCollection() + { + this.scenarioContext.Get() + .AddHalDocumentMapper(); + } + + [Then("a service is available as a Singleton for type IOpenApiHost{HttpRequest, IActionResult}")] + public void ThenAServiceIsAddedAsASingletonForTypeIOpenApiHostHttpRequestIActionResult() + { + this.AssertServiceIsAvailableFromServiceProvider>(); + this.AssertServiceIsASingleton>(); + } + + [Then("it should be available as a Singleton with the service type matching the concrete type of the mapper")] + public void ThenItShouldBeAvailableAsASingletonWithTheServiceTypeMatchingTheMapperType() + { + this.AssertServiceIsAvailableFromServiceProvider(); + this.AssertServiceIsASingleton(); + } + + [Then("it should be available as a Singleton with the service type matching the concrete type of the mapper with context")] + public void ThenItShouldBeAvailableAsASingletonWithTheServiceTypeMatchingTheMapperTypeWithContext() + { + this.AssertServiceIsAvailableFromServiceProvider(); + this.AssertServiceIsASingleton(); + } + + [Then("It should be available as a Singleton with a service type of IHalDocumentMapper")] + public void ThenItShouldBeAvailableAsASingletonWithAServiceTypeOfIHalDocumentMapper() + { + this.AssertServiceIsAvailableFromServiceProvider(); + this.AssertServiceIsASingleton(); + } + + [Then("it should be available as a Singleton with a service type of IHalDocumentMapper{TResource}")] + public void ThenItShouldBeAvailableAsASingletonWithAServiceTypeOfIHalDocumentMapperTResource() + { + this.AssertServiceIsAvailableFromServiceProvider>(); + this.AssertServiceIsASingleton>(); + } + + [Then("it should be available as a Singleton with a service type of IHalDocumentMapper{TResource, TContext}")] + public void ThenItShouldBeAvailableAsASingletonWithAServiceTypeOfIHalDocumentMapperWithResourceAndContext() + { + this.AssertServiceIsAvailableFromServiceProvider>(); + this.AssertServiceIsASingleton>(); + } + + [When("I request an instance of the OpenApi host")] + public void WhenIRequestAnInstanceOfTheOpenApiHost() + { + ServiceProvider provider = this.scenarioContext.Get(); + IOpenApiHost host = + provider.GetRequiredService>(); + + this.scenarioContext.Set(host); + } + + [Then("the HalDocumentMapper for resource type has configured its links")] + public void ThenTheHalDocumentMapperForResourceTypeHasConfiguredItsLinks() + { + ServiceProvider provider = this.scenarioContext.Get(); + PetHalDocumentMapper mapper = provider.GetRequiredService(); + Assert.IsTrue(mapper.LinkMapConfigured); + } + + [Then("the HalDocumentMapper for resource and context types has configured its links")] + public void ThenTheHalDocumentMapperForResourceAndContextTypesHasConfiguredItsLinks() + { + ServiceProvider provider = this.scenarioContext.Get(); + PetHalDocumentMapperWithContext mapper = provider.GetRequiredService(); + Assert.IsTrue(mapper.LinkMapConfigured); + } + + [Then("the exception of type '(.*)' is mapped to response code '(.*)'")] + public void ThenTheExceptionOfTypeIsMappedToResponseCode(string exceptionType, int statusCode) + { + IOpenApiExceptionMapper exceptionMapper = this.scenarioContext.Get() + .GetRequiredService(); + + // The only way to tell if it's been mapped without reflecting into the guts of the mapper is to try and register + // a new mapper for the given type/status code... even with this we need a little bit of reflection to invoke the + // method. + var type = Type.GetType(exceptionType); + MethodInfo mapMethod = exceptionMapper + .GetType() + .GetMethods() + .First(x => x.Name == "Map" && x.GetGenericArguments().Length == 1); + + MethodInfo createdMapMethod = mapMethod.MakeGenericMethod(type); + + try + { + createdMapMethod.Invoke(exceptionMapper, new object[] { statusCode, null }); + + Assert.Fail($"Exception of type '{exceptionType}' was not registered"); + } + catch (TargetInvocationException ex) when (ex.InnerException is ArgumentException) + { + // This is the expected exception result, so swallow to let the test pass. Anything + } + } + + [Then("an audit log builder service is available for auditing operations which return OpenApiResults")] + public void ThenAnAuditLogBuilderServiceIsAddedForAuditingOperationsWhichReturnOpenApiResults() + { + this.AssertServiceIsAvailableFromServiceProvider(); + } + + [Then("an audit log builder service is available for auditing operations which return a POCO")] + public void ThenAnAuditLogBuilderServiceIsAddedForAuditingOperationsWhichReturnAPoco() + { + this.AssertServiceIsAvailableFromServiceProvider(); + } + + [Then("an audit log sink service is available for console logging")] + public void ThenAnAuditLogSinkServiceIsAddedForConsoleLogging() + { + this.AssertServiceIsAvailableFromServiceProvider(); + } + + [Then("auditing is enabled")] + public void ThenAuditingIsEnabled() + { + IAuditContext auditContext = this.scenarioContext.Get().GetRequiredService(); + Assert.IsTrue(auditContext.IsAuditingEnabled); + } + + private void AssertServiceIsAvailableFromServiceProvider() + { + TService service = this.scenarioContext.Get().GetService(); + + Assert.IsNotNull(service); + } + + private void AssertServiceIsAvailableFromServiceProvider() + { + TService[] services = this.scenarioContext.Get().GetServices().ToArray(); + + Assert.IsNotEmpty(services); + Assert.IsTrue(services.Any(x => typeof(TExpectedConcreteType) == x.GetType())); + } + + private void AssertServiceIsASingleton() + { + ServiceProvider provider = this.scenarioContext.Get(); + + using IServiceScope scope1 = provider.CreateScope(); + using IServiceScope scope2 = provider.CreateScope(); + + TService serviceInScope1 = scope1.ServiceProvider.GetRequiredService(); + TService serviceInScope2 = scope2.ServiceProvider.GetRequiredService(); + + Assert.AreSame(serviceInScope1, serviceInScope2); + } + } +} diff --git a/Solutions/Menes.Specs/Steps/OpenApiOperationInvokerSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiOperationInvokerSteps.cs index 00f02aeff..f2fa6af8a 100644 --- a/Solutions/Menes.Specs/Steps/OpenApiOperationInvokerSteps.cs +++ b/Solutions/Menes.Specs/Steps/OpenApiOperationInvokerSteps.cs @@ -232,14 +232,14 @@ public void ThenTheInvokerShouldPassTheResultFromTheExceptionMapperToTheResultBu [Then("the invoker should return the result from the result builder")] public async Task ThenTheInvokerShouldReturnTheResultFromTheResultBuilder() { - object result = await this.invokerResultTask; + object result = await this.invokerResultTask.ConfigureAwait(false); Assert.AreSame(this.resultBuilderResult, result); } [Then("invoker should return a (.*) error result")] public async Task ThenInvokerShouldReturnAErrorResult(int statusCode) { - object result = await this.invokerResultTask; + object result = await this.invokerResultTask.ConfigureAwait(false); Assert.AreSame(this.resultBuilderErrorResult, result); this.resultBuilder.Verify(m => m.BuildErrorResult(statusCode)); } diff --git a/Solutions/Menes.Specs/Steps/ShortCircuitingAccessControlPolicyAdapterSteps.cs b/Solutions/Menes.Specs/Steps/ShortCircuitingAccessControlPolicyAdapterSteps.cs index 12273cb23..278ff61f3 100644 --- a/Solutions/Menes.Specs/Steps/ShortCircuitingAccessControlPolicyAdapterSteps.cs +++ b/Solutions/Menes.Specs/Steps/ShortCircuitingAccessControlPolicyAdapterSteps.cs @@ -176,7 +176,7 @@ public void ThenTheOtherPoliciesShouldReceiveAPathOf(string path) { foreach ((_, CompletionSourceWithArgs> completion) in this.otherPolicies) { - Assert.AreEqual(path, completion.Arguments[0].Requests.First().Path); + Assert.AreEqual(path, completion.Arguments[0].Requests[0].Path); } } @@ -185,7 +185,7 @@ public void ThenTheOtherPoliciesShouldReceiveAnOperationIdOf(string operationId) { foreach ((_, CompletionSourceWithArgs> completion) in this.otherPolicies) { - Assert.AreEqual(operationId, completion.Arguments[0].Requests.First().OperationId); + Assert.AreEqual(operationId, completion.Arguments[0].Requests[0].OperationId); } } @@ -194,7 +194,7 @@ public void ThenTheOtherPoliciesShouldReceiveAnHttpMethodOf(string method) { foreach ((_, CompletionSourceWithArgs> completion) in this.otherPolicies) { - Assert.AreEqual(method, completion.Arguments[0].Requests.First().Method); + Assert.AreEqual(method, completion.Arguments[0].Requests[0].Method); } } @@ -230,7 +230,7 @@ public async Task ThenTheAdapterResultShouldHaveAnExplanationOfAsync(string expl Assert.AreEqual(explanation, result.Values.First().Explanation); } - [When(@"the first policy denies access")] + [When("the first policy denies access")] public void WhenTheFirstPolicyDeniesAccess() { var result = new Dictionary(); @@ -239,7 +239,7 @@ public void WhenTheFirstPolicyDeniesAccess() this.firstPolicyCompletion.SupplyResult(result); } - [Then(@"the adapter result type should be '(.*)'")] + [Then("the adapter result type should be '(.*)'")] public async Task ThenTheAdapterResultTypeShouldBeAsync(string resultTypeString) { AccessControlPolicyResultType resultType = Enum.Parse(resultTypeString); @@ -247,7 +247,7 @@ public async Task ThenTheAdapterResultTypeShouldBeAsync(string resultTypeString) Assert.AreEqual(resultType, result.Values.First().ResultType); } - [When(@"the other policy (.*) denies access with result '(.*)' and explanation '(.*)'")] + [When("the other policy (.*) denies access with result '(.*)' and explanation '(.*)'")] public void WhenTheOtherPolicyDeniesAccessWithResultAndExplanation(int policyIndex, string resultTypeString, string explanation) { AccessControlPolicyResultType resultType = Enum.Parse(resultTypeString); @@ -263,7 +263,6 @@ private class ShouldAllowArgs public AccessCheckOperationDescriptor[] Requests { get; set; } public IOpenApiContext Context { get; set; } - } } } diff --git a/Solutions/Menes.Specs/Steps/TestClasses/MappingContext.cs b/Solutions/Menes.Specs/Steps/TestClasses/MappingContext.cs new file mode 100644 index 000000000..183b27a16 --- /dev/null +++ b/Solutions/Menes.Specs/Steps/TestClasses/MappingContext.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Endjin. All rights reserved. +// + +namespace Menes.Specs.Steps.TestClasses +{ + public class MappingContext + { + public string ContextProperty1 { get; set; } + + public int ContextProperty2 { get; set; } + } +} diff --git a/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapper.cs b/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapper.cs new file mode 100644 index 000000000..d97b11da5 --- /dev/null +++ b/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapper.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Endjin. All rights reserved. +// + +namespace Menes.Specs.Steps.TestClasses +{ + using Menes.Hal; + + public class PetHalDocumentMapper : IHalDocumentMapper + { + public bool LinkMapConfigured { get; private set; } + + public void ConfigureLinkMap(IOpenApiLinkOperationMap links) + { + this.LinkMapConfigured = true; + } + + public HalDocument Map(Pet resource) + { + return null; + } + } +} diff --git a/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapperWithContext.cs b/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapperWithContext.cs new file mode 100644 index 000000000..db2f04aba --- /dev/null +++ b/Solutions/Menes.Specs/Steps/TestClasses/PetHalDocumentMapperWithContext.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Endjin. All rights reserved. +// + +namespace Menes.Specs.Steps.TestClasses +{ + using Menes.Hal; + + public class PetHalDocumentMapperWithContext : IHalDocumentMapper + { + public bool LinkMapConfigured { get; private set; } + + public void ConfigureLinkMap(IOpenApiLinkOperationMap links) + { + this.LinkMapConfigured = true; + } + + public HalDocument Map(Pet resource, MappingContext context) + { + return null; + } + } +}