mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
116 lines
4.5 KiB
C#
116 lines
4.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Framework-backed route contract tests that prove the public procurement webhook endpoint is
|
|
/// exactly <c>POST api/webhooks/work-orders</c> exposed by
|
|
/// <see cref="WorkOrderWebhookController.Receive"/>, that it is anonymous (no JWT required), and
|
|
/// that it is constrained to <c>application/json</c>. 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.
|
|
/// </summary>
|
|
public class WorkOrderWebhookRouteContractTests
|
|
{
|
|
private readonly ITestOutputHelper _output;
|
|
|
|
public WorkOrderWebhookRouteContractTests(ITestOutputHelper output)
|
|
{
|
|
_output = output;
|
|
}
|
|
|
|
private static IReadOnlyList<ControllerActionDescriptor> WebhookActions()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddMvcCore()
|
|
.AddApplicationPart(typeof(WorkOrderWebhookController).Assembly);
|
|
|
|
using var provider = services.BuildServiceProvider();
|
|
var actionProvider = provider.GetRequiredService<IActionDescriptorCollectionProvider>();
|
|
return actionProvider.ActionDescriptors.Items
|
|
.OfType<ControllerActionDescriptor>()
|
|
.Where(cad => cad.ControllerTypeInfo == typeof(WorkOrderWebhookController))
|
|
.ToList();
|
|
}
|
|
|
|
private static IReadOnlyList<string> VerbAndTemplates(ControllerActionDescriptor cad)
|
|
{
|
|
var template = cad.AttributeRouteInfo?.Template?.Trim('/');
|
|
var methods = (cad.ActionConstraints ?? Array.Empty<IActionConstraintMetadata>())
|
|
.OfType<HttpMethodActionConstraint>()
|
|
.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<ConsumesAttribute>()
|
|
.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<string> endpoints)
|
|
{
|
|
_output.WriteLine("WorkOrderWebhook public endpoints (verb + route):");
|
|
foreach (var endpoint in endpoints.OrderBy(x => x, StringComparer.Ordinal))
|
|
_output.WriteLine(" " + endpoint);
|
|
}
|
|
}
|