diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md index 7250bb55ce..7596300fb2 100644 --- a/docs/usage/openapi-client.md +++ b/docs/usage/openapi-client.md @@ -2,46 +2,56 @@ You can generate a JSON:API client in various programming languages from the [OpenAPI specification](https://swagger.io/specification/) file that JsonApiDotNetCore APIs provide. -For C# .NET clients generated using [NSwag](https://github.com/RicoSuter/NSwag), we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". +For C# .NET clients generated using [NSwag](https://github.com/RicoSuter/NSwag), we provide an additional package +that introduces support for partial PATCH/POST requests. The concern here is that a property on a generated C# class +being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". ## Getting started ### Visual Studio -The easiest way to get started is by using the built-in capabilities of Visual Studio. The next steps describe how to generate a JSON:API client library and use our package. +The easiest way to get started is by using the built-in capabilities of Visual Studio. +The next steps describe how to generate a JSON:API client library and use our package. 1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**. 2. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`. - Optionally provide a class name and namespace and click **Finish**. - Visual Studio now downloads your swagger.json and updates your project file. This results in a pre-build step that generates the client code. + Specify `ExampleApiClient` as class name, optionally provide a namespace and click **Finish**. + Visual Studio now downloads your swagger.json and updates your project file. + This adds a pre-build step that generates the client code. - Tip: To later re-download swagger.json and regenerate the client code, right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon. -3. Although not strictly required, we recommend to run package update now, which fixes some issues and removes the `Stream` parameter from generated calls. + > [!TIP] + > To later re-download swagger.json and regenerate the client code, + > right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon. -4. Add some demo code that calls one of your JSON:API endpoints. For example: +3. Although not strictly required, we recommend to run package update now, which fixes some issues. + +4. Add code that calls one of your JSON:API endpoints. ```c# using var httpClient = new HttpClient(); var apiClient = new ExampleApiClient("http://localhost:14140", httpClient); - PersonCollectionResponseDocument getResponse = - await apiClient.GetPersonCollectionAsync(); + PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(); foreach (PersonDataInResponse person in getResponse.Data) { - Console.WriteLine($"Found user {person.Id} named " + - $"'{person.Attributes.FirstName} {person.Attributes.LastName}'."); + Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName}"); } ``` 5. Add our client package to your project: - ``` - dotnet add package JsonApiDotNetCore.OpenApi.Client - ``` + ``` + dotnet add package JsonApiDotNetCore.OpenApi.Client + ``` + +6. Add the following glue code to connect our package with your generated code. -6. Add the following glue code to connect our package with your generated code. The code below assumes you specified `ExampleApiClient` as class name in step 2. + > [!NOTE] + > The class name must be the same as specified in step 2. + > If you also specified a namespace, put this class in the same namespace. + > For example, add `namespace GeneratedCode;` below the `using` lines. ```c# using JsonApiDotNetCore.OpenApi.Client; @@ -56,6 +66,9 @@ The easiest way to get started is by using the built-in capabilities of Visual S } ``` + > [!TIP] + > The project at src/Examples/JsonApiDotNetCoreExampleClient contains an enhanced version that logs the HTTP requests and responses. + 7. Extend your demo code to send a partial PATCH request with the help of our package: ```c# @@ -66,17 +79,16 @@ The easiest way to get started is by using the built-in capabilities of Visual S Id = "1", Attributes = new PersonAttributesInPatchRequest { - FirstName = "Jack" + LastName = "Doe" } } }; - // This line results in sending "lastName: null" instead of omitting it. - using (apiClient.RegisterAttributesForRequestDocument(patchRequest, person => person.LastName)) + // This line results in sending "firstName: null" instead of omitting it. + using (apiClient.WithPartialAttributeSerialization(patchRequest, + person => person.FirstName)) { - PersonPrimaryResponseDocument patchResponse = - await apiClient.PatchPersonAsync("1", patchRequest); + await TranslateAsync(async () => await apiClient.PatchPersonAsync(1, patchRequest)); // The sent request looks like this: // { @@ -84,12 +96,26 @@ The easiest way to get started is by using the built-in capabilities of Visual S // "type": "people", // "id": "1", // "attributes": { - // "firstName": "Jack", - // "lastName": null + // "firstName": null, + // "lastName": "Doe" // } // } // } } + + static async Task TranslateAsync(Func> operation) + where TResponse : class + { + try + { + return await operation(); + } + catch (ApiException exception) when (exception.StatusCode == 204) + { + // Workaround for https://github.com/RicoSuter/NSwag/issues/2499 + return null; + } + } ``` ### Other IDEs @@ -100,12 +126,12 @@ Alternatively, the next section shows what to add to your client project file di ```xml - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/docs/usage/openapi.md b/docs/usage/openapi.md index f0880e1c5f..fb37a60954 100644 --- a/docs/usage/openapi.md +++ b/docs/usage/openapi.md @@ -16,9 +16,10 @@ JsonApiDotNetCore provides an extension package that enables you to produce an [ ```c# IMvcCoreBuilder mvcCoreBuilder = builder.Services.AddMvcCore(); + // Include the mvcBuilder parameter. builder.Services.AddJsonApi(mvcBuilder: mvcCoreBuilder); - // Configures Swashbuckle for JSON:API. + // Configure Swashbuckle for JSON:API. builder.Services.AddOpenApi(mvcCoreBuilder); var app = builder.Build(); @@ -26,7 +27,7 @@ JsonApiDotNetCore provides an extension package that enables you to produce an [ app.UseRouting(); app.UseJsonApi(); - // Adds the Swashbuckle middleware. + // Add the Swashbuckle middleware. app.UseSwagger(); ``` @@ -34,7 +35,7 @@ By default, the OpenAPI specification will be available at `http://localhost: {request}"); + string? requestBody = request.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + + if (!string.IsNullOrEmpty(requestBody)) + { + Console.WriteLine(); + Console.WriteLine(requestBody); + } + } + + // Optional: Log incoming response to the console. + partial void ProcessResponse(HttpClient client, HttpResponseMessage response) + { + using var _ = new UsingConsoleColor(ConsoleColor.Cyan); + + Console.WriteLine($"<-- {response}"); + string responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + if (!string.IsNullOrEmpty(responseBody)) + { + Console.WriteLine(); + Console.WriteLine(responseBody); + } + } + + private sealed class UsingConsoleColor : IDisposable + { + public UsingConsoleColor(ConsoleColor foregroundColor) + { + Console.ForegroundColor = foregroundColor; + } + + public void Dispose() + { + Console.ResetColor(); + } + } } diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json b/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json index 7b5139257e..797f5e7708 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json +++ b/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json @@ -2051,6 +2051,10 @@ "additionalProperties": false }, "personAttributesInResponse": { + "required": [ + "displayName", + "lastName" + ], "type": "object", "properties": { "firstName": { @@ -2341,6 +2345,9 @@ "additionalProperties": false }, "tagAttributesInResponse": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -2715,6 +2722,10 @@ "additionalProperties": false }, "todoItemAttributesInResponse": { + "required": [ + "description", + "priority" + ], "type": "object", "properties": { "description": { @@ -2970,6 +2981,9 @@ "additionalProperties": false }, "todoItemRelationshipsInResponse": { + "required": [ + "owner" + ], "type": "object", "properties": { "owner": { diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index 4ca5e5f58d..1ef318fa65 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -1,26 +1,47 @@ -namespace JsonApiDotNetCoreExampleClient; +using JsonApiDotNetCoreExampleClient; -internal static class Program -{ - private const string BaseUrl = "http://localhost:14140"; +using var httpClient = new HttpClient(); +var apiClient = new ExampleApiClient("http://localhost:14140", httpClient); - private static async Task Main() - { - using var httpClient = new HttpClient(); +PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(); - ExampleApiClient exampleApiClient = new(BaseUrl, httpClient); +foreach (PersonDataInResponse person in getResponse.Data) +{ + Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName}"); +} - try - { - const int nonExistingId = int.MaxValue; - await exampleApiClient.DeletePersonAsync(nonExistingId); - } - catch (ApiException exception) +var patchRequest = new PersonPatchRequestDocument +{ + Data = new PersonDataInPatchRequest + { + Id = "1", + Attributes = new PersonAttributesInPatchRequest { - Console.WriteLine(exception.Response); + LastName = "Doe" } + } +}; - Console.WriteLine("Press any key to close."); - Console.ReadKey(); +// This line results in sending "firstName: null" instead of omitting it. +using (apiClient.WithPartialAttributeSerialization(patchRequest, person => person.FirstName)) +{ + await TranslateAsync(async () => await apiClient.PatchPersonAsync(1, patchRequest)); +} + +Console.WriteLine("Press any key to close."); +Console.ReadKey(); + +// ReSharper disable once UnusedLocalFunctionReturnValue +static async Task TranslateAsync(Func> operation) + where TResponse : class +{ + try + { + return await operation(); + } + catch (ApiException exception) when (exception.StatusCode == 204) + { + // Workaround for https://github.com/RicoSuter/NSwag/issues/2499 + return null; } } diff --git a/src/JsonApiDotNetCore.OpenApi.Client/Exceptions/ApiException.cs b/src/JsonApiDotNetCore.OpenApi.Client/Exceptions/ApiException.cs index 0c159dab05..314ea0cea1 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/Exceptions/ApiException.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/Exceptions/ApiException.cs @@ -14,7 +14,7 @@ public sealed class ApiException : Exception public IReadOnlyDictionary> Headers { get; } - public ApiException(string message, int statusCode, string? response, IReadOnlyDictionary> headers, Exception innerException) + public ApiException(string message, int statusCode, string? response, IReadOnlyDictionary> headers, Exception? innerException) : base($"{message}\n\nStatus: {statusCode}\nResponse: \n{response ?? "(null)"}", innerException) { StatusCode = statusCode; diff --git a/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs index a994d78a0e..9c71e32380 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs @@ -5,17 +5,19 @@ namespace JsonApiDotNetCore.OpenApi.Client; public interface IJsonApiClient { /// - /// Ensures correct serialization of attributes in a POST/PATCH Resource request body. In JSON:API, an omitted attribute indicates to ignore it, while an - /// attribute that is set to "null" means to clear it. This poses a problem because the serializer cannot distinguish between "you have explicitly set - /// this .NET property to null" vs "you didn't touch it, so it is null by default" when converting an instance to JSON. Therefore, calling this method - /// treats all attributes that contain their default value (null for reference types, 0 for integers, false for booleans, etc) as - /// omitted unless explicitly listed to include them using . + /// Ensures correct serialization of JSON:API attributes in the request body of a POST/PATCH request at a resource endpoint. Properties with default + /// values are omitted, unless explicitly included using + /// + /// In JSON:API, an omitted attribute indicates to ignore it, while an attribute that is set to null means to clear it. This poses a problem, + /// because the serializer cannot distinguish between "you have explicitly set this .NET property to its default value" vs "you didn't touch it, so it + /// contains its default value" when converting to JSON. + /// /// /// - /// The request document instance for which this registration applies. + /// The request document instance for which default values should be omitted. /// /// - /// Optional. A list of expressions to indicate which properties to unconditionally include in the JSON request body. For example: + /// Optional. A list of lambda expressions that indicate which properties to always include in the JSON request body. For example: /// video.Title, video => video.Summary /// ]]> @@ -28,9 +30,10 @@ public interface IJsonApiClient /// /// /// An to clear the current registration. For efficient memory usage, it is recommended to wrap calls to this method in a - /// using statement, so the registrations are cleaned up after executing the request. + /// using statement, so the registrations are cleaned up after executing the request. After disposal, the client can be reused without the + /// registrations added earlier. /// - IDisposable RegisterAttributesForRequestDocument(TRequestDocument requestDocument, + IDisposable WithPartialAttributeSerialization(TRequestDocument requestDocument, params Expression>[] alwaysIncludedAttributeSelectors) where TRequestDocument : class; } diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs index deadccce07..4702c8af7c 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs @@ -1,29 +1,29 @@ using System.Linq.Expressions; using System.Reflection; -using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.OpenApi.Client; /// -/// Base class to inherit auto-generated client from. Enables to mark fields to be explicitly included in a request body, even if they are null or -/// default. +/// Base class to inherit auto-generated OpenAPI clients from. Provides support for partial POST/PATCH in JSON:API requests. /// -[PublicAPI] public abstract class JsonApiClient : IJsonApiClient { - private readonly JsonApiJsonConverter _jsonApiJsonConverter = new(); + private readonly DocumentJsonConverter _documentJsonConverter = new(); + /// + /// Initial setup. Call this from the UpdateJsonSerializerSettings partial method in the auto-generated OpenAPI client. + /// protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings) { ArgumentGuard.NotNull(settings); - settings.Converters.Add(_jsonApiJsonConverter); + settings.Converters.Add(_documentJsonConverter); } /// - public IDisposable RegisterAttributesForRequestDocument(TRequestDocument requestDocument, + public IDisposable WithPartialAttributeSerialization(TRequestDocument requestDocument, params Expression>[] alwaysIncludedAttributeSelectors) where TRequestDocument : class { @@ -39,13 +39,15 @@ public IDisposable RegisterAttributesForRequestDocument article.Title'."); + throw new ArgumentException($"The expression '{selector}' should select a single property. For example: 'article => article.Title'.", + nameof(alwaysIncludedAttributeSelectors)); } } - _jsonApiJsonConverter.RegisterRequestDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject))); + var alwaysIncludedAttributes = new AlwaysIncludedAttributes(attributeNames, typeof(TAttributesObject)); + _documentJsonConverter.RegisterDocument(requestDocument, alwaysIncludedAttributes); - return new AttributesRegistrationScope(_jsonApiJsonConverter, requestDocument); + return new DocumentRegistrationScope(_documentJsonConverter, requestDocument); } private static Expression RemoveConvert(Expression expression) @@ -65,40 +67,95 @@ private static Expression RemoveConvert(Expression expression) } } - private sealed class JsonApiJsonConverter : JsonConverter + /// + /// Tracks a JSON:API attributes registration for a JSON:API document instance in the serializer. Disposing removes the registration, so the client can + /// be reused. + /// + private sealed class DocumentRegistrationScope : IDisposable { - private readonly Dictionary _alwaysIncludedAttributesPerRequestDocumentInstance = new(); - private readonly Dictionary> _requestDocumentInstancesPerRequestDocumentType = new(); + private readonly DocumentJsonConverter _documentJsonConverter; + private readonly object _document; + + public DocumentRegistrationScope(DocumentJsonConverter documentJsonConverter, object document) + { + ArgumentGuard.NotNull(documentJsonConverter); + ArgumentGuard.NotNull(document); + + _documentJsonConverter = documentJsonConverter; + _document = document; + } + + public void Dispose() + { + _documentJsonConverter.UnRegisterDocument(_document); + } + } + + /// + /// Represents the set of JSON:API attributes to always send to the server, even if they are uninitialized (contain default value). + /// + private sealed class AlwaysIncludedAttributes + { + private readonly ISet _propertyNames; + private readonly Type _attributesObjectType; + + public AlwaysIncludedAttributes(ISet propertyNames, Type attributesObjectType) + { + ArgumentGuard.NotNull(propertyNames); + ArgumentGuard.NotNull(attributesObjectType); + + _propertyNames = propertyNames; + _attributesObjectType = attributesObjectType; + } + + public bool ContainsAttribute(string propertyName) + { + return _propertyNames.Contains(propertyName); + } + + public bool IsAttributesObjectType(Type type) + { + return _attributesObjectType == type; + } + } + + /// + /// A that acts on JSON:API documents. + /// + private sealed class DocumentJsonConverter : JsonConverter + { + private readonly Dictionary _alwaysIncludedAttributesByDocument = new(); + private readonly Dictionary> _documentsByType = new(); private bool _isSerializing; public override bool CanRead => false; - public void RegisterRequestDocument(object requestDocument, AttributeNamesContainer attributes) + public void RegisterDocument(object document, AlwaysIncludedAttributes alwaysIncludedAttributes) { - _alwaysIncludedAttributesPerRequestDocumentInstance[requestDocument] = attributes; + _alwaysIncludedAttributesByDocument[document] = alwaysIncludedAttributes; - Type requestDocumentType = requestDocument.GetType(); + Type documentType = document.GetType(); - if (!_requestDocumentInstancesPerRequestDocumentType.ContainsKey(requestDocumentType)) + if (!_documentsByType.ContainsKey(documentType)) { - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType] = new HashSet(); + _documentsByType[documentType] = new HashSet(); } - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Add(requestDocument); + _documentsByType[documentType].Add(document); } - public void RemoveAttributeRegistration(object requestDocument) + public void UnRegisterDocument(object document) { - if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(requestDocument)) + if (_alwaysIncludedAttributesByDocument.ContainsKey(document)) { - _alwaysIncludedAttributesPerRequestDocumentInstance.Remove(requestDocument); + _alwaysIncludedAttributesByDocument.Remove(document); - Type requestDocumentType = requestDocument.GetType(); - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Remove(requestDocument); + Type documentType = document.GetType(); + _documentsByType[documentType].Remove(document); - if (!_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Any()) + if (!_documentsByType[documentType].Any()) { - _requestDocumentInstancesPerRequestDocumentType.Remove(requestDocumentType); + _documentsByType.Remove(documentType); } } } @@ -107,12 +164,18 @@ public override bool CanConvert(Type objectType) { ArgumentGuard.NotNull(objectType); - return !_isSerializing && _requestDocumentInstancesPerRequestDocumentType.ContainsKey(objectType); + if (_isSerializing) + { + // Protect against infinite recursion. + return false; + } + + return _documentsByType.ContainsKey(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - throw new Exception("This code should not be reachable."); + throw new UnreachableCodeException(); } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) @@ -122,9 +185,10 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer if (value != null) { - if (_alwaysIncludedAttributesPerRequestDocumentInstance.TryGetValue(value, out AttributeNamesContainer? attributeNamesContainer)) + if (_alwaysIncludedAttributesByDocument.TryGetValue(value, out AlwaysIncludedAttributes? alwaysIncludedAttributes)) { - serializer.ContractResolver = new JsonApiDocumentContractResolver(attributeNamesContainer); + var attributesJsonConverter = new AttributesJsonConverter(alwaysIncludedAttributes); + serializer.Converters.Add(attributesJsonConverter); } try @@ -140,81 +204,148 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer } } - private sealed class AttributeNamesContainer + /// + /// A that acts on JSON:API attribute objects. + /// + private sealed class AttributesJsonConverter : JsonConverter { - private readonly ISet _attributeNames; - private readonly Type _containerType; + private readonly AlwaysIncludedAttributes _alwaysIncludedAttributes; + private bool _isSerializing; + + public override bool CanRead => false; - public AttributeNamesContainer(ISet attributeNames, Type containerType) + public AttributesJsonConverter(AlwaysIncludedAttributes alwaysIncludedAttributes) { - ArgumentGuard.NotNull(attributeNames); - ArgumentGuard.NotNull(containerType); + ArgumentGuard.NotNull(alwaysIncludedAttributes); - _attributeNames = attributeNames; - _containerType = containerType; + _alwaysIncludedAttributes = alwaysIncludedAttributes; } - public bool ContainsAttribute(string name) + public override bool CanConvert(Type objectType) { - return _attributeNames.Contains(name); + ArgumentGuard.NotNull(objectType); + + if (_isSerializing) + { + // Protect against infinite recursion. + return false; + } + + return _alwaysIncludedAttributes.IsAttributesObjectType(objectType); } - public bool ContainerMatchesType(Type type) + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - return _containerType == type; + throw new UnreachableCodeException(); } - } - private sealed class AttributesRegistrationScope : IDisposable - { - private readonly JsonApiJsonConverter _jsonApiJsonConverter; - private readonly object _requestDocument; + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + ArgumentGuard.NotNull(writer); + ArgumentGuard.NotNull(serializer); + + if (value != null) + { + if (_alwaysIncludedAttributes.IsAttributesObjectType(value.GetType())) + { + AssertRequiredAttributesHaveNonDefaultValues(value, writer.Path); - public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, object requestDocument) + serializer.ContractResolver = new JsonApiAttributeContractResolver(_alwaysIncludedAttributes); + } + + try + { + _isSerializing = true; + serializer.Serialize(writer, value); + } + finally + { + _isSerializing = false; + } + } + } + + private void AssertRequiredAttributesHaveNonDefaultValues(object attributesObject, string jsonPath) { - ArgumentGuard.NotNull(jsonApiJsonConverter); - ArgumentGuard.NotNull(requestDocument); + foreach (PropertyInfo propertyInfo in attributesObject.GetType().GetProperties()) + { + bool isExplicitlyIncluded = _alwaysIncludedAttributes.ContainsAttribute(propertyInfo.Name); - _jsonApiJsonConverter = jsonApiJsonConverter; - _requestDocument = requestDocument; + if (!isExplicitlyIncluded) + { + AssertPropertyHasNonDefaultValueIfRequired(attributesObject, propertyInfo, jsonPath); + } + } } - public void Dispose() + private static void AssertPropertyHasNonDefaultValueIfRequired(object attributesObject, PropertyInfo propertyInfo, string jsonPath) + { + var jsonProperty = propertyInfo.GetCustomAttribute(); + + if (jsonProperty is { Required: Required.Always or Required.AllowNull }) + { + bool propertyHasDefaultValue = PropertyHasDefaultValue(propertyInfo, attributesObject); + + if (propertyHasDefaultValue) + { + throw new InvalidOperationException( + $"Required property '{propertyInfo.Name}' at JSON path '{jsonPath}.{jsonProperty.PropertyName}' is not set. If sending its default value is intended, include it explicitly."); + } + } + } + + private static bool PropertyHasDefaultValue(PropertyInfo propertyInfo, object instance) + { + object? propertyValue = propertyInfo.GetValue(instance); + object? defaultValue = GetDefaultValue(propertyInfo.PropertyType); + + return EqualityComparer.Default.Equals(propertyValue, defaultValue); + } + + private static object? GetDefaultValue(Type type) { - _jsonApiJsonConverter.RemoveAttributeRegistration(_requestDocument); + return type.IsValueType ? Activator.CreateInstance(type) : null; } } - private sealed class JsonApiDocumentContractResolver : DefaultContractResolver + /// + /// Corrects the and JSON annotations at runtime, which appear on the auto-generated + /// properties for JSON:API attributes. For example: + /// + /// + /// + private sealed class JsonApiAttributeContractResolver : DefaultContractResolver { - private readonly AttributeNamesContainer _attributeNamesContainer; + private readonly AlwaysIncludedAttributes _alwaysIncludedAttributes; - public JsonApiDocumentContractResolver(AttributeNamesContainer attributeNamesContainer) + public JsonApiAttributeContractResolver(AlwaysIncludedAttributes alwaysIncludedAttributes) { - ArgumentGuard.NotNull(attributeNamesContainer); + ArgumentGuard.NotNull(alwaysIncludedAttributes); - _attributeNamesContainer = attributeNamesContainer; + _alwaysIncludedAttributes = alwaysIncludedAttributes; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { - JsonProperty property = base.CreateProperty(member, memberSerialization); + JsonProperty jsonProperty = base.CreateProperty(member, memberSerialization); - if (_attributeNamesContainer.ContainerMatchesType(property.DeclaringType!)) + if (_alwaysIncludedAttributes.IsAttributesObjectType(jsonProperty.DeclaringType!)) { - if (_attributeNamesContainer.ContainsAttribute(property.UnderlyingName!)) + if (_alwaysIncludedAttributes.ContainsAttribute(jsonProperty.UnderlyingName!)) { - property.NullValueHandling = NullValueHandling.Include; - property.DefaultValueHandling = DefaultValueHandling.Include; + jsonProperty.NullValueHandling = NullValueHandling.Include; + jsonProperty.DefaultValueHandling = DefaultValueHandling.Include; } else { - property.NullValueHandling = NullValueHandling.Ignore; - property.DefaultValueHandling = DefaultValueHandling.Ignore; + jsonProperty.NullValueHandling = NullValueHandling.Ignore; + jsonProperty.DefaultValueHandling = DefaultValueHandling.Ignore; } } - return property; + return jsonProperty; } } } diff --git a/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs b/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs new file mode 100644 index 0000000000..f1821329d0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.OpenApi.Client; + +internal sealed class UnreachableCodeException : Exception +{ + public UnreachableCodeException() + : base("This code should not be reachable.") + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs index 024891e8ab..772eed3c1d 100644 --- a/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs @@ -21,7 +21,7 @@ public static MethodInfo GetActionMethod(this ActionDescriptor descriptor) ArgumentGuard.NotNull(descriptor); IFilterMetadata? filterMetadata = descriptor.FilterDescriptors.Select(filterDescriptor => filterDescriptor.Filter) - .FirstOrDefault(filter => filter is TFilterMetaData); + .OfType().FirstOrDefault(); return (TFilterMetaData?)filterMetadata; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index 98ae9938b4..16b173dd17 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -22,13 +22,14 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors(); - public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider) + public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(controllerResourceMapping); ArgumentGuard.NotNull(defaultProvider); _defaultProvider = defaultProvider; - _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping); + _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping, resourceFieldValidationMetadataProvider); } private ActionDescriptorCollection GetActionDescriptors() diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs index d0fe2f1234..ad8360591c 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs @@ -1,7 +1,6 @@ using System.Reflection; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.Resources.Annotations; @@ -15,11 +14,13 @@ internal sealed class JsonApiEndpointMetadataProvider { private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); + private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory; - public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping) + public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(controllerResourceMapping); - + _nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider); _controllerResourceMapping = controllerResourceMapping; } @@ -48,27 +49,14 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction) private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType) { - switch (endpoint) + return endpoint switch { - case JsonApiEndpoint.Post: - { - return GetPostRequestMetadata(primaryResourceType.ClrType); - } - case JsonApiEndpoint.Patch: - { - return GetPatchRequestMetadata(primaryResourceType.ClrType); - } - case JsonApiEndpoint.PostRelationship: - case JsonApiEndpoint.PatchRelationship: - case JsonApiEndpoint.DeleteRelationship: - { - return GetRelationshipRequestMetadata(primaryResourceType.Relationships, endpoint != JsonApiEndpoint.PatchRelationship); - } - default: - { - return null; - } - } + JsonApiEndpoint.Post => GetPostRequestMetadata(primaryResourceType.ClrType), + JsonApiEndpoint.Patch => GetPatchRequestMetadata(primaryResourceType.ClrType), + JsonApiEndpoint.PostRelationship or JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship => GetRelationshipRequestMetadata( + primaryResourceType.Relationships, endpoint != JsonApiEndpoint.PatchRelationship), + _ => null + }; } private static PrimaryRequestMetadata GetPostRequestMetadata(Type resourceClrType) @@ -85,40 +73,26 @@ private static PrimaryRequestMetadata GetPatchRequestMetadata(Type resourceClrTy return new PrimaryRequestMetadata(documentType); } - private static RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, bool ignoreHasOneRelationships) + private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, bool ignoreHasOneRelationships) { IEnumerable relationshipsOfEndpoint = ignoreHasOneRelationships ? relationships.OfType() : relationships; IDictionary requestDocumentTypesByRelationshipName = relationshipsOfEndpoint.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest); + _nonPrimaryDocumentTypeFactory.GetForRelationshipRequest); return new RelationshipRequestMetadata(requestDocumentTypesByRelationshipName); } private IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType) { - switch (endpoint) + return endpoint switch { - case JsonApiEndpoint.GetCollection: - case JsonApiEndpoint.GetSingle: - case JsonApiEndpoint.Post: - case JsonApiEndpoint.Patch: - { - return GetPrimaryResponseMetadata(primaryResourceType.ClrType, endpoint == JsonApiEndpoint.GetCollection); - } - case JsonApiEndpoint.GetSecondary: - { - return GetSecondaryResponseMetadata(primaryResourceType.Relationships); - } - case JsonApiEndpoint.GetRelationship: - { - return GetRelationshipResponseMetadata(primaryResourceType.Relationships); - } - default: - { - return null; - } - } + JsonApiEndpoint.GetCollection or JsonApiEndpoint.GetSingle or JsonApiEndpoint.Post or JsonApiEndpoint.Patch => GetPrimaryResponseMetadata( + primaryResourceType.ClrType, endpoint == JsonApiEndpoint.GetCollection), + JsonApiEndpoint.GetSecondary => GetSecondaryResponseMetadata(primaryResourceType.Relationships), + JsonApiEndpoint.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships), + _ => null + }; } private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceClrType, bool endpointReturnsCollection) @@ -129,18 +103,18 @@ private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceC return new PrimaryResponseMetadata(documentType); } - private static SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships) + private SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships) { IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForSecondaryResponse); + _nonPrimaryDocumentTypeFactory.GetForSecondaryResponse); return new SecondaryResponseMetadata(responseDocumentTypesByRelationshipName); } - private static RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships) + private RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships) { IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipResponse); + _nonPrimaryDocumentTypeFactory.GetForRelationshipResponse); return new RelationshipResponseMetadata(responseDocumentTypesByRelationshipName); } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/NonPrimaryDocumentTypeFactory.cs similarity index 81% rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs rename to src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/NonPrimaryDocumentTypeFactory.cs index 95d7e2f6e5..a8f1ce1d8d 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/NonPrimaryDocumentTypeFactory.cs @@ -2,7 +2,7 @@ using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata; internal sealed class NonPrimaryDocumentTypeFactory { @@ -15,10 +15,13 @@ internal sealed class NonPrimaryDocumentTypeFactory private static readonly DocumentOpenTypes RelationshipResponseDocumentOpenTypes = new(typeof(ResourceIdentifierCollectionResponseDocument<>), typeof(NullableResourceIdentifierResponseDocument<>), typeof(ResourceIdentifierResponseDocument<>)); - public static NonPrimaryDocumentTypeFactory Instance { get; } = new(); + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; - private NonPrimaryDocumentTypeFactory() + public NonPrimaryDocumentTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { + ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider); + + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; } public Type GetForSecondaryResponse(RelationshipAttribute relationship) @@ -42,13 +45,13 @@ public Type GetForRelationshipResponse(RelationshipAttribute relationship) return Get(relationship, RelationshipResponseDocumentOpenTypes); } - private static Type Get(RelationshipAttribute relationship, DocumentOpenTypes types) + private Type Get(RelationshipAttribute relationship, DocumentOpenTypes types) { // @formatter:nested_ternary_style expanded Type documentOpenType = relationship is HasManyAttribute ? types.ManyDataOpenType - : relationship.IsNullable() + : _resourceFieldValidationMetadataProvider.IsNullable(relationship) ? types.NullableSingleDataOpenType : types.SingleDataOpenType; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipTypeFactory.cs similarity index 54% rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs rename to src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipTypeFactory.cs index 7593c9f9f5..033d875b31 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipTypeFactory.cs @@ -1,21 +1,24 @@ using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata; internal sealed class RelationshipTypeFactory { - public static RelationshipTypeFactory Instance { get; } = new(); + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; + private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory; - private RelationshipTypeFactory() + public RelationshipTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { + _nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider); + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; } public Type GetForRequest(RelationshipAttribute relationship) { ArgumentGuard.NotNull(relationship); - return NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest(relationship); + return _nonPrimaryDocumentTypeFactory.GetForRelationshipRequest(relationship); } public Type GetForResponse(RelationshipAttribute relationship) @@ -26,7 +29,7 @@ public Type GetForResponse(RelationshipAttribute relationship) Type relationshipDataOpenType = relationship is HasManyAttribute ? typeof(ToManyRelationshipInResponse<>) - : relationship.IsNullable() + : _resourceFieldValidationMetadataProvider.IsNullable(relationship) ? typeof(NullableToOneRelationshipInResponse<>) : typeof(ToOneRelationshipInResponse<>); diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index 215ea8c24a..a32d781f5f 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -13,9 +13,9 @@ namespace JsonApiDotNetCore.OpenApi; internal sealed class JsonApiOperationIdSelector { private const string ResourceOperationIdTemplate = "[Method] [PrimaryResourceName]"; - private const string ResourceCollectionOperationIdTemplate = ResourceOperationIdTemplate + " Collection"; - private const string SecondaryOperationIdTemplate = ResourceOperationIdTemplate + " [RelationshipName]"; - private const string RelationshipOperationIdTemplate = SecondaryOperationIdTemplate + " Relationship"; + private const string ResourceCollectionOperationIdTemplate = $"{ResourceOperationIdTemplate} Collection"; + private const string SecondaryOperationIdTemplate = $"{ResourceOperationIdTemplate} [RelationshipName]"; + private const string RelationshipOperationIdTemplate = $"{SecondaryOperationIdTemplate} Relationship"; private static readonly IDictionary DocumentOpenTypeToOperationIdTemplateMap = new Dictionary { diff --git a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs b/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs deleted file mode 100644 index f13c91e837..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace JsonApiDotNetCore.OpenApi; - -internal static class MemberInfoExtensions -{ - public static TypeCategory GetTypeCategory(this MemberInfo source) - { - ArgumentGuard.NotNull(source); - - Type memberType; - - if (source.MemberType.HasFlag(MemberTypes.Field)) - { - memberType = ((FieldInfo)source).FieldType; - } - else if (source.MemberType.HasFlag(MemberTypes.Property)) - { - memberType = ((PropertyInfo)source).PropertyType; - } - else - { - throw new NotSupportedException($"Member type '{source.MemberType}' must be a property or field."); - } - - if (memberType.IsValueType) - { - return Nullable.GetUnderlyingType(memberType) != null ? TypeCategory.NullableValueType : TypeCategory.ValueType; - } - - // Once we switch to .NET 6, we should rely instead on the built-in reflection APIs for nullability information. - // See https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#libraries-reflection-apis-for-nullability-information. - return source.IsNonNullableReferenceType() ? TypeCategory.NonNullableReferenceType : TypeCategory.NullableReferenceType; - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs index 1ec0d0dbcf..fdae3e5d78 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -79,11 +79,9 @@ private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoint endpoint) JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship; } - private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint) + private static void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint) { - IList statusCodes = GetStatusCodesForEndpoint(endpoint); - - foreach (int statusCode in statusCodes) + foreach (int statusCode in GetStatusCodesForEndpoint(endpoint)) { action.Filters.Add(new ProducesResponseTypeAttribute(statusCode)); @@ -103,51 +101,30 @@ private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint) } } - private static IList GetStatusCodesForEndpoint(JsonApiEndpoint endpoint) + private static IEnumerable GetStatusCodesForEndpoint(JsonApiEndpoint endpoint) { - switch (endpoint) + return endpoint switch { - case JsonApiEndpoint.GetCollection: - case JsonApiEndpoint.GetSingle: - case JsonApiEndpoint.GetSecondary: - case JsonApiEndpoint.GetRelationship: - { - return new[] - { - StatusCodes.Status200OK - }; - } - case JsonApiEndpoint.Post: + JsonApiEndpoint.GetCollection or JsonApiEndpoint.GetSingle or JsonApiEndpoint.GetSecondary or JsonApiEndpoint.GetRelationship => new[] { - return new[] - { - StatusCodes.Status201Created, - StatusCodes.Status204NoContent - }; - } - case JsonApiEndpoint.Patch: + StatusCodes.Status200OK + }, + JsonApiEndpoint.Post => new[] { - return new[] - { - StatusCodes.Status200OK, - StatusCodes.Status204NoContent - }; - } - case JsonApiEndpoint.Delete: - case JsonApiEndpoint.PostRelationship: - case JsonApiEndpoint.PatchRelationship: - case JsonApiEndpoint.DeleteRelationship: + StatusCodes.Status201Created, + StatusCodes.Status204NoContent + }, + JsonApiEndpoint.Patch => new[] { - return new[] - { - StatusCodes.Status204NoContent - }; - } - default: + StatusCodes.Status200OK, + StatusCodes.Status204NoContent + }, + JsonApiEndpoint.Delete or JsonApiEndpoint.PostRelationship or JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship => new[] { - throw new UnreachableCodeException(); - } - } + StatusCodes.Status204NoContent + }, + _ => throw new UnreachableCodeException() + }; } private static void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint) diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs deleted file mode 100644 index 32bf9f5187..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using JsonApiDotNetCore.Resources.Annotations; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace JsonApiDotNetCore.OpenApi; - -internal static class ResourceFieldAttributeExtensions -{ - public static bool IsNullable(this ResourceFieldAttribute source) - { - TypeCategory fieldTypeCategory = source.Property.GetTypeCategory(); - bool hasRequiredAttribute = source.Property.HasAttribute(); - - return fieldTypeCategory switch - { - TypeCategory.NonNullableReferenceType or TypeCategory.ValueType => false, - TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => !hasRequiredAttribute, - _ => throw new UnreachableCodeException() - }; - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs new file mode 100644 index 0000000000..e40ab57618 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi; + +internal sealed class ResourceFieldValidationMetadataProvider +{ + private readonly bool _validateModelState; + private readonly NullabilityInfoContext _nullabilityContext = new(); + private readonly IModelMetadataProvider _modelMetadataProvider; + + public ResourceFieldValidationMetadataProvider(IJsonApiOptions options, IModelMetadataProvider modelMetadataProvider) + { + ArgumentGuard.NotNull(options); + ArgumentGuard.NotNull(modelMetadataProvider); + + _validateModelState = options.ValidateModelState; + _modelMetadataProvider = modelMetadataProvider; + } + + public bool IsNullable(ResourceFieldAttribute field) + { + ArgumentGuard.NotNull(field); + + if (field is HasManyAttribute) + { + return false; + } + + bool hasRequiredAttribute = field.Property.HasAttribute(); + + if (_validateModelState && hasRequiredAttribute) + { + return false; + } + + NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property); + return nullabilityInfo.ReadState != NullabilityState.NotNull; + } + + public bool IsRequired(ResourceFieldAttribute field) + { + ArgumentGuard.NotNull(field); + + bool hasRequiredAttribute = field.Property.HasAttribute(); + + if (!_validateModelState) + { + return hasRequiredAttribute; + } + + if (field is HasManyAttribute) + { + return false; + } + + NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property); + bool isRequiredValueType = field.Property.PropertyType.IsValueType && hasRequiredAttribute && nullabilityInfo.ReadState == NullabilityState.NotNull; + + if (isRequiredValueType) + { + // Special case: ASP.NET ModelState Validation effectively ignores value types with [Required]. + return false; + } + + return IsModelStateValidationRequired(field); + } + + private bool IsModelStateValidationRequired(ResourceFieldAttribute field) + { + ModelMetadata modelMetadata = _modelMetadataProvider.GetMetadataForProperty(field.Type.ClrType, field.Property.Name); + + // Non-nullable reference types are implicitly required, unless SuppressImplicitRequiredAttributeForNonNullableReferenceTypes is set. + return modelMetadata.ValidatorMetadata.Any(validatorMetadata => validatorMetadata is RequiredAttribute); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index f223c7d994..def25eb0dd 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -34,13 +34,18 @@ public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { + services.AddSingleton(); + services.AddSingleton(provider => { var controllerResourceMapping = provider.GetRequiredService(); var actionDescriptorCollectionProvider = provider.GetRequiredService(); var apiDescriptionProviders = provider.GetRequiredService>(); + var resourceFieldValidationMetadataProvider = provider.GetRequiredService(); + + JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = + new(controllerResourceMapping, actionDescriptorCollectionProvider, resourceFieldValidationMetadataProvider); - JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = new(controllerResourceMapping, actionDescriptorCollectionProvider); return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders); }); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs index f78ce52271..c4af79fc5d 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs @@ -3,8 +3,6 @@ using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; -#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks - namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; /// diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs index 4de3beffe8..7740d2973b 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class EndpointOrderingFilter : IDocumentFilter { private static readonly Regex RelationshipNameInUrlPattern = - new($".*{JsonApiRoutingTemplate.PrimaryEndpoint}/(?>{JsonApiRoutingTemplate.RelationshipsPart}\\/)?(\\w+)", RegexOptions.Compiled); + new($@".*{JsonApiRoutingTemplate.PrimaryEndpoint}/(?>{JsonApiRoutingTemplate.RelationshipsPart}\/)?(\w+)", RegexOptions.Compiled); public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { @@ -34,7 +34,7 @@ private static string GetPrimaryResourcePublicName(KeyValuePair entry) { - return entry.Key.Contains("/" + JsonApiRoutingTemplate.RelationshipsPart); + return entry.Key.Contains($"/{JsonApiRoutingTemplate.RelationshipsPart}"); } private static string GetRelationshipName(KeyValuePair entry) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index b4abe196ca..1e8efa8452 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -27,12 +27,20 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator typeof(NullableToOneRelationshipInRequest<>) }; + private static readonly Type[] JsonApiDocumentWithNullableDataOpenTypes = + { + typeof(NullableSecondaryResourceResponseDocument<>), + typeof(NullableResourceIdentifierResponseDocument<>), + typeof(NullableToOneRelationshipInRequest<>) + }; + private readonly ISchemaGenerator _defaultSchemaGenerator; private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator; private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); - public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options) + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceGraph); @@ -40,7 +48,9 @@ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceG _defaultSchemaGenerator = defaultSchemaGenerator; _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy); - _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); + + _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor, + resourceFieldValidationMetadataProvider); } public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepository, MemberInfo? memberInfo = null, ParameterInfo? parameterInfo = null, @@ -60,7 +70,7 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos { OpenApiSchema schema = GenerateJsonApiDocumentSchema(modelType); - if (IsDataPropertyNullable(modelType)) + if (IsDataPropertyNullableInDocument(modelType)) { SetDataObjectSchemaToNullable(schema); } @@ -100,18 +110,11 @@ private static bool IsManyDataDocument(Type documentType) return documentType.BaseType!.GetGenericTypeDefinition() == typeof(ManyData<>); } - private static bool IsDataPropertyNullable(Type type) + private static bool IsDataPropertyNullableInDocument(Type documentType) { - PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data)); - - if (dataProperty == null) - { - throw new UnreachableCodeException(); - } - - TypeCategory typeCategory = dataProperty.GetTypeCategory(); + Type documentOpenType = documentType.GetGenericTypeDefinition(); - return typeCategory == TypeCategory.NullableReferenceType; + return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType); } private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocument) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs index 48e65d721c..054c49d2ea 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -1,7 +1,5 @@ -using System.ComponentModel.DataAnnotations; -using System.Reflection; using System.Text.Json; -using JsonApiDotNetCore.OpenApi.JsonApiObjects; +using JsonApiDotNetCore.OpenApi.JsonApiMetadata; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources.Annotations; @@ -12,13 +10,19 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class ResourceFieldObjectSchemaBuilder { - private static readonly Type[] RelationshipInResponseOpenTypes = + private static readonly Type[] RelationshipSchemaInResponseOpenTypes = { typeof(ToOneRelationshipInResponse<>), typeof(ToManyRelationshipInResponse<>), typeof(NullableToOneRelationshipInResponse<>) }; + private static readonly Type[] NullableRelationshipSchemaOpenTypes = + { + typeof(NullableToOneRelationshipInRequest<>), + typeof(NullableToOneRelationshipInResponse<>) + }; + private readonly ResourceTypeInfo _resourceTypeInfo; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly SchemaGenerator _defaultSchemaGenerator; @@ -26,21 +30,27 @@ internal sealed class ResourceFieldObjectSchemaBuilder private readonly SchemaRepository _resourceSchemaRepository = new(); private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; private readonly IDictionary _schemasForResourceFields; + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; + private readonly RelationshipTypeFactory _relationshipTypeFactory; public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor, - SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy) + SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(resourceTypeInfo); ArgumentGuard.NotNull(schemaRepositoryAccessor); ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceTypeSchemaGenerator); + ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider); _resourceTypeInfo = resourceTypeInfo; _schemaRepositoryAccessor = schemaRepositoryAccessor; _defaultSchemaGenerator = defaultSchemaGenerator; _resourceTypeSchemaGenerator = resourceTypeSchemaGenerator; + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy); + _relationshipTypeFactory = new RelationshipTypeFactory(resourceFieldValidationMetadataProvider); _schemasForResourceFields = GetFieldSchemas(); } @@ -67,7 +77,7 @@ public void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesOb { AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema); - resourceFieldSchema.Nullable = matchingAttribute.IsNullable(); + resourceFieldSchema.Nullable = _resourceFieldValidationMetadataProvider.IsNullable(matchingAttribute); if (IsFieldRequired(matchingAttribute)) { @@ -103,21 +113,9 @@ private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresente private bool IsFieldRequired(ResourceFieldAttribute field) { - if (field is HasManyAttribute || _resourceTypeInfo.ResourceObjectOpenType != typeof(ResourceObjectInPostRequest<>)) - { - return false; - } - - TypeCategory fieldTypeCategory = field.Property.GetTypeCategory(); - bool hasRequiredAttribute = field.Property.HasAttribute(); + bool isSchemaForUpdateResourceEndpoint = _resourceTypeInfo.ResourceObjectOpenType == typeof(ResourceObjectInPatchRequest<>); - return fieldTypeCategory switch - { - TypeCategory.NonNullableReferenceType => true, - TypeCategory.ValueType => hasRequiredAttribute, - TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => hasRequiredAttribute, - _ => throw new UnreachableCodeException() - }; + return !isSchemaForUpdateResourceEndpoint && _resourceFieldValidationMetadataProvider.IsRequired(field); } public void SetMembersOfRelationshipsObject(OpenApiSchema fullSchemaForRelationshipsObject) @@ -175,11 +173,11 @@ private void AddRelationshipSchemaToResourceObject(RelationshipAttribute relatio } } - private static Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type resourceObjectType) + private Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type resourceObjectType) { return resourceObjectType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceObjectInResponse<>)) - ? RelationshipTypeFactory.Instance.GetForResponse(relationship) - : RelationshipTypeFactory.Instance.GetForRequest(relationship); + ? _relationshipTypeFactory.GetForResponse(relationship) + : _relationshipTypeFactory.GetForRequest(relationship); } private OpenApiSchema? GetReferenceSchemaForRelationship(Type relationshipSchemaType) @@ -194,7 +192,7 @@ private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType) OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id]; - if (IsDataPropertyNullable(relationshipSchemaType)) + if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType)) { fullSchema.Properties[JsonApiObjectPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]); @@ -212,18 +210,12 @@ private static bool IsRelationshipInResponseType(Type relationshipSchemaType) { Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); - return RelationshipInResponseOpenTypes.Contains(relationshipSchemaOpenType); + return RelationshipSchemaInResponseOpenTypes.Contains(relationshipSchemaOpenType); } - private static bool IsDataPropertyNullable(Type type) + private static bool IsDataPropertyNullableInRelationshipSchemaType(Type relationshipSchemaType) { - PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data)); - - if (dataProperty == null) - { - throw new UnreachableCodeException(); - } - - return dataProperty.GetTypeCategory() == TypeCategory.NullableReferenceType; + Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); + return NullableRelationshipSchemaOpenTypes.Contains(relationshipSchemaOpenType); } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index 95c8f29eb9..4be66f8f8d 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -15,7 +15,7 @@ internal sealed class ResourceObjectSchemaGenerator private readonly Func _resourceFieldObjectSchemaBuilderFactory; public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, - ISchemaRepositoryAccessor schemaRepositoryAccessor) + ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceGraph); @@ -30,7 +30,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph, options.SerializerOptions.PropertyNamingPolicy); _resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, - defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy); + defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy, resourceFieldValidationMetadataProvider); } public OpenApiSchema GenerateSchema(Type resourceObjectType) diff --git a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs b/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs deleted file mode 100644 index 1641e31775..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace JsonApiDotNetCore.OpenApi; - -internal enum TypeCategory -{ - NonNullableReferenceType, - NullableReferenceType, - ValueType, - NullableValueType -} diff --git a/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs b/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs index d290ba80eb..43a5989d59 100644 --- a/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs +++ b/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs @@ -8,5 +8,5 @@ namespace JsonApiDotNetCore.Middleware; public static class HeaderConstants { public const string MediaType = "application/vnd.api+json"; - public const string AtomicOperationsMediaType = $"{MediaType}; ext=\"https://jsonapi.org/ext/atomic\""; + public const string AtomicOperationsMediaType = MediaType + "; ext=\"https://jsonapi.org/ext/atomic\""; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs index 9e104eef01..5a0738b0a9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs @@ -125,7 +125,7 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); - error.Detail.Should().Be(@"The field Name must match the regular expression '^[\w\s]+$'."); + error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/data/attributes/directoryName"); } @@ -534,7 +534,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); - error.Detail.Should().Be(@"The field Name must match the regular expression '^[\w\s]+$'."); + error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); error.Source.ShouldNotBeNull(); error.Source.Pointer.Should().Be("/data/attributes/directoryName"); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index f7d407696e..d70f50de0e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -74,8 +74,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = - $"/public-api/swimming-pools/{pool.StringId}/water-slides?filter=greaterThan(length-in-meters,'1')&fields[water-slides]=length-in-meters"; + string route = $"/public-api/swimming-pools/{pool.StringId}/water-slides" + + "?filter=greaterThan(length-in-meters,'1')&fields[water-slides]=length-in-meters"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs index 9f71d9de16..59d442fcba 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs @@ -79,7 +79,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/PublicApi/SwimmingPools/{pool.StringId}/WaterSlides?filter=greaterThan(LengthInMeters,'1')&fields[WaterSlides]=LengthInMeters"; + string route = $"/PublicApi/SwimmingPools/{pool.StringId}/WaterSlides" + "?filter=greaterThan(LengthInMeters,'1')&fields[WaterSlides]=LengthInMeters"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index d2163d95cd..ad6f8a1609 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -323,10 +323,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.ShouldNotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); - responseDocument.Links.First.Should().Be($"{basePath}?page%5Bsize%5D=1"); + responseDocument.Links.First.Should().Be(basePath + "?page%5Bsize%5D=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); - responseDocument.Links.Next.Should().Be($"{basePath}?page%5Bnumber%5D=3&page%5Bsize%5D=1"); + responseDocument.Links.Next.Should().Be(basePath + "?page%5Bnumber%5D=3&page%5Bsize%5D=1"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index 2dacb19ea7..44148b19e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -71,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().BeNull(); } @@ -106,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("caption").With(value => value.Should().Be(post.Caption)); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().BeNull(); } @@ -149,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().BeNull(); postCaptured.Url.Should().BeNull(); } @@ -193,7 +193,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); blogCaptured.Title.Should().BeNull(); @@ -240,7 +240,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Url.Should().Be(post.Url); postCaptured.Caption.Should().BeNull(); } @@ -299,7 +299,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); postCaptured.Caption.Should().Be(post.Caption); @@ -361,7 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var accountCaptured = (WebAccount)store.Resources.Should().ContainSingle(resource => resource is WebAccount).Which; + var accountCaptured = (WebAccount)store.Resources.Should().ContainSingle(resource => resource is WebAccount).And.Subject.Single(); accountCaptured.Id.Should().Be(account.Id); accountCaptured.DisplayName.Should().Be(account.DisplayName); @@ -423,7 +423,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); blogCaptured.Owner.ShouldNotBeNull(); blogCaptured.Owner.DisplayName.Should().Be(blog.Owner.DisplayName); @@ -476,7 +476,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes.ShouldContainKey("color").With(value => value.Should().Be(post.Labels.Single().Color)); responseDocument.Included[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); postCaptured.Caption.Should().Be(post.Caption); @@ -532,7 +532,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Attributes.ShouldContainKey("caption").With(value => value.Should().Be(blog.Owner.Posts[0].Caption)); responseDocument.Included[1].Relationships.Should().BeNull(); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); blogCaptured.Title.Should().Be(blog.Title); blogCaptured.PlatformName.Should().BeNull(); @@ -620,7 +620,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); blogCaptured.Title.Should().Be(blog.Title); blogCaptured.PlatformName.Should().BeNull(); @@ -656,7 +656,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("caption").With(value => value.Should().Be(post.Caption)); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().BeNull(); @@ -691,7 +691,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); postCaptured.Url.Should().BeNull(); } @@ -824,7 +824,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.SingleValue.Attributes.ShouldContainKey("showAdvertisements").With(value => value.Should().Be(blog.ShowAdvertisements)); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.ShowAdvertisements.Should().Be(blog.ShowAdvertisements); blogCaptured.IsPublished.Should().Be(blog.IsPublished); blogCaptured.Title.Should().Be(blog.Title); @@ -869,7 +869,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => value.Links.Related.ShouldNotBeNull(); }); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().Be(post.Url); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 5f1f894a9b..3354afd6bb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -684,7 +684,7 @@ public async Task Cannot_create_resource_on_unknown_resource_type_in_url() } }; - const string route = $"/{Unknown.ResourceType}"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 853e09d95c..487c6d72d5 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -62,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_primary_resources_for_unknown_type() { // Arrange - const string route = $"/{Unknown.ResourceType}"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/FieldChains/FieldChainPatternParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/FieldChains/FieldChainPatternParseTests.cs index d7e5c22cc1..bdb5c6c681 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/FieldChains/FieldChainPatternParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/FieldChains/FieldChainPatternParseTests.cs @@ -49,7 +49,7 @@ public void ParseFails(string patternText, string errorMessage) Action action = () => FieldChainPattern.Parse(patternSource.Text); // Assert - PatternFormatException exception = action.Should().ThrowExactly().Which; + PatternFormatException exception = action.Should().Throw().Which; exception.Message.Should().Be(errorMessage); exception.Position.Should().Be(patternSource.Position); exception.Pattern.Should().Be(patternSource.Text); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionRewriterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionRewriterTests.cs index 3d2d8faa85..3e4a6b2276 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionRewriterTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Queries/QueryExpressionRewriterTests.cs @@ -53,7 +53,7 @@ public void VisitInclude(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); @@ -76,7 +76,7 @@ public void VisitSparseFieldSet(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); @@ -136,7 +136,7 @@ public void VisitFilter(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); @@ -160,7 +160,7 @@ public void VisitSort(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); @@ -183,7 +183,7 @@ public void VisitPagination(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); @@ -208,7 +208,7 @@ public void VisitParameterScope(string expressionText, string expectedTypes) // Assert List visitedTypeNames = rewriter.ExpressionsVisited.Select(queryExpression => queryExpression.GetType().Name).ToList(); - List expectedTypeNames = expectedTypes.Split(',').Select(type => $"{type}Expression").ToList(); + List expectedTypeNames = expectedTypes.Split(',').Select(type => type + "Expression").ToList(); visitedTypeNames.Should().ContainInOrder(expectedTypeNames); visitedTypeNames.Should().HaveCount(expectedTypeNames.Count); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/ResourceDefinitions/CreateSortExpressionFromLambdaTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/ResourceDefinitions/CreateSortExpressionFromLambdaTests.cs index 8b919a87cc..3d178073c7 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/ResourceDefinitions/CreateSortExpressionFromLambdaTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/ResourceDefinitions/CreateSortExpressionFromLambdaTests.cs @@ -317,7 +317,7 @@ public void Cannot_convert_concatenation_operator() // Act Action action = () => resourceDefinition.GetSortExpressionFromLambda(new JsonApiResourceDefinition.PropertySortOrder { - (file => $"{file.Name}:{file.Content}", ListSortDirection.Ascending) + (file => file.Name + ":" + file.Content, ListSortDirection.Ascending) }); // Assert diff --git a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs b/test/OpenApiClientTests/ApiResponse.cs similarity index 66% rename from test/OpenApiClientTests/LegacyClient/ApiResponse.cs rename to test/OpenApiClientTests/ApiResponse.cs index e62353d17a..10e9ecf6ca 100644 --- a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs +++ b/test/OpenApiClientTests/ApiResponse.cs @@ -3,28 +3,22 @@ #pragma warning disable AV1008 // Class should not be static -namespace OpenApiClientTests.LegacyClient; +namespace OpenApiClientTests; internal static class ApiResponse { public static async Task TranslateAsync(Func> operation) where TResponse : class { - // Workaround for https://github.com/RicoSuter/NSwag/issues/2499 - ArgumentGuard.NotNull(operation); try { return await operation(); } - catch (ApiException exception) + catch (ApiException exception) when (exception.StatusCode == 204) { - if (exception.StatusCode != 204) - { - throw; - } - + // Workaround for https://github.com/RicoSuter/NSwag/issues/2499 return null; } } diff --git a/test/OpenApiClientTests/BaseOpenApiClientTests.cs b/test/OpenApiClientTests/BaseOpenApiClientTests.cs new file mode 100644 index 0000000000..6ebfa04e18 --- /dev/null +++ b/test/OpenApiClientTests/BaseOpenApiClientTests.cs @@ -0,0 +1,88 @@ +using System.Linq.Expressions; +using System.Reflection; +using JsonApiDotNetCore.OpenApi.Client; + +namespace OpenApiClientTests; + +public abstract class BaseOpenApiClientTests +{ + private const string AttributesObjectParameterName = "attributesObject"; + + protected static Expression> CreateAttributeSelectorFor(string propertyName) + where TAttributesObject : class + { + Type attributesObjectType = typeof(TAttributesObject); + + ParameterExpression parameter = Expression.Parameter(attributesObjectType, AttributesObjectParameterName); + MemberExpression property = Expression.Property(parameter, propertyName); + UnaryExpression toObjectConversion = Expression.Convert(property, typeof(object)); + + return Expression.Lambda>(toObjectConversion, parameter); + } + + /// + /// Sets the property on the specified source to its default value (null for string, 0 for int, false for bool, etc). + /// + protected static object? SetPropertyToDefaultValue(T source, string propertyName) + where T : class + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo property = GetExistingProperty(typeof(T), propertyName); + + object? defaultValue = property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null; + property.SetValue(source, defaultValue); + + return defaultValue; + } + + /// + /// Sets the property on the specified source to its initial value, when the type was constructed. This takes the presence of a type initializer into + /// account. + /// + protected static void SetPropertyToInitialValue(T source, string propertyName) + where T : class, new() + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + var emptyRelationshipsObject = new T(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(propertyName); + + source.SetPropertyValue(propertyName, defaultValue); + } + + /// + /// Sets the 'Data' property of the specified relationship to null. + /// + protected static void SetDataPropertyToNull(T source, string relationshipPropertyName) + where T : class + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(relationshipPropertyName); + + PropertyInfo relationshipProperty = GetExistingProperty(typeof(T), relationshipPropertyName); + object? relationshipValue = relationshipProperty.GetValue(source); + + if (relationshipValue == null) + { + throw new InvalidOperationException($"Property '{typeof(T).Name}.{relationshipPropertyName}' is null."); + } + + PropertyInfo dataProperty = GetExistingProperty(relationshipProperty.PropertyType, "Data"); + dataProperty.SetValue(relationshipValue, null); + } + + private static PropertyInfo GetExistingProperty(Type type, string propertyName) + { + PropertyInfo? property = type.GetProperty(propertyName); + + if (property == null) + { + throw new InvalidOperationException($"Type '{type.Name}' does not contain a property named '{propertyName}'."); + } + + return property; + } +} diff --git a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/FakeHttpClientWrapper.cs similarity index 89% rename from test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs rename to test/OpenApiClientTests/FakeHttpClientWrapper.cs index 8a5501354f..0d3e868384 100644 --- a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs +++ b/test/OpenApiClientTests/FakeHttpClientWrapper.cs @@ -1,9 +1,10 @@ using System.Net; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using JsonApiDotNetCore.OpenApi.Client; -namespace OpenApiClientTests.LegacyClient; +namespace OpenApiClientTests; /// /// Enables to inject an outgoing response body and inspect the incoming request. @@ -22,6 +23,17 @@ private FakeHttpClientWrapper(HttpClient httpClient, FakeHttpMessageHandler hand _handler = handler; } + public JsonElement GetRequestBodyAsJson() + { + if (RequestBody == null) + { + throw new InvalidOperationException("No body was provided with the request."); + } + + using JsonDocument jsonDocument = JsonDocument.Parse(RequestBody); + return jsonDocument.RootElement.Clone(); + } + public static FakeHttpClientWrapper Create(HttpStatusCode statusCode, string? responseBody) { HttpResponseMessage response = CreateResponse(statusCode, responseBody); diff --git a/test/OpenApiClientTests/FakerFactory.cs b/test/OpenApiClientTests/FakerFactory.cs new file mode 100644 index 0000000000..74b6a81223 --- /dev/null +++ b/test/OpenApiClientTests/FakerFactory.cs @@ -0,0 +1,72 @@ +using System.Reflection; +using AutoBogus; +using JetBrains.Annotations; +using TestBuildingBlocks; + +namespace OpenApiClientTests; + +internal sealed class FakerFactory +{ + public static FakerFactory Instance { get; } = new(); + + private FakerFactory() + { + } + + public AutoFaker Create() + where TTarget : class + { + return GetDeterministicFaker(); + } + + private static AutoFaker GetDeterministicFaker() + where TTarget : class + { + var autoFaker = new AutoFaker(); + autoFaker.UseSeed(FakerContainer.GetFakerSeed()); + return autoFaker; + } + + public AutoFaker CreateForObjectWithResourceId() + where TTarget : class + { + return GetDeterministicFaker().Configure(builder => builder.WithOverride(new ResourceStringIdOverride())); + } + + private sealed class ResourceStringIdOverride : AutoGeneratorOverride + { + // AutoFaker has a class constraint, while TId has not, so we need to wrap it. + private readonly AutoFaker> _idContainerFaker = GetDeterministicFaker>(); + + public override bool CanOverride(AutoGenerateContext context) + { + PropertyInfo? resourceIdPropertyInfo = context.GenerateType.GetProperty("Id"); + return resourceIdPropertyInfo != null && resourceIdPropertyInfo.PropertyType == typeof(string); + } + + public override void Generate(AutoGenerateOverrideContext context) + { + object idValue = _idContainerFaker.Generate().Value!; + idValue = ToPositiveValue(idValue); + + ((dynamic)context.Instance).Id = idValue.ToString()!; + } + + private static object ToPositiveValue(object idValue) + { + return idValue switch + { + short shortValue => Math.Abs(shortValue), + int intValue => Math.Abs(intValue), + long longValue => Math.Abs(longValue), + _ => idValue + }; + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class ObjectContainer + { + public TValue? Value { get; set; } + } + } +} diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/PartialAttributeSerializationLifetimeTests.cs similarity index 80% rename from test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs rename to test/OpenApiClientTests/LegacyClient/PartialAttributeSerializationLifetimeTests.cs index 4582f9578d..be71dfa1e3 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/PartialAttributeSerializationLifetimeTests.cs @@ -6,10 +6,10 @@ namespace OpenApiClientTests.LegacyClient; -public sealed class ClientAttributeRegistrationLifetimeTests +public sealed class PartialAttributeSerializationLifetimeTests { [Fact] - public async Task Disposed_attribute_registration_for_document_does_not_affect_request() + public async Task Disposed_registration_does_not_affect_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -27,7 +27,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -51,7 +51,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } [Fact] - public async Task Attribute_registration_can_be_used_for_multiple_requests() + public async Task Registration_can_be_used_for_multiple_requests() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -72,7 +72,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -98,7 +98,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } [Fact] - public async Task Request_is_unaffected_by_attribute_registration_for_different_document_of_same_type() + public async Task Request_is_unaffected_by_registration_for_different_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -128,10 +128,10 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.SerialNumber)) { } @@ -153,7 +153,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } [Fact] - public async Task Attribute_values_can_be_changed_after_attribute_registration() + public async Task Attribute_values_can_be_changed_after_registration() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -174,7 +174,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.IsInMaintenance)) { requestDocument.Data.Attributes.IsInMaintenance = false; @@ -196,7 +196,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } [Fact] - public async Task Attribute_registration_is_unaffected_by_successive_attribute_registration_for_document_of_different_type() + public async Task Registration_is_unaffected_by_successive_registration_for_document_of_different_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -223,10 +223,10 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.AirtimeInHours)) { // Act @@ -247,7 +247,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_different_document_of_same_type() + public async Task Registration_is_unaffected_by_preceding_disposed_registration_for_different_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -265,7 +265,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); @@ -288,7 +288,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.SerialNumber)) { // Act @@ -309,7 +309,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_document_of_different_type() + public async Task Registration_is_unaffected_by_preceding_disposed_registration_for_document_of_different_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -320,11 +320,14 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att Data = new AirplaneDataInPostRequest { Type = AirplaneResourceType.Airplanes, - Attributes = new AirplaneAttributesInPostRequest() + Attributes = new AirplaneAttributesInPostRequest + { + Name = "Jay Jay the Jet Plane" + } } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument1)); @@ -347,7 +350,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.SerialNumber)) { // Act @@ -368,7 +371,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_attribute_registration_for_different_document_of_same_type() + public async Task Registration_is_unaffected_by_preceding_registration_for_different_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -398,10 +401,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.SerialNumber)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index dfdc2ce2a7..628106f4d1 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -28,7 +28,7 @@ public async Task Getting_resource_collection_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + "flights"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights"); wrapper.RequestBody.Should().BeNull(); } @@ -48,7 +48,7 @@ public async Task Getting_resource_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}"); wrapper.RequestBody.Should().BeNull(); } @@ -93,7 +93,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + "flights"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -137,7 +137,7 @@ public async Task Partial_posting_resource_produces_expected_request() capitalLWithStroke }); - string name = "anAirplaneName " + specialCharacters; + string name = $"anAirplaneName {specialCharacters}"; var requestDocument = new AirplanePostRequestDocument { @@ -152,7 +152,7 @@ public async Task Partial_posting_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.SerialNumber)) { // Act @@ -163,7 +163,7 @@ public async Task Partial_posting_resource_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + "airplanes"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}airplanes"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -203,7 +203,7 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act @@ -214,7 +214,7 @@ public async Task Partial_patching_resource_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"airplanes/{airplaneId}"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}airplanes/{airplaneId}"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -248,7 +248,7 @@ public async Task Deleting_resource_produces_expected_request() // Assert wrapper.Request.ShouldNotBeNull(); wrapper.Request.Method.Should().Be(HttpMethod.Delete); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}"); wrapper.RequestBody.Should().BeNull(); } @@ -268,7 +268,7 @@ public async Task Getting_secondary_resource_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/purser"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/purser"); wrapper.RequestBody.Should().BeNull(); } @@ -288,7 +288,7 @@ public async Task Getting_secondary_resources_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/cabin-crew-members"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/cabin-crew-members"); wrapper.RequestBody.Should().BeNull(); } @@ -308,7 +308,7 @@ public async Task Getting_ToOne_relationship_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/purser"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/purser"); wrapper.RequestBody.Should().BeNull(); } @@ -336,7 +336,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() // Assert wrapper.Request.ShouldNotBeNull(); wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/purser"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/purser"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -365,7 +365,7 @@ public async Task Getting_ToMany_relationship_produces_expected_request() wrapper.Request.ShouldNotBeNull(); wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/cabin-crew-members"); wrapper.RequestBody.Should().BeNull(); } @@ -401,7 +401,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() // Assert wrapper.Request.ShouldNotBeNull(); wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -452,7 +452,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() // Assert wrapper.Request.ShouldNotBeNull(); wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -503,7 +503,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() // Assert wrapper.Request.ShouldNotBeNull(); wrapper.Request.Method.Should().Be(HttpMethod.Delete); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); + wrapper.Request.RequestUri.Should().Be($"{HostPrefix}flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 2fab8ceb05..73f1a9d0e5 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -25,8 +25,8 @@ public async Task Getting_resource_collection_translates_response() const string purserMetaValue = "https://api.jsonapi.net/docs/#get-flight-purser"; const string cabinCrewMembersMetaValue = "https://api.jsonapi.net/docs/#get-flight-cabin-crew-members"; const string passengersMetaValue = "https://api.jsonapi.net/docs/#get-flight-passengers"; - const string topLevelLink = HostPrefix + "flights"; - const string flightResourceLink = topLevelLink + "/" + flightId; + const string topLevelLink = $"{HostPrefix}flights"; + const string flightResourceLink = $"{topLevelLink}/{flightId}"; const string responseBody = @"{ ""meta"": { @@ -125,20 +125,20 @@ public async Task Getting_resource_collection_translates_response() flight.Attributes.ArrivesAt.Should().BeNull(); flight.Relationships.Purser.Data.Should().BeNull(); - flight.Relationships.Purser.Links.Self.Should().Be(flightResourceLink + "/relationships/purser"); - flight.Relationships.Purser.Links.Related.Should().Be(flightResourceLink + "/purser"); + flight.Relationships.Purser.Links.Self.Should().Be($"{flightResourceLink}/relationships/purser"); + flight.Relationships.Purser.Links.Related.Should().Be($"{flightResourceLink}/purser"); flight.Relationships.Purser.Meta.Should().HaveCount(1); flight.Relationships.Purser.Meta["docs"].Should().Be(purserMetaValue); flight.Relationships.CabinCrewMembers.Data.Should().BeNull(); - flight.Relationships.CabinCrewMembers.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-crew-members"); - flight.Relationships.CabinCrewMembers.Links.Related.Should().Be(flightResourceLink + "/cabin-crew-members"); + flight.Relationships.CabinCrewMembers.Links.Self.Should().Be($"{flightResourceLink}/relationships/cabin-crew-members"); + flight.Relationships.CabinCrewMembers.Links.Related.Should().Be($"{flightResourceLink}/cabin-crew-members"); flight.Relationships.CabinCrewMembers.Meta.Should().HaveCount(1); flight.Relationships.CabinCrewMembers.Meta["docs"].Should().Be(cabinCrewMembersMetaValue); flight.Relationships.Passengers.Data.Should().BeNull(); - flight.Relationships.Passengers.Links.Self.Should().Be(flightResourceLink + "/relationships/passengers"); - flight.Relationships.Passengers.Links.Related.Should().Be(flightResourceLink + "/passengers"); + flight.Relationships.Passengers.Links.Self.Should().Be($"{flightResourceLink}/relationships/passengers"); + flight.Relationships.Passengers.Links.Related.Should().Be($"{flightResourceLink}/passengers"); flight.Relationships.Passengers.Meta.Should().HaveCount(1); flight.Relationships.Passengers.Meta["docs"].Should().Be(passengersMetaValue); } @@ -149,7 +149,9 @@ public async Task Getting_resource_translates_response() // Arrange const string flightId = "ZvuH1"; const string departsAtInZuluTime = "2021-06-08T12:53:30.554Z"; + const string flightDestination = "Amsterdam"; const string arrivesAtWithUtcOffset = "2019-02-20T11:56:33.0721266+01:00"; + const string flightServiceOnBoard = "Movies"; const string responseBody = @"{ ""links"": { @@ -160,7 +162,9 @@ public async Task Getting_resource_translates_response() ""id"": """ + flightId + @""", ""attributes"": { ""departs-at"": """ + departsAtInZuluTime + @""", - ""arrives-at"": """ + arrivesAtWithUtcOffset + @""" + ""arrives-at"": """ + arrivesAtWithUtcOffset + @""", + ""final-destination"": """ + flightDestination + @""", + ""services-on-board"": [""" + flightServiceOnBoard + @"""] }, ""links"": { ""self"": """ + HostPrefix + "flights/" + flightId + @""" @@ -181,8 +185,8 @@ public async Task Getting_resource_translates_response() document.Data.Relationships.Should().BeNull(); document.Data.Attributes.DepartsAt.Should().Be(DateTimeOffset.Parse(departsAtInZuluTime)); document.Data.Attributes.ArrivesAt.Should().Be(DateTimeOffset.Parse(arrivesAtWithUtcOffset)); - document.Data.Attributes.ServicesOnBoard.Should().BeNull(); - document.Data.Attributes.FinalDestination.Should().BeNull(); + document.Data.Attributes.ServicesOnBoard.Should().Contain(flightServiceOnBoard); + document.Data.Attributes.FinalDestination.Should().Be(flightDestination); document.Data.Attributes.StopOverDestination.Should().BeNull(); document.Data.Attributes.OperatedBy.Should().Be(default); } @@ -212,10 +216,9 @@ public async Task Getting_unknown_resource_translates_error_response() // Assert ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); - ApiException exception = assertion.Subject.Single(); - exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound); - exception.Response.Should().Be(responseBody); + assertion.Which.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + assertion.Which.Response.Should().Be(responseBody); } [Fact] diff --git a/test/OpenApiClientTests/LegacyClient/swagger.g.json b/test/OpenApiClientTests/LegacyClient/swagger.g.json index d408c3589c..d1e73c4747 100644 --- a/test/OpenApiClientTests/LegacyClient/swagger.g.json +++ b/test/OpenApiClientTests/LegacyClient/swagger.g.json @@ -1970,6 +1970,9 @@ "additionalProperties": false }, "airplane-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -2238,6 +2241,10 @@ "additionalProperties": false }, "flight-attendant-attributes-in-response": { + "required": [ + "email-address", + "profile-image-url" + ], "type": "object", "properties": { "email-address": { @@ -2557,6 +2564,10 @@ "additionalProperties": false }, "flight-attributes-in-response": { + "required": [ + "final-destination", + "services-on-board" + ], "type": "object", "properties": { "final-destination": { @@ -2815,6 +2826,9 @@ "additionalProperties": false }, "flight-relationships-in-response": { + "required": [ + "purser" + ], "type": "object", "properties": { "cabin-crew-members": { diff --git a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json index d2850c032d..5d2569050d 100644 --- a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json @@ -1001,6 +1001,9 @@ "additionalProperties": false }, "staffMemberAttributesInResponse": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -1190,6 +1193,9 @@ "additionalProperties": false }, "supermarketAttributesInResponse": { + "required": [ + "nameOfCity" + ], "type": "object", "properties": { "nameOfCity": { @@ -1380,6 +1386,9 @@ "additionalProperties": false }, "supermarketRelationshipsInResponse": { + "required": [ + "storeManager" + ], "type": "object", "properties": { "storeManager": { diff --git a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json index 1d7feb51f0..9d7656c612 100644 --- a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json @@ -1001,6 +1001,9 @@ "additionalProperties": false }, "staff-member-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -1190,6 +1193,9 @@ "additionalProperties": false }, "supermarket-attributes-in-response": { + "required": [ + "name-of-city" + ], "type": "object", "properties": { "name-of-city": { @@ -1380,6 +1386,9 @@ "additionalProperties": false }, "supermarket-relationships-in-response": { + "required": [ + "store-manager" + ], "type": "object", "properties": { "store-manager": { diff --git a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json index fa5041afad..ae79e8470b 100644 --- a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json @@ -1001,6 +1001,9 @@ "additionalProperties": false }, "StaffMemberAttributesInResponse": { + "required": [ + "Name" + ], "type": "object", "properties": { "Name": { @@ -1190,6 +1193,9 @@ "additionalProperties": false }, "SupermarketAttributesInResponse": { + "required": [ + "NameOfCity" + ], "type": "object", "properties": { "NameOfCity": { @@ -1380,6 +1386,9 @@ "additionalProperties": false }, "SupermarketRelationshipsInResponse": { + "required": [ + "StoreManager" + ], "type": "object", "properties": { "StoreManager": { diff --git a/test/OpenApiClientTests/ObjectExtensions.cs b/test/OpenApiClientTests/ObjectExtensions.cs new file mode 100644 index 0000000000..3f2633f5ff --- /dev/null +++ b/test/OpenApiClientTests/ObjectExtensions.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using JsonApiDotNetCore.OpenApi.Client; + +namespace OpenApiClientTests; + +internal static class ObjectExtensions +{ + public static object? GetPropertyValue(this object source, string propertyName) + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo propertyInfo = GetExistingProperty(source.GetType(), propertyName); + return propertyInfo.GetValue(source); + } + + public static void SetPropertyValue(this object source, string propertyName, object? value) + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo propertyInfo = GetExistingProperty(source.GetType(), propertyName); + propertyInfo.SetValue(source, value); + } + + private static PropertyInfo GetExistingProperty(Type type, string propertyName) + { + PropertyInfo? propertyInfo = type.GetProperty(propertyName); + + if (propertyInfo == null) + { + throw new InvalidOperationException($"Type '{type}' does not contain a property named '{propertyName}'."); + } + + return propertyInfo; + } +} diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index c28ed7959e..fe363ffaa5 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -9,6 +9,7 @@ + @@ -47,5 +48,33 @@ NSwagCSharp /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions + + NrtOffMsvOffClient + NrtOffMsvOffClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false + + + NrtOffMsvOnClient + NrtOffMsvOnClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false + + + NrtOnMsvOffClient + NrtOnMsvOffClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true + + + NrtOnMsvOnClient + NrtOnMsvOnClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true + - \ No newline at end of file + diff --git a/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs new file mode 100644 index 0000000000..e49bef1a0d --- /dev/null +++ b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Types; + +namespace OpenApiClientTests; + +internal static class PropertyInfoAssertionsExtensions +{ + [CustomAssertion] + public static void HaveNullabilityState(this PropertyInfoAssertions source, NullabilityState expected, string because = "", params object[] becauseArgs) + { + PropertyInfo propertyInfo = source.Subject; + + NullabilityInfoContext nullabilityContext = new(); + NullabilityInfo nullabilityInfo = nullabilityContext.Create(propertyInfo); + + nullabilityInfo.ReadState.Should().Be(expected, because, becauseArgs); + nullabilityInfo.WriteState.Should().Be(expected, because, becauseArgs); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs new file mode 100644 index 0000000000..684a961a1b --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs @@ -0,0 +1,255 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +public sealed class CreateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOffMsvOffFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().ContainPath(jsonPropertyName).With(attribute => attribute.Should().Be(defaultValue)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), "requiredToOne")] + public async Task Can_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Cannot_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs new file mode 100644 index 0000000000..f3ee9a9c53 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; + +internal partial class NrtOffMsvOffClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NrtOffMsvOffFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NrtOffMsvOffFakers.cs new file mode 100644 index 0000000000..adb0e97833 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NrtOffMsvOffFakers.cs @@ -0,0 +1,24 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +internal sealed class NrtOffMsvOffFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs new file mode 100644 index 0000000000..d15ba31e2b --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.Nullable)] + public void Nullability_of_generated_attribute_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? property = typeof(ResourceAttributesInPostRequest).GetProperty(propertyName); + + // Assert + property.ShouldNotBeNull(); + property.Should().HaveNullabilityState(expectedState); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), NullabilityState.Unknown)] + public void Nullability_of_generated_relationship_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? relationshipProperty = typeof(ResourceRelationshipsInPostRequest).GetProperty(propertyName); + + // Assert + relationshipProperty.ShouldNotBeNull(); + + PropertyInfo? dataProperty = relationshipProperty.PropertyType.GetProperty("Data"); + dataProperty.ShouldNotBeNull(); + dataProperty.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs new file mode 100644 index 0000000000..baad3e94d6 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs @@ -0,0 +1,132 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +public sealed class UpdateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOffMsvOffFakers _fakers = new(); + + [Fact] + public async Task Cannot_omit_Id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PatchResourceAsync(Unknown.TypedId.Int32, requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToOne), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/swagger.g.json new file mode 100644 index 0000000000..c0b2808012 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/swagger.g.json @@ -0,0 +1,1677 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/resources": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "emptyIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyResourceType": { + "enum": [ + "empties" + ], + "type": "string" + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceObject": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableEmptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableEmptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType", + "requiredValueType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType", + "requiredValueType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInResponse" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredToMany", + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredToMany", + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "requiredToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "resources" + ], + "type": "string" + }, + "toManyEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs new file mode 100644 index 0000000000..f6eb4c4f1e --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs @@ -0,0 +1,329 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +public sealed class CreateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOffMsvOnFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().ContainPath(jsonPropertyName).With(attribute => attribute.Should().Be(defaultValue)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + public async Task Cannot_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be($"Cannot write a null value for property '{jsonPropertyName}'. Property requires a value. Path 'data.attributes'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + public async Task Can_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Cannot_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), "requiredToOne")] + public async Task Cannot_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'id'. Property requires a value. Path 'data.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs new file mode 100644 index 0000000000..a722c5d49b --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; + +internal partial class NrtOffMsvOnClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NrtOffMsvOnFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NrtOffMsvOnFakers.cs new file mode 100644 index 0000000000..14d23cb247 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NrtOffMsvOnFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +internal sealed class NrtOffMsvOnFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs new file mode 100644 index 0000000000..11e082fdb9 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.NotNull)] + public void Nullability_of_generated_attribute_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? property = typeof(ResourceAttributesInPostRequest).GetProperty(propertyName); + + // Assert + property.ShouldNotBeNull(); + property.Should().HaveNullabilityState(expectedState); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), NullabilityState.Unknown)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), NullabilityState.Unknown)] + public void Nullability_of_generated_relationship_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? relationshipProperty = typeof(ResourceRelationshipsInPostRequest).GetProperty(propertyName); + + // Assert + relationshipProperty.ShouldNotBeNull(); + + PropertyInfo? dataProperty = relationshipProperty.PropertyType.GetProperty("Data"); + dataProperty.ShouldNotBeNull(); + dataProperty.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs new file mode 100644 index 0000000000..9d049df43b --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs @@ -0,0 +1,132 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +public sealed class UpdateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOffMsvOnFakers _fakers = new(); + + [Fact] + public async Task Cannot_omit_Id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PatchResourceAsync(Unknown.TypedId.Int32, requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToOne), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/swagger.g.json new file mode 100644 index 0000000000..cbf832157e --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/swagger.g.json @@ -0,0 +1,1744 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/resources": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "emptyIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "emptyResourceType": { + "enum": [ + "empties" + ], + "type": "string" + }, + "emptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceObject": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableEmptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableEmptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInResponse" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "requiredToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "resources" + ], + "type": "string" + }, + "toManyEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "additionalProperties": false + }, + "toOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs new file mode 100644 index 0000000000..ec143cb9ee --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs @@ -0,0 +1,352 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +public sealed class CreateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOnMsvOffFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().ContainPath(jsonPropertyName).With(attribute => attribute.Should().Be(defaultValue)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + public async Task Cannot_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().StartWith($"Cannot write a null value for property '{jsonPropertyName}'."); + assertion.Which.Message.Should().EndWith("Path 'data.attributes'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), "requiredNullableToOne")] + public async Task Can_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Cannot_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + public async Task Cannot_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'id'. Property requires a value. Path 'data.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs new file mode 100644 index 0000000000..77b0854984 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; + +internal partial class NrtOnMsvOffClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NrtOnMsvOffFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NrtOnMsvOffFakers.cs new file mode 100644 index 0000000000..4b6365764e --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NrtOnMsvOffFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +internal sealed class NrtOnMsvOffFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs new file mode 100644 index 0000000000..633133a63c --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.Nullable)] + public void Nullability_of_generated_attribute_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? property = typeof(ResourceAttributesInPostRequest).GetProperty(propertyName); + + // Assert + property.ShouldNotBeNull(); + property.Should().HaveNullabilityState(expectedState); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), NullabilityState.Nullable)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), NullabilityState.Nullable)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), NullabilityState.NotNull)] + public void Nullability_of_generated_relationship_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? relationshipProperty = typeof(ResourceRelationshipsInPostRequest).GetProperty(propertyName); + + // Assert + relationshipProperty.ShouldNotBeNull(); + + PropertyInfo? dataProperty = relationshipProperty.PropertyType.GetProperty("Data"); + dataProperty.ShouldNotBeNull(); + dataProperty.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs new file mode 100644 index 0000000000..f17e9e08d3 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs @@ -0,0 +1,142 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +public sealed class UpdateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOnMsvOffFakers _fakers = new(); + + [Fact] + public async Task Cannot_omit_Id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PatchResourceAsync(Unknown.TypedId.Int32, requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNullableToOne), "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/swagger.g.json new file mode 100644 index 0000000000..76623a9819 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/swagger.g.json @@ -0,0 +1,2099 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/resources": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/nonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/nonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/nullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/nullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredNonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredNonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "emptyIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "emptyResourceType": { + "enum": [ + "empties" + ], + "type": "string" + }, + "emptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceObject": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableEmptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableEmptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType", + "requiredValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType", + "requiredValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInResponse" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredNonNullableToOne", + "requiredNullableToOne", + "requiredToMany" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredNonNullableToOne", + "requiredNullableToOne", + "requiredToMany" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "resources" + ], + "type": "string" + }, + "toManyEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "additionalProperties": false + }, + "toOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs new file mode 100644 index 0000000000..b569c008ff --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs @@ -0,0 +1,353 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +public sealed class CreateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOnMsvOnFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().ContainPath(jsonPropertyName).With(attribute => attribute.Should().Be(defaultValue)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + public async Task Cannot_set_attribute_to_default_value(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateAttributeSelectorFor(attributePropertyName); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be($"Cannot write a null value for property '{jsonPropertyName}'. Property requires a value. Path 'data.attributes'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + public async Task Can_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Cannot_clear_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), "requiredNullableToOne")] + public async Task Cannot_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.PostResourceAsync(requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be( + $"Cannot write a null value for property 'id'. Property requires a value. Path 'data.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs new file mode 100644 index 0000000000..6679b1c168 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; + +internal partial class NrtOnMsvOnClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NrtOnMsvOnFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NrtOnMsvOnFakers.cs new file mode 100644 index 0000000000..9906627046 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NrtOnMsvOnFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +internal sealed class NrtOnMsvOnFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs new file mode 100644 index 0000000000..83fce2a945 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.NotNull)] + public void Nullability_of_generated_attribute_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? property = typeof(ResourceAttributesInPostRequest).GetProperty(propertyName); + + // Assert + property.ShouldNotBeNull(); + property.Should().HaveNullabilityState(expectedState); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), NullabilityState.Nullable)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), NullabilityState.NotNull)] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), NullabilityState.NotNull)] + public void Nullability_of_generated_relationship_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + // Act + PropertyInfo? relationshipProperty = typeof(ResourceRelationshipsInPostRequest).GetProperty(propertyName); + + // Assert + relationshipProperty.ShouldNotBeNull(); + + PropertyInfo? dataProperty = relationshipProperty.PropertyType.GetProperty("Data"); + dataProperty.ShouldNotBeNull(); + dataProperty.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs new file mode 100644 index 0000000000..11876a7a02 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs @@ -0,0 +1,142 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +public sealed class UpdateResourceTests : BaseOpenApiClientTests +{ + private readonly NrtOnMsvOnFakers _fakers = new(); + + [Fact] + public async Task Cannot_omit_Id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PatchResourceAsync(Unknown.TypedId.Int32, requestDocument); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + + assertion.Which.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_omit_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.attributes").With(attributesObject => + { + attributesObject.Should().NotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNullableToOne), "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_omit_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + + // Assert + JsonElement document = wrapper.GetRequestBodyAsJson(); + + document.Should().ContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.Should().NotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/swagger.g.json new file mode 100644 index 0000000000..73f9c6048a --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/swagger.g.json @@ -0,0 +1,2093 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/resources": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/nonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/nonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/nullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/nullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableEmptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredNonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNonNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredNonNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNullableToOne", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptySecondaryResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredNullableToOne": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/resources/{id}/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToMany", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyCollectionResponseDocument" + } + } + } + } + } + } + }, + "/resources/{id}/relationships/toMany": { + "get": { + "tags": [ + "resources" + ], + "operationId": "getResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "resources" + ], + "operationId": "headResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/emptyIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "resources" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "resources" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "resources" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "emptyIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/emptyResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "emptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "emptyResourceType": { + "enum": [ + "empties" + ], + "type": "string" + }, + "emptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceObject": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableEmptyIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableEmptySecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/emptyIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "nonNullableReferenceType", + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "nonNullableReferenceType", + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/resourceResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/resourceAttributesInResponse" + }, + "relationships": { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/resourceDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "nonNullableToOne", + "requiredNonNullableToOne", + "requiredNullableToOne" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInRequest" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInRequest" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInRequest" + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "nonNullableToOne", + "requiredNonNullableToOne", + "requiredNullableToOne" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "requiredNonNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "nullableToOne": { + "$ref": "#/components/schemas/nullableToOneEmptyInResponse" + }, + "requiredNullableToOne": { + "$ref": "#/components/schemas/toOneEmptyInResponse" + }, + "toMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + }, + "requiredToMany": { + "$ref": "#/components/schemas/toManyEmptyInResponse" + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "resources" + ], + "type": "string" + }, + "toManyEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toOneEmptyInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + } + }, + "additionalProperties": false + }, + "toOneEmptyInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/emptyIdentifier" + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiTests/JsonElementExtensions.cs b/test/OpenApiTests/JsonElementExtensions.cs deleted file mode 100644 index d8dc7d4e20..0000000000 --- a/test/OpenApiTests/JsonElementExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Text.Json; -using BlushingPenguin.JsonPath; -using FluentAssertions; -using FluentAssertions.Execution; -using TestBuildingBlocks; - -namespace OpenApiTests; - -internal static class JsonElementExtensions -{ - public static JsonElementAssertions Should(this JsonElement source) - { - return new JsonElementAssertions(source); - } - - public static JsonElement ShouldContainPath(this JsonElement source, string path) - { - Func elementSelector = () => source.SelectToken(path, true)!.Value; - return elementSelector.Should().NotThrow().Subject; - } - - public static void ShouldBeString(this JsonElement source, string value) - { - source.ValueKind.Should().Be(JsonValueKind.String); - source.GetString().Should().Be(value); - } - - public static SchemaReferenceIdContainer ShouldBeSchemaReferenceId(this JsonElement source, string value) - { - source.ValueKind.Should().Be(JsonValueKind.String); - - string? jsonElementValue = source.GetString(); - jsonElementValue.ShouldNotBeNull(); - - string schemaReferenceId = jsonElementValue.Split('/').Last(); - schemaReferenceId.Should().Be(value); - - return new SchemaReferenceIdContainer(value); - } - - public sealed class SchemaReferenceIdContainer - { - public string SchemaReferenceId { get; } - - public SchemaReferenceIdContainer(string schemaReferenceId) - { - SchemaReferenceId = schemaReferenceId; - } - } - - public sealed class JsonElementAssertions : JsonElementAssertions - { - public JsonElementAssertions(JsonElement subject) - : base(subject) - { - } - } - - public class JsonElementAssertions - where TAssertions : JsonElementAssertions - { - private readonly JsonElement _subject; - - protected JsonElementAssertions(JsonElement subject) - { - _subject = subject; - } - - public void ContainProperty(string propertyName) - { - string json = _subject.ToString(); - - string escapedJson = json.Replace("{", "{{").Replace("}", "}}"); - - Execute.Assertion.ForCondition(_subject.TryGetProperty(propertyName, out _)) - .FailWith($"Expected JSON element '{escapedJson}' to contain a property named '{propertyName}'."); - } - } -} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index ef84909096..3809d15c91 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1970,6 +1970,9 @@ "additionalProperties": false }, "airplane-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -2238,6 +2241,10 @@ "additionalProperties": false }, "flight-attendant-attributes-in-response": { + "required": [ + "email-address", + "profile-image-url" + ], "type": "object", "properties": { "email-address": { @@ -2557,6 +2564,10 @@ "additionalProperties": false }, "flight-attributes-in-response": { + "required": [ + "final-destination", + "services-on-board" + ], "type": "object", "properties": { "final-destination": { @@ -2815,6 +2826,9 @@ "additionalProperties": false }, "flight-relationships-in-response": { + "required": [ + "purser" + ], "type": "object", "properties": { "cabin-crew-members": { diff --git a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs index e0bbd5bad3..9a694feab5 100644 --- a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs @@ -25,34 +25,34 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets.get").With(getElement => + document.Should().ContainPath("paths./supermarkets.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketCollection"); + operationElement.Should().Be("getSupermarketCollection"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarketCollectionResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceCollectionDocumentSchemaRefId = null; string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapiObject"); + propertiesElement.Should().ContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapiObject"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("linksInResourceCollectionDocument").SchemaReferenceId; - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarketDataInResponse") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarketDataInResponse") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -67,68 +67,68 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? resourceAttributesInResponseSchemaRefId = null; string? resourceRelationshipInResponseSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceObject") + linksInResourceObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceObject") .SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarketResourceType") + primaryResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarketResourceType") .SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.Should().ContainPath("attributes.$ref") .ShouldBeSchemaReferenceId("supermarketAttributesInResponse").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("supermarketRelationshipsInResponse").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); }); - schemasElement.ShouldContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => + schemasElement.Should().ContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => { - enumValueElement.ShouldBeString("supermarkets"); + enumValueElement.Should().Be("supermarkets"); }); - schemasElement.ShouldContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("nameOfCity"); propertiesElement.Should().ContainProperty("kind"); - propertiesElement.ShouldContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarketType"); + propertiesElement.Should().ContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarketType"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("storeManager"); - propertiesElement.ShouldContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInResponse"); + propertiesElement.Should().ContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInResponse"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backupStoreManager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.Should().ContainPath("backupStoreManager.$ref") .ShouldBeSchemaReferenceId("nullableToOneStaffMemberInResponse").SchemaReferenceId; propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInResponse"); + propertiesElement.Should().ContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInResponse"); }); string? linksInRelationshipObjectSchemaRefId = null; string? relatedResourceIdentifierSchemaRefId = null; - schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInRelationshipObject") - .SchemaReferenceId; + linksInRelationshipObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") + .ShouldBeSchemaReferenceId("linksInRelationshipObject").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.Should().ContainPath("data.oneOf[0].$ref") .ShouldBeSchemaReferenceId("staffMemberIdentifier").SchemaReferenceId; - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + propertiesElement.Should().ContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); }); - schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("related"); @@ -136,13 +136,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? relatedResourceTypeSchemaRefId = null; - schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("staffMemberResourceType") + relatedResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("staffMemberResourceType") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("staffMembers"); + schemasElement.Should().ContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("staffMembers"); }); } @@ -155,28 +155,28 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarket"); + operationElement.Should().Be("getSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarketPrimaryResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceDocument") + linksInResourceDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceDocument") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -193,30 +193,30 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/storeManager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/storeManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketStoreManager"); + operationElement.Should().Be("getSupermarketStoreManager"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staffMemberSecondaryResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("staffMemberDataInResponse") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("staffMemberDataInResponse") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staffMemberAttributesInResponse"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staffMemberAttributesInResponse"); }); }); } @@ -228,14 +228,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_nul JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/backupStoreManager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/backupStoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketBackupStoreManager"); + operationElement.Should().Be("getSupermarketBackupStoreManager"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("nullableStaffMemberSecondaryResponseDocument"); }); } @@ -247,14 +247,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_res JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/cashiers.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketCashiers"); + operationElement.Should().Be("getSupermarketCashiers"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staffMemberCollectionResponseDocument"); }); } @@ -268,28 +268,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/relationships/storeManager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/storeManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketStoreManagerRelationship"); + operationElement.Should().Be("getSupermarketStoreManagerRelationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staffMemberIdentifierResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("linksInResourceIdentifierDocument").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -305,14 +305,14 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/backupStoreManager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/backupStoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketBackupStoreManagerRelationship"); + operationElement.Should().Be("getSupermarketBackupStoreManagerRelationship"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("nullableStaffMemberIdentifierResponseDocument"); }); } @@ -326,28 +326,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("getSupermarketCashiersRelationship"); + operationElement.Should().Be("getSupermarketCashiersRelationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staffMemberIdentifierCollectionResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierCollectionDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("linksInResourceIdentifierCollectionDocument").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -369,47 +369,47 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets.post").With(getElement => + document.Should().ContainPath("paths./supermarkets.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("postSupermarket"); + operationElement.Should().Be("postSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarketPostRequestDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPostRequest") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPostRequest") .SchemaReferenceId; }); string? resourceRelationshipInPostRequestSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPostRequest"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPostRequest"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("supermarketRelationshipsInPostRequest").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("storeManager"); - propertiesElement.ShouldContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInRequest"); + propertiesElement.Should().ContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("backupStoreManager"); - propertiesElement.ShouldContainPath("backupStoreManager.$ref").ShouldBeSchemaReferenceId("nullableToOneStaffMemberInRequest"); + propertiesElement.Should().ContainPath("backupStoreManager.$ref").ShouldBeSchemaReferenceId("nullableToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInRequest"); + propertiesElement.Should().ContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInRequest"); }); }); } @@ -421,11 +421,11 @@ public async Task Casing_convention_is_applied_to_PostRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.post").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("postSupermarketCashiersRelationship"); + operationElement.Should().Be("postSupermarketCashiersRelationship"); }); }); } @@ -439,31 +439,31 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patchSupermarket"); + operationElement.Should().Be("patchSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarketPatchRequestDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPatchRequest") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPatchRequest") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPatchRequest"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarketRelationshipsInPatchRequest"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPatchRequest"); + propertiesElement.Should().ContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarketRelationshipsInPatchRequest"); }); }); } @@ -475,11 +475,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/storeManager.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/storeManager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patchSupermarketStoreManagerRelationship"); + operationElement.Should().Be("patchSupermarketStoreManagerRelationship"); }); }); } @@ -491,11 +491,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/backupStoreManager.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/backupStoreManager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patchSupermarketBackupStoreManagerRelationship"); + operationElement.Should().Be("patchSupermarketBackupStoreManagerRelationship"); }); }); } @@ -507,11 +507,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patchSupermarketCashiersRelationship"); + operationElement.Should().Be("patchSupermarketCashiersRelationship"); }); }); } @@ -523,11 +523,11 @@ public async Task Casing_convention_is_applied_to_Delete_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}.delete").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("deleteSupermarket"); + operationElement.Should().Be("deleteSupermarket"); }); }); } @@ -539,11 +539,11 @@ public async Task Casing_convention_is_applied_to_DeleteRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.delete").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("deleteSupermarketCashiersRelationship"); + operationElement.Should().Be("deleteSupermarketCashiersRelationship"); }); }); } diff --git a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs index 0a532a6e3f..e0447fc482 100644 --- a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs @@ -25,34 +25,34 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets.get").With(getElement => + document.Should().ContainPath("paths./supermarkets.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-collection"); + operationElement.Should().Be("get-supermarket-collection"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarket-collection-response-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceCollectionDocumentSchemaRefId = null; string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapi-object"); + propertiesElement.Should().ContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapi-object"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("links-in-resource-collection-document").SchemaReferenceId; - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-response") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-response") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -67,68 +67,68 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? resourceAttributesInResponseSchemaRefId = null; string? resourceRelationshipInResponseSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("links-in-resource-object") + linksInResourceObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref").ShouldBeSchemaReferenceId("links-in-resource-object") .SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarket-resource-type") + primaryResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarket-resource-type") .SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.Should().ContainPath("attributes.$ref") .ShouldBeSchemaReferenceId("supermarket-attributes-in-response").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("supermarket-relationships-in-response").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); }); - schemasElement.ShouldContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => + schemasElement.Should().ContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => { - enumValueElement.ShouldBeString("supermarkets"); + enumValueElement.Should().Be("supermarkets"); }); - schemasElement.ShouldContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("name-of-city"); propertiesElement.Should().ContainProperty("kind"); - propertiesElement.ShouldContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarket-type"); + propertiesElement.Should().ContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarket-type"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("store-manager"); - propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-response"); + propertiesElement.Should().ContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-response"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backup-store-manager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.Should().ContainPath("backup-store-manager.$ref") .ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-response").SchemaReferenceId; propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-response"); + propertiesElement.Should().ContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-response"); }); string? linksInRelationshipObjectSchemaRefId = null; string? relatedResourceIdentifierSchemaRefId = null; - schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInRelationshipObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("links-in-relationship-object").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.Should().ContainPath("data.oneOf[0].$ref") .ShouldBeSchemaReferenceId("staff-member-identifier").SchemaReferenceId; - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("null-value"); + propertiesElement.Should().ContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("null-value"); }); - schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("related"); @@ -136,13 +136,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? relatedResourceTypeSchemaRefId = null; - schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("staff-member-resource-type") + relatedResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("staff-member-resource-type") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("staff-members"); + schemasElement.Should().ContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("staff-members"); }); } @@ -155,28 +155,28 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket"); + operationElement.Should().Be("get-supermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarket-primary-response-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("links-in-resource-document") - .SchemaReferenceId; + linksInResourceDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") + .ShouldBeSchemaReferenceId("links-in-resource-document").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -193,30 +193,30 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/store-manager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/store-manager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-store-manager"); + operationElement.Should().Be("get-supermarket-store-manager"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staff-member-secondary-response-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("staff-member-data-in-response") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("staff-member-data-in-response") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staff-member-attributes-in-response"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staff-member-attributes-in-response"); }); }); } @@ -228,14 +228,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_nul JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/backup-store-manager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/backup-store-manager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-backup-store-manager"); + operationElement.Should().Be("get-supermarket-backup-store-manager"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("nullable-staff-member-secondary-response-document"); }); } @@ -247,14 +247,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_res JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/cashiers.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-cashiers"); + operationElement.Should().Be("get-supermarket-cashiers"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staff-member-collection-response-document"); }); } @@ -268,28 +268,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/relationships/store-manager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/store-manager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-store-manager-relationship"); + operationElement.Should().Be("get-supermarket-store-manager-relationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staff-member-identifier-response-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("links-in-resource-identifier-document").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -305,14 +305,14 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-backup-store-manager-relationship"); + operationElement.Should().Be("get-supermarket-backup-store-manager-relationship"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("nullable-staff-member-identifier-response-document"); }); } @@ -326,28 +326,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.get").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("get-supermarket-cashiers-relationship"); + operationElement.Should().Be("get-supermarket-cashiers-relationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("staff-member-identifier-collection-response-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierCollectionDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("links-in-resource-identifier-collection-document").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -369,47 +369,47 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets.post").With(getElement => + document.Should().ContainPath("paths./supermarkets.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("post-supermarket"); + operationElement.Should().Be("post-supermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarket-post-request-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-post-request") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-post-request") .SchemaReferenceId; }); string? resourceRelationshipInPostRequestSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-post-request"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-post-request"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("supermarket-relationships-in-post-request").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("store-manager"); - propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-request"); + propertiesElement.Should().ContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-request"); propertiesElement.Should().ContainProperty("backup-store-manager"); - propertiesElement.ShouldContainPath("backup-store-manager.$ref").ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-request"); + propertiesElement.Should().ContainPath("backup-store-manager.$ref").ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-request"); propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-request"); + propertiesElement.Should().ContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-request"); }); }); } @@ -421,11 +421,11 @@ public async Task Casing_convention_is_applied_to_PostRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.post").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("post-supermarket-cashiers-relationship"); + operationElement.Should().Be("post-supermarket-cashiers-relationship"); }); }); } @@ -439,31 +439,31 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./supermarkets/{id}.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patch-supermarket"); + operationElement.Should().Be("patch-supermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("supermarket-patch-request-document").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-patch-request") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-patch-request") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-patch-request"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarket-relationships-in-patch-request"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-patch-request"); + propertiesElement.Should().ContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarket-relationships-in-patch-request"); }); }); } @@ -475,11 +475,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/store-manager.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/store-manager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patch-supermarket-store-manager-relationship"); + operationElement.Should().Be("patch-supermarket-store-manager-relationship"); }); }); } @@ -491,11 +491,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/backup-store-manager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patch-supermarket-backup-store-manager-relationship"); + operationElement.Should().Be("patch-supermarket-backup-store-manager-relationship"); }); }); } @@ -507,11 +507,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.patch").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("patch-supermarket-cashiers-relationship"); + operationElement.Should().Be("patch-supermarket-cashiers-relationship"); }); }); } @@ -523,11 +523,11 @@ public async Task Casing_convention_is_applied_to_Delete_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}.delete").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("delete-supermarket"); + operationElement.Should().Be("delete-supermarket"); }); }); } @@ -539,11 +539,11 @@ public async Task Casing_convention_is_applied_to_DeleteRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./supermarkets/{id}/relationships/cashiers.delete").With(getElement => + document.Should().ContainPath("paths./supermarkets/{id}/relationships/cashiers.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("delete-supermarket-cashiers-relationship"); + operationElement.Should().Be("delete-supermarket-cashiers-relationship"); }); }); } diff --git a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs index 73868dcdad..48497186c5 100644 --- a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs @@ -26,34 +26,34 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketCollection"); + operationElement.Should().Be("GetSupermarketCollection"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("SupermarketCollectionResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceCollectionDocumentSchemaRefId = null; string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("JsonapiObject"); + propertiesElement.Should().ContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("JsonapiObject"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("LinksInResourceCollectionDocument").SchemaReferenceId; - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("SupermarketDataInResponse") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.items.$ref").ShouldBeSchemaReferenceId("SupermarketDataInResponse") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -68,68 +68,68 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? resourceAttributesInResponseSchemaRefId = null; string? resourceRelationshipInResponseSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceObject") + linksInResourceObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceObject") .SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("SupermarketResourceType") + primaryResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("SupermarketResourceType") .SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.Should().ContainPath("attributes.$ref") .ShouldBeSchemaReferenceId("SupermarketAttributesInResponse").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("SupermarketRelationshipsInResponse").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); }); - schemasElement.ShouldContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => + schemasElement.Should().ContainPath($"{primaryResourceTypeSchemaRefId}.enum[0]").With(enumValueElement => { - enumValueElement.ShouldBeString("Supermarkets"); + enumValueElement.Should().Be("Supermarkets"); }); - schemasElement.ShouldContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceAttributesInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("NameOfCity"); propertiesElement.Should().ContainProperty("Kind"); - propertiesElement.ShouldContainPath("Kind.$ref").ShouldBeSchemaReferenceId("SupermarketType"); + propertiesElement.Should().ContainPath("Kind.$ref").ShouldBeSchemaReferenceId("SupermarketType"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInResponseSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("StoreManager"); - propertiesElement.ShouldContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInResponse"); + propertiesElement.Should().ContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInResponse"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("BackupStoreManager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.Should().ContainPath("BackupStoreManager.$ref") .ShouldBeSchemaReferenceId("NullableToOneStaffMemberInResponse").SchemaReferenceId; propertiesElement.Should().ContainProperty("Cashiers"); - propertiesElement.ShouldContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInResponse"); + propertiesElement.Should().ContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInResponse"); }); string? linksInRelationshipObjectSchemaRefId = null; string? relatedResourceIdentifierSchemaRefId = null; - schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInRelationshipObject") - .SchemaReferenceId; + linksInRelationshipObjectSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") + .ShouldBeSchemaReferenceId("LinksInRelationshipObject").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.Should().ContainPath("data.oneOf[0].$ref") .ShouldBeSchemaReferenceId("StaffMemberIdentifier").SchemaReferenceId; - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("NullValue"); + propertiesElement.Should().ContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("NullValue"); }); - schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("related"); @@ -137,13 +137,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() string? relatedResourceTypeSchemaRefId = null; - schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("StaffMemberResourceType") + relatedResourceTypeSchemaRefId = propertiesElement.Should().ContainPath("type.$ref").ShouldBeSchemaReferenceId("StaffMemberResourceType") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("StaffMembers"); + schemasElement.Should().ContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("StaffMembers"); }); } @@ -156,28 +156,28 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets/{id}.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarket"); + operationElement.Should().Be("GetSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("SupermarketPrimaryResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceDocument") + linksInResourceDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceDocument") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -194,30 +194,30 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets/{id}/StoreManager.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/StoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketStoreManager"); + operationElement.Should().Be("GetSupermarketStoreManager"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("StaffMemberSecondaryResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("StaffMemberDataInResponse") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("StaffMemberDataInResponse") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("StaffMemberAttributesInResponse"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("StaffMemberAttributesInResponse"); }); }); } @@ -229,14 +229,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_nul JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/BackupStoreManager.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/BackupStoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketBackupStoreManager"); + operationElement.Should().Be("GetSupermarketBackupStoreManager"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("NullableStaffMemberSecondaryResponseDocument"); }); } @@ -248,14 +248,14 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_res JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/Cashiers.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/Cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketCashiers"); + operationElement.Should().Be("GetSupermarketCashiers"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("StaffMemberCollectionResponseDocument"); }); } @@ -269,28 +269,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/StoreManager.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/StoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketStoreManagerRelationship"); + operationElement.Should().Be("GetSupermarketStoreManagerRelationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("StaffMemberIdentifierResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("LinksInResourceIdentifierDocument").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -306,14 +306,14 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/BackupStoreManager.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/BackupStoreManager.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketBackupStoreManagerRelationship"); + operationElement.Should().Be("GetSupermarketBackupStoreManagerRelationship"); }); - getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("NullableStaffMemberIdentifierResponseDocument"); }); } @@ -327,28 +327,28 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/Cashiers.get").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/Cashiers.get").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("GetSupermarketCashiersRelationship"); + operationElement.Should().Be("GetSupermarketCashiersRelationship"); }); - documentSchemaRefId = getElement.ShouldContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("responses.200.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("StaffMemberIdentifierCollectionResponseDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? linksInResourceIdentifierCollectionDocumentSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.Should().ContainPath("links.$ref") .ShouldBeSchemaReferenceId("LinksInResourceIdentifierCollectionDocument").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{linksInResourceIdentifierCollectionDocumentSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("self"); propertiesElement.Should().ContainProperty("describedby"); @@ -370,47 +370,47 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets.post").With(getElement => + document.Should().ContainPath("paths./Supermarkets.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PostSupermarket"); + operationElement.Should().Be("PostSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("SupermarketPostRequestDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPostRequest") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPostRequest") .SchemaReferenceId; }); string? resourceRelationshipInPostRequestSchemaRefId = null; - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPostRequest"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPostRequest"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.Should().ContainPath("relationships.$ref") .ShouldBeSchemaReferenceId("SupermarketRelationshipsInPostRequest").SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("StoreManager"); - propertiesElement.ShouldContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInRequest"); + propertiesElement.Should().ContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("BackupStoreManager"); - propertiesElement.ShouldContainPath("BackupStoreManager.$ref").ShouldBeSchemaReferenceId("NullableToOneStaffMemberInRequest"); + propertiesElement.Should().ContainPath("BackupStoreManager.$ref").ShouldBeSchemaReferenceId("NullableToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("Cashiers"); - propertiesElement.ShouldContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInRequest"); + propertiesElement.Should().ContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInRequest"); }); }); } @@ -422,11 +422,11 @@ public async Task Casing_convention_is_applied_to_PostRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/Cashiers.post").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/Cashiers.post").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PostSupermarketCashiersRelationship"); + operationElement.Should().Be("PostSupermarketCashiersRelationship"); }); }); } @@ -440,31 +440,31 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() // Assert string? documentSchemaRefId = null; - document.ShouldContainPath("paths./Supermarkets/{id}.patch").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PatchSupermarket"); + operationElement.Should().Be("PatchSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.Should().ContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") .ShouldBeSchemaReferenceId("SupermarketPatchRequestDocument").SchemaReferenceId; }); - document.ShouldContainPath("components.schemas").With(schemasElement => + document.Should().ContainPath("components.schemas").With(schemasElement => { string? resourceDataSchemaRefId = null; - schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPatchRequest") + resourceDataSchemaRefId = propertiesElement.Should().ContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPatchRequest") .SchemaReferenceId; }); - schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => + schemasElement.Should().ContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPatchRequest"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("SupermarketRelationshipsInPatchRequest"); + propertiesElement.Should().ContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPatchRequest"); + propertiesElement.Should().ContainPath("relationships.$ref").ShouldBeSchemaReferenceId("SupermarketRelationshipsInPatchRequest"); }); }); } @@ -476,11 +476,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/StoreManager.patch").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/StoreManager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PatchSupermarketStoreManagerRelationship"); + operationElement.Should().Be("PatchSupermarketStoreManagerRelationship"); }); }); } @@ -492,11 +492,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/BackupStoreManager.patch").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/BackupStoreManager.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PatchSupermarketBackupStoreManagerRelationship"); + operationElement.Should().Be("PatchSupermarketBackupStoreManagerRelationship"); }); }); } @@ -508,11 +508,11 @@ public async Task Casing_convention_is_applied_to_PatchRelationship_endpoint_wit JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/Cashiers.patch").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/Cashiers.patch").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("PatchSupermarketCashiersRelationship"); + operationElement.Should().Be("PatchSupermarketCashiersRelationship"); }); }); } @@ -524,11 +524,11 @@ public async Task Casing_convention_is_applied_to_Delete_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}.delete").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("DeleteSupermarket"); + operationElement.Should().Be("DeleteSupermarket"); }); }); } @@ -540,11 +540,11 @@ public async Task Casing_convention_is_applied_to_DeleteRelationship_endpoint() JsonElement document = await _testContext.GetSwaggerDocumentAsync(); // Assert - document.ShouldContainPath("paths./Supermarkets/{id}/relationships/Cashiers.delete").With(getElement => + document.Should().ContainPath("paths./Supermarkets/{id}/relationships/Cashiers.delete").With(getElement => { - getElement.ShouldContainPath("operationId").With(operationElement => + getElement.Should().ContainPath("operationId").With(operationElement => { - operationElement.ShouldBeString("DeleteSupermarketCashiersRelationship"); + operationElement.Should().Be("DeleteSupermarketCashiersRelationship"); }); }); } diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs index 46978e7f67..e34ed1894c 100644 --- a/test/OpenApiTests/OpenApiStartup.cs +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -6,7 +6,7 @@ namespace OpenApiTests; -public abstract class OpenApiStartup : TestableStartup +public class OpenApiStartup : TestableStartup where TDbContext : TestableDbContext { public override void ConfigureServices(IServiceCollection services) diff --git a/test/OpenApiTests/OpenApiTestContext.cs b/test/OpenApiTests/OpenApiTestContext.cs index f720df7d26..e54d09a269 100644 --- a/test/OpenApiTests/OpenApiTestContext.cs +++ b/test/OpenApiTests/OpenApiTestContext.cs @@ -2,7 +2,6 @@ using System.Text.Json; using JetBrains.Annotations; using TestBuildingBlocks; -using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; namespace OpenApiTests; @@ -27,34 +26,27 @@ internal async Task GetSwaggerDocumentAsync() private async Task CreateSwaggerDocumentAsync() { - string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath); - string content = await GetAsync("swagger/v1/swagger.json"); JsonElement rootElement = ParseSwaggerDocument(content); - await WriteToDiskAsync(absoluteOutputPath, rootElement); + + if (SwaggerDocumentOutputPath != null) + { + string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath); + await WriteToDiskAsync(absoluteOutputPath, rootElement); + } return rootElement; } - private static string GetSwaggerDocumentAbsoluteOutputPath(string? relativePath) + private static string GetSwaggerDocumentAbsoluteOutputPath(string relativePath) { - AssertHasSwaggerDocumentOutputPath(relativePath); - string solutionRoot = Path.Combine(Assembly.GetExecutingAssembly().Location, "../../../../../../"); string outputPath = Path.Combine(solutionRoot, relativePath, "swagger.g.json"); return Path.GetFullPath(outputPath); } - private static void AssertHasSwaggerDocumentOutputPath([SysNotNull] string? relativePath) - { - if (relativePath is null) - { - throw new Exception($"Property '{nameof(OpenApiTestContext)}.{nameof(SwaggerDocumentOutputPath)}' must be set."); - } - } - private async Task GetAsync(string requestUrl) { using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index f6f1441fc1..32ea69850c 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -6,7 +6,7 @@ - + @@ -18,6 +18,5 @@ - diff --git a/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs b/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs new file mode 100644 index 0000000000..838803f9c4 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class MsvOffStartup : OpenApiStartup + where TDbContext : TestableDbContext +{ + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.ValidateModelState = false; + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs new file mode 100644 index 0000000000..d02338445e --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/NullabilityTests.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +public sealed class NullabilityTests : IClassFixture, NrtOffDbContext>> +{ + private readonly OpenApiTestContext, NrtOffDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NrtOffDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff"; + } + + [Theory] + [InlineData("referenceType")] + [InlineData("requiredReferenceType")] + [InlineData("nullableValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().ContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("valueType")] + [InlineData("requiredValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().NotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("requiredToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data").Should().NotContainPath("oneOf[1].$ref"); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/RequiredTests.cs new file mode 100644 index 0000000000..6974a721c2 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/RequiredTests.cs @@ -0,0 +1,103 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff; + +public sealed class RequiredTests : IClassFixture, NrtOffDbContext>> +{ + private readonly OpenApiTestContext, NrtOffDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NrtOffDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("referenceType")] + [InlineData("valueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredToOne")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("toMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs new file mode 100644 index 0000000000..185cd9a5e1 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/NullabilityTests.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +public sealed class NullabilityTests : IClassFixture, NrtOffDbContext>> +{ + private readonly OpenApiTestContext, NrtOffDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NrtOffDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn"; + } + + [Theory] + [InlineData("referenceType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().ContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().NotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("toOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("requiredToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data").Should().NotContainPath("oneOf[1].$ref"); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/RequiredTests.cs new file mode 100644 index 0000000000..bab06b8b23 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/RequiredTests.cs @@ -0,0 +1,103 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn; + +public sealed class RequiredTests : IClassFixture, NrtOffDbContext>> +{ + private readonly OpenApiTestContext, NrtOffDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NrtOffDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("referenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredToOne")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffDbContext.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffDbContext.cs new file mode 100644 index 0000000000..0b6ea5d875 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffDbContext.cs @@ -0,0 +1,44 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff; + +// @formatter:wrap_chained_method_calls chop_always + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class NrtOffDbContext : TestableDbContext +{ + public DbSet Resources => Set(); + public DbSet Empties => Set(); + + public NrtOffDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.ToOne) + .WithOne() + .HasForeignKey("ToOneId"); + + builder.Entity() + .HasOne(resource => resource.RequiredToOne) + .WithOne() + .HasForeignKey("RequiredToOneId"); + + builder.Entity() + .HasMany(resource => resource.ToMany) + .WithOne() + .HasForeignKey("ToManyId"); + + builder.Entity() + .HasMany(resource => resource.RequiredToMany) + .WithOne() + .HasForeignKey("RequiredToManyId"); + + base.OnModelCreating(builder); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffEmpty.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffEmpty.cs new file mode 100644 index 0000000000..4df36bca01 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffEmpty.cs @@ -0,0 +1,13 @@ +#nullable disable + +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +[Resource(PublicName = "empties", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public class NrtOffEmpty : Identifiable +{ +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffResource.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffResource.cs new file mode 100644 index 0000000000..968d579d49 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/NrtOffResource.cs @@ -0,0 +1,48 @@ +#nullable disable + +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOff; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(PublicName = "resources", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public sealed class NrtOffResource : Identifiable +{ + [Attr] + public string ReferenceType { get; set; } + + [Attr] + [Required] + public string RequiredReferenceType { get; set; } + + [Attr] + public int ValueType { get; set; } + + [Attr] + [Required] + public int RequiredValueType { get; set; } + + [Attr] + public int? NullableValueType { get; set; } + + [Attr] + [Required] + public int? RequiredNullableValueType { get; set; } + + [HasOne] + public NrtOffEmpty ToOne { get; set; } + + [Required] + [HasOne] + public NrtOffEmpty RequiredToOne { get; set; } + + [HasMany] + public ICollection ToMany { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection RequiredToMany { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs new file mode 100644 index 0000000000..a41c3f7508 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/NullabilityTests.cs @@ -0,0 +1,97 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +public sealed class NullabilityTests : IClassFixture, NrtOnDbContext>> +{ + private readonly OpenApiTestContext, NrtOnDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NrtOnDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff"; + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("nullableValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().ContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().NotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("nullableToOne")] + [InlineData("requiredNullableToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data").Should().NotContainPath("oneOf[1].$ref"); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/RequiredTests.cs new file mode 100644 index 0000000000..7020004021 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/RequiredTests.cs @@ -0,0 +1,107 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff; + +public sealed class RequiredTests : IClassFixture, NrtOnDbContext>> +{ + private readonly OpenApiTestContext, NrtOnDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NrtOnDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("nullableReferenceType")] + [InlineData("valueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("nullableToOne")] + [InlineData("toMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs new file mode 100644 index 0000000000..2d1cf8c117 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/NullabilityTests.cs @@ -0,0 +1,97 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +public sealed class NullabilityTests : IClassFixture, NrtOnDbContext>> +{ + private readonly OpenApiTestContext, NrtOnDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NrtOnDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn"; + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().ContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.Should().NotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("nullableToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.Should().ContainPath($"{jsonPropertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.Should().ContainPath($"components.schemas.{schemaReferenceId}.properties.data").Should().NotContainPath("oneOf[1].$ref"); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/RequiredTests.cs new file mode 100644 index 0000000000..e33a4ae3d7 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/RequiredTests.cs @@ -0,0 +1,107 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn; + +public sealed class RequiredTests : IClassFixture, NrtOnDbContext>> +{ + private readonly OpenApiTestContext, NrtOnDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NrtOnDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().ContainArrayElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.Should().ContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.Should().ContainPath("required").With(propertySet => propertySet.Should().NotContainArrayElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_for_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().NotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnDbContext.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnDbContext.cs new file mode 100644 index 0000000000..bf213ea812 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnDbContext.cs @@ -0,0 +1,54 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn; + +// @formatter:wrap_chained_method_calls chop_always + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class NrtOnDbContext : TestableDbContext +{ + public DbSet Resources => Set(); + public DbSet Empties => Set(); + + public NrtOnDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.NonNullableToOne) + .WithOne() + .HasForeignKey("NonNullableToOneId"); + + builder.Entity() + .HasOne(resource => resource.RequiredNonNullableToOne) + .WithOne() + .HasForeignKey("RequiredNonNullableToOneId"); + + builder.Entity() + .HasOne(resource => resource.NullableToOne) + .WithOne() + .HasForeignKey("NullableToOneId"); + + builder.Entity() + .HasOne(resource => resource.RequiredNullableToOne) + .WithOne() + .HasForeignKey("RequiredNullableToOneId"); + + builder.Entity() + .HasMany(resource => resource.ToMany) + .WithOne() + .HasForeignKey("ToManyId"); + + builder.Entity() + .HasMany(resource => resource.RequiredToMany) + .WithOne() + .HasForeignKey("RequiredToManyId"); + + base.OnModelCreating(builder); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnEmpty.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnEmpty.cs new file mode 100644 index 0000000000..65df582c71 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnEmpty.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +[Resource(PublicName = "empties", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public class NrtOnEmpty : Identifiable +{ +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnResource.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnResource.cs new file mode 100644 index 0000000000..efc897cea2 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/NrtOnResource.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesOn; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(PublicName = "resources", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public sealed class NrtOnResource : Identifiable +{ + [Attr] + public string NonNullableReferenceType { get; set; } = null!; + + [Attr] + [Required] + public string RequiredNonNullableReferenceType { get; set; } = null!; + + [Attr] + public string? NullableReferenceType { get; set; } + + [Attr] + [Required] + public string? RequiredNullableReferenceType { get; set; } + + [Attr] + public int ValueType { get; set; } + + [Attr] + [Required] + public int RequiredValueType { get; set; } + + [Attr] + public int? NullableValueType { get; set; } + + [Attr] + [Required] + public int? RequiredNullableValueType { get; set; } + + [HasOne] + public NrtOnEmpty NonNullableToOne { get; set; } = null!; + + [Required] + [HasOne] + public NrtOnEmpty RequiredNonNullableToOne { get; set; } = null!; + + [HasOne] + public NrtOnEmpty? NullableToOne { get; set; } + + [Required] + [HasOne] + public NrtOnEmpty? RequiredNullableToOne { get; set; } + + [HasMany] + public ICollection ToMany { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection RequiredToMany { get; set; } = new HashSet(); +} diff --git a/test/TestBuildingBlocks/FakerContainer.cs b/test/TestBuildingBlocks/FakerContainer.cs index 72f9a05567..c7104852d1 100644 --- a/test/TestBuildingBlocks/FakerContainer.cs +++ b/test/TestBuildingBlocks/FakerContainer.cs @@ -15,7 +15,7 @@ static FakerContainer() Date.SystemClock = () => 1.January(2020).At(1, 1, 1).AsUtc(); } - protected static int GetFakerSeed() + public static int GetFakerSeed() { // The goal here is to have stable data over multiple test runs, but at the same time different data per test case. diff --git a/test/TestBuildingBlocks/JsonElementAssertionExtensions.cs b/test/TestBuildingBlocks/JsonElementAssertionExtensions.cs new file mode 100644 index 0000000000..b541c46754 --- /dev/null +++ b/test/TestBuildingBlocks/JsonElementAssertionExtensions.cs @@ -0,0 +1,139 @@ +using System.Text.Json; +using BlushingPenguin.JsonPath; +using FluentAssertions; +using FluentAssertions.Execution; +using JetBrains.Annotations; + +namespace TestBuildingBlocks; + +public static class JsonElementAssertionExtensions +{ + public static JsonElementAssertions Should(this JsonElement source) + { + return new JsonElementAssertions(source); + } + + [CustomAssertion] + public static SchemaReferenceIdContainer ShouldBeSchemaReferenceId(this JsonElement source, string value) + { + string schemaReferenceId = GetSchemaReferenceId(source); + schemaReferenceId.Should().Be(value); + + return new SchemaReferenceIdContainer(value); + } + + [CustomAssertion] + private static string GetSchemaReferenceId(this JsonElement source) + { + source.ValueKind.Should().Be(JsonValueKind.String); + + string? jsonElementValue = source.GetString(); + jsonElementValue.ShouldNotBeNull(); + + return jsonElementValue.Split('/').Last(); + } + + [CustomAssertion] + public static void WithSchemaReferenceId(this JsonElement subject, [InstantHandle] Action continuation) + { + string schemaReferenceId = GetSchemaReferenceId(subject); + + continuation(schemaReferenceId); + } + + public sealed class SchemaReferenceIdContainer + { + public string SchemaReferenceId { get; } + + internal SchemaReferenceIdContainer(string schemaReferenceId) + { + SchemaReferenceId = schemaReferenceId; + } + } + + public sealed class JsonElementAssertions : JsonElementAssertions + { + internal JsonElementAssertions(JsonElement subject) + : base(subject) + { + } + } + + public class JsonElementAssertions + where TAssertions : JsonElementAssertions + { + private readonly JsonElement _subject; + + protected JsonElementAssertions(JsonElement subject) + { + _subject = subject; + } + + public void ContainProperty(string propertyName) + { + string json = _subject.ToString(); + string escapedJson = json.Replace("{", "{{").Replace("}", "}}"); + + Execute.Assertion.ForCondition(_subject.TryGetProperty(propertyName, out _)) + .FailWith($"Expected JSON element '{escapedJson}' to contain a property named '{propertyName}'."); + } + + public JsonElement ContainPath(string path) + { + Func elementSelector = () => _subject.SelectToken(path, true)!.Value; + return elementSelector.Should().NotThrow().Subject; + } + + public void NotContainPath(string path) + { + JsonElement? pathToken = _subject.SelectToken(path); + pathToken.Should().BeNull(); + } + + public void Be(object? value) + { + if (value == null) + { + _subject.ValueKind.Should().Be(JsonValueKind.Null); + } + else if (value is bool boolValue) + { + _subject.ValueKind.Should().Be(boolValue ? JsonValueKind.True : JsonValueKind.False); + } + else if (value is int intValue) + { + _subject.ValueKind.Should().Be(JsonValueKind.Number); + _subject.GetInt32().Should().Be(intValue); + } + else if (value is string stringValue) + { + _subject.ValueKind.Should().Be(JsonValueKind.String); + _subject.GetString().Should().Be(stringValue); + } + else + { + throw new NotSupportedException($"Unknown object of type '{value.GetType()}'."); + } + } + + public void ContainArrayElement(string value) + { + _subject.ValueKind.Should().Be(JsonValueKind.Array); + + string?[] stringValues = _subject.EnumerateArray().Where(element => element.ValueKind == JsonValueKind.String) + .Select(element => element.GetString()).ToArray(); + + stringValues.Should().Contain(value); + } + + public void NotContainArrayElement(string value) + { + _subject.ValueKind.Should().Be(JsonValueKind.Array); + + string?[] stringValues = _subject.EnumerateArray().Where(element => element.ValueKind == JsonValueKind.String) + .Select(element => element.GetString()).ToArray(); + + stringValues.Should().NotContain(value); + } + } +} diff --git a/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs b/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs index 0627449664..90d4eaba7c 100644 --- a/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs +++ b/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs @@ -2,7 +2,6 @@ using JetBrains.Annotations; using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; -// ReSharper disable PossibleMultipleEnumeration #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. namespace TestBuildingBlocks; diff --git a/test/TestBuildingBlocks/TestBuildingBlocks.csproj b/test/TestBuildingBlocks/TestBuildingBlocks.csproj index ba9a2f5da3..6e26f0af4b 100644 --- a/test/TestBuildingBlocks/TestBuildingBlocks.csproj +++ b/test/TestBuildingBlocks/TestBuildingBlocks.csproj @@ -8,6 +8,7 @@ +