Skip to content

Commit e9feae4

Browse files
committed
Added OpenApi client tests for nullable and required properties
1 parent f7194d9 commit e9feae4

14 files changed

+1489
-62
lines changed

test/OpenApiClientTests/LegacyClient/ApiResponse.cs renamed to test/OpenApiClientTests/ApiResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
#pragma warning disable AV1008 // Class should not be static
55

6-
namespace OpenApiClientTests.LegacyClient;
6+
namespace OpenApiClientTests;
77

88
internal static class ApiResponse
99
{

test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs renamed to test/OpenApiClientTests/FakeHttpClientWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using System.Text;
44
using JsonApiDotNetCore.OpenApi.Client;
55

6-
namespace OpenApiClientTests.LegacyClient;
6+
namespace OpenApiClientTests;
77

88
/// <summary>
99
/// Enables to inject an outgoing response body and inspect the incoming request.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.OpenApi.Client;
3+
using Swashbuckle.AspNetCore.SwaggerGen;
4+
5+
namespace OpenApiClientTests;
6+
7+
internal static class MemberInfoExtensions
8+
{
9+
public static TypeCategory GetTypeCategory(this MemberInfo source)
10+
{
11+
ArgumentGuard.NotNull(source, nameof(source));
12+
13+
Type memberType;
14+
15+
if (source.MemberType.HasFlag(MemberTypes.Field))
16+
{
17+
memberType = ((FieldInfo)source).FieldType;
18+
}
19+
else if (source.MemberType.HasFlag(MemberTypes.Property))
20+
{
21+
memberType = ((PropertyInfo)source).PropertyType;
22+
}
23+
else
24+
{
25+
throw new NotSupportedException($"Member type '{source.MemberType}' must be a property or field.");
26+
}
27+
28+
if (memberType.IsValueType)
29+
{
30+
return Nullable.GetUnderlyingType(memberType) != null ? TypeCategory.NullableValueType : TypeCategory.ValueType;
31+
}
32+
33+
// Once we switch to .NET 6, we should rely instead on the built-in reflection APIs for nullability information.
34+
// See https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#libraries-reflection-apis-for-nullability-information.
35+
return source.IsNonNullableReferenceType() ? TypeCategory.NonNullableReferenceType : TypeCategory.NullableReferenceType;
36+
}
37+
}
Lines changed: 70 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,76 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2-
<PropertyGroup>
3-
<TargetFramework>$(TargetFrameworkName)</TargetFramework>
4-
</PropertyGroup>
2+
<PropertyGroup>
3+
<TargetFramework>$(TargetFrameworkName)</TargetFramework>
4+
</PropertyGroup>
55

6-
<ItemGroup>
7-
<None Update="xunit.runner.json">
8-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
9-
</None>
10-
</ItemGroup>
6+
<ItemGroup>
7+
<None Update="xunit.runner.json">
8+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
9+
</None>
10+
</ItemGroup>
1111

12-
<ItemGroup>
13-
<ProjectReference Include="..\..\src\JsonApiDotNetCore.OpenApi.Client\JsonApiDotNetCore.OpenApi.Client.csproj" />
14-
<ProjectReference Include="..\TestBuildingBlocks\TestBuildingBlocks.csproj" />
15-
</ItemGroup>
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\JsonApiDotNetCore.OpenApi.Client\JsonApiDotNetCore.OpenApi.Client.csproj" />
14+
<ProjectReference Include="..\TestBuildingBlocks\TestBuildingBlocks.csproj" />
15+
</ItemGroup>
1616

17-
<ItemGroup>
18-
<PackageReference Include="coverlet.collector" Version="$(CoverletVersion)" PrivateAssets="All" />
19-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetVersion)" />
20-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
21-
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
22-
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.10.9">
23-
<PrivateAssets>all</PrivateAssets>
24-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25-
</PackageReference>
26-
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="5.0.9">
27-
<PrivateAssets>all</PrivateAssets>
28-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29-
</PackageReference>
30-
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.13.2">
31-
<PrivateAssets>all</PrivateAssets>
32-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
33-
</PackageReference>
34-
</ItemGroup>
17+
<ItemGroup>
18+
<PackageReference Include="coverlet.collector" Version="$(CoverletVersion)" PrivateAssets="All" />
19+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetVersion)" />
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
21+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
22+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="5.0.9">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.13.2">
27+
<PrivateAssets>all</PrivateAssets>
28+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29+
</PackageReference>
30+
</ItemGroup>
3531

36-
<ItemGroup>
37-
<OpenApiReference Include="LegacyClient\swagger.g.json">
38-
<Namespace>OpenApiClientTests.LegacyClient.GeneratedCode</Namespace>
39-
<ClassName>OpenApiClient</ClassName>
40-
<OutputPath>OpenApiClient.cs</OutputPath>
41-
<CodeGenerator>NSwagCSharp</CodeGenerator>
42-
<Options>/UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
43-
</OpenApiReference>
44-
<OpenApiReference Include="NamingConventions\KebabCase\swagger.g.json">
45-
<Namespace>OpenApiClientTests.NamingConventions.KebabCase.GeneratedCode</Namespace>
46-
<ClassName>KebabCaseClient</ClassName>
47-
<OutputPath>KebabCaseClient.cs</OutputPath>
48-
<CodeGenerator>NSwagCSharp</CodeGenerator>
49-
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
50-
</OpenApiReference>
51-
<OpenApiReference Include="NamingConventions\CamelCase\swagger.g.json">
52-
<Namespace>OpenApiClientTests.NamingConventions.CamelCase.GeneratedCode</Namespace>
53-
<ClassName>CamelCaseClient</ClassName>
54-
<OutputPath>CamelCaseClient.cs</OutputPath>
55-
<CodeGenerator>NSwagCSharp</CodeGenerator>
56-
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
57-
</OpenApiReference>
58-
<OpenApiReference Include="NamingConventions\PascalCase\swagger.g.json">
59-
<Namespace>OpenApiClientTests.NamingConventions.PascalCase.GeneratedCode</Namespace>
60-
<ClassName>PascalCaseClient</ClassName>
61-
<OutputPath>PascalCaseClient.cs</OutputPath>
62-
<CodeGenerator>NSwagCSharp</CodeGenerator>
63-
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
64-
</OpenApiReference>
65-
</ItemGroup>
32+
<ItemGroup>
33+
<OpenApiReference Include="LegacyClient\swagger.g.json">
34+
<Namespace>OpenApiClientTests.LegacyClient.GeneratedCode</Namespace>
35+
<ClassName>OpenApiClient</ClassName>
36+
<OutputPath>OpenApiClient.cs</OutputPath>
37+
<CodeGenerator>NSwagCSharp</CodeGenerator>
38+
<Options>/UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
39+
</OpenApiReference>
40+
<OpenApiReference Include="NamingConventions\KebabCase\swagger.g.json">
41+
<Namespace>OpenApiClientTests.NamingConventions.KebabCase.GeneratedCode</Namespace>
42+
<ClassName>KebabCaseClient</ClassName>
43+
<OutputPath>KebabCaseClient.cs</OutputPath>
44+
<CodeGenerator>NSwagCSharp</CodeGenerator>
45+
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
46+
</OpenApiReference>
47+
<OpenApiReference Include="NamingConventions\CamelCase\swagger.g.json">
48+
<Namespace>OpenApiClientTests.NamingConventions.CamelCase.GeneratedCode</Namespace>
49+
<ClassName>CamelCaseClient</ClassName>
50+
<OutputPath>CamelCaseClient.cs</OutputPath>
51+
<CodeGenerator>NSwagCSharp</CodeGenerator>
52+
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
53+
</OpenApiReference>
54+
<OpenApiReference Include="NamingConventions\PascalCase\swagger.g.json">
55+
<Namespace>OpenApiClientTests.NamingConventions.PascalCase.GeneratedCode</Namespace>
56+
<ClassName>PascalCaseClient</ClassName>
57+
<OutputPath>PascalCaseClient.cs</OutputPath>
58+
<CodeGenerator>NSwagCSharp</CodeGenerator>
59+
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
60+
</OpenApiReference>
61+
<OpenApiReference Include="SchemaProperties\NullableReferenceTypesEnabled\swagger.g.json">
62+
<Namespace>OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode</Namespace>
63+
<ClassName>NullableReferenceTypesEnabledClient</ClassName>
64+
<OutputPath>NullableReferenceTypesEnabledClient.cs</OutputPath>
65+
<CodeGenerator>NSwagCSharp</CodeGenerator>
66+
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true</Options>
67+
</OpenApiReference>
68+
<OpenApiReference Include="SchemaProperties\NullableReferenceTypesDisabled\swagger.g.json">
69+
<Namespace>OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode</Namespace>
70+
<ClassName>NullableReferenceTypesDisabledClient</ClassName>
71+
<OutputPath>NullableReferenceTypesDisabledClient.cs</OutputPath>
72+
<CodeGenerator>NSwagCSharp</CodeGenerator>
73+
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false</Options>
74+
</OpenApiReference>
75+
</ItemGroup>
6676
</Project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Reflection;
2+
using FluentAssertions;
3+
using FluentAssertions.Types;
4+
5+
namespace OpenApiClientTests;
6+
7+
internal static class PropertyInfoAssertionsExtensions
8+
{
9+
[CustomAssertion]
10+
public static void BeNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
11+
{
12+
MemberInfo memberInfo = source.Subject;
13+
14+
TypeCategory typeCategory = memberInfo.GetTypeCategory();
15+
16+
typeCategory.Should().Match(category => category == TypeCategory.NullableReferenceType || category == TypeCategory.NullableValueType, because,
17+
becauseArgs);
18+
}
19+
20+
[CustomAssertion]
21+
public static void BeNonNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
22+
{
23+
MemberInfo memberInfo = source.Subject;
24+
25+
TypeCategory typeCategory = memberInfo.GetTypeCategory();
26+
27+
typeCategory.Should().Match(category => category == TypeCategory.NonNullableReferenceType || category == TypeCategory.ValueType, because, becauseArgs);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using JsonApiDotNetCore.OpenApi.Client;
2+
using Newtonsoft.Json;
3+
4+
namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
5+
6+
internal partial class NullableReferenceTypesDisabledClient : JsonApiClient
7+
{
8+
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
9+
{
10+
SetSerializerSettingsForJsonApi(settings);
11+
12+
settings.Formatting = Formatting.Indented;
13+
}
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Reflection;
2+
using FluentAssertions;
3+
using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
4+
using Xunit;
5+
6+
namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled;
7+
8+
public sealed class NullabilityTests
9+
{
10+
[Fact]
11+
public void Nullability_of_generated_types_is_as_expected()
12+
{
13+
PropertyInfo[] propertyInfos = typeof(ChickenAttributesInResponse).GetProperties();
14+
15+
PropertyInfo? propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Name));
16+
propertyInfo.Should().BeNullable();
17+
18+
propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.NameOfCurrentFarm));
19+
propertyInfo.Should().BeNullable();
20+
21+
propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Age));
22+
propertyInfo.Should().BeNonNullable();
23+
24+
propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.TimeAtCurrentFarmInDays));
25+
propertyInfo.Should().BeNullable();
26+
}
27+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System.Net;
2+
using FluentAssertions;
3+
using FluentAssertions.Specialized;
4+
using JsonApiDotNetCore.Middleware;
5+
using Microsoft.Net.Http.Headers;
6+
using Newtonsoft.Json;
7+
using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
8+
using TestBuildingBlocks;
9+
using Xunit;
10+
using NullableReferenceTypesDisabledClient = OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode.NullableReferenceTypesDisabledClient;
11+
12+
namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled;
13+
14+
public sealed class RequiredAttributesTests
15+
{
16+
private const string HostPrefix = "http://localhost/";
17+
18+
[Fact]
19+
public async Task Partial_posting_resource_with_explicitly_omitting_required_fields_produces_expected_request()
20+
{
21+
// Arrange
22+
using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
23+
var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
24+
25+
var requestDocument = new ChickenPostRequestDocument
26+
{
27+
Data = new ChickenDataInPostRequest
28+
{
29+
Attributes = new ChickenAttributesInPostRequest
30+
{
31+
HasProducedEggs = true
32+
}
33+
}
34+
};
35+
36+
using (apiClient.RegisterAttributesForRequestDocument<ChickenPostRequestDocument, ChickenAttributesInPostRequest>(requestDocument,
37+
chicken => chicken.HasProducedEggs))
38+
{
39+
// Act
40+
await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument));
41+
}
42+
43+
// Assert
44+
wrapper.Request.ShouldNotBeNull();
45+
wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType);
46+
wrapper.Request.Method.Should().Be(HttpMethod.Post);
47+
wrapper.Request.RequestUri.Should().Be(HostPrefix + "chickens");
48+
wrapper.Request.Content.Should().NotBeNull();
49+
wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull();
50+
wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType);
51+
52+
wrapper.RequestBody.Should().BeJson(@"{
53+
""data"": {
54+
""type"": ""chickens"",
55+
""attributes"": {
56+
""hasProducedEggs"": true
57+
}
58+
}
59+
}");
60+
}
61+
62+
[Fact]
63+
public async Task Partial_posting_resource_without_explicitly_omitting_required_fields_fails()
64+
{
65+
// Arrange
66+
using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
67+
var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
68+
69+
var requestDocument = new ChickenPostRequestDocument
70+
{
71+
Data = new ChickenDataInPostRequest
72+
{
73+
Attributes = new ChickenAttributesInPostRequest
74+
{
75+
Weight = 3
76+
}
77+
}
78+
};
79+
80+
// Act
81+
Func<Task<ChickenPrimaryResponseDocument?>> action = async () =>
82+
await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument));
83+
84+
// Assert
85+
ExceptionAssertions<JsonSerializationException> assertion = await action.Should().ThrowExactlyAsync<JsonSerializationException>();
86+
JsonSerializationException exception = assertion.Subject.Single();
87+
88+
exception.Message.Should().Be("Cannot write a null value for property 'nameOfCurrentFarm'. Property requires a value. Path 'data.attributes'.");
89+
}
90+
91+
[Fact]
92+
public async Task Patching_resource_with_missing_id_fails()
93+
{
94+
// Arrange
95+
using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
96+
var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
97+
98+
var requestDocument = new ChickenPatchRequestDocument
99+
{
100+
Data = new ChickenDataInPatchRequest
101+
{
102+
Attributes = new ChickenAttributesInPatchRequest
103+
{
104+
Age = 1
105+
}
106+
}
107+
};
108+
109+
Func<Task> action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchChickenAsync(1, requestDocument));
110+
111+
// Assert
112+
await action.Should().ThrowAsync<JsonSerializationException>();
113+
}
114+
}

0 commit comments

Comments
 (0)