Skip to content

Commit bb9f706

Browse files
Merge pull request #2327 from microsoft/fix/test-for-mutations-during-serialization
chore: inspect for mutations in serialization methods
2 parents d7866c9 + ef6c76e commit bb9f706

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Net.Http;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
using System.Text.Json.Serialization;
8+
using System.Threading.Tasks;
9+
using Microsoft.OpenApi.Models;
10+
using Microsoft.OpenApi.Writers;
11+
using Xunit;
12+
13+
namespace Microsoft.OpenApi.Readers.Tests.V31Tests
14+
{
15+
public class OpenApiDocumentSerializationTests
16+
{
17+
private const string SampleFolderPath = "V31Tests/Samples/OpenApiDocument/";
18+
19+
[Theory]
20+
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
21+
[InlineData(OpenApiSpecVersion.OpenApi3_0)]
22+
[InlineData(OpenApiSpecVersion.OpenApi2_0)]
23+
public async Task Serialize_DoesNotMutateDom(OpenApiSpecVersion version)
24+
{
25+
// Arrange
26+
var filePath = Path.Combine(SampleFolderPath, "docWith31properties.json");
27+
var (doc, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);
28+
29+
// Act: Serialize using System.Text.Json
30+
var options = new JsonSerializerOptions
31+
{
32+
Converters =
33+
{
34+
new HttpMethodOperationDictionaryConverter()
35+
},
36+
};
37+
var originalSerialized = JsonSerializer.Serialize(doc, options);
38+
Assert.NotNull(originalSerialized); // sanity check
39+
40+
// Serialize using native OpenAPI writer
41+
var jsonWriter = new StringWriter();
42+
var openApiWriter = new OpenApiJsonWriter(jsonWriter);
43+
switch (version)
44+
{
45+
case OpenApiSpecVersion.OpenApi3_1:
46+
doc.SerializeAsV31(openApiWriter);
47+
break;
48+
case OpenApiSpecVersion.OpenApi3_0:
49+
doc.SerializeAsV3(openApiWriter);
50+
break;
51+
default:
52+
doc.SerializeAsV2(openApiWriter);
53+
break;
54+
}
55+
56+
// Serialize again with STJ after native writer serialization
57+
var finalSerialized = JsonSerializer.Serialize(doc, options);
58+
Assert.NotNull(finalSerialized); // sanity check
59+
60+
// Assert: Ensure no mutation occurred in the DOM after native serialization
61+
Assert.True(JsonNode.DeepEquals(originalSerialized, finalSerialized), "OpenAPI DOM was mutated by the native serializer.");
62+
}
63+
}
64+
65+
public class HttpMethodOperationDictionaryConverter : JsonConverter<Dictionary<HttpMethod, OpenApiOperation>>
66+
{
67+
public override Dictionary<HttpMethod, OpenApiOperation> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
68+
{
69+
throw new NotImplementedException();
70+
}
71+
72+
public override void Write(Utf8JsonWriter writer, Dictionary<HttpMethod, OpenApiOperation> value, JsonSerializerOptions options)
73+
{
74+
writer.WriteStartObject();
75+
76+
foreach (var kvp in value)
77+
{
78+
writer.WritePropertyName(kvp.Key.Method.ToLowerInvariant());
79+
JsonSerializer.Serialize(writer, kvp.Value, options);
80+
}
81+
82+
writer.WriteEndObject();
83+
}
84+
}
85+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
{
2+
"openapi": "3.1.1",
3+
"info": {
4+
"title": "Sample OpenAPI 3.1 API",
5+
"description": "A sample API demonstrating OpenAPI 3.1 features",
6+
"version": "2.0.0",
7+
"summary": "Sample OpenAPI 3.1 API with the latest features",
8+
"license": {
9+
"name": "Apache 2.0",
10+
"identifier": "Apache-2.0"
11+
}
12+
},
13+
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
14+
"servers": [
15+
{
16+
"url": "https://api.example.com/v2",
17+
"description": "Main production server"
18+
}
19+
],
20+
"webhooks": {
21+
"newPetAlert": {
22+
"post": {
23+
"summary": "Notify about a new pet being added",
24+
"requestBody": {
25+
"required": true,
26+
"content": {
27+
"application/json": {
28+
"schema": {
29+
"type": "string"
30+
}
31+
}
32+
}
33+
},
34+
"responses": {
35+
"200": {
36+
"description": "Webhook processed successfully"
37+
}
38+
}
39+
}
40+
}
41+
},
42+
"paths": {
43+
"/pets": {
44+
"get": {
45+
"summary": "List all pets",
46+
"operationId": "listPets",
47+
"parameters": [
48+
{
49+
"name": "limit",
50+
"in": "query",
51+
"description": "How many items to return at one time (max 100)",
52+
"required": false,
53+
"schema": {
54+
"type": "integer",
55+
"exclusiveMinimum": 1,
56+
"exclusiveMaximum": 100
57+
}
58+
}
59+
],
60+
"responses": {
61+
"200": {
62+
"description": "A paged array of pets",
63+
"content": {
64+
"application/json": {
65+
"schema": {
66+
"$ref": "https://example.com/schemas/pet.json"
67+
}
68+
}
69+
}
70+
}
71+
}
72+
}
73+
},
74+
"/sample": {
75+
"get": {
76+
"summary": "Sample endpoint",
77+
"responses": {
78+
"200": {
79+
"description": "Sample response",
80+
"content": {
81+
"application/json": {
82+
"schema": {
83+
"$schema": "https://json-schema.org/draft/2020-12/schema",
84+
"$id": "https://example.com/schemas/person.schema.yaml",
85+
"$comment": "A schema defining a pet object with optional references to dynamic components.",
86+
"$vocabulary": {
87+
"https://json-schema.org/draft/2020-12/vocab/core": true,
88+
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
89+
"https://json-schema.org/draft/2020-12/vocab/validation": true,
90+
"https://json-schema.org/draft/2020-12/vocab/meta-data": false,
91+
"https://json-schema.org/draft/2020-12/vocab/format-annotation": false
92+
},
93+
"title": "Pet",
94+
"description": "Schema for a pet object",
95+
"type": "object",
96+
"properties": {
97+
"name": {
98+
"type": "string",
99+
"$comment": "The pet's full name"
100+
},
101+
"address": {
102+
"$dynamicRef": "#addressDef",
103+
"$comment": "Reference to an address definition which can change dynamically"
104+
}
105+
},
106+
"required": [
107+
"name"
108+
],
109+
"$dynamicAnchor": "addressDef"
110+
}
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}
117+
},
118+
"components": {
119+
"securitySchemes": {
120+
"api_key": {
121+
"type": "apiKey",
122+
"name": "api_key",
123+
"in": "header"
124+
}
125+
},
126+
"schemas": {
127+
"Pet": {
128+
"$id": "https://example.com/schemas/pet.json",
129+
"type": "object",
130+
"required": [
131+
"id",
132+
"weight"
133+
],
134+
"properties": {
135+
"id": {
136+
"type": "string",
137+
"format": "uuid"
138+
},
139+
"weight": {
140+
"type": "number",
141+
"exclusiveMinimum": 0,
142+
"description": "Weight of the pet in kilograms"
143+
},
144+
"attributes": {
145+
"type": [
146+
"object",
147+
"null"
148+
],
149+
"description": "Dynamic attributes for the pet",
150+
"patternProperties": {
151+
"^attr_[A-Za-z]+$": {
152+
"type": "string"
153+
}
154+
}
155+
}
156+
},
157+
"$comment": "This schema represents a pet in the system.",
158+
"$defs": {
159+
"ExtraInfo": {
160+
"type": "string"
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)