diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs index d1ad41781..7f1a8ec04 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs @@ -96,6 +96,22 @@ public static class OpenApiParameterRules context.Exit(); }); + /// + /// Validate that a path parameter should always appear in the path + /// + public static ValidationRule PathParameterShouldBeInThePath => + new ValidationRule( + (context, parameter) => + { + if (parameter.In == ParameterLocation.Path && !context.PathString.Contains("{" + parameter.Name + "}")) + { + context.Enter("in"); + context.CreateError( + nameof(PathParameterShouldBeInThePath), + $"Declared path parameter \"{parameter.Name}\" needs to be defined as a path parameter at either the path or operation level"); + context.Exit(); + } + }); // add more rule. } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 9e7af0f05..d5a89e586 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1168,6 +1168,7 @@ namespace Microsoft.OpenApi.Validations.Rules { public static Microsoft.OpenApi.Validations.ValidationRule ParameterMismatchedDataType { get; } public static Microsoft.OpenApi.Validations.ValidationRule ParameterRequiredFields { get; } + public static Microsoft.OpenApi.Validations.ValidationRule PathParameterShouldBeInThePath { get; } public static Microsoft.OpenApi.Validations.ValidationRule RequiredMustBeTrueWhenInIsPath { get; } } [Microsoft.OpenApi.Validations.Rules.OpenApiRule] diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs index 667286f0a..41a9a6ab0 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs @@ -48,8 +48,11 @@ public void ValidateRequiredIsTrueWhenInIsPathInParameter() }; // Act - var errors = parameter.Validate(ValidationRuleSet.GetDefaultRuleSet()); - + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("{name}"); + var walker = new OpenApiWalker(validator); + walker.Walk(parameter); + var errors = validator.Errors; // Assert errors.Should().NotBeEmpty(); errors.Select(e => e.Message).Should().BeEquivalentTo(new[] @@ -77,6 +80,7 @@ public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("{parameter1}"); var walker = new OpenApiWalker(validator); walker.Walk(parameter); @@ -91,7 +95,7 @@ public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() }); errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] { - "#/example", + "#/{parameter1}/example", }); } @@ -150,6 +154,7 @@ public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("{parameter1}"); var walker = new OpenApiWalker(validator); walker.Walk(parameter); @@ -168,10 +173,83 @@ public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() { // #enum/0 is not an error since the spec allows // representing an object using a string. - "#/examples/example1/value/y", - "#/examples/example1/value/z", - "#/examples/example2/value" + "#/{parameter1}/examples/example1/value/y", + "#/{parameter1}/examples/example1/value/z", + "#/{parameter1}/examples/example2/value" }); } + + [Fact] + public void PathParameterNotInThePathShouldReturnAnError() + { + // Arrange + IEnumerable errors; + + var parameter = new OpenApiParameter() + { + Name = "parameter1", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = "string", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + + var walker = new OpenApiWalker(validator); + walker.Walk(parameter); + + errors = validator.Errors; + bool result = errors.Any(); + + // Assert + result.Should().BeTrue(); + errors.OfType().Select(e => e.RuleName).Should().BeEquivalentTo(new[] + { + "PathParameterShouldBeInThePath" + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/in" + }); + } + + [Fact] + public void PathParameterInThePastShouldBeOk() + { + // Arrange + IEnumerable errors; + + var parameter = new OpenApiParameter() + { + Name = "parameter1", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = "string", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("paths"); + validator.Enter("/{parameter1}"); + validator.Enter("get"); + validator.Enter("parameters"); + validator.Enter("1"); + + var walker = new OpenApiWalker(validator); + walker.Walk(parameter); + + errors = validator.Errors; + bool result = errors.Any(); + + // Assert + result.Should().BeFalse(); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index f300761b4..af259cf1b 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -43,7 +43,7 @@ public void DefaultRuleSetPropertyReturnsTheCorrectRules() Assert.NotEmpty(rules); // Update the number if you add new default rule(s). - Assert.Equal(20, rules.Count); + Assert.Equal(21, rules.Count); } } }