diff --git a/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache b/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache index 1a6c8de..6484baa 100644 --- a/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache +++ b/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache @@ -58,6 +58,7 @@ CsvBindingException.cs CsvBindingExceptionFilter.cs CsvRequest.cs CsvResponse.cs +ICsvBindingErrors.cs obj/Debug/net10.0/ .NETCoreApp,Version=v10.0.AssemblyAttributes.cs AxisEndpoints.Extensions.CsvHelper.AssemblyInfo.cs diff --git a/src/AxisEndpoints/IEndpointConfiguration.cs b/src/AxisEndpoints/IEndpointConfiguration.cs index e3a9edb..ef0bcb1 100644 --- a/src/AxisEndpoints/IEndpointConfiguration.cs +++ b/src/AxisEndpoints/IEndpointConfiguration.cs @@ -40,14 +40,16 @@ IEndpointConfiguration Group() IEndpointConfiguration Summary(string summary); IEndpointConfiguration Description(string description); - // OpenAPI response metadata for IResult-returning endpoints. - // When HandleAsync returns Response, the 200 schema is inferred automatically and - // these methods are not needed. Use them when the return type is IResult to declare the - // success and error response shapes explicitly. + // OpenAPI response metadata. + // When HandleAsync returns Response, a 200 success response is inferred automatically. + // Use ProducesSuccess to override that default when the endpoint returns a different success + // status code such as 201 or 204. When HandleAsync returns IResult, use ProducesSuccess and + // ProducesError to declare the response shapes explicitly. /// - /// Declares a success response for OpenAPI. Use when HandleAsync returns - /// and the 200 schema cannot be inferred automatically. + /// Declares a success response for OpenAPI. Use this to override the default 200 response + /// metadata for endpoints or to declare success metadata when + /// HandleAsync returns . /// IEndpointConfiguration ProducesSuccess(HttpStatusCode statusCode = HttpStatusCode.OK); diff --git a/src/AxisEndpoints/Internal/EndpointRegistry.cs b/src/AxisEndpoints/Internal/EndpointRegistry.cs index 81aaaec..463a0d6 100644 --- a/src/AxisEndpoints/Internal/EndpointRegistry.cs +++ b/src/AxisEndpoints/Internal/EndpointRegistry.cs @@ -474,20 +474,55 @@ EndpointConfiguration config // When TResult is Response, register Produces(200) automatically from the // inferred response type. When TResult is IResult, skip auto-registration — the // caller is responsible for declaring response shapes via ProducesSuccess/ProducesError. + var openApiResponseType = GetOpenApiResponseType(config.ResponseType); var isIResult = - config.ResponseType == typeof(IResult) - || typeof(IResult).IsAssignableFrom(config.ResponseType); + openApiResponseType == typeof(IResult) + || typeof(IResult).IsAssignableFrom(openApiResponseType); + var hasExplicitSuccessResponse = config.ExtraProducesEntries.Any(entry => + entry.StatusCode is >= 200 and < 300 + ); - if (!isIResult) + if (!isIResult && !hasExplicitSuccessResponse) { - routeBuilder.Produces(200, config.ResponseType); + RegisterProduces(routeBuilder, 200, openApiResponseType); } } foreach (var (statusCode, bodyType) in config.ExtraProducesEntries) { - routeBuilder.Produces(statusCode, bodyType); + RegisterProduces(routeBuilder, statusCode, bodyType); + } + } + + private static Type GetOpenApiResponseType(Type responseType) + { + if ( + responseType.IsGenericType + && responseType.GetGenericTypeDefinition() == typeof(Response<>) + ) + { + return responseType.GetGenericArguments()[0]; + } + + return responseType; + } + + private static void RegisterProduces( + RouteHandlerBuilder routeBuilder, + int statusCode, + Type bodyType + ) + { + var isNoBodyResponse = + bodyType == typeof(EmptyResponse) || typeof(EmptyResponse).IsAssignableFrom(bodyType); + + if (isNoBodyResponse) + { + routeBuilder.Produces(statusCode); + return; } + + routeBuilder.Produces(statusCode, bodyType); } private static void ApplyGroupMetadata( diff --git a/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj b/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj index 0d29ef5..dc52533 100644 --- a/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj +++ b/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs b/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs index e1190f4..4ff8d02 100644 --- a/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs +++ b/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs @@ -4,6 +4,7 @@ using AxisEndpoints.Extensions.CsvHelper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace AxisEndpoints.Example.Tests; @@ -22,10 +23,12 @@ public async Task InitializeAsync() var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); + builder.Services.AddOpenApi(); builder.Services.AddAxisEndpoints(ExampleAssembly); builder.Services.AddAxisEndpointsCsvHelper(); _app = builder.Build(); + _app.MapOpenApi(); _app.MapAxisEndpoints(ExampleAssembly); await _app.StartAsync(); diff --git a/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs new file mode 100644 index 0000000..2b24c81 --- /dev/null +++ b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs @@ -0,0 +1,56 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using FluentAssertions; +using AxisEndpoints.Example.Features.Health; + +namespace AxisEndpoints.Example.Tests; + +public class OpenApiTests : IClassFixture +{ + private readonly HttpClient _client; + + public OpenApiTests(ExampleWebApplicationFactory factory) + { + _client = factory.Client; + } + + [Fact] + public async Task OpenApi_UsesBodyTypeForJsonResponseEndpoints() + { + var document = await _client.GetFromJsonAsync("/openapi/v1.json"); + + document.Should().NotBeNull(); + + var schema = document!["paths"]?["/health"]?["get"]?["responses"]?["200"]?["content"]?["application/json"]?["schema"]; + schema.Should().NotBeNull(); + schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HealthResponse"); + + document["components"]?["schemas"]?["ResponseOfHealthResponse"].Should().BeNull(); + + var createdSchema = + document["paths"]?["/api/users"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"] + ?? document["paths"]?["/api/users/"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"]; + createdSchema.Should().NotBeNull(); + createdSchema!["$ref"]!.GetValue().Should().Be("#/components/schemas/UserResponse"); + } + + [Fact] + public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody() + { + var document = await _client.GetFromJsonAsync("/openapi/v1.json"); + + document.Should().NotBeNull(); + + var responses = document!["paths"]?["/api/users/{id}"]?["delete"]?["responses"]?.AsObject(); + responses.Should().NotBeNull(); + + responses!.ContainsKey("200").Should().BeFalse(); + + var successResponse = responses["204"]; + successResponse.Should().NotBeNull(); + successResponse!["content"]?["application/json"]?["schema"].Should().BeNull(); + + document["components"]?["schemas"]?["ResponseOfEmptyResponse"].Should().BeNull(); + document["components"]?["schemas"]?["EmptyResponse"].Should().BeNull(); + } +} diff --git a/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache b/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache index 96b621f..5c8a91b 100644 --- a/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache +++ b/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache @@ -73,7 +73,6 @@ Features/ FindById/ FindByIdEndpoint.cs FindByIdRequest.cs - FindUserByIdRequest.cs ImportFromCsv/ImportFromCsvEndpoint.cs List/ ListUsersEndpoint.cs diff --git a/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs index b5c3f26..0303847 100644 --- a/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs +++ b/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs @@ -20,6 +20,7 @@ public void Configure(IEndpointConfiguration config) .Group() .Summary("Create a user") .Description("Creates a new user. Returns 400 if validation fails.") + .ProducesSuccess(HttpStatusCode.Created) .AddFilter(); } diff --git a/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs index cf64e1f..8f68ed2 100644 --- a/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs +++ b/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs @@ -1,4 +1,5 @@ using AxisEndpoints; +using System.Net; namespace AxisEndpoints.Example.Features.Users.Delete; @@ -21,6 +22,7 @@ public void Configure(IEndpointConfiguration config) "Permanently removes a user. " + "In a real application, restrict this to the Admin role via .RequireAuthorization(\"Admin\")." ) + .ProducesSuccess(HttpStatusCode.NoContent) .AllowAnonymous(); } diff --git a/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs index 907d9be..163e30f 100644 --- a/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs +++ b/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Net; using AxisEndpoints.Extensions.CsvHelper; using CsvHelper.Configuration.Attributes; @@ -50,7 +51,8 @@ public void Configure(IEndpointConfiguration config) .Description( "Accepts a CSV file with columns: name, email, role. " + "Validates each row and returns a ValidationProblem on failure." - ); + ) + .ProducesSuccess(HttpStatusCode.NoContent); } public Task> HandleAsync( diff --git a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj index 1fb715b..dbf6f0e 100644 --- a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj +++ b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache index a5c9fa1..adf0896 100644 --- a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache +++ b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache @@ -39,7 +39,7 @@ TemporaryDependencyNodeTargetIdentifier=net10.0 /define:TRACE;DEBUG;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER /highentropyva+ /nullable:enable -/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/features:"InterceptorsNamespaces=;Microsoft.AspNetCore.OpenApi.Generated;;Microsoft.Extensions.Validation.Generated" /debug+ /debug:portable /filealign:512 @@ -54,14 +54,186 @@ TemporaryDependencyNodeTargetIdentifier=net10.0 /warnaserror+:NU1605,SYSLIB0011 [sourceFiles] -/microsoft.net.test.sdk/17.14.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs +../../.dotnet-cli/.nuget/packages/microsoft.net.test.sdk/17.14.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs +Integration/ + EndpointIntegrationTests.cs + TestEndpoints.cs + TestWebApplicationFactory.cs obj/Debug/net10.0/ .NETCoreApp,Version=v10.0.AssemblyAttributes.cs AxisEndpoints.Tests.AssemblyInfo.cs AxisEndpoints.Tests.GlobalUsings.g.cs -UnitTest1.cs +Unit/ + EndpointConfigurationTests.cs + EndpointGroupConfigurationTests.cs + ResponseExecutorTests.cs + ServiceRegistrationTests.cs [metadataReferences] +../../.dotnet-cli/.nuget/packages/ + fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll + microsoft.aspnetcore.mvc.testing/10.0.7/lib/net10.0/Microsoft.AspNetCore.Mvc.Testing.dll + microsoft.aspnetcore.openapi/10.0.0/lib/net10.0/Microsoft.AspNetCore.OpenApi.dll + microsoft.aspnetcore.testhost/10.0.7/lib/net10.0/Microsoft.AspNetCore.TestHost.dll + microsoft.codecoverage/17.14.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll + microsoft.extensions.configuration.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll + microsoft.extensions.configuration.binder/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Binder.dll + microsoft.extensions.configuration.commandline/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.CommandLine.dll + microsoft.extensions.configuration.environmentvariables/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.EnvironmentVariables.dll + microsoft.extensions.configuration.fileextensions/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.FileExtensions.dll + microsoft.extensions.configuration.json/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Json.dll + microsoft.extensions.configuration.usersecrets/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.UserSecrets.dll + microsoft.extensions.configuration/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.dll + microsoft.extensions.dependencyinjection.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll + microsoft.extensions.dependencyinjection/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll + microsoft.extensions.dependencymodel/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.extensions.diagnostics.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Diagnostics.Abstractions.dll + microsoft.extensions.diagnostics/10.0.7/lib/net10.0/Microsoft.Extensions.Diagnostics.dll + microsoft.extensions.fileproviders.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.FileProviders.Abstractions.dll + microsoft.extensions.fileproviders.physical/10.0.7/lib/net10.0/Microsoft.Extensions.FileProviders.Physical.dll + microsoft.extensions.filesystemglobbing/10.0.7/lib/net10.0/Microsoft.Extensions.FileSystemGlobbing.dll + microsoft.extensions.hosting.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Hosting.Abstractions.dll + microsoft.extensions.hosting/10.0.7/lib/net10.0/Microsoft.Extensions.Hosting.dll + microsoft.extensions.logging.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll + microsoft.extensions.logging.configuration/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Configuration.dll + microsoft.extensions.logging.console/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Console.dll + microsoft.extensions.logging.debug/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Debug.dll + microsoft.extensions.logging.eventlog/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.EventLog.dll + microsoft.extensions.logging.eventsource/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.EventSource.dll + microsoft.extensions.logging/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.dll + microsoft.extensions.options.configurationextensions/10.0.7/lib/net10.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll + microsoft.extensions.options/10.0.7/lib/net10.0/Microsoft.Extensions.Options.dll + microsoft.extensions.primitives/10.0.7/lib/net10.0/Microsoft.Extensions.Primitives.dll + microsoft.openapi/2.0.0/lib/net8.0/Microsoft.OpenApi.dll + microsoft.testplatform.testhost/17.14.1/lib/net8.0/ + Microsoft.TestPlatform.CommunicationUtilities.dll + Microsoft.TestPlatform.CoreUtilities.dll + Microsoft.TestPlatform.CrossPlatEngine.dll + Microsoft.TestPlatform.PlatformAbstractions.dll + Microsoft.TestPlatform.Utilities.dll + Microsoft.VisualStudio.TestPlatform.Common.dll + Microsoft.VisualStudio.TestPlatform.ObjectModel.dll + testhost.dll + newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll + system.diagnostics.eventlog/10.0.7/lib/net10.0/System.Diagnostics.EventLog.dll + xunit.abstractions/2.0.3/lib/netstandard2.0/xunit.abstractions.dll + xunit.assert/2.9.3/lib/net6.0/xunit.assert.dll + xunit.extensibility.core/2.9.3/lib/netstandard1.1/xunit.core.dll + xunit.extensibility.execution/2.9.3/lib/netstandard1.1/xunit.execution.dotnet.dll +../../src/AxisEndpoints/obj/Debug/net10.0/ref/AxisEndpoints.dll +/packs/Microsoft.AspNetCore.App.Ref/10.0.2/ref/net10.0/ + Microsoft.AspNetCore.Antiforgery.dll + Microsoft.AspNetCore.Authentication.Abstractions.dll + Microsoft.AspNetCore.Authentication.BearerToken.dll + Microsoft.AspNetCore.Authentication.Cookies.dll + Microsoft.AspNetCore.Authentication.Core.dll + Microsoft.AspNetCore.Authentication.dll + Microsoft.AspNetCore.Authentication.OAuth.dll + Microsoft.AspNetCore.Authorization.dll + Microsoft.AspNetCore.Authorization.Policy.dll + Microsoft.AspNetCore.Components.Authorization.dll + Microsoft.AspNetCore.Components.dll + Microsoft.AspNetCore.Components.Endpoints.dll + Microsoft.AspNetCore.Components.Forms.dll + Microsoft.AspNetCore.Components.Server.dll + Microsoft.AspNetCore.Components.Web.dll + Microsoft.AspNetCore.Connections.Abstractions.dll + Microsoft.AspNetCore.CookiePolicy.dll + Microsoft.AspNetCore.Cors.dll + Microsoft.AspNetCore.Cryptography.Internal.dll + Microsoft.AspNetCore.Cryptography.KeyDerivation.dll + Microsoft.AspNetCore.DataProtection.Abstractions.dll + Microsoft.AspNetCore.DataProtection.dll + Microsoft.AspNetCore.DataProtection.Extensions.dll + Microsoft.AspNetCore.Diagnostics.Abstractions.dll + Microsoft.AspNetCore.Diagnostics.dll + Microsoft.AspNetCore.Diagnostics.HealthChecks.dll + Microsoft.AspNetCore.dll + Microsoft.AspNetCore.HostFiltering.dll + Microsoft.AspNetCore.Hosting.Abstractions.dll + Microsoft.AspNetCore.Hosting.dll + Microsoft.AspNetCore.Hosting.Server.Abstractions.dll + Microsoft.AspNetCore.Html.Abstractions.dll + Microsoft.AspNetCore.Http.Abstractions.dll + Microsoft.AspNetCore.Http.Connections.Common.dll + Microsoft.AspNetCore.Http.Connections.dll + Microsoft.AspNetCore.Http.dll + Microsoft.AspNetCore.Http.Extensions.dll + Microsoft.AspNetCore.Http.Features.dll + Microsoft.AspNetCore.Http.Results.dll + Microsoft.AspNetCore.HttpLogging.dll + Microsoft.AspNetCore.HttpOverrides.dll + Microsoft.AspNetCore.HttpsPolicy.dll + Microsoft.AspNetCore.Identity.dll + Microsoft.AspNetCore.Localization.dll + Microsoft.AspNetCore.Localization.Routing.dll + Microsoft.AspNetCore.Metadata.dll + Microsoft.AspNetCore.Mvc.Abstractions.dll + Microsoft.AspNetCore.Mvc.ApiExplorer.dll + Microsoft.AspNetCore.Mvc.Core.dll + Microsoft.AspNetCore.Mvc.Cors.dll + Microsoft.AspNetCore.Mvc.DataAnnotations.dll + Microsoft.AspNetCore.Mvc.dll + Microsoft.AspNetCore.Mvc.Formatters.Json.dll + Microsoft.AspNetCore.Mvc.Formatters.Xml.dll + Microsoft.AspNetCore.Mvc.Localization.dll + Microsoft.AspNetCore.Mvc.Razor.dll + Microsoft.AspNetCore.Mvc.RazorPages.dll + Microsoft.AspNetCore.Mvc.TagHelpers.dll + Microsoft.AspNetCore.Mvc.ViewFeatures.dll + Microsoft.AspNetCore.OutputCaching.dll + Microsoft.AspNetCore.RateLimiting.dll + Microsoft.AspNetCore.Razor.dll + Microsoft.AspNetCore.Razor.Runtime.dll + Microsoft.AspNetCore.RequestDecompression.dll + Microsoft.AspNetCore.ResponseCaching.Abstractions.dll + Microsoft.AspNetCore.ResponseCaching.dll + Microsoft.AspNetCore.ResponseCompression.dll + Microsoft.AspNetCore.Rewrite.dll + Microsoft.AspNetCore.Routing.Abstractions.dll + Microsoft.AspNetCore.Routing.dll + Microsoft.AspNetCore.Server.HttpSys.dll + Microsoft.AspNetCore.Server.IIS.dll + Microsoft.AspNetCore.Server.IISIntegration.dll + Microsoft.AspNetCore.Server.Kestrel.Core.dll + Microsoft.AspNetCore.Server.Kestrel.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll + Microsoft.AspNetCore.Session.dll + Microsoft.AspNetCore.SignalR.Common.dll + Microsoft.AspNetCore.SignalR.Core.dll + Microsoft.AspNetCore.SignalR.dll + Microsoft.AspNetCore.SignalR.Protocols.Json.dll + Microsoft.AspNetCore.StaticAssets.dll + Microsoft.AspNetCore.StaticFiles.dll + Microsoft.AspNetCore.WebSockets.dll + Microsoft.AspNetCore.WebUtilities.dll + Microsoft.Extensions.Caching.Abstractions.dll + Microsoft.Extensions.Caching.Memory.dll + Microsoft.Extensions.Configuration.Ini.dll + Microsoft.Extensions.Configuration.KeyPerFile.dll + Microsoft.Extensions.Configuration.Xml.dll + Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll + Microsoft.Extensions.Diagnostics.HealthChecks.dll + Microsoft.Extensions.Features.dll + Microsoft.Extensions.FileProviders.Composite.dll + Microsoft.Extensions.FileProviders.Embedded.dll + Microsoft.Extensions.Http.dll + Microsoft.Extensions.Identity.Core.dll + Microsoft.Extensions.Identity.Stores.dll + Microsoft.Extensions.Localization.Abstractions.dll + Microsoft.Extensions.Localization.dll + Microsoft.Extensions.Logging.TraceSource.dll + Microsoft.Extensions.ObjectPool.dll + Microsoft.Extensions.Options.DataAnnotations.dll + Microsoft.Extensions.Validation.dll + Microsoft.Extensions.WebEncoders.dll + Microsoft.JSInterop.dll + Microsoft.Net.Http.Headers.dll + System.Formats.Cbor.dll + System.Security.Cryptography.Xml.dll + System.Threading.RateLimiting.dll /packs/Microsoft.NETCore.App.Ref/10.0.2/ref/net10.0/ Microsoft.CSharp.dll Microsoft.VisualBasic.Core.dll @@ -230,24 +402,21 @@ UnitTest1.cs System.Xml.XPath.dll System.Xml.XPath.XDocument.dll WindowsBase.dll -/ - microsoft.codecoverage/17.14.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll - microsoft.testplatform.testhost/17.14.1/lib/net8.0/ - Microsoft.TestPlatform.CommunicationUtilities.dll - Microsoft.TestPlatform.CoreUtilities.dll - Microsoft.TestPlatform.CrossPlatEngine.dll - Microsoft.TestPlatform.PlatformAbstractions.dll - Microsoft.TestPlatform.Utilities.dll - Microsoft.VisualStudio.TestPlatform.Common.dll - Microsoft.VisualStudio.TestPlatform.ObjectModel.dll - testhost.dll - newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll - xunit.abstractions/2.0.3/lib/netstandard2.0/xunit.abstractions.dll - xunit.assert/2.9.3/lib/net6.0/xunit.assert.dll - xunit.extensibility.core/2.9.3/lib/netstandard1.1/xunit.core.dll - xunit.extensibility.execution/2.9.3/lib/netstandard1.1/xunit.execution.dotnet.dll [analyzerReferences] +../../.dotnet-cli/.nuget/packages/ + microsoft.aspnetcore.openapi/10.0.0/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll + microsoft.extensions.logging.abstractions/10.0.7/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll + microsoft.extensions.options/10.0.7/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll + xunit.analyzers/1.18.0/analyzers/dotnet/cs/ + xunit.analyzers.dll + xunit.analyzers.fixes.dll +/packs/Microsoft.AspNetCore.App.Ref/10.0.2/analyzers/dotnet/cs/ + Microsoft.AspNetCore.App.Analyzers.dll + Microsoft.AspNetCore.App.CodeFixes.dll + Microsoft.AspNetCore.App.SourceGenerators.dll + Microsoft.AspNetCore.Components.Analyzers.dll + Microsoft.Extensions.Validation.ValidationsGenerator.dll /packs/Microsoft.NETCore.App.Ref/10.0.2/analyzers/dotnet/cs/ Microsoft.Interop.ComInterfaceGenerator.dll Microsoft.Interop.JavaScript.JSImportGenerator.dll @@ -258,9 +427,6 @@ UnitTest1.cs /sdk/10.0.102/Sdks/Microsoft.NET.Sdk/analyzers/ Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll Microsoft.CodeAnalysis.NetAnalyzers.dll -/xunit.analyzers/1.18.0/analyzers/dotnet/cs/ - xunit.analyzers.dll - xunit.analyzers.fixes.dll [analyzerConfigFiles] /sdk/10.0.102/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig diff --git a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs index 1cda29f..e1ffc77 100644 --- a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs +++ b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Json; +using System.Text.Json.Nodes; using FluentAssertions; namespace AxisEndpoints.Tests.Integration; @@ -69,4 +70,53 @@ public async Task GetGrouped_Returns200WithGroupPrefix() body.Should().NotBeNull(); body!.Message.Should().Be("Grouped!"); } + + [Fact] + public async Task PostCreatedHello_Returns201WithMessage() + { + var response = await _client.PostAsync("/hello-created", content: null); + + response.StatusCode.Should().Be(HttpStatusCode.Created); + var body = await response.Content.ReadFromJsonAsync(); + body.Should().NotBeNull(); + body!.Message.Should().Be("Created!"); + } + + [Fact] + public async Task OpenApi_UsesResponseBodyTypeInsteadOfResponseWrapper() + { + var document = await _client.GetFromJsonAsync("/openapi/v1.json"); + + document.Should().NotBeNull(); + + var schema = document!["paths"]?["/hello"]?["get"]?["responses"]?["200"]?["content"]?["application/json"]?["schema"]; + schema.Should().NotBeNull(); + schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HelloResponse"); + + document["components"]?["schemas"]?["ResponseOfHelloResponse"].Should().BeNull(); + + var createdSchema = document["paths"]?["/hello-created"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"]; + createdSchema.Should().NotBeNull(); + createdSchema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HelloResponse"); + } + + [Fact] + public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody() + { + var document = await _client.GetFromJsonAsync("/openapi/v1.json"); + + document.Should().NotBeNull(); + + var responses = document!["paths"]?["/items/{id}"]?["delete"]?["responses"]?.AsObject(); + responses.Should().NotBeNull(); + + responses!.ContainsKey("200").Should().BeFalse(); + + var successResponse = responses["204"]; + successResponse.Should().NotBeNull(); + successResponse!["content"]?["application/json"]?["schema"].Should().BeNull(); + + document["components"]?["schemas"]?["ResponseOfEmptyResponse"].Should().BeNull(); + document["components"]?["schemas"]?["EmptyResponse"].Should().BeNull(); + } } diff --git a/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs b/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs index 56e3245..fc4894f 100644 --- a/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs +++ b/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Net; using Microsoft.AspNetCore.Mvc; namespace AxisEndpoints.Tests.Integration; @@ -58,7 +59,7 @@ public class DeleteItemEndpoint : IEndpoint(HttpStatusCode.NoContent); } public Task> HandleAsync(DeleteItemRequest request, CancellationToken cancel) @@ -69,6 +70,25 @@ public Task> HandleAsync(DeleteItemRequest request, Canc public record DeleteItemRequest([property: FromRoute] int Id); +public class CreatedHelloEndpoint : IEndpoint> +{ + public void Configure(IEndpointConfiguration config) + { + config.Post("/hello-created").ProducesSuccess(HttpStatusCode.Created); + } + + public Task> HandleAsync(CancellationToken cancel) + { + return Task.FromResult( + new Response + { + StatusCode = HttpStatusCode.Created, + Body = new HelloResponse("Created!") + } + ); + } +} + public class ApiGroup : IEndpointGroup { public void Configure(IEndpointGroupConfiguration config) diff --git a/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs b/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs index 3798c88..ae6385f 100644 --- a/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs +++ b/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs @@ -2,6 +2,7 @@ using AxisEndpoints.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace AxisEndpoints.Tests.Integration; @@ -17,9 +18,11 @@ public async Task InitializeAsync() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); + builder.Services.AddOpenApi(); builder.Services.AddAxisEndpoints(_testAssembly); _app = builder.Build(); + _app.MapOpenApi(); _app.MapAxisEndpoints(_testAssembly); await _app.StartAsync();