Skip to content

Commit b375ed4

Browse files
Validate entire OpenAPI document
Refactor to use `OpenApiVisitorBase` and `OpenApiWalker` to recursively validate the entire OpenAPI document.
1 parent 502f8c7 commit b375ed4

File tree

1 file changed

+68
-122
lines changed

1 file changed

+68
-122
lines changed

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs

Lines changed: 68 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public async Task OpenApiDocumentIsValid(string documentName, OpenApiSpecVersion
6868
Assert.Empty(errors);
6969
}
7070

71+
// The test below can be removed when https://github.com/microsoft/OpenAPI.NET/issues/2453 is implemented
72+
7173
[Theory] // See https://github.com/dotnet/aspnetcore/issues/63090
7274
[MemberData(nameof(OpenApiDocuments))]
7375
public async Task OpenApiDocumentReferencesAreValid(string documentName, OpenApiSpecVersion version)
@@ -79,146 +81,90 @@ public async Task OpenApiDocumentReferencesAreValid(string documentName, OpenApi
7981
var document = result.Document;
8082
var documentNode = JsonNode.Parse(json);
8183

82-
var actual = new List<string>();
83-
84-
// TODO What other parts of the document should also be validated for references to be comprehensive?
85-
// Likely also needs to be recursive to validate all references in schemas, parameters, etc.
86-
if (document.Components is { Schemas.Count: > 0 } components)
84+
var ruleName = "OpenApiDocumentReferencesAreValid";
85+
var rule = new ValidationRule<OpenApiDocument>(ruleName, (context, item) =>
8786
{
88-
foreach (var schema in components.Schemas)
89-
{
90-
if (schema.Value.Properties is { Count: > 0 } properties)
91-
{
92-
foreach (var property in properties)
93-
{
94-
if (property.Value is not OpenApiSchemaReference reference)
95-
{
96-
continue;
97-
}
98-
99-
var id = reference.Reference.ReferenceV3;
100-
101-
if (!IsValidSchemaReference(id, documentNode))
102-
{
103-
actual.Add($"Reference '{id}' on property '{property.Key}' of schema '{schema.Key}' is invalid.");
104-
}
105-
}
106-
}
87+
var visitor = new OpenApiSchemaReferenceVisitor(ruleName, context, documentNode);
10788

108-
if (schema.Value.AllOf is { Count: > 0 } allOf)
109-
{
110-
foreach (var child in allOf)
111-
{
112-
if (child is not OpenApiSchemaReference reference)
113-
{
114-
continue;
115-
}
116-
117-
var id = reference.Reference.ReferenceV3;
118-
119-
if (!IsValidSchemaReference(id, documentNode))
120-
{
121-
actual.Add($"Reference '{id}' for AllOf of schema '{schema.Key}' is invalid.");
122-
}
123-
}
124-
}
89+
var walker = new OpenApiWalker(visitor);
90+
walker.Walk(item);
91+
});
12592

126-
if (schema.Value.AnyOf is { Count: > 0 } anyOf)
127-
{
128-
foreach (var child in anyOf)
129-
{
130-
if (child is not OpenApiSchemaReference reference)
131-
{
132-
continue;
133-
}
134-
135-
var id = reference.Reference.ReferenceV3;
136-
137-
if (!IsValidSchemaReference(id, documentNode))
138-
{
139-
actual.Add($"Reference '{id}' for AnyOf of schema '{schema.Key}' is invalid.");
140-
}
141-
}
142-
}
93+
var ruleSet = new ValidationRuleSet();
94+
ruleSet.Add(typeof(OpenApiDocument), rule);
14395

144-
if (schema.Value.OneOf is { Count: > 0 } oneOf)
145-
{
146-
foreach (var child in oneOf)
147-
{
148-
if (child is not OpenApiSchemaReference reference)
149-
{
150-
continue;
151-
}
152-
153-
var id = reference.Reference.ReferenceV3;
154-
155-
if (!IsValidSchemaReference(id, documentNode))
156-
{
157-
actual.Add($"Reference '{id}' for OneOf of schema '{schema.Key}' is invalid.");
158-
}
159-
}
160-
}
96+
var errors = document.Validate(ruleSet);
16197

162-
if (schema.Value.Discriminator is { Mapping.Count: > 0 } discriminator)
163-
{
164-
foreach (var child in discriminator.Mapping)
165-
{
166-
if (child.Value is not OpenApiSchemaReference reference)
167-
{
168-
continue;
169-
}
170-
171-
var id = reference.Reference.ReferenceV3;
172-
173-
if (!IsValidSchemaReference(id, documentNode))
174-
{
175-
actual.Add($"Reference '{id}' for Discriminator '{child.Key}' of schema '{schema.Key}' is invalid.");
176-
}
177-
}
178-
}
98+
Assert.Empty(errors);
99+
}
100+
101+
private async Task<string> GetOpenApiDocument(string documentName, OpenApiSpecVersion version)
102+
{
103+
var documentService = fixture.Services.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
104+
var scopedServiceProvider = fixture.Services.CreateScope();
105+
var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider);
106+
return await document.SerializeAsJsonAsync(version);
107+
}
108+
109+
private sealed class OpenApiSchemaReferenceVisitor(
110+
string ruleName,
111+
IValidationContext context,
112+
JsonNode document) : OpenApiVisitorBase
113+
{
114+
public override void Visit(IOpenApiReferenceHolder referenceHolder)
115+
{
116+
if (referenceHolder is OpenApiSchemaReference { Reference.IsLocal: true } reference)
117+
{
118+
ValidateSchemaReference(reference);
119+
}
120+
}
121+
122+
public override void Visit(IOpenApiSchema schema)
123+
{
124+
if (schema is OpenApiSchemaReference { Reference.IsLocal: true } reference)
125+
{
126+
ValidateSchemaReference(reference);
179127
}
180128
}
181129

182-
foreach (var path in document.Paths)
130+
private void ValidateSchemaReference(OpenApiSchemaReference reference)
183131
{
184-
foreach (var operation in path.Value.Operations)
132+
var id = reference.Reference.ReferenceV3;
133+
134+
if (id is { Length: > 0 } && !IsValidSchemaReference(id, document))
185135
{
186-
if (operation.Value.Parameters is not { Count: > 0 } parameters)
136+
var isValid = false;
137+
138+
// Sometimes ReferenceV3 is not a JSON valid JSON pointer, but the $ref
139+
// associated with it still points to a valid location in the document.
140+
// In these cases, we need to find it manually to verify that fact before
141+
// generating a warning that the schema reference is indeed invalid.
142+
var parent = Find(PathString, document);
143+
var @ref = parent[OpenApiSchemaKeywords.RefKeyword];
144+
145+
if (@ref is not null && @ref.GetValueKind() is System.Text.Json.JsonValueKind.String &&
146+
@ref.GetValue<string>() is { Length: > 0 } refId)
187147
{
188-
continue;
148+
id = refId;
149+
isValid = IsValidSchemaReference(id, document);
189150
}
190151

191-
foreach (var parameter in parameters)
152+
if (!isValid)
192153
{
193-
if (parameter.Schema is not OpenApiSchemaReference reference)
194-
{
195-
continue;
196-
}
197-
198-
var id = reference.Reference.ReferenceV3;
199-
200-
if (!IsValidSchemaReference(id, documentNode))
201-
{
202-
actual.Add($"Reference '{id}' on parameter '{parameter.Name}' of path '{path.Key}' of operation '{operation.Key}' is invalid.");
203-
}
154+
context.Enter(PathString[2..]); // Trim off the leading "#/" as the context is already at the root
155+
context.CreateWarning(ruleName, $"The schema reference '{id}' does not point to an existing schema.");
156+
context.Exit();
204157
}
205158
}
206-
}
207159

208-
Assert.Empty(actual);
160+
static bool IsValidSchemaReference(string id, JsonNode baseNode)
161+
=> Find(id, baseNode) is not null;
209162

210-
static bool IsValidSchemaReference(string id, JsonNode baseNode)
211-
{
212-
var pointer = new JsonPointer(id.Replace("#/", "/"));
213-
return pointer.Find(baseNode) is not null;
163+
static JsonNode Find(string id, JsonNode baseNode)
164+
{
165+
var pointer = new JsonPointer(id.Replace("#/", "/"));
166+
return pointer.Find(baseNode);
167+
}
214168
}
215169
}
216-
217-
private async Task<string> GetOpenApiDocument(string documentName, OpenApiSpecVersion version)
218-
{
219-
var documentService = fixture.Services.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
220-
var scopedServiceProvider = fixture.Services.CreateScope();
221-
var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider);
222-
return await document.SerializeAsJsonAsync(version);
223-
}
224170
}

0 commit comments

Comments
 (0)