Skip to content

Commit 7b7cea4

Browse files
Add recommended rules
Add new `OpenApiRecommendedRules` class and adds a new `GetOperationShouldNotHaveRequestBody` rule. Resolves #2454.
1 parent 1783904 commit 7b7cea4

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Net.Http;
5+
6+
namespace Microsoft.OpenApi
7+
{
8+
/// <summary>
9+
/// Additional recommended validation rules for OpenAPI.
10+
/// </summary>
11+
public static class OpenApiRecommendedRules
12+
{
13+
/// <summary>
14+
/// A relative path to an individual endpoint. The field name MUST begin with a slash.
15+
/// </summary>
16+
public static ValidationRule<OpenApiPaths> GetOperationShouldNotHaveRequestBody =>
17+
new(nameof(GetOperationShouldNotHaveRequestBody),
18+
(context, item) =>
19+
{
20+
foreach (var path in item)
21+
{
22+
if (path.Value.Operations is not { Count: > 0 } operations)
23+
{
24+
continue;
25+
}
26+
27+
context.Enter(path.Key);
28+
29+
foreach (var operation in operations)
30+
{
31+
if (!operation.Key.Equals(HttpMethod.Get))
32+
{
33+
continue;
34+
}
35+
36+
if (operation.Value.RequestBody != null)
37+
{
38+
context.Enter(operation.Key.Method.ToLowerInvariant());
39+
context.Enter("requestBody");
40+
41+
context.CreateWarning(
42+
nameof(GetOperationShouldNotHaveRequestBody),
43+
"GET operations should not have a request body.");
44+
45+
context.Exit();
46+
context.Exit();
47+
}
48+
}
49+
50+
context.Exit();
51+
}
52+
});
53+
}
54+
}

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,10 @@ namespace Microsoft.OpenApi
10311031
public OpenApiReaderException(string message, Microsoft.OpenApi.Reader.ParsingContext context) { }
10321032
public OpenApiReaderException(string message, System.Exception innerException) { }
10331033
}
1034+
public static class OpenApiRecommendedRules
1035+
{
1036+
public static Microsoft.OpenApi.ValidationRule<Microsoft.OpenApi.OpenApiPaths> GetOperationShouldNotHaveRequestBody { get; }
1037+
}
10341038
public class OpenApiReferenceError : Microsoft.OpenApi.OpenApiError
10351039
{
10361040
public readonly Microsoft.OpenApi.BaseOpenApiReference? Reference;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using Xunit;
8+
9+
namespace Microsoft.OpenApi.Validations.Tests;
10+
11+
public static class OpenApiRecommendedRulesTests
12+
{
13+
[Fact]
14+
public static void GetOperationWithoutRequestBodyIsValid()
15+
{
16+
// Arrange
17+
var document = new OpenApiDocument
18+
{
19+
Components = new OpenApiComponents(),
20+
Info = new OpenApiInfo
21+
{
22+
Title = "People Document",
23+
Version = "1.0.0"
24+
},
25+
Paths = [],
26+
Workspace = new()
27+
};
28+
29+
document.AddComponent("Person", new OpenApiSchema
30+
{
31+
Type = JsonSchemaType.Object,
32+
Properties = new Dictionary<string, IOpenApiSchema>()
33+
{
34+
["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
35+
["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
36+
}
37+
});
38+
39+
document.Paths.Add("people", new OpenApiPathItem
40+
{
41+
Operations = new Dictionary<HttpMethod, OpenApiOperation>()
42+
{
43+
[HttpMethod.Get] = new OpenApiOperation
44+
{
45+
RequestBody = null,
46+
Responses = new()
47+
{
48+
["200"] = new OpenApiResponse
49+
{
50+
Description = "OK",
51+
Content = new Dictionary<string, OpenApiMediaType>()
52+
{
53+
["application/json"] = new OpenApiMediaType
54+
{
55+
Schema = new OpenApiSchemaReference("Person", document),
56+
}
57+
}
58+
}
59+
}
60+
},
61+
[HttpMethod.Post] = new OpenApiOperation
62+
{
63+
RequestBody = new OpenApiRequestBody
64+
{
65+
Content = new Dictionary<string, OpenApiMediaType>()
66+
{
67+
["application/json"] = new OpenApiMediaType
68+
{
69+
Schema = new OpenApiSchemaReference("Person", document),
70+
}
71+
}
72+
},
73+
Responses = new()
74+
{
75+
["200"] = new OpenApiResponse
76+
{
77+
Description = "OK",
78+
Content = new Dictionary<string, OpenApiMediaType>()
79+
{
80+
["application/json"] = new OpenApiMediaType
81+
{
82+
Schema = new OpenApiSchemaReference("Person", document),
83+
}
84+
}
85+
}
86+
}
87+
}
88+
}
89+
});
90+
91+
var ruleSet = new ValidationRuleSet();
92+
ruleSet.Add(typeof(OpenApiPaths), OpenApiRecommendedRules.GetOperationShouldNotHaveRequestBody);
93+
94+
// Act
95+
96+
var errors = document.Validate(ruleSet);
97+
var result = !errors.Any();
98+
99+
// Assert
100+
Assert.True(result);
101+
Assert.NotNull(errors);
102+
Assert.Empty(errors);
103+
}
104+
105+
[Fact]
106+
public static void GetOperationWithRequestBodyIsInvalid()
107+
{
108+
// Arrange
109+
var document = new OpenApiDocument
110+
{
111+
Components = new OpenApiComponents(),
112+
Info = new OpenApiInfo
113+
{
114+
Title = "People Document",
115+
Version = "1.0.0"
116+
},
117+
Paths = [],
118+
Workspace = new()
119+
};
120+
121+
document.AddComponent("Person", new OpenApiSchema
122+
{
123+
Type = JsonSchemaType.Object,
124+
Properties = new Dictionary<string, IOpenApiSchema>()
125+
{
126+
["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
127+
["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
128+
}
129+
});
130+
131+
document.Paths.Add("people", new OpenApiPathItem
132+
{
133+
Operations = new Dictionary<HttpMethod, OpenApiOperation>()
134+
{
135+
[HttpMethod.Get] = new OpenApiOperation
136+
{
137+
RequestBody = new OpenApiRequestBody
138+
{
139+
Content = new Dictionary<string, OpenApiMediaType>()
140+
{
141+
["application/json"] = new OpenApiMediaType
142+
{
143+
Schema = new OpenApiSchemaReference("Person", document),
144+
}
145+
}
146+
},
147+
Responses = new()
148+
{
149+
["200"] = new OpenApiResponse
150+
{
151+
Description = "OK",
152+
Content = new Dictionary<string, OpenApiMediaType>()
153+
{
154+
["application/json"] = new OpenApiMediaType
155+
{
156+
Schema = new OpenApiSchemaReference("Person", document),
157+
}
158+
}
159+
}
160+
}
161+
},
162+
[HttpMethod.Post] = new OpenApiOperation
163+
{
164+
RequestBody = new OpenApiRequestBody
165+
{
166+
Content = new Dictionary<string, OpenApiMediaType>()
167+
{
168+
["application/json"] = new OpenApiMediaType
169+
{
170+
Schema = new OpenApiSchemaReference("Person", document),
171+
}
172+
}
173+
},
174+
Responses = new()
175+
{
176+
["200"] = new OpenApiResponse
177+
{
178+
Description = "OK",
179+
Content = new Dictionary<string, OpenApiMediaType>()
180+
{
181+
["application/json"] = new OpenApiMediaType
182+
{
183+
Schema = new OpenApiSchemaReference("Person", document),
184+
}
185+
}
186+
}
187+
}
188+
}
189+
}
190+
});
191+
192+
var ruleSet = new ValidationRuleSet();
193+
ruleSet.Add(typeof(OpenApiPaths), OpenApiRecommendedRules.GetOperationShouldNotHaveRequestBody);
194+
195+
// Act
196+
197+
var errors = document.Validate(ruleSet);
198+
var result = !errors.Any();
199+
200+
// Assert
201+
Assert.False(result);
202+
Assert.NotNull(errors);
203+
var error = Assert.Single(errors);
204+
Assert.Equal("GET operations should not have a request body.", error.Message);
205+
Assert.Equal("#/paths/people/get/requestbody", error.Pointer);
206+
}
207+
}

0 commit comments

Comments
 (0)