using Api.SeaHavenIndustries.Controllers; using FluentAssertions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; namespace Api.SeaHavenIndustries.Tests; /// /// Framework-backed route contract tests that prove the public procurement webhook endpoint is /// exactly POST api/webhooks/work-orders exposed by /// , that it is anonymous (no JWT required), and /// that it is constrained to application/json. Routes and metadata are read from the /// ASP.NET Core action descriptor provider so the EXACT templates and attributes the framework /// will dispatch are compared. /// public class WorkOrderWebhookRouteContractTests { private readonly ITestOutputHelper _output; public WorkOrderWebhookRouteContractTests(ITestOutputHelper output) { _output = output; } private static IReadOnlyList WebhookActions() { var services = new ServiceCollection(); services.AddLogging(); services.AddMvcCore() .AddApplicationPart(typeof(WorkOrderWebhookController).Assembly); using var provider = services.BuildServiceProvider(); var actionProvider = provider.GetRequiredService(); return actionProvider.ActionDescriptors.Items .OfType() .Where(cad => cad.ControllerTypeInfo == typeof(WorkOrderWebhookController)) .ToList(); } private static IReadOnlyList VerbAndTemplates(ControllerActionDescriptor cad) { var template = cad.AttributeRouteInfo?.Template?.Trim('/'); var methods = (cad.ActionConstraints ?? Array.Empty()) .OfType() .SelectMany(c => c.HttpMethods) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(m => m.ToUpperInvariant()); return methods.Select(m => $"{m} {template}").ToList(); } [Fact] public void WorkOrderWebhook_exposes_exactly_post_api_webhooks_work_orders() { var actions = WebhookActions(); actions.Should().ContainSingle( "the webhook controller must expose exactly one action"); var receive = actions.Single(); receive.ActionName.Should().Be( nameof(WorkOrderWebhookController.Receive), "the single webhook action must be Receive"); var endpoints = VerbAndTemplates(receive); Dump(endpoints); endpoints.Should().BeEquivalentTo( new[] { "POST api/webhooks/work-orders" }, "the procurement webhook must be reachable only as POST api/webhooks/work-orders"); } [Fact] public void WorkOrderWebhook_Receive_is_anonymous() { var receive = WebhookActions().Single(a => a.ActionName == nameof(WorkOrderWebhookController.Receive)); var hasAllowAnonymous = receive.ControllerTypeInfo .GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true).Any() || receive.MethodInfo .GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true).Any(); hasAllowAnonymous.Should().BeTrue( "the webhook must accept anonymous delivery and must not require a JWT bearer token"); } [Fact] public void WorkOrderWebhook_Receive_consumes_application_json_only() { var receive = WebhookActions().Single(a => a.ActionName == nameof(WorkOrderWebhookController.Receive)); var contentTypes = receive.MethodInfo .GetCustomAttributes(typeof(ConsumesAttribute), inherit: true) .Cast() .SelectMany(a => a.ContentTypes) .Select(c => c.ToString()) .ToList(); contentTypes.Should().BeEquivalentTo( new[] { "application/json" }, "the webhook must be constrained to application/json payloads"); } private void Dump(IEnumerable endpoints) { _output.WriteLine("WorkOrderWebhook public endpoints (verb + route):"); foreach (var endpoint in endpoints.OrderBy(x => x, StringComparer.Ordinal)) _output.WriteLine(" " + endpoint); } }