diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1dc9384515..750c0476bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -127,10 +127,13 @@ jobs: dotnet build --no-restore --configuration Release /p:VersionSuffix=$env:PACKAGE_VERSION_SUFFIX - name: Test run: | - dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;summary.includeSkippedTests=true" -- RunConfiguration.CollectSourceInformation=true DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DeterministicReport=true + dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;summary.includeSkippedTests=true" - name: Upload coverage to codecov.io if: matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true + verbose: true - name: Generate packages shell: pwsh run: | diff --git a/Build.ps1 b/Build.ps1 index 4854651d67..3abc926e6a 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -10,13 +10,16 @@ Write-Host "$(pwsh --version)" Write-Host "Active .NET SDK: $(dotnet --version)" Write-Host "Using version suffix: $versionSuffix" +Remove-Item -Recurse -Force artifacts -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force * -Include coverage.cobertura.xml + dotnet tool restore VerifySuccessExitCode dotnet build --configuration Release /p:VersionSuffix=$versionSuffix VerifySuccessExitCode -dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DeterministicReport=true +dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" VerifySuccessExitCode dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs diff --git a/Directory.Build.props b/Directory.Build.props index 73e1b54135..90de08a271 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -53,6 +53,7 @@ false false $(MSBuildThisFileDirectory)CodingGuidelines.ruleset + $(MSBuildThisFileDirectory)tests.runsettings 5.4.1 diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index 2f8e9f9127..e10df8567a 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CodingGuidelines.ruleset = CodingGuidelines.ruleset CSharpGuidelinesAnalyzer.config = CSharpGuidelinesAnalyzer.config Directory.Build.props = Directory.Build.props + tests.runsettings = tests.runsettings EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}" diff --git a/test/NoEntityFrameworkTests/TagTests.cs b/test/NoEntityFrameworkTests/TagTests.cs new file mode 100644 index 0000000000..83842b7725 --- /dev/null +++ b/test/NoEntityFrameworkTests/TagTests.cs @@ -0,0 +1,229 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using NoEntityFrameworkExample.Models; +using TestBuildingBlocks; +using Xunit; + +namespace NoEntityFrameworkTests; + +public sealed class TagTests : IntegrationTest, IClassFixture> +{ + private readonly NoLoggingWebApplicationFactory _factory; + + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = _factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + + public TagTests(NoLoggingWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task Can_get_primary_resources() + { + // Arrange + const string route = "/api/tags"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(3); + + responseDocument.Meta.Should().ContainTotal(3); + } + + [Fact] + public async Task Can_filter_in_primary_resources() + { + // Arrange + const string route = "/api/tags?filter=equals(name,'Personal')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Personal")); + + responseDocument.Meta.Should().ContainTotal(1); + } + + [Fact] + public async Task Can_filter_in_related_resources() + { + // Arrange + const string route = "/api/tags?filter=has(todoItems,equals(description,'Check emails'))"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Business")); + + responseDocument.Meta.Should().ContainTotal(1); + } + + [Fact] + public async Task Can_sort_on_attribute_in_primary_resources() + { + // Arrange + const string route = "/api/tags?sort=-id"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be("3"); + responseDocument.Data.ManyValue[1].Id.Should().Be("2"); + responseDocument.Data.ManyValue[2].Id.Should().Be("1"); + } + + [Fact] + public async Task Can_sort_on_count_in_primary_resources() + { + // Arrange + const string route = "/api/tags?sort=-count(todoItems),id"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be("1"); + responseDocument.Data.ManyValue[1].Id.Should().Be("2"); + responseDocument.Data.ManyValue[2].Id.Should().Be("3"); + } + + [Fact] + public async Task Can_paginate_in_primary_resources() + { + // Arrange + const string route = "/api/tags?page[size]=1&page[number]=2&sort=id"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Family")); + + responseDocument.Meta.Should().ContainTotal(3); + } + + [Fact] + public async Task Can_select_fields_in_primary_resources() + { + // Arrange + const string route = "/api/tags?fields[tags]=todoItems"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldNotBeEmpty(); + responseDocument.Data.ManyValue.Should().AllSatisfy(resource => resource.Attributes.Should().BeNull()); + responseDocument.Data.ManyValue.Should().AllSatisfy(resource => resource.Relationships.ShouldOnlyContainKeys("todoItems")); + } + + [Fact] + public async Task Can_include_in_primary_resources() + { + // Arrange + const string route = "/api/tags?include=todoItems.owner"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().NotBeEmpty(); + responseDocument.Included.Should().NotBeEmpty(); + } + + [Fact] + public async Task Can_get_primary_resource() + { + // Arrange + const string route = "/api/tags/1"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("1"); + } + + [Fact] + public async Task Can_get_secondary_resources() + { + // Arrange + const string route = "/api/tags/1/todoItems?sort=id"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Make homework")); + responseDocument.Data.ManyValue[1].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Book vacation")); + responseDocument.Data.ManyValue[2].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Cook dinner")); + + responseDocument.Meta.Should().ContainTotal(3); + } + + [Fact] + public async Task Can_get_ToMany_relationship() + { + // Arrange + const string route = "/api/tags/2/relationships/todoItems"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be("3"); + + responseDocument.Meta.Should().ContainTotal(1); + } + + protected override HttpClient CreateClient() + { + return _factory.CreateClient(); + } +} diff --git a/test/TestBuildingBlocks/AssemblyInfo.cs b/test/TestBuildingBlocks/AssemblyInfo.cs index 82d12912a4..2af69d7b71 100644 --- a/test/TestBuildingBlocks/AssemblyInfo.cs +++ b/test/TestBuildingBlocks/AssemblyInfo.cs @@ -1,4 +1,4 @@ using System.Diagnostics.CodeAnalysis; -// https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#excluding-from-coverage +// Justification: This assembly contains building blocks for writing tests. It does not contain code that ships. [assembly: ExcludeFromCodeCoverage] diff --git a/tests.runsettings b/tests.runsettings new file mode 100644 index 0000000000..db83eb983e --- /dev/null +++ b/tests.runsettings @@ -0,0 +1,16 @@ + + + + true + + + + + + ObsoleteAttribute,GeneratedCodeAttribute + true + + + + +