From 41340c28b023ffb9fbf1a1efe035ebae87706a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Alt=C3=A9s?= Date: Tue, 18 Nov 2025 11:05:07 +0100 Subject: [PATCH 1/3] Adds tests for validating attribute visibility on schema generation based on attribute capabilities (#1056) --- .../AttributeCapabilitiesTests.cs | 71 +++++++++++++++++++ .../AttributeTypes/AttributeTypesDbContext.cs | 1 + test/OpenApiTests/AttributeTypes/Book.cs | 34 +++++++++ 3 files changed, 106 insertions(+) create mode 100644 test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs create mode 100644 test/OpenApiTests/AttributeTypes/Book.cs diff --git a/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs b/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs new file mode 100644 index 0000000000..af77c9636a --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using JsonApiDotNetCore.Resources.Annotations; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; + +namespace OpenApiTests.AttributeTypes; + +public sealed class AttributeCapabilitiesTests : IClassFixture> +{ + private enum RequestType{ Response, Create, Update } + + private static readonly Dictionary CapabilitiesByRequestType = new() + { + { RequestType.Response, AttrCapabilities.AllowView }, + { RequestType.Create, AttrCapabilities.AllowCreate }, + { RequestType.Update, AttrCapabilities.AllowChange } + }; + + private static readonly Dictionary SchemaPathByRequestType = new() + { + { RequestType.Response, "components.schemas.attributesInBookResponse.allOf[1].properties" }, + { RequestType.Create, "components.schemas.attributesInCreateBookRequest.allOf[1].properties" }, + { RequestType.Update, "components.schemas.attributesInUpdateBookRequest.allOf[1].properties" } + }; + + private static readonly Dictionary> BookModelAttrsByCapability = new() + { + { AttrCapabilities.AllowView, ["title", "isbn", "publishedOn"] }, + { AttrCapabilities.AllowChange, ["title", "internalNotes"] }, + { AttrCapabilities.AllowCreate, ["draftContent"] }, + { AttrCapabilities.None, ["secretCode"] } + }; + + private readonly OpenApiTestContext _testContext; + + public AttributeCapabilitiesTests( + OpenApiTestContext testContext, + ITestOutputHelper output) + { + _testContext = testContext; + _testContext.UseController(); + _testContext.SetTestOutputHelper(output); + } + + [Theory] + [InlineData(RequestType.Response)] + [InlineData(RequestType.Create)] + [InlineData(RequestType.Update)] + private async Task Generated_Schema_Includes_Only_Expected_Attrs_For_Request_Type_Async(RequestType requestType) + { + // Arrange + string path = SchemaPathByRequestType[requestType]; + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + JsonElement attrs = document.Should().ContainPath(path); + + IList bookExpectedAttrs = BookModelAttrsByCapability[CapabilitiesByRequestType[requestType]]; + IList bookUnexpectedAttrs = BookModelAttrsByCapability.SelectMany(kvPair => kvPair.Value) + .Except(bookExpectedAttrs).ToList(); + + // Assert + foreach (string attrName in bookExpectedAttrs) + { + attrs.Should().ContainProperty(attrName); + } + foreach (string attrName in bookUnexpectedAttrs) + { + attrs.Should().NotContainPath($"properties.{attrName}"); + } + } +} diff --git a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs index 5d28701a44..b9a7641958 100644 --- a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs +++ b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs @@ -14,6 +14,7 @@ namespace OpenApiTests.AttributeTypes; public sealed class AttributeTypesDbContext(DbContextOptions options) : TestableDbContext(options) { + public DbSet Books => Set(); public DbSet TypeContainers => Set(); protected override void ConfigureConventions(ModelConfigurationBuilder builder) diff --git a/test/OpenApiTests/AttributeTypes/Book.cs b/test/OpenApiTests/AttributeTypes/Book.cs new file mode 100644 index 0000000000..212c17a31d --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/Book.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.AttributeTypes; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.AttributeTypes")] +public sealed class Book : Identifiable +{ + // Visible in GET, PATCH, included in all operations + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public string Title { get; set; } = null!; + + // Only visible in GET + [Attr(Capabilities = AttrCapabilities.AllowView)] + public string ISBN { get; set; } = null!; + + // Only visible in GET + [Attr(Capabilities = AttrCapabilities.AllowView)] + public DateOnly PublishedOn { get; set; } + + // Only usable in POST + [Attr(Capabilities = AttrCapabilities.AllowCreate)] + public string DraftContent { get; set; } = null!; + + // Only usable in PATCH + [Attr(Capabilities = AttrCapabilities.AllowChange)] + public string InternalNotes { get; set; } = null!; + + // No visibility or modifiers whatsoever + [Attr(Capabilities = AttrCapabilities.None)] + public string SecretCode { get; set; } = null!; +} From 8d93ee93d1be1c0ea4d5ecfc656636cd6cd89b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Alt=C3=A9s?= Date: Tue, 18 Nov 2025 12:33:00 +0100 Subject: [PATCH 2/3] Adds tests for validating attribute visibility on schema generation based on relationship capabilities (#1056) --- .../AttributeCapabilitiesTests.cs | 22 ++++-- .../AttributeTypes/AttributeTypesDbContext.cs | 2 + test/OpenApiTests/AttributeTypes/Author.cs | 7 ++ test/OpenApiTests/AttributeTypes/Book.cs | 8 ++- .../AttributeTypes/CapabilitiesUtils.cs | 11 +++ .../RelationshipCapabilitiesTests.cs | 69 +++++++++++++++++++ test/OpenApiTests/AttributeTypes/Review.cs | 7 ++ 7 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 test/OpenApiTests/AttributeTypes/Author.cs create mode 100644 test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs create mode 100644 test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs create mode 100644 test/OpenApiTests/AttributeTypes/Review.cs diff --git a/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs b/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs index af77c9636a..662ac28c04 100644 --- a/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs +++ b/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs @@ -3,13 +3,12 @@ using TestBuildingBlocks; using Xunit; using Xunit.Abstractions; +using RequestType = OpenApiTests.AttributeTypes.CapabilitiesUtils.RequestType; namespace OpenApiTests.AttributeTypes; public sealed class AttributeCapabilitiesTests : IClassFixture> { - private enum RequestType{ Response, Create, Update } - private static readonly Dictionary CapabilitiesByRequestType = new() { { RequestType.Response, AttrCapabilities.AllowView }, @@ -26,10 +25,21 @@ private enum RequestType{ Response, Create, Update } private static readonly Dictionary> BookModelAttrsByCapability = new() { - { AttrCapabilities.AllowView, ["title", "isbn", "publishedOn"] }, - { AttrCapabilities.AllowChange, ["title", "internalNotes"] }, + { + AttrCapabilities.AllowView, [ + "title", + "isbn", + "publishedOn" + ] + }, + { + AttrCapabilities.AllowChange, [ + "title", + "internalNotes" + ] + }, { AttrCapabilities.AllowCreate, ["draftContent"] }, - { AttrCapabilities.None, ["secretCode"] } + { AttrCapabilities.None, ["secretCode"] } }; private readonly OpenApiTestContext _testContext; @@ -55,6 +65,7 @@ private async Task Generated_Schema_Includes_Only_Expected_Attrs_For_Request_Typ JsonElement attrs = document.Should().ContainPath(path); IList bookExpectedAttrs = BookModelAttrsByCapability[CapabilitiesByRequestType[requestType]]; + IList bookUnexpectedAttrs = BookModelAttrsByCapability.SelectMany(kvPair => kvPair.Value) .Except(bookExpectedAttrs).ToList(); @@ -63,6 +74,7 @@ private async Task Generated_Schema_Includes_Only_Expected_Attrs_For_Request_Typ { attrs.Should().ContainProperty(attrName); } + foreach (string attrName in bookUnexpectedAttrs) { attrs.Should().NotContainPath($"properties.{attrName}"); diff --git a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs index b9a7641958..222402f5dd 100644 --- a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs +++ b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs @@ -14,7 +14,9 @@ namespace OpenApiTests.AttributeTypes; public sealed class AttributeTypesDbContext(DbContextOptions options) : TestableDbContext(options) { + public DbSet Authors => Set(); public DbSet Books => Set(); + public DbSet Reviews => Set(); public DbSet TypeContainers => Set(); protected override void ConfigureConventions(ModelConfigurationBuilder builder) diff --git a/test/OpenApiTests/AttributeTypes/Author.cs b/test/OpenApiTests/AttributeTypes/Author.cs new file mode 100644 index 0000000000..cc3230b372 --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/Author.cs @@ -0,0 +1,7 @@ +using JsonApiDotNetCore.Resources; + +namespace OpenApiTests.AttributeTypes; + +public sealed class Author : Identifiable +{ +} \ No newline at end of file diff --git a/test/OpenApiTests/AttributeTypes/Book.cs b/test/OpenApiTests/AttributeTypes/Book.cs index 212c17a31d..651daa3bc9 100644 --- a/test/OpenApiTests/AttributeTypes/Book.cs +++ b/test/OpenApiTests/AttributeTypes/Book.cs @@ -14,7 +14,7 @@ public sealed class Book : Identifiable // Only visible in GET [Attr(Capabilities = AttrCapabilities.AllowView)] - public string ISBN { get; set; } = null!; + public string Isbn { get; set; } = null!; // Only visible in GET [Attr(Capabilities = AttrCapabilities.AllowView)] @@ -31,4 +31,10 @@ public sealed class Book : Identifiable // No visibility or modifiers whatsoever [Attr(Capabilities = AttrCapabilities.None)] public string SecretCode { get; set; } = null!; + + [HasOne(Capabilities = HasOneCapabilities.AllowView)] + public Author? Author { get; set; } + + [HasMany(Capabilities = HasManyCapabilities.AllowSet)] + public ISet Reviews { get; set; } = new HashSet(); } diff --git a/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs b/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs new file mode 100644 index 0000000000..465cf6deec --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs @@ -0,0 +1,11 @@ +namespace OpenApiTests.AttributeTypes; + +public static class CapabilitiesUtils +{ + public enum RequestType + { + Response, + Create, + Update + } +} diff --git a/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs b/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs new file mode 100644 index 0000000000..a594ca6b8e --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; +using RequestType = OpenApiTests.AttributeTypes.CapabilitiesUtils.RequestType; + +namespace OpenApiTests.AttributeTypes; + +public sealed class RelationshipCapabilitiesTests : IClassFixture> +{ + private static readonly Dictionary SchemaPathByRequestType = new() + { + { RequestType.Response, "components.schemas.relationshipsInBookResponse.allOf[1].properties" }, + { RequestType.Create, "components.schemas.relationshipsInCreateBookRequest.allOf[1].properties" }, + { RequestType.Update, "components.schemas.relationshipsInUpdateBookRequest.allOf[1].properties" } + }; + + private static readonly Dictionary> BookModelRelsByCapability = new() + { + { "AllowView", ["author"] }, + { "AllowSet", ["reviews"] } + }; + + private static readonly Dictionary CapabilitiesByRequestType = new() + { + { RequestType.Response, "AllowView" }, + { RequestType.Create, "AllowSet" }, + { RequestType.Update, "AllowSet" } + }; + + private readonly OpenApiTestContext _testContext; + + public RelationshipCapabilitiesTests( + OpenApiTestContext testContext, + ITestOutputHelper output) + { + _testContext = testContext; + _testContext.UseController(); + _testContext.SetTestOutputHelper(output); + } + + [Theory] + [InlineData(RequestType.Response)] + [InlineData(RequestType.Create)] + [InlineData(RequestType.Update)] + public async Task Generated_Schema_Includes_Only_Expected_Relationships_For_Request_Type_Async(RequestType requestType) + { + // Arrange + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + JsonElement properties = document.Should().ContainPath(SchemaPathByRequestType[requestType]); + string selectedCapability = CapabilitiesByRequestType[requestType]; + + IList expected = BookModelRelsByCapability[selectedCapability]; + + IList unexpected = BookModelRelsByCapability.SelectMany(kvp => kvp.Value) + .Except(expected).ToList(); + + // Assert + foreach (string relName in expected) + { + properties.Should().ContainProperty(relName); + } + + foreach (string relName in unexpected) + { + properties.Should().NotContainPath($"properties.{relName}"); + } + } +} diff --git a/test/OpenApiTests/AttributeTypes/Review.cs b/test/OpenApiTests/AttributeTypes/Review.cs new file mode 100644 index 0000000000..25322386d8 --- /dev/null +++ b/test/OpenApiTests/AttributeTypes/Review.cs @@ -0,0 +1,7 @@ +using JsonApiDotNetCore.Resources; + +namespace OpenApiTests.AttributeTypes; + +public sealed class Review : Identifiable +{ +} \ No newline at end of file From 82abcb7100fecd8d5223df9967a33c926252ad82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Alt=C3=A9s?= Date: Tue, 27 Jan 2026 11:27:45 +0100 Subject: [PATCH 3/3] Restructure capability test into dedicated directory with expanded coverage --- .../AttributeCapabilitiesTests.cs | 83 ------- .../AttributeTypes/AttributeTypesDbContext.cs | 3 - test/OpenApiTests/AttributeTypes/Author.cs | 7 - .../AttributeTypes/CapabilitiesUtils.cs | 11 - .../RelationshipCapabilitiesTests.cs | 69 ------ test/OpenApiTests/AttributeTypes/Review.cs | 7 - test/OpenApiTests/Capabilities/Article.cs | 25 ++ .../AttributeCapabilitiesTests.cs | 192 +++++++++++++++ test/OpenApiTests/Capabilities/Author.cs | 13 ++ .../{AttributeTypes => Capabilities}/Book.cs | 18 +- .../Capabilities/CapabilitiesDbContext.cs | 19 ++ test/OpenApiTests/Capabilities/Category.cs | 13 ++ test/OpenApiTests/Capabilities/Comment.cs | 13 ++ .../RelationshipCapabilitiesTests.cs | 220 ++++++++++++++++++ test/OpenApiTests/Capabilities/Review.cs | 16 ++ test/OpenApiTests/Capabilities/Tag.cs | 13 ++ test/OpenApiTests/Capabilities/Writer.cs | 13 ++ 17 files changed, 546 insertions(+), 189 deletions(-) delete mode 100644 test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs delete mode 100644 test/OpenApiTests/AttributeTypes/Author.cs delete mode 100644 test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs delete mode 100644 test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs delete mode 100644 test/OpenApiTests/AttributeTypes/Review.cs create mode 100644 test/OpenApiTests/Capabilities/Article.cs create mode 100644 test/OpenApiTests/Capabilities/AttributeCapabilitiesTests.cs create mode 100644 test/OpenApiTests/Capabilities/Author.cs rename test/OpenApiTests/{AttributeTypes => Capabilities}/Book.cs (74%) create mode 100644 test/OpenApiTests/Capabilities/CapabilitiesDbContext.cs create mode 100644 test/OpenApiTests/Capabilities/Category.cs create mode 100644 test/OpenApiTests/Capabilities/Comment.cs create mode 100644 test/OpenApiTests/Capabilities/RelationshipCapabilitiesTests.cs create mode 100644 test/OpenApiTests/Capabilities/Review.cs create mode 100644 test/OpenApiTests/Capabilities/Tag.cs create mode 100644 test/OpenApiTests/Capabilities/Writer.cs diff --git a/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs b/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs deleted file mode 100644 index 662ac28c04..0000000000 --- a/test/OpenApiTests/AttributeTypes/AttributeCapabilitiesTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Text.Json; -using JsonApiDotNetCore.Resources.Annotations; -using TestBuildingBlocks; -using Xunit; -using Xunit.Abstractions; -using RequestType = OpenApiTests.AttributeTypes.CapabilitiesUtils.RequestType; - -namespace OpenApiTests.AttributeTypes; - -public sealed class AttributeCapabilitiesTests : IClassFixture> -{ - private static readonly Dictionary CapabilitiesByRequestType = new() - { - { RequestType.Response, AttrCapabilities.AllowView }, - { RequestType.Create, AttrCapabilities.AllowCreate }, - { RequestType.Update, AttrCapabilities.AllowChange } - }; - - private static readonly Dictionary SchemaPathByRequestType = new() - { - { RequestType.Response, "components.schemas.attributesInBookResponse.allOf[1].properties" }, - { RequestType.Create, "components.schemas.attributesInCreateBookRequest.allOf[1].properties" }, - { RequestType.Update, "components.schemas.attributesInUpdateBookRequest.allOf[1].properties" } - }; - - private static readonly Dictionary> BookModelAttrsByCapability = new() - { - { - AttrCapabilities.AllowView, [ - "title", - "isbn", - "publishedOn" - ] - }, - { - AttrCapabilities.AllowChange, [ - "title", - "internalNotes" - ] - }, - { AttrCapabilities.AllowCreate, ["draftContent"] }, - { AttrCapabilities.None, ["secretCode"] } - }; - - private readonly OpenApiTestContext _testContext; - - public AttributeCapabilitiesTests( - OpenApiTestContext testContext, - ITestOutputHelper output) - { - _testContext = testContext; - _testContext.UseController(); - _testContext.SetTestOutputHelper(output); - } - - [Theory] - [InlineData(RequestType.Response)] - [InlineData(RequestType.Create)] - [InlineData(RequestType.Update)] - private async Task Generated_Schema_Includes_Only_Expected_Attrs_For_Request_Type_Async(RequestType requestType) - { - // Arrange - string path = SchemaPathByRequestType[requestType]; - JsonElement document = await _testContext.GetSwaggerDocumentAsync(); - JsonElement attrs = document.Should().ContainPath(path); - - IList bookExpectedAttrs = BookModelAttrsByCapability[CapabilitiesByRequestType[requestType]]; - - IList bookUnexpectedAttrs = BookModelAttrsByCapability.SelectMany(kvPair => kvPair.Value) - .Except(bookExpectedAttrs).ToList(); - - // Assert - foreach (string attrName in bookExpectedAttrs) - { - attrs.Should().ContainProperty(attrName); - } - - foreach (string attrName in bookUnexpectedAttrs) - { - attrs.Should().NotContainPath($"properties.{attrName}"); - } - } -} diff --git a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs index 222402f5dd..5d28701a44 100644 --- a/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs +++ b/test/OpenApiTests/AttributeTypes/AttributeTypesDbContext.cs @@ -14,9 +14,6 @@ namespace OpenApiTests.AttributeTypes; public sealed class AttributeTypesDbContext(DbContextOptions options) : TestableDbContext(options) { - public DbSet Authors => Set(); - public DbSet Books => Set(); - public DbSet Reviews => Set(); public DbSet TypeContainers => Set(); protected override void ConfigureConventions(ModelConfigurationBuilder builder) diff --git a/test/OpenApiTests/AttributeTypes/Author.cs b/test/OpenApiTests/AttributeTypes/Author.cs deleted file mode 100644 index cc3230b372..0000000000 --- a/test/OpenApiTests/AttributeTypes/Author.cs +++ /dev/null @@ -1,7 +0,0 @@ -using JsonApiDotNetCore.Resources; - -namespace OpenApiTests.AttributeTypes; - -public sealed class Author : Identifiable -{ -} \ No newline at end of file diff --git a/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs b/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs deleted file mode 100644 index 465cf6deec..0000000000 --- a/test/OpenApiTests/AttributeTypes/CapabilitiesUtils.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace OpenApiTests.AttributeTypes; - -public static class CapabilitiesUtils -{ - public enum RequestType - { - Response, - Create, - Update - } -} diff --git a/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs b/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs deleted file mode 100644 index a594ca6b8e..0000000000 --- a/test/OpenApiTests/AttributeTypes/RelationshipCapabilitiesTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.Json; -using TestBuildingBlocks; -using Xunit; -using Xunit.Abstractions; -using RequestType = OpenApiTests.AttributeTypes.CapabilitiesUtils.RequestType; - -namespace OpenApiTests.AttributeTypes; - -public sealed class RelationshipCapabilitiesTests : IClassFixture> -{ - private static readonly Dictionary SchemaPathByRequestType = new() - { - { RequestType.Response, "components.schemas.relationshipsInBookResponse.allOf[1].properties" }, - { RequestType.Create, "components.schemas.relationshipsInCreateBookRequest.allOf[1].properties" }, - { RequestType.Update, "components.schemas.relationshipsInUpdateBookRequest.allOf[1].properties" } - }; - - private static readonly Dictionary> BookModelRelsByCapability = new() - { - { "AllowView", ["author"] }, - { "AllowSet", ["reviews"] } - }; - - private static readonly Dictionary CapabilitiesByRequestType = new() - { - { RequestType.Response, "AllowView" }, - { RequestType.Create, "AllowSet" }, - { RequestType.Update, "AllowSet" } - }; - - private readonly OpenApiTestContext _testContext; - - public RelationshipCapabilitiesTests( - OpenApiTestContext testContext, - ITestOutputHelper output) - { - _testContext = testContext; - _testContext.UseController(); - _testContext.SetTestOutputHelper(output); - } - - [Theory] - [InlineData(RequestType.Response)] - [InlineData(RequestType.Create)] - [InlineData(RequestType.Update)] - public async Task Generated_Schema_Includes_Only_Expected_Relationships_For_Request_Type_Async(RequestType requestType) - { - // Arrange - JsonElement document = await _testContext.GetSwaggerDocumentAsync(); - JsonElement properties = document.Should().ContainPath(SchemaPathByRequestType[requestType]); - string selectedCapability = CapabilitiesByRequestType[requestType]; - - IList expected = BookModelRelsByCapability[selectedCapability]; - - IList unexpected = BookModelRelsByCapability.SelectMany(kvp => kvp.Value) - .Except(expected).ToList(); - - // Assert - foreach (string relName in expected) - { - properties.Should().ContainProperty(relName); - } - - foreach (string relName in unexpected) - { - properties.Should().NotContainPath($"properties.{relName}"); - } - } -} diff --git a/test/OpenApiTests/AttributeTypes/Review.cs b/test/OpenApiTests/AttributeTypes/Review.cs deleted file mode 100644 index 25322386d8..0000000000 --- a/test/OpenApiTests/AttributeTypes/Review.cs +++ /dev/null @@ -1,7 +0,0 @@ -using JsonApiDotNetCore.Resources; - -namespace OpenApiTests.AttributeTypes; - -public sealed class Review : Identifiable -{ -} \ No newline at end of file diff --git a/test/OpenApiTests/Capabilities/Article.cs b/test/OpenApiTests/Capabilities/Article.cs new file mode 100644 index 0000000000..94b8e1940a --- /dev/null +++ b/test/OpenApiTests/Capabilities/Article.cs @@ -0,0 +1,25 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Article : Identifiable +{ + [Attr] + public string Headline { get; set; } = null!; + + [HasOne(Capabilities = HasOneCapabilities.AllowSet)] + public Writer? Writer { get; set; } + + [HasMany(Capabilities = HasManyCapabilities.AllowView)] + public ISet Categories { get; set; } = new HashSet(); + + [HasMany(Capabilities = HasManyCapabilities.AllowAdd)] + public ISet Tags { get; set; } = new HashSet(); + + [HasMany(Capabilities = HasManyCapabilities.AllowRemove)] + public ISet Comments { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/Capabilities/AttributeCapabilitiesTests.cs b/test/OpenApiTests/Capabilities/AttributeCapabilitiesTests.cs new file mode 100644 index 0000000000..9cfd7f1fdb --- /dev/null +++ b/test/OpenApiTests/Capabilities/AttributeCapabilitiesTests.cs @@ -0,0 +1,192 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; + +namespace OpenApiTests.Capabilities; + +public sealed class AttributeCapabilitiesTests : IClassFixture, CapabilitiesDbContext>> +{ + private readonly OpenApiTestContext, CapabilitiesDbContext> _testContext; + + public AttributeCapabilitiesTests(OpenApiTestContext, CapabilitiesDbContext> testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + + testContext.UseController(); + + testContext.SetTestOutputHelper(testOutputHelper); + testContext.SwaggerDocumentOutputDirectory = $"{GetType().Namespace!.Replace('.', '/')}/GeneratedSwagger"; + } + + [Theory] + [InlineData("title")] + [InlineData("isbn")] + [InlineData("publishedOn")] + [InlineData("hasEmptyTitle")] + public async Task Attribute_with_AllowView_capability_appears_in_response_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty(attributeName); + }); + } + + [Theory] + [InlineData("draftContent")] + [InlineData("internalNotes")] + [InlineData("secretCode")] + [InlineData("isDeleted")] + public async Task Attribute_without_AllowView_capability_does_not_appear_in_response_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath(attributeName); + }); + } + + [Theory] + [InlineData("draftContent")] + [InlineData("isDeleted")] + public async Task Attribute_with_AllowCreate_capability_appears_in_create_request_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty(attributeName); + }); + } + + [Theory] + [InlineData("title")] + [InlineData("isbn")] + [InlineData("publishedOn")] + [InlineData("internalNotes")] + [InlineData("secretCode")] + [InlineData("hasEmptyTitle")] + public async Task Attribute_without_AllowCreate_capability_does_not_appear_in_create_request_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath(attributeName); + }); + } + + [Theory] + [InlineData("title")] + [InlineData("internalNotes")] + [InlineData("isDeleted")] + public async Task Attribute_with_AllowChange_capability_appears_in_update_request_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty(attributeName); + }); + } + + [Theory] + [InlineData("isbn")] + [InlineData("publishedOn")] + [InlineData("draftContent")] + [InlineData("secretCode")] + [InlineData("hasEmptyTitle")] + public async Task Attribute_without_AllowChange_capability_does_not_appear_in_update_request_schema(string attributeName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath(attributeName); + }); + } + + [Fact] + public async Task Attribute_with_None_capability_does_not_appear_in_any_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("secretCode"); + }); + + document.Should().ContainPath("components.schemas.attributesInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("secretCode"); + }); + + document.Should().ContainPath("components.schemas.attributesInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("secretCode"); + }); + } + + [Fact] + public async Task Get_only_property_only_appears_in_response_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("hasEmptyTitle"); + }); + + document.Should().ContainPath("components.schemas.attributesInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("hasEmptyTitle"); + }); + + document.Should().ContainPath("components.schemas.attributesInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("hasEmptyTitle"); + }); + } + + [Fact] + public async Task Set_only_property_only_appears_in_request_schemas() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.attributesInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("isDeleted"); + }); + + document.Should().ContainPath("components.schemas.attributesInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("isDeleted"); + }); + + document.Should().ContainPath("components.schemas.attributesInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("isDeleted"); + }); + } +} \ No newline at end of file diff --git a/test/OpenApiTests/Capabilities/Author.cs b/test/OpenApiTests/Capabilities/Author.cs new file mode 100644 index 0000000000..30e071a76f --- /dev/null +++ b/test/OpenApiTests/Capabilities/Author.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Author : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; +} diff --git a/test/OpenApiTests/AttributeTypes/Book.cs b/test/OpenApiTests/Capabilities/Book.cs similarity index 74% rename from test/OpenApiTests/AttributeTypes/Book.cs rename to test/OpenApiTests/Capabilities/Book.cs index 651daa3bc9..abe572d236 100644 --- a/test/OpenApiTests/AttributeTypes/Book.cs +++ b/test/OpenApiTests/Capabilities/Book.cs @@ -2,36 +2,36 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests.AttributeTypes; +namespace OpenApiTests.Capabilities; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -[Resource(ControllerNamespace = "OpenApiTests.AttributeTypes")] -public sealed class Book : Identifiable +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Book : Identifiable { - // Visible in GET, PATCH, included in all operations [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] public string Title { get; set; } = null!; - // Only visible in GET [Attr(Capabilities = AttrCapabilities.AllowView)] public string Isbn { get; set; } = null!; - // Only visible in GET [Attr(Capabilities = AttrCapabilities.AllowView)] public DateOnly PublishedOn { get; set; } - // Only usable in POST [Attr(Capabilities = AttrCapabilities.AllowCreate)] public string DraftContent { get; set; } = null!; - // Only usable in PATCH [Attr(Capabilities = AttrCapabilities.AllowChange)] public string InternalNotes { get; set; } = null!; - // No visibility or modifiers whatsoever [Attr(Capabilities = AttrCapabilities.None)] public string SecretCode { get; set; } = null!; + [Attr] + public bool HasEmptyTitle => string.IsNullOrEmpty(Title); + + [Attr] + public bool IsDeleted { set => _ = value; } + [HasOne(Capabilities = HasOneCapabilities.AllowView)] public Author? Author { get; set; } diff --git a/test/OpenApiTests/Capabilities/CapabilitiesDbContext.cs b/test/OpenApiTests/Capabilities/CapabilitiesDbContext.cs new file mode 100644 index 0000000000..9176bd3107 --- /dev/null +++ b/test/OpenApiTests/Capabilities/CapabilitiesDbContext.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class CapabilitiesDbContext(DbContextOptions options) + : TestableDbContext(options) +{ + public DbSet Books => Set(); + public DbSet Authors => Set(); + public DbSet Reviews => Set(); + public DbSet
Articles => Set
(); + public DbSet Writers => Set(); + public DbSet Categories => Set(); + public DbSet Tags => Set(); + public DbSet Comments => Set(); +} diff --git a/test/OpenApiTests/Capabilities/Category.cs b/test/OpenApiTests/Capabilities/Category.cs new file mode 100644 index 0000000000..99fcde3ee7 --- /dev/null +++ b/test/OpenApiTests/Capabilities/Category.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Category : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; +} diff --git a/test/OpenApiTests/Capabilities/Comment.cs b/test/OpenApiTests/Capabilities/Comment.cs new file mode 100644 index 0000000000..54db05a2df --- /dev/null +++ b/test/OpenApiTests/Capabilities/Comment.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Comment : Identifiable +{ + [Attr] + public string Text { get; set; } = null!; +} diff --git a/test/OpenApiTests/Capabilities/RelationshipCapabilitiesTests.cs b/test/OpenApiTests/Capabilities/RelationshipCapabilitiesTests.cs new file mode 100644 index 0000000000..7f051fa63b --- /dev/null +++ b/test/OpenApiTests/Capabilities/RelationshipCapabilitiesTests.cs @@ -0,0 +1,220 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; + +namespace OpenApiTests.Capabilities; + +public sealed class RelationshipCapabilitiesTests : IClassFixture, CapabilitiesDbContext>> +{ + private readonly OpenApiTestContext, CapabilitiesDbContext> _testContext; + + public RelationshipCapabilitiesTests(OpenApiTestContext, CapabilitiesDbContext> testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + + testContext.SetTestOutputHelper(testOutputHelper); + testContext.SwaggerDocumentOutputDirectory = $"{GetType().Namespace!.Replace('.', '/')}/GeneratedSwagger"; + } + + [Fact] + public async Task HasOne_relationship_with_AllowView_appears_in_response_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("author"); + }); + } + + [Fact] + public async Task HasOne_relationship_without_AllowView_does_not_appear_in_response_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInArticleResponse.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("writer"); + }); + } + + [Fact] + public async Task HasOne_relationship_with_AllowSet_appears_in_create_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInCreateArticleRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("writer"); + }); + } + + [Fact] + public async Task HasOne_relationship_without_AllowSet_does_not_appear_in_create_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("author"); + }); + } + + [Fact] + public async Task HasOne_relationship_with_AllowSet_appears_in_update_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInUpdateArticleRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("writer"); + }); + } + + [Fact] + public async Task HasOne_relationship_without_AllowSet_does_not_appear_in_update_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("author"); + }); + } + + [Fact] + public async Task HasMany_relationship_with_AllowView_appears_in_response_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInArticleResponse.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("categories"); + }); + } + + [Fact] + public async Task HasMany_relationship_without_AllowView_does_not_appear_in_response_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInBookResponse.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("reviews"); + }); + } + + [Fact] + public async Task HasMany_relationship_with_AllowSet_appears_in_create_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInCreateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("reviews"); + }); + } + + [Fact] + public async Task HasMany_relationship_without_AllowSet_does_not_appear_in_create_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInCreateArticleRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("categories"); + }); + } + + [Fact] + public async Task HasMany_relationship_with_AllowSet_appears_in_update_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInUpdateBookRequest.allOf[1].properties").With(properties => + { + properties.Should().ContainProperty("reviews"); + }); + } + + [Fact] + public async Task HasMany_relationship_without_AllowSet_does_not_appear_in_update_request_schema() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.relationshipsInUpdateArticleRequest.allOf[1].properties").With(properties => + { + properties.Should().NotContainPath("categories"); + }); + } + + [Fact] + public async Task HasMany_relationship_with_AllowAdd_has_relationship_post_endpoint() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./articles/{id}/relationships/tags.post"); + } + + [Fact] + public async Task HasMany_relationship_without_AllowAdd_does_not_have_relationship_post_endpoint() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("paths./articles/{id}/relationships/categories.post"); + document.Should().NotContainPath("paths./books/{id}/relationships/reviews.post"); + } + + [Fact] + public async Task HasMany_relationship_with_AllowRemove_has_relationship_delete_endpoint() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./articles/{id}/relationships/comments.delete"); + } + + [Fact] + public async Task HasMany_relationship_without_AllowRemove_does_not_have_relationship_delete_endpoint() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("paths./articles/{id}/relationships/categories.delete"); + document.Should().NotContainPath("paths./books/{id}/relationships/reviews.delete"); + } +} \ No newline at end of file diff --git a/test/OpenApiTests/Capabilities/Review.cs b/test/OpenApiTests/Capabilities/Review.cs new file mode 100644 index 0000000000..e4941b87b6 --- /dev/null +++ b/test/OpenApiTests/Capabilities/Review.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Review : Identifiable +{ + [Attr] + public string Content { get; set; } = null!; + + [Attr] + public int Rating { get; set; } +} diff --git a/test/OpenApiTests/Capabilities/Tag.cs b/test/OpenApiTests/Capabilities/Tag.cs new file mode 100644 index 0000000000..198f8bbb1c --- /dev/null +++ b/test/OpenApiTests/Capabilities/Tag.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Tag : Identifiable +{ + [Attr] + public string Label { get; set; } = null!; +} diff --git a/test/OpenApiTests/Capabilities/Writer.cs b/test/OpenApiTests/Capabilities/Writer.cs new file mode 100644 index 0000000000..01311df477 --- /dev/null +++ b/test/OpenApiTests/Capabilities/Writer.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Writer : Identifiable +{ + [Attr] + public string PenName { get; set; } = null!; +}