diff --git a/src/Http/Wolverine.Http.Tests/Persistence/RequiredTodoAttributeEndpoint.cs b/src/Http/Wolverine.Http.Tests/Persistence/RequiredTodoAttributeEndpoint.cs new file mode 100644 index 000000000..6802fbb5f --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Persistence/RequiredTodoAttributeEndpoint.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using Alba; +using IntegrationTests; +using Marten; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shouldly; +using Wolverine.Marten; +using Wolverine.Persistence; +using WolverineWebApi.Todos; +using Xunit.Abstractions; + +namespace Wolverine.Http.Tests.Persistence; + +public class RequiredTodoAttributeEndpoint { + + public static async Task<(Todo2?, ProblemDetails)> LoadAsync(string id, IDocumentSession session) + { + var todo = await session.LoadAsync(id); + return (todo, todo != null ? WolverineContinue.NoProblems : new ProblemDetails { Detail = "Todo not found by id", Status = StatusCodes.Status404NotFound } ); + } + // Should 404 w/ ProblemDetails on missing + [WolverineGet("/required/todo404required/{id}")] + public static Todo2 GetWithAttribute([Required] Todo2 todo) + => todo; +} \ No newline at end of file diff --git a/src/Http/Wolverine.Http.Tests/Persistence/reacting_to_required_attribute.cs b/src/Http/Wolverine.Http.Tests/Persistence/reacting_to_required_attribute.cs new file mode 100644 index 000000000..22403e866 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Persistence/reacting_to_required_attribute.cs @@ -0,0 +1,65 @@ +using Alba; +using IntegrationTests; +using Marten; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shouldly; +using Wolverine; +using Wolverine.Http; +using Wolverine.Marten; +using Xunit.Abstractions; + +public class reacting_to_required_attribute : IAsyncLifetime +{ + private readonly ITestOutputHelper _output; + private IAlbaHost theHost; + + public reacting_to_required_attribute(ITestOutputHelper output) + { + _output = output; + } + + public async Task InitializeAsync() + { + var builder = WebApplication.CreateBuilder([]); + + // config + builder.Services.AddMarten(opts => + { + // Establish the connection string to your Marten database + opts.Connection(Servers.PostgresConnectionString); + opts.DatabaseSchemaName = "onmissing"; + }).IntegrateWithWolverine().UseLightweightSessions(); + + builder.Host.UseWolverine(opts => opts.Discovery.IncludeAssembly(GetType().Assembly)); + + builder.Services.AddWolverineHttp(); + + // This is using Alba, which uses WebApplicationFactory under the covers + theHost = await AlbaHost.For(builder, app => + { + app.UseDeveloperExceptionPage(); + app.MapWolverineEndpoints(); + }); + } + + async Task IAsyncLifetime.DisposeAsync() + { + if (theHost != null) await theHost.StopAsync(); + } + + [Fact] + public async Task required_attribute_404_behavior_on_missing() + { + var tracked = await theHost.Scenario(x => + { + x.Get.Url("/required/todo404required/nonexistent"); + x.StatusCodeShouldBe(404); + x.ContentTypeShouldBe("application/problem+json"); + }); + + var details = tracked.ReadAsJson(); + details.Detail.ShouldBe("Todo not found by id"); + } +} \ No newline at end of file diff --git a/src/Http/Wolverine.Http/Policies/SetStatusCodeAndReturnIfEntityIsNullFrame.cs b/src/Http/Wolverine.Http/Policies/SetStatusCodeAndReturnIfEntityIsNullFrame.cs index e646d906b..433e1842c 100644 --- a/src/Http/Wolverine.Http/Policies/SetStatusCodeAndReturnIfEntityIsNullFrame.cs +++ b/src/Http/Wolverine.Http/Policies/SetStatusCodeAndReturnIfEntityIsNullFrame.cs @@ -3,6 +3,7 @@ using JasperFx.CodeGeneration.Model; using JasperFx.Core.Reflection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace Wolverine.Http.Policies; @@ -25,17 +26,19 @@ public SetStatusCodeAndReturnIfEntityIsNullFrame(Variable entity) public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) { + ValueTypeReturnVariable.TupleVariable? problemDetailsVariable = null; + if (_entity?.Creator is MethodCall { ReturnVariable: ValueTypeReturnVariable vrv }) + problemDetailsVariable = vrv.Inners.FirstOrDefault(v => v.Inner.VariableType == typeof(ProblemDetails)); writer.WriteComment("404 if this required object is null"); - writer.Write($"BLOCK:if ({_entity.Usage} == null)"); + if (problemDetailsVariable != null) + writer.WriteComment($"Take no action if {problemDetailsVariable.Inner.Usage}.Status == 404"); + writer.Write( + $"BLOCK:if ({_entity.Usage} == null{(problemDetailsVariable == null ? "" : $" && {problemDetailsVariable.Inner.Usage}.Status != 404")})"); writer.Write($"{_httpResponse.Usage}.{nameof(HttpResponse.StatusCode)} = 404;"); if (method.AsyncMode == AsyncMode.ReturnCompletedTask) - { writer.Write($"return {typeof(Task).FullNameInCode()}.{nameof(Task.CompletedTask)};"); - } else - { writer.Write("return;"); - } writer.FinishBlock();