Skip to content

Commit 962e0e4

Browse files
authored
fix: handle deserializing and writing empty security requirements #1426 (#2323)
* fix!: handle deserializing and writing empty security requirements #1426 make distinction between empty security requirements and no security requirements on an operation. empty security requirements are read as an empty list, no security requirements are read as null for OpenAPI v2/v3/v3.1. This is a breaking change, previously both cases were read as an empty list. also includes a change to OpenApiOperation.SerializeInternal so it can serialize these two cases separately. this required a new method OpenApiWriterExtensions.WriteOptionalOrEmptyCollection. includes unit tests, change to PublicApi.approved.txt to include the new method, and I removed a couple of unused usings and a typo in test name `SerializeDocWithSecuritySchemeWithInlineReferencesWorks`. * Review: make security property tests more explicit * Review: remove unneeded list creation from list
1 parent 27bf18f commit 962e0e4

File tree

10 files changed

+160
-8
lines changed

10 files changed

+160
-8
lines changed

src/Microsoft.OpenApi/Models/OpenApiOperation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
214214
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);
215215

216216
// security
217-
writer.WriteOptionalCollection(OpenApiConstants.Security, Security, callback);
218-
217+
writer.WriteOptionalOrEmptyCollection(OpenApiConstants.Security, Security, callback);
218+
219219
// servers
220220
writer.WriteOptionalCollection(OpenApiConstants.Servers, Servers, callback);
221221

src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.OpenApi.Models.References;
1010
using Microsoft.OpenApi.Models.Interfaces;
1111
using System;
12+
using System.Text.Json.Nodes;
1213

1314
namespace Microsoft.OpenApi.Reader.V2
1415
{
@@ -94,7 +95,10 @@ internal static partial class OpenApiV2Deserializer
9495
},
9596
{
9697
"security",
97-
(o, n, t) => o.Security = n.CreateList(LoadSecurityRequirement, t)
98+
(o, n, t) => { if (n.JsonNode is JsonArray)
99+
{
100+
o.Security = n.CreateList(LoadSecurityRequirement, t);
101+
} }
98102
},
99103
};
100104

src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text.Json.Nodes;
78
using Microsoft.OpenApi.Extensions;
89
using Microsoft.OpenApi.Models;
910
using Microsoft.OpenApi.Models.References;
@@ -83,7 +84,13 @@ internal static partial class OpenApiV3Deserializer
8384
},
8485
{
8586
"security",
86-
(o, n, t) => o.Security = n.CreateList(LoadSecurityRequirement, t)
87+
(o, n, t) =>
88+
{
89+
if (n.JsonNode is JsonArray)
90+
{
91+
o.Security = n.CreateList(LoadSecurityRequirement, t);
92+
}
93+
}
8794
},
8895
{
8996
"servers",

src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text.Json.Nodes;
45
using Microsoft.OpenApi.Extensions;
56
using Microsoft.OpenApi.Models;
67
using Microsoft.OpenApi.Models.References;
@@ -96,8 +97,11 @@ internal static partial class OpenApiV31Deserializer
9697
},
9798
{
9899
"security", (o, n, t) =>
99-
{
100-
o.Security = n.CreateList(LoadSecurityRequirement, t);
100+
{
101+
if (n.JsonNode is JsonArray)
102+
{
103+
o.Security = n.CreateList(LoadSecurityRequirement, t);
104+
}
101105
}
102106
},
103107
{

src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ public static void WriteOptionalCollection<T>(
217217
writer.WriteCollectionInternal(name, elements, action);
218218
}
219219
}
220+
221+
/// <summary>
222+
/// Write the optional or empty Open API object/element collection.
223+
/// </summary>
224+
/// <typeparam name="T">The Open API element type. <see cref="IOpenApiElement"/></typeparam>
225+
/// <param name="writer">The Open API writer.</param>
226+
/// <param name="name">The property name.</param>
227+
/// <param name="elements">The collection values.</param>
228+
/// <param name="action">The collection element writer action.</param>
229+
public static void WriteOptionalOrEmptyCollection<T>(
230+
this IOpenApiWriter writer,
231+
string name,
232+
IEnumerable<T>? elements,
233+
Action<IOpenApiWriter, T> action)
234+
{
235+
if (elements != null)
236+
{
237+
writer.WriteCollectionInternal(name, elements, action);
238+
}
239+
}
220240

221241
/// <summary>
222242
/// Write the required Open API object/element collection.

test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,13 @@
5656
</None>
5757

5858
<None Update="PublicApi\PublicApi.approved.txt" CopyToOutputDirectory="Always" />
59+
60+
<None Update="Models\Samples\docWithoutOperationSecurity.yaml">
61+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
62+
</None>
63+
64+
<None Update="Models\Samples\docWithEmptyOperationSecurity.yaml">
65+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
66+
</None>
5967
</ItemGroup>
6068
</Project>

test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Microsoft.OpenApi.Models.Interfaces;
1313
using Microsoft.OpenApi.Models.References;
1414
using Microsoft.OpenApi.Writers;
15-
using Microsoft.VisualBasic;
1615
using VerifyXunit;
1716
using Xunit;
1817

@@ -2177,7 +2176,7 @@ public void SerializeAsThrowsIfVersionIsNotSupported()
21772176
}
21782177

21792178
[Fact]
2180-
public async Task SerializeDocWithSecuritySchemeWithInlineRefererencesWorks()
2179+
public async Task SerializeDocWithSecuritySchemeWithInlineReferencesWorks()
21812180
{
21822181
var expected = @"openapi: 3.0.4
21832182
info:
@@ -2218,5 +2217,75 @@ public async Task SerializeDocWithSecuritySchemeWithInlineRefererencesWorks()
22182217
var actual = stringWriter.ToString();
22192218
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
22202219
}
2220+
2221+
[Fact]
2222+
public async Task SerializeDocWithoutOperationSecurityWorks()
2223+
{
2224+
var expected = """
2225+
openapi: 3.0.4
2226+
info:
2227+
title: Repair Service
2228+
version: 1.0.0
2229+
servers:
2230+
- url: https://pluginrentu.azurewebsites.net/api
2231+
paths:
2232+
/repairs:
2233+
get:
2234+
summary: List all repairs
2235+
description: Returns a list of repairs with their details and images
2236+
operationId: listRepairs
2237+
responses:
2238+
'200':
2239+
description: A list of repairs
2240+
content:
2241+
application/json:
2242+
schema:
2243+
type: object
2244+
""";
2245+
2246+
var doc = (await OpenApiDocument.LoadAsync("Models/Samples/docWithoutOperationSecurity.yaml", SettingsFixture.ReaderSettings)).Document;
2247+
var stringWriter = new StringWriter();
2248+
doc!.SerializeAsV3(new OpenApiYamlWriter(stringWriter, new OpenApiWriterSettings { InlineLocalReferences = true }));
2249+
var actual = stringWriter.ToString();
2250+
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
2251+
var actualOperation = doc.Paths["/repairs"]!.Operations![HttpMethod.Get];
2252+
Assert.Null(actualOperation.Security);
2253+
}
2254+
2255+
[Fact]
2256+
public async Task SerializeDocWithEmptyOperationSecurityWorks()
2257+
{
2258+
var expected = """
2259+
openapi: 3.0.4
2260+
info:
2261+
title: Repair Service
2262+
version: 1.0.0
2263+
servers:
2264+
- url: https://pluginrentu.azurewebsites.net/api
2265+
paths:
2266+
/repairs:
2267+
get:
2268+
summary: List all repairs
2269+
description: Returns a list of repairs with their details and images
2270+
operationId: listRepairs
2271+
responses:
2272+
'200':
2273+
description: A list of repairs
2274+
content:
2275+
application/json:
2276+
schema:
2277+
type: object
2278+
security: [ ]
2279+
""";
2280+
2281+
var doc = (await OpenApiDocument.LoadAsync("Models/Samples/docWithEmptyOperationSecurity.yaml", SettingsFixture.ReaderSettings)).Document;
2282+
var stringWriter = new StringWriter();
2283+
doc!.SerializeAsV3(new OpenApiYamlWriter(stringWriter, new OpenApiWriterSettings { InlineLocalReferences = true }));
2284+
var actual = stringWriter.ToString();
2285+
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
2286+
var actualOperation = doc.Paths["/repairs"]!.Operations![HttpMethod.Get];
2287+
Assert.NotNull(actualOperation.Security);
2288+
Assert.Empty(actualOperation.Security);
2289+
}
22212290
}
22222291
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Repair Service
4+
version: 1.0.0
5+
servers:
6+
- url: https://pluginrentu.azurewebsites.net/api
7+
paths:
8+
/repairs:
9+
get:
10+
operationId: listRepairs
11+
summary: List all repairs
12+
description: Returns a list of repairs with their details and images
13+
responses:
14+
'200':
15+
description: A list of repairs
16+
content:
17+
application/json:
18+
schema:
19+
type: object
20+
security: []
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Repair Service
4+
version: 1.0.0
5+
servers:
6+
- url: https://pluginrentu.azurewebsites.net/api
7+
paths:
8+
/repairs:
9+
get:
10+
operationId: listRepairs
11+
summary: List all repairs
12+
description: Returns a list of repairs with their details and images
13+
responses:
14+
'200':
15+
description: A list of repairs
16+
content:
17+
application/json:
18+
schema:
19+
type: object

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,7 @@ namespace Microsoft.OpenApi.Writers
19671967
public static void WriteOptionalMap<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.Dictionary<string, T>? elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string, T> action)
19681968
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
19691969
public static void WriteOptionalObject<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T? value, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action) { }
1970+
public static void WriteOptionalOrEmptyCollection<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable<T>? elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action) { }
19701971
public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, string? value) { }
19711972
public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool value, bool defaultValue = false) { }
19721973
public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool? value, bool defaultValue = false) { }

0 commit comments

Comments
 (0)