Skip to content

Commit 449a079

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

15 files changed

+1437
-6
lines changed

.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ insert_final_newline = true
1212
[*.{csproj,json}]
1313
indent_size = 2
1414

15+
[test/OpenApiClientTests/obj/*.{cs}]
16+
# Ignore compiler warnings triggered by code auto-generated by NSWag
17+
dotnet_diagnostic.CS8625.severity = suggestion
18+
1519
[*.{cs}]
1620
#### .NET Coding Conventions ####
1721

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

+1-1
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

+1-1
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.
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+
}

test/OpenApiClientTests/OpenApiClientTests.csproj

+14-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@
1919
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetVersion)" />
2020
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
2121
<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>
2622
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="5.0.9">
2723
<PrivateAssets>all</PrivateAssets>
2824
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -62,5 +58,19 @@
6258
<CodeGenerator>NSwagCSharp</CodeGenerator>
6359
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
6460
</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>
6575
</ItemGroup>
6676
</Project>
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+
}
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+
}
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)