Skip to content

Commit

Permalink
Further changes to StringConverter to ensure special characters are p…
Browse files Browse the repository at this point in the history
…roperly escaped (#331)

* Add spec to demonstrate etag problem. Change StringConverter to use JsonConvert.SerializeObject to ensure correct escaping.

* Added specs for string output parsing.
  • Loading branch information
jongeorge1 authored Mar 7, 2023
1 parent de9085b commit 4abff75
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 18 deletions.
22 changes: 9 additions & 13 deletions Solutions/Menes.Abstractions/Menes/Converters/StringConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ namespace Menes.Converters
{
using Menes.Validation;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

/// <summary>
/// An OpenAPI converter for strings.
/// </summary>
public class StringConverter : IOpenApiConverter
{
private readonly OpenApiSchemaValidator validator;
private readonly IOpenApiConfiguration configuration;

/// <summary>
/// Initializes a new instance of the <see cref="StringConverter"/> class.
/// </summary>
/// <param name="validator">The <see cref="OpenApiSchemaValidator"/>.</param>
public StringConverter(OpenApiSchemaValidator validator)
/// <param name="configuration">The OpenAPI host configuration.</param>
public StringConverter(OpenApiSchemaValidator validator, IOpenApiConfiguration configuration)
{
this.validator = validator;
this.configuration = configuration;
}

/// <inheritdoc/>
Expand All @@ -40,20 +45,11 @@ public object ConvertFrom(string content, OpenApiSchema schema)
/// <inheritdoc/>
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;
}
}
}
1 change: 1 addition & 0 deletions Solutions/Menes.PetStore.Specs/Features/GetPetById.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
11 changes: 7 additions & 4 deletions Solutions/Menes.PetStore.Specs/Steps/Steps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,14 @@ public void ThenTheResponseShouldContainTheHeader(string headerName)
Assert.IsTrue(response.Headers.TryGetValues(headerName, out IEnumerable<string>? 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")]
Expand Down
4 changes: 4 additions & 0 deletions Solutions/Menes.PetStore/Menes/PetStore/PetStore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion Solutions/Menes.PetStore/Menes/PetStore/PetStoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,13 @@ private async Task<OpenApiResult> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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 '<Value>' of type 'System.String'
Then the response body should be '<ExpectedResult>'

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 '<Value>' of type 'System.String'
Then the response should container a header called 'X-Test' with value '<ExpectedResult>'

Examples:
| Value | ExpectedResult |
| Foo | Foo |
| /1234/abc | /1234/abc |
| | |
| "Foo" | "Foo" |
81 changes: 81 additions & 0 deletions Solutions/Menes.Specs/Steps/OpenApiParameterParsingSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,6 +38,7 @@ public class OpenApiParameterParsingSteps
private IDictionary<string, object>? parameters;
private Exception? exception;
private string? responseBody;
private HttpResponse? response;

public OpenApiParameterParsingSteps(ScenarioContext scenarioContext)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<IResponseOutputBuilder<IHttpResponseResult>> builders = ContainerBindings.GetServiceProvider(this.scenarioContext).
GetServices<IResponseOutputBuilder<IHttpResponseResult>>();

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<IHttpResponseResult> 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 '([^']*)'")]
Expand All @@ -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)
{
Expand Down

0 comments on commit 4abff75

Please sign in to comment.