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/Capabilities/Book.cs b/test/OpenApiTests/Capabilities/Book.cs new file mode 100644 index 0000000000..abe572d236 --- /dev/null +++ b/test/OpenApiTests/Capabilities/Book.cs @@ -0,0 +1,40 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.Capabilities; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.Capabilities")] +public sealed class Book : Identifiable +{ + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public string Title { get; set; } = null!; + + [Attr(Capabilities = AttrCapabilities.AllowView)] + public string Isbn { get; set; } = null!; + + [Attr(Capabilities = AttrCapabilities.AllowView)] + public DateOnly PublishedOn { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowCreate)] + public string DraftContent { get; set; } = null!; + + [Attr(Capabilities = AttrCapabilities.AllowChange)] + public string InternalNotes { get; set; } = null!; + + [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; } + + [HasMany(Capabilities = HasManyCapabilities.AllowSet)] + public ISet Reviews { get; set; } = new HashSet(); +} 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!; +}