# Integration tests — REAL SQL Server LocalDB

Integration tests run the API in-process against a **real SQL Server LocalDB** (never EF
InMemory) — this is the layer that catches what InMemory cannot: relational constraints
(FK/unique/cascade), SQL translation, and SqlObjects/TVF. The CLI scaffolds the harness into
`Tests/Common/`:

- **`DatabaseFixture`** — creates a throwaway LocalDB, `MigrateAsync()`, resets the
  `extensions` schema between tests with **Respawn**, drops the DB on dispose. Bound to every
  integration class via `[CollectionDefinition("Integration")]`.
- **`{App}WebAppFactory : WebApplicationFactory<Program>`** — points `ExtensionsDbContext` at
  the test DB and **overrides JWT via `PostConfigure<JwtBearerOptions>`** (the only reliable
  hook — config is read too early by `AddInfrastructure`).
- **`JwtTokenHelper`** — mints HMAC-SHA256 tokens with the same key the factory installs.

## Canonical skeleton

```csharp
using System.Net;
using System.Net.Http.Headers;
using FluentAssertions;
using Xunit;
using MyApp.Tests.Common;

namespace MyApp.Tests.Orders.Api;

[Trait("Category", "Integration")]
[Trait("Type", "Integration")]
[Collection("Integration")]
public class OrdersIntegrationTests : IAsyncDisposable
{
    private readonly DatabaseFixture _db;
    private readonly MyAppWebAppFactory _factory;
    private readonly HttpClient _client;

    public OrdersIntegrationTests(DatabaseFixture db)
    {
        _db = db;
        _factory = new MyAppWebAppFactory(db.ConnectionString);
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetAll_Unauthenticated_Returns401()
    {
        var response = await _client.GetAsync("/api/orders/orders");
        response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Fact]
    public async Task GetAll_Authenticated_HitsTheRealRoute()
    {
        await _db.ResetAsync();

        // Seed in GLOBAL scope (no tenant filter, correct FK order):
        await using (var ctx = _db.CreateContext())
        {
            ctx.Set<Order>().Add(Order.Create("ORD-001", Guid.NewGuid(), 100m));
            await ctx.SaveChangesAsync();
        }

        var client = _factory.CreateClient();
        var token = JwtTokenHelper.GenerateToken(permissions: new[] { "orders.view" });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var response = await client.GetAsync("/api/orders/orders");

        response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden);
    }

    public async ValueTask DisposeAsync()
    {
        _client.Dispose();
        await _factory.DisposeAsync();
    }
}
```

## Rules

- Test **real routes** that exist (`/api/{module}/{plural}`), not fictional ones.
- Seed via `_db.CreateContext()` (global scope) so query filters don't hide writes / break FK order.
- **Dispose** the factory + client (`IAsyncDisposable`).
- If your API enforces a server-side session, a valid token without a session row may return
  `401` with a `SESSION_EXPIRED` body — seed a session row to reach `200`, or assert the body.

## Execution pitfalls (hard-won)

- **MSB3021 — the dev API locks the build output.** A running `*.Api.exe` locks the DLL the
  test project references → build/test fails. Stop the dev API, or
  `dotnet test -p:BuildProjectReferences=false`.
- **`npm install`, not `npm ci`, while a Vite dev server runs** — `npm ci` wipes
  `node_modules` and collides with the `@tailwindcss/oxide` native lock held by Vite.

## NuGet packages

`Microsoft.AspNetCore.Mvc.Testing`, `Respawn`, `Microsoft.Data.SqlClient`,
`System.IdentityModel.Tokens.Jwt`. The API project must expose `public partial class Program { }`
and be referenced by the test project.

## Anti-patterns

- ❌ EF InMemory / SQLite for integration — use real LocalDB (constraints + SQL must be proven).
- ❌ Overriding JWT via config — it's read too early; use `PostConfigure`.
- ❌ Not disposing the factory (it owns the in-process host).
