diff --git a/Solutions/Menes.Abstractions/Menes/Converters/StringConverter.cs b/Solutions/Menes.Abstractions/Menes/Converters/StringConverter.cs index e37c0621..b3cec67f 100644 --- a/Solutions/Menes.Abstractions/Menes/Converters/StringConverter.cs +++ b/Solutions/Menes.Abstractions/Menes/Converters/StringConverter.cs @@ -6,6 +6,8 @@ namespace Menes.Converters { using Menes.Validation; using Microsoft.OpenApi.Models; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; /// /// An OpenAPI converter for strings. @@ -13,14 +15,17 @@ namespace Menes.Converters public class StringConverter : IOpenApiConverter { private readonly OpenApiSchemaValidator validator; + private readonly IOpenApiConfiguration configuration; /// /// Initializes a new instance of the class. /// /// The . - public StringConverter(OpenApiSchemaValidator validator) + /// The OpenAPI host configuration. + public StringConverter(OpenApiSchemaValidator validator, IOpenApiConfiguration configuration) { this.validator = validator; + this.configuration = configuration; } /// @@ -40,20 +45,11 @@ public object ConvertFrom(string content, OpenApiSchema schema) /// public string ConvertTo(object instance, OpenApiSchema schema) { - string result; + string result = JsonConvert.SerializeObject(instance, this.configuration.Formatting, this.configuration.SerializerSettings); - if (instance is string stringValue) - { - result = stringValue; - } - else - { - result = instance.ToString()!; - } + this.validator.ValidateAndThrow(JToken.Parse(result), schema); - this.validator.ValidateAndThrow(result, schema); - - return '"' + result + '"'; + return result; } } } \ No newline at end of file diff --git a/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature b/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature index ef82307f..4b09ab3a 100644 --- a/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature +++ b/Solutions/Menes.PetStore.Specs/Features/GetPetById.feature @@ -9,6 +9,7 @@ Feature: Get Pet By Id Scenario: Request a pet When I request the pet with Id 1 Then the response status code should be 'OK' + And the response should contain the 'ETag' header And the response object should have a property called '_links.self' And the response object should have a property called 'id' And the response object should have a property called 'name' diff --git a/Solutions/Menes.PetStore.Specs/Steps/Steps.cs b/Solutions/Menes.PetStore.Specs/Steps/Steps.cs index f25298af..fa4fcc02 100644 --- a/Solutions/Menes.PetStore.Specs/Steps/Steps.cs +++ b/Solutions/Menes.PetStore.Specs/Steps/Steps.cs @@ -111,11 +111,14 @@ public void ThenTheResponseShouldContainTheHeader(string headerName) Assert.IsTrue(response.Headers.TryGetValues(headerName, out IEnumerable? values)); Assert.IsNotEmpty(values!); - // Ensure the supplied value is a valid URI - string rawLocation = values!.First(); - string decodedLocation = Uri.UnescapeDataString(rawLocation); + if (headerName == "Location") + { + // Ensure the supplied value is a valid URI + string rawLocation = values!.First(); + string decodedLocation = Uri.UnescapeDataString(rawLocation); - Assert.IsTrue(Uri.IsWellFormedUriString(decodedLocation, UriKind.RelativeOrAbsolute)); + Assert.IsTrue(Uri.IsWellFormedUriString(decodedLocation, UriKind.RelativeOrAbsolute)); + } } [Then("the response should not contain the '(.*)' header")] diff --git a/Solutions/Menes.PetStore/Menes/PetStore/PetStore.yaml b/Solutions/Menes.PetStore/Menes/PetStore/PetStore.yaml index a576ae75..608ce140 100644 --- a/Solutions/Menes.PetStore/Menes/PetStore/PetStore.yaml +++ b/Solutions/Menes.PetStore/Menes/PetStore/PetStore.yaml @@ -189,6 +189,10 @@ paths: application/hal+json: schema: $ref: "#/components/schemas/Pet" + headers: + ETag: + schema: + type: string '404': description: No pet with that ID default: diff --git a/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs index 5610943c..8aa6d6f3 100644 --- a/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs +++ b/Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs @@ -342,9 +342,13 @@ private async Task MapAndReturnPetAsync(PetResource result) HalDocument response = await this.petMapper.MapAsync(result).ConfigureAwait(false); - return this + OpenApiResult openApiResponse = this .OkResult(response, "application/hal+json") .WithAuditData(("id", result.Id)); + + openApiResponse.Results.Add("ETag", $"\"{result.GetHashCode()}\""); + + return openApiResponse; } } } \ No newline at end of file diff --git a/Solutions/Menes.Specs/Features/JsonTypeConversion/StringOutputParsing.feature b/Solutions/Menes.Specs/Features/JsonTypeConversion/StringOutputParsing.feature new file mode 100644 index 00000000..c2f88649 --- /dev/null +++ b/Solutions/Menes.Specs/Features/JsonTypeConversion/StringOutputParsing.feature @@ -0,0 +1,29 @@ +@perScenarioContainer + +Feature: String Output Parsing + In order to implement a web API + As a developer + I want to be able to specify string values as or in response bodies within the OpenAPI specification and have corresponding response bodies deserialized and validated + +Scenario Outline: Body with valid values + Given I have constructed the OpenAPI specification with a response body of type 'string', and format '' + When I try to build a response body from the value '' of type 'System.String' + Then the response body should be '' + + Examples: + | Value | ExpectedResult | + | Foo | "Foo" | + | /1234/abc | "/1234/abc" | + | | "" | + +Scenario Outline: Header with valid values + Given I have constructed the OpenAPI specification with a response header called 'X-Test' of type 'string', and format '' + When I try to build a response with a header called 'X-Test' from the value '' of type 'System.String' + Then the response should container a header called 'X-Test' with value '' + + Examples: + | Value | ExpectedResult | + | Foo | Foo | + | /1234/abc | /1234/abc | + | | | + | "Foo" | "Foo" | \ No newline at end of file diff --git a/Solutions/Menes.Specs/Steps/OpenApiParameterParsingSteps.cs b/Solutions/Menes.Specs/Steps/OpenApiParameterParsingSteps.cs index 8775fc19..31ecf386 100644 --- a/Solutions/Menes.Specs/Steps/OpenApiParameterParsingSteps.cs +++ b/Solutions/Menes.Specs/Steps/OpenApiParameterParsingSteps.cs @@ -20,6 +20,7 @@ namespace Menes.Specs.Steps using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; @@ -37,6 +38,7 @@ public class OpenApiParameterParsingSteps private IDictionary? parameters; private Exception? exception; private string? responseBody; + private HttpResponse? response; public OpenApiParameterParsingSteps(ScenarioContext scenarioContext) { @@ -122,6 +124,44 @@ public void GivenIHaveConstructedTheOpenAPISpecificationWithAResponseBodyOfTypeA this.InitializeDocumentProviderAndPathMatcher(openApiSpec); } + [Given("I have constructed the OpenAPI specification with a response header called '([^']*)' of type '([^']*)', and format '([^']*)'")] + public void GivenIHaveConstructedTheOpenAPISpecificationWithAResponseHeaderCalledOfTypeAndFormat(string headerName, string headerType, string headerFormat) + { + string openApiSpec = $$""" + { + "openapi": "3.0.1", + "info": { + "title": "Swagger Petstore (Simple)", + "version": "1.0.0" + }, + "servers": [ { "url": "http://petstore.swagger.io/api" } ], + "paths": { + "/pets": { + "get": { + "summary": "Get a pet", + "operationId": "getPet", + "responses": { + "201": { + "description": "A pet", + "headers": { + "{{headerName}}": { + "schema": { + "type": "{{headerType}}", + "format": "{{headerFormat}}" + } + } + } + } + } + } + } + } + } + """; + + this.InitializeDocumentProviderAndPathMatcher(openApiSpec); + } + [Given("I have constructed the OpenAPI specification with a response body of type object, containing properties in the structure '([^']*)'")] public void GivenIHaveConstructedTheOpenAPISpecificationWithAResponseBodyOfTypeObjectContainingPropertiesInTheStructure( string objectProperties) @@ -1038,6 +1078,40 @@ public void WhenITryToBuildAResponseBodyFromTheValueOfTypeSystem_Boolean( context.Response.Body.Position = 0; using StreamReader sr = new(context.Response.Body); this.responseBody = sr.ReadToEnd(); + this.response = context.Response; + } + + [When("I try to build a response with a header called '([^']*)' from the value '([^']*)' of type '([^']*)'")] + public void WhenITryToBuildAResponseWithAHeaderCalledFromTheValueOfType(string headerName, string valueAsString, string valueType) + { + object value = GetResultFromStringAndType(valueAsString, valueType); + + IEnumerable> builders = ContainerBindings.GetServiceProvider(this.scenarioContext). + GetServices>(); + + this.Matcher.FindOperationPathTemplate("/pets", "GET", out OpenApiOperationPathTemplate? operationPathTemplate); + OpenApiOperation operation = operationPathTemplate!.Operation; + + OpenApiResult openApiResult = new() { StatusCode = 201 }; + openApiResult.Results.Add(headerName, value); + + IHttpResponseResult? result = null; + foreach (IResponseOutputBuilder builder in builders) + { + if (builder.CanBuildOutput(openApiResult, operation)) + { + result = builder.BuildOutput(openApiResult, operation); + break; + } + } + + var context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + result!.ExecuteResultAsync(context.Response); + context.Response.Body.Position = 0; + using StreamReader sr = new(context.Response.Body); + this.responseBody = sr.ReadToEnd(); + this.response = context.Response; } [Then("the response body should be '([^']*)'")] @@ -1046,6 +1120,13 @@ public void ThenTheResponseBodyShouldBeTrue(string expectedBody) Assert.AreEqual(expectedBody, this.responseBody); } + [Then("the response should container a header called '([^']*)' with value '([^']*)'")] + public void ThenTheResponseShouldContainerAHeaderCalledWithValue(string headerName, string expectedValue) + { + Assert.IsTrue(this.response!.Headers.TryGetValue(headerName, out StringValues value)); + Assert.AreEqual(expectedValue, value[0]); + } + [Then("the parameter (.*?) should be (.*?) of type (.*)")] public void ThenTheParameterShouldBe(string parameterName, string expectedResultAsString, string expectedType) {