# Controller tests — unit (no HTTP pipeline)

Controllers are thin: they delegate to MediatR (`ISender`). Unit-test them by mocking the
mediator and asserting the `ActionResult<T>` shape. Authorization is verified by **reflection**
on the attributes — never through the middleware pipeline (that's integration). Tests go to
`Tests/{Module}/Api/{Plural}ControllerTests.cs` (unit) — note the integration tests share the
same folder but carry `[Collection("Integration")]`.

## Rules

- Build the controller in the ctor with `Mock<ISender>` (a.k.a. `IMediator`).
- `ActionResult<T>`: assert `result.Result.Should().BeOfType<OkObjectResult>().Subject`, then
  `.Value.Should().BeOfType<TDto>()`. NotFound → `NotFoundObjectResult`/`NotFoundResult`;
  Created → check `.ActionName` + `.Value`; NoContent → `NoContentResult`.
- Match commands with `It.Is<TCommand>(c => c.Field == value)` when verifying mapping, else `It.IsAny<TCommand>()`.
- `[RequirePermission]` by reflection (see below).
- `TestContext.Current.CancellationToken` in async actions.

## Canonical skeleton

```csharp
using Xunit;
using FluentAssertions;
using Moq;
using MediatR;
using Microsoft.AspNetCore.Mvc;

[Trait("Category", "Business")]
[Trait("Type", "Unit")]
public class OrdersControllerUnitTests
{
    private readonly Mock<ISender> _mediator = new();
    private readonly OrdersController _controller;

    public OrdersControllerUnitTests() => _controller = new OrdersController(_mediator.Object);

    [Fact]
    public async Task GetById_WhenFound_ReturnsOkWithDto()
    {
        var id = Guid.NewGuid();
        _mediator.Setup(m => m.Send(It.Is<GetOrderQuery>(q => q.Id == id), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new OrderDetailDto(id, "ORD-001"));

        var result = await _controller.GetOrder(id, TestContext.Current.CancellationToken);

        var ok = result.Result.Should().BeOfType<OkObjectResult>().Subject;
        ok.Value.Should().BeOfType<OrderDetailDto>().Which.Id.Should().Be(id);
    }
}
```

## Authorization by reflection

```csharp
private static string[] RequiredPermissions(string method)
{
    var attr = typeof(OrdersController).GetMethod(method)!
        .GetCustomAttribute<RequirePermissionAttribute>();
    attr.Should().NotBeNull($"{method} must carry [RequirePermission]");
    return (string[])attr!.Arguments![0]!;
}

[Fact]
public void GetOrders_RequiresViewPermission()
    => RequiredPermissions(nameof(OrdersController.GetOrders)).Should().Contain("orders.view");
```

## Anti-patterns

- ❌ `WebApplicationFactory`/`TestServer` in a controller UNIT test — that's integration.
- ❌ Reading `.Value` on `ActionResult<T>` without unwrapping `.Result`.
- ❌ Testing `[RequirePermission]` through the pipeline — use reflection.
- ❌ `CancellationToken.None`.
