Skip to content

Commit 5389c78

Browse files
committed
chore: add test to validate DOM properties don't mutate on serialization
1 parent 25136af commit 5389c78

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
[Fact]
20+
public async Task Serialize_DoesNotMutateDom()
21+
{
22+
// Arrange
23+
var filePath = Path.Combine(SampleFolderPath, "docWith31properties.json");
24+
var (doc, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);
25+
26+
// Act: Serialize using System.Text.Json
27+
var options = new JsonSerializerOptions
28+
{
29+
Converters =
30+
{
31+
new HttpMethodOperationDictionaryConverter()
32+
},
33+
};
34+
var originalSerialized = JsonSerializer.Serialize(doc, options);
35+
Assert.NotNull(originalSerialized); // sanity check
36+
37+
// Serialize using native OpenAPI writer
38+
var jsonWriter = new StringWriter();
39+
var openApiWriter = new OpenApiJsonWriter(jsonWriter);
40+
doc.SerializeAsV31(openApiWriter);
41+
42+
// Serialize again with STJ after native writer serialization
43+
var finalSerialized = JsonSerializer.Serialize(doc, options);
44+
Assert.NotNull(finalSerialized); // sanity check
45+
46+
// Assert: Ensure no mutation occurred in the DOM after native serialization
47+
Assert.True(JsonNode.DeepEquals(originalSerialized, finalSerialized), "OpenAPI DOM was mutated by the native serializer.");
48+
}
49+
}
50+
51+
public class HttpMethodOperationDictionaryConverter : JsonConverter<Dictionary<HttpMethod, OpenApiOperation>>
52+
{
53+
public override Dictionary<HttpMethod, OpenApiOperation> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
54+
{
55+
throw new NotImplementedException();
56+
}
57+
58+
public override void Write(Utf8JsonWriter writer, Dictionary<HttpMethod, OpenApiOperation> value, JsonSerializerOptions options)
59+
{
60+
writer.WriteStartObject();
61+
62+
foreach (var kvp in value)
63+
{
64+
writer.WritePropertyName(kvp.Key.Method.ToLowerInvariant());
65+
JsonSerializer.Serialize(writer, kvp.Value, options);
66+
}
67+
68+
writer.WriteEndObject();
69+
}
70+
}
71+
}
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)