Skip to content

Commit

Permalink
Property validation in JObjects (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmbillie authored Nov 12, 2021
1 parent eee709f commit 1a15a1e
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public override IValidationResult Validate(JsonSchema schema)
{
ICollection<NJsonSchema.Validation.ValidationError> errors = schema.Validate(BodyString);
List<ValidationError> errList = errors.ToValidationErrorList();

errList.AddRange(schema.ValidateUndefinedProperties(BodyString));

List<IValidationErrorFilter> filters = FilterFactory.CreateApplicableFilters(schema, BodyString);
foreach (IValidationErrorFilter filter in filters)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Newtonsoft.Json.Linq;
using NJsonSchema;
using System.Collections.Generic;

namespace HotPotato.OpenApi.Validators
{
internal static class JsonSchemaExtensions
{
public static List<ValidationError> ValidateUndefinedProperties(this JsonSchema schema, string bodyString)
{
JToken bodyToken = JToken.Parse(bodyString);
List<ValidationError> validationErrors = new List<ValidationError>();

if (bodyToken is JObject)
{
JEnumerable<JToken> childTokens = bodyToken.Children();
var schemaProperties = schema?.ActualSchema?.ActualProperties;

if (schemaProperties != null)
{
foreach (JToken childToken in childTokens)
{
if (!schemaProperties.ContainsKey(childToken.Path))
{
validationErrors.Add(new ValidationError($"Property not found in spec: {childToken.Path}", ValidationErrorKind.PropertyNotInSpec, childToken.Path, 0, 0));
}
}
}
}

return validationErrors;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ public enum ValidationErrorKind
/// <summary>
/// A uuid is expected
/// </summary>
UuidExpected
UuidExpected,
/// <summary>
/// Property in response is not documented in the spec
/// </summary>
PropertyNotInSpec
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,17 @@ public IEnumerator<object[]> GetEnumerator()
direction = "inbound",
}
};

yield return new object[] {"specs/cv/", HttpMethod.Options,
HttpStatusCode.BadRequest, "https://api.hyland.com/combined-viewer/combined-view-types/42/search-keyword-types", "application/problem+json", new {
items = new[]{
new {
id = "onetype",
name = "anothertype"
},
new {
id = "anothertype",
name = "anothertype"
}
}
HttpStatusCode.BadRequest, "https://api.hyland.com/combined-viewer/combined-view-types/42/search-keyword-types", "application/problem+json", new {
type = "https://www.example.net/bad-request/",
title = "Bad Request",
status = 400,
detail = "The method 'OPTIONS' is not allowed.",
instance = "https://api.hyland.com/combined-viewer/combined-view-types/{combinedViewTypeId}/search-keyword-types"
}
};

yield return new object[] { "specs/deficiencies/", HttpMethod.Get,
HttpStatusCode.OK, "http://api.docs.hyland.io/deficiencies/deficiencies", "application/json", new {
items = new[] {
Expand All @@ -75,6 +72,7 @@ public IEnumerator<object[]> GetEnumerator()
}
}
};

yield return new object[] { "specs/document/", HttpMethod.Put,
HttpStatusCode.BadRequest, "http://api.docs.hyland.io/document/documents/27/keywords", "application/problem+json", new {
type = "https://example.net/validation_error",
Expand Down Expand Up @@ -111,10 +109,11 @@ public IEnumerator<object[]> GetEnumerator()
HttpStatusCode.OK, "https://api.hyland.com/onbase-workflow/life-cycles/48/", "application/json", new {
id = "string",
name = "string",
smallIconId = "string"
smallImageID = "string"
}
};

//good use case for property testing in a JArray
yield return new object[] { "specs/onbase-workflow/", HttpMethod.Get,
HttpStatusCode.OK, "https://api.hyland.com/onbase-workflow/life-cycles/", "application/json", new {
items = new[]
Expand Down Expand Up @@ -153,19 +152,44 @@ public IEnumerator<object[]> GetEnumerator()
};

yield return new object[] { "specs/ccm/", HttpMethod.Get,
HttpStatusCode.OK, "https://api.hyland.com/sms/messages/41", "application/json", new {
id = "SM4262411b90e5464b98a4f66a49c57a97",
created = "2019-01-04T15:08=09Z",
modified = "2019-01-04T15:08:09Z",
sent = "2019-01-04T15:08:09Z",
accountId = "AC0db966d80e9f1662da09c61287f8bba1",
from = "+5622089048",
to = "+15622089096",
body = "Test",
status = "accepted",
direction = "inbound",
HttpStatusCode.OK, "https://api.hyland.com/sms/messages/41", "application/json", new {
id = "SM4262411b90e5464b98a4f66a49c57a97",
created = "2019-01-04T15:08=09Z",
modified = "2019-01-04T15:08:09Z",
sent = "2019-01-04T15:08:09Z",
accountId = "AC0db966d80e9f1662da09c61287f8bba1",
from = "+5622089048",
to = "+15622089096",
body = "Test",
status = "accepted",
direction = "inbound",
}, ValidationErrorKind.DateTimeExpected, ValidationErrorKind.IntegerExpected
};

//case for property validation: a completely different response was marked as valid because of no required properties
yield return new object[] {"specs/cv/", HttpMethod.Options,
HttpStatusCode.BadRequest, "https://api.hyland.com/combined-viewer/combined-view-types/42/search-keyword-types", "application/problem+json", new {
items = new[] {
new {
id = "onetype",
name = "anothertype"
},
new {
id = "anothertype",
name = "anothertype"
}
}
}, ValidationErrorKind.PropertyNotInSpec, null
};

//case for property validation: smallIconId was changed to smallImageID
yield return new object[] { "specs/onbase-workflow/", HttpMethod.Get,
HttpStatusCode.OK, "https://api.hyland.com/onbase-workflow/life-cycles/48/", "application/json", new {
id = "string",
name = "string",
smallIconId = "string"
}, ValidationErrorKind.PropertyNotInSpec, null
};
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ public async Task BodyValidator_CreatesInvalidResult(string specSubPath, HttpMet

Assert.Equal(Reason.InvalidBody, result.Reasons.ElementAt(0));
Assert.Equal(expectedKind1, result.ValidationErrors[0].Kind);
Assert.Equal(expectedKind2, result.ValidationErrors[1].Kind);
if (result.ValidationErrors.Count > 1)
{
Assert.Equal(expectedKind2, result.ValidationErrors[1].Kind);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ namespace HotPotato.OpenApi.Validators
{
public class JsonBodyValidatorTest
{
private const string AValidBody = "{'foo': '1'}";
private const string AValidBody = "{'foo': 1}";
private const string AValidSchema = @"{'properties':{'foo':{'type':'integer'}}}";

private const string AnInvalidBody = "{'foo': 'abc'}";
private const string AValidSchema = @"{'type': 'integer'}";
private const string ABodyWithAnUnexpectedProperty = "{'bar': 2}";

private const string AValidNullableBody = "{'foo': null}";
//nullable in yaml converts to x-nullable in json
Expand All @@ -17,7 +19,7 @@ public class JsonBodyValidatorTest
[Fact]
public void JsonBodyValidator_ReturnsTrueWithValid()
{
JsonSchema schema = JsonSchema.CreateAnySchema();
JsonSchema schema = JsonSchema.FromJsonAsync(AValidSchema).Result;
JsonBodyValidator subject = new JsonBodyValidator(AValidBody);

IValidationResult result = subject.Validate(schema);
Expand All @@ -38,6 +40,32 @@ public void JsonBodyValidator_ReturnsFalseWithInvalid()
Assert.Equal(ValidationErrorKind.IntegerExpected, result.Errors[0].Kind);
}

[Fact]
public void JsonBodyValidator_ReturnsFalseWithUndocumentedProperty()
{
JsonSchema schema = JsonSchema.FromJsonAsync(AValidSchema).Result;
JsonBodyValidator subject = new JsonBodyValidator(ABodyWithAnUnexpectedProperty);

InvalidResult result = (InvalidResult)subject.Validate(schema);

Assert.False(result.Valid);
Assert.Equal(Reason.InvalidBody, result.Reason);
Assert.Equal(ValidationErrorKind.PropertyNotInSpec, result.Errors[0].Kind);
}

[Fact]
public void JsonBodyValidator_ReturnsFalseWithUndocumentedPropertyAndBlankSchema()
{
JsonSchema schema = new JsonSchema();
JsonBodyValidator subject = new JsonBodyValidator(ABodyWithAnUnexpectedProperty);

InvalidResult result = (InvalidResult)subject.Validate(schema);

Assert.False(result.Valid);
Assert.Equal(Reason.InvalidBody, result.Reason);
Assert.Equal(ValidationErrorKind.PropertyNotInSpec, result.Errors[0].Kind);
}

//the cases for null body and null schema will now be addressed by the ContentValidator

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using HotPotato.OpenApi.Models;
using Moq;
using NJsonSchema;
using System.Collections.Generic;
using Xunit;

namespace HotPotato.OpenApi.Validators
{
public class JsonSchemaExtensionsTest
{
private const string AValidBody = "{'foo': 1}";
private const string AValidSchema = @"{'properties':{'foo':{'type':'integer'}}}";
private const string ABodyWithAnUnexpectedProperty = "{'bar': 2}";

[Fact]
public void ValidateUndefinedProperties_ReturnsEmptyList_WithValidSchema()
{
JsonSchema subject = JsonSchema.FromJsonAsync(AValidSchema).Result;

List<ValidationError> results = subject.ValidateUndefinedProperties(AValidBody);

Assert.Empty(results);
}

[Fact]
public void ValidateUndefinedProperties_ReturnsEmptyList_WithNullActualSchema()
{
JsonSchema nullSchema = null;
Mock<JsonSchema> subject = new Mock<JsonSchema>();
subject.SetupGet(x => x.ActualSchema).Returns(nullSchema);

List<ValidationError> results = subject.Object.ValidateUndefinedProperties(AValidBody);

Assert.Empty(results);
}

[Fact]
public void JsonBodyValidator_ReturnsAListWithCorrectValidationErrorKind_WithInvalidSchema()
{
JsonSchema subject = JsonSchema.FromJsonAsync(AValidSchema).Result;

List<ValidationError> results = subject.ValidateUndefinedProperties(ABodyWithAnUnexpectedProperty);

Assert.Equal(ValidationErrorKind.PropertyNotInSpec, results[0].Kind);
}

[Fact]
public void JsonBodyValidator_ReturnsAListWithCorrectValidationErrorKind_WithBlankSchema()
{
JsonSchema subject = new JsonSchema();

List<ValidationError> results = subject.ValidateUndefinedProperties(ABodyWithAnUnexpectedProperty);

Assert.Equal(ValidationErrorKind.PropertyNotInSpec, results[0].Kind);
}
}
}

0 comments on commit 1a15a1e

Please sign in to comment.