Skip to content

Commit 4c228f7

Browse files
committed
test: add V32 sibling preservation tests mirroring V31
Mirrors the 6 V31 sibling preservation tests in V32Tests, using SerializeAsV32 for the round-trip tests. Parse tests are identical since both versions share the same LoadSchema + SetAdditional31MetadataFromMapNode path. Ref: #2895
1 parent 32fb216 commit 4c228f7

1 file changed

Lines changed: 248 additions & 0 deletions

File tree

test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,5 +786,253 @@ public void DeserializeFalseSchemaParsesAsNotEmptySchema()
786786
Assert.Empty(schema.Not.AllOf ?? []);
787787
Assert.Empty(schema.Not.OneOf ?? []);
788788
}
789+
790+
[Fact]
791+
public async Task ParseSchemaReferencePreservesJsonSchema2020KeywordSiblings()
792+
{
793+
// Arrange
794+
var yaml = """
795+
openapi: 3.2.0
796+
info:
797+
title: Sibling preservation repro
798+
version: 1.0.0
799+
paths: {}
800+
components:
801+
schemas:
802+
Target:
803+
type: object
804+
properties:
805+
name:
806+
type: string
807+
Referencing:
808+
$ref: '#/components/schemas/Target'
809+
description: Sibling description
810+
$dynamicAnchor: anchor
811+
$defs:
812+
sibling:
813+
$dynamicAnchor: inner
814+
$ref: '#/components/schemas/Target'
815+
""";
816+
817+
// Act
818+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
819+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
820+
var referencing = result.Document.Components!.Schemas["Referencing"];
821+
822+
// Assert — siblings are preserved on the OpenApiSchemaReference
823+
referencing.Should().BeOfType<OpenApiSchemaReference>();
824+
referencing.Description.Should().Be("Sibling description");
825+
referencing.DynamicAnchor.Should().Be("anchor");
826+
referencing.Definitions.Should().NotBeNull();
827+
referencing.Definitions!.Should().ContainKey("sibling");
828+
referencing.Definitions["sibling"].DynamicAnchor.Should().Be("inner");
829+
}
830+
831+
[Fact]
832+
public async Task SerializeSchemaReferencePreservesJsonSchema2020KeywordSiblings()
833+
{
834+
// Arrange
835+
var yaml = """
836+
openapi: 3.2.0
837+
info:
838+
title: Sibling preservation repro
839+
version: 1.0.0
840+
paths: {}
841+
components:
842+
schemas:
843+
Target:
844+
type: object
845+
properties:
846+
name:
847+
type: string
848+
Referencing:
849+
$ref: '#/components/schemas/Target'
850+
$dynamicAnchor: anchor
851+
$defs:
852+
itemType:
853+
$dynamicAnchor: itemType
854+
$ref: '#/components/schemas/Target'
855+
""";
856+
857+
// Act — parse then serialize back
858+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
859+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
860+
var writer = new StringWriter();
861+
result.Document.SerializeAsV32(new OpenApiYamlWriter(writer));
862+
var output = writer.ToString();
863+
864+
// Assert — round-trip preserves $dynamicAnchor and $defs alongside $ref
865+
using var roundTripStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(output));
866+
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
867+
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
868+
869+
referencing.Should().BeOfType<OpenApiSchemaReference>();
870+
referencing.DynamicAnchor.Should().Be("anchor");
871+
referencing.Definitions.Should().NotBeNull();
872+
referencing.Definitions!.Should().ContainKey("itemType");
873+
referencing.Definitions["itemType"].DynamicAnchor.Should().Be("itemType");
874+
}
875+
876+
[Fact]
877+
public async Task ParseSchemaReferencePreservesScalarKeywordSiblings()
878+
{
879+
// Arrange
880+
var yaml = """
881+
openapi: 3.2.0
882+
info:
883+
title: Scalar sibling repro
884+
version: 1.0.0
885+
paths: {}
886+
components:
887+
schemas:
888+
Target:
889+
type: object
890+
Referencing:
891+
$ref: '#/components/schemas/Target'
892+
$id: 'https://example.com/referencing.json'
893+
$schema: 'https://json-schema.org/draft/2020-12/schema'
894+
$comment: A comment sibling
895+
$anchor: myAnchor
896+
$dynamicRef: '#myAnchor'
897+
""";
898+
899+
// Act
900+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
901+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
902+
var referencing = result.Document.Components!.Schemas["Referencing"];
903+
904+
// Assert
905+
referencing.Should().BeOfType<OpenApiSchemaReference>();
906+
referencing.Id.Should().Be("https://example.com/referencing.json");
907+
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
908+
referencing.Comment.Should().Be("A comment sibling");
909+
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
910+
referencing.DynamicRef.Should().Be("#myAnchor");
911+
}
912+
913+
[Fact]
914+
public async Task SerializeSchemaReferencePreservesScalarKeywordSiblings()
915+
{
916+
// Arrange
917+
var yaml = """
918+
openapi: 3.2.0
919+
info:
920+
title: Scalar round-trip
921+
version: 1.0.0
922+
paths: {}
923+
components:
924+
schemas:
925+
Target:
926+
type: object
927+
Referencing:
928+
$ref: '#/components/schemas/Target'
929+
$id: 'https://example.com/referencing.json'
930+
$schema: 'https://json-schema.org/draft/2020-12/schema'
931+
$comment: A comment sibling
932+
$anchor: myAnchor
933+
$dynamicRef: '#myAnchor'
934+
""";
935+
936+
// Act — parse then serialize back
937+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
938+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
939+
var writer = new StringWriter();
940+
result.Document.SerializeAsV32(new OpenApiYamlWriter(writer));
941+
var output = writer.ToString();
942+
943+
// Assert — round-trip preserves scalar siblings alongside $ref
944+
using var roundTripStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(output));
945+
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
946+
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
947+
948+
referencing.Should().BeOfType<OpenApiSchemaReference>();
949+
referencing.Id.Should().Be("https://example.com/referencing.json");
950+
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
951+
referencing.Comment.Should().Be("A comment sibling");
952+
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
953+
referencing.DynamicRef.Should().Be("#myAnchor");
954+
}
955+
956+
[Fact]
957+
public async Task ParseSchemaReferencePreservesVocabularySibling()
958+
{
959+
// Arrange
960+
var yaml = """
961+
openapi: 3.2.0
962+
info:
963+
title: Vocabulary sibling repro
964+
version: 1.0.0
965+
paths: {}
966+
components:
967+
schemas:
968+
Target:
969+
type: object
970+
Referencing:
971+
$ref: '#/components/schemas/Target'
972+
$vocabulary:
973+
'https://json-schema.org/draft/2020-12/vocab/core': true
974+
'https://json-schema.org/draft/2020-12/vocab/applicator': false
975+
""";
976+
977+
// Act
978+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
979+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
980+
var referencing = result.Document.Components!.Schemas["Referencing"];
981+
982+
// Assert
983+
referencing.Should().BeOfType<OpenApiSchemaReference>();
984+
referencing.Vocabulary.Should().NotBeNull();
985+
referencing.Vocabulary!.Should().HaveCount(2);
986+
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"].Should().BeTrue();
987+
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"].Should().BeFalse();
988+
}
989+
990+
[Fact]
991+
public async Task ParseSchemaReferencePreservesDynamicAnchorInsideDefsInAllOf()
992+
{
993+
// Arrange — the allOf-based binding variant: $defs sits inside allOf[0],
994+
// and the nested schema has $ref + $dynamicAnchor (the binding entry).
995+
var yaml = """
996+
openapi: 3.2.0
997+
info:
998+
title: allOf binding variant
999+
version: 1.0.0
1000+
paths: {}
1001+
components:
1002+
schemas:
1003+
Asset:
1004+
type: object
1005+
properties:
1006+
id:
1007+
type: string
1008+
Paged:
1009+
type: object
1010+
properties:
1011+
items:
1012+
type: array
1013+
AssetPaged:
1014+
allOf:
1015+
- $defs:
1016+
contentType:
1017+
$dynamicAnchor: contentType
1018+
$ref: '#/components/schemas/Asset'
1019+
- $ref: '#/components/schemas/Paged'
1020+
""";
1021+
1022+
// Act
1023+
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
1024+
var result = await OpenApiDocument.LoadAsync(stream, "yaml", SettingsFixture.ReaderSettings);
1025+
var assetPaged = result.Document.Components!.Schemas["AssetPaged"];
1026+
1027+
// Assert
1028+
assetPaged.AllOf.Should().NotBeNull();
1029+
assetPaged.AllOf!.Count.Should().Be(2);
1030+
var defsHolder = assetPaged.AllOf[0];
1031+
defsHolder.Definitions.Should().NotBeNull();
1032+
defsHolder.Definitions!.Should().ContainKey("contentType");
1033+
var contentType = defsHolder.Definitions["contentType"];
1034+
contentType.Should().BeOfType<OpenApiSchemaReference>();
1035+
contentType.DynamicAnchor.Should().Be("contentType");
1036+
}
7891037
}
7901038
}

0 commit comments

Comments
 (0)