diff --git a/src/OpenApiValidate/Helpers/OpenApiExtensions.cs b/src/OpenApiValidate/Helpers/OpenApiExtensions.cs index 68058d3..f7a436c 100644 --- a/src/OpenApiValidate/Helpers/OpenApiExtensions.cs +++ b/src/OpenApiValidate/Helpers/OpenApiExtensions.cs @@ -67,22 +67,45 @@ out IOpenApiPathItem path { var requestPathString = new PathString(requestPath); + IOpenApiPathItem? matchingTemplatePathItem = null; + foreach (var kvp in paths) { var specPath = new PathString(kvp.Key); - if (IsPathMatch(specPath, requestPathString)) + + if (!IsPathMatch(specPath, requestPathString, out var isTemplatePath)) + { + continue; + } + + if (isTemplatePath) { - path = kvp.Value; - return true; + matchingTemplatePathItem = kvp.Value; + continue; } + + path = kvp.Value; + return true; + } + + if (matchingTemplatePathItem is not null) + { + path = matchingTemplatePathItem; + return true; } path = null!; return false; } - private static bool IsPathMatch(PathString specPath, PathString requestPath) + private static bool IsPathMatch( + PathString specPath, + PathString requestPath, + out bool isTemplatePath + ) { + isTemplatePath = false; + if (specPath.Segments.Length != requestPath.Segments.Length) { return false; @@ -95,6 +118,7 @@ private static bool IsPathMatch(PathString specPath, PathString requestPath) if (segment.StartsWith('{') && segment.EndsWith('}')) { // Is template parameter, so skip checking + isTemplatePath = true; continue; } @@ -105,6 +129,7 @@ private static bool IsPathMatch(PathString specPath, PathString requestPath) ) ) { + isTemplatePath = false; return false; } } diff --git a/test/OpenApiValidate.Tests/ResponseValidatorTests.cs b/test/OpenApiValidate.Tests/ResponseValidatorTests.cs index 52bb911..8cc1ff0 100644 --- a/test/OpenApiValidate.Tests/ResponseValidatorTests.cs +++ b/test/OpenApiValidate.Tests/ResponseValidatorTests.cs @@ -211,6 +211,60 @@ public async Task Petstore_DeletePet() validateAction.ShouldNotThrow(); } + [Fact] + public async Task LiteralAndTemplatedPath_GetUser() + { + var openApiDocument = await GetDocument("TestData/LiteralAndTemplatedPath.yaml"); + + var validator = new OpenApiValidator(openApiDocument); + + var request = new Request("GET", new Uri("http://api.example.com/v1/user/abcdef")); + var response = new Response(200, "application/json", """{"id":"abcdef"}"""); + + var validateAction = () => + { + validator.Validate(request, response); + }; + + validateAction.ShouldNotThrow(); + } + + [Fact] + public async Task LiteralAndTemplatedPath_GetMeUser() + { + var openApiDocument = await GetDocument("TestData/LiteralAndTemplatedPath.yaml"); + + var validator = new OpenApiValidator(openApiDocument); + + var request = new Request("GET", new Uri("http://api.example.com/v1/user/me")); + var response = new Response(200, "application/json", """{"id":"abcdef"}"""); + + var validateAction = () => + { + validator.Validate(request, response); + }; + + validateAction.ShouldNotThrow(); + } + + [Fact] + public async Task LiteralAndTemplatedPath_DeleteMeUser() + { + var openApiDocument = await GetDocument("TestData/LiteralAndTemplatedPath.yaml"); + + var validator = new OpenApiValidator(openApiDocument); + + var request = new Request("DELETE", new Uri("http://api.example.com/v1/user/me")); + var response = new Response(204); + + var validateAction = () => + { + validator.Validate(request, response); + }; + + validateAction.ShouldNotThrow(); + } + private static async Task GetDocument(string filename) { var settings = new OpenApiReaderSettings(); diff --git a/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml new file mode 100644 index 0000000..114b025 --- /dev/null +++ b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.0 + +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +paths: + /user/{UserId}: + get: + summary: Returns a single user + parameters: + - name: UserId + in: path + required: true + schema: + type: string + responses: + "200": + description: A user object + content: + application/json: + schema: + type: object + properties: + id: + type: string + + /user/me: + get: + summary: Returns my user. + responses: + "200": + description: A user object + content: + application/json: + schema: + type: object + properties: + id: + type: string + delete: + summary: Deletes my user. + responses: + "204": + description: User deleted successfully \ No newline at end of file