---
name: test-conventions
description: |
  Conventions for WRITING tests in a SmartStack client extension, distilled from the
  SmartStack.app corpus and adapted to the extension context (IExtensionsDbContext,
  in-memory ExtensionsDbContext + a fake tenant, real SQL Server LocalDB integration).
  Backend: xUnit v3 + FluentAssertions 8.x + Moq. Frontend: Vitest + Testing Library + MSW.
  Organised by TYPE of test — one reference per type in references/.

  Read the matching reference file BEFORE writing or extending ANY test, when:
  - the user asks to "write/add tests", "génère les tests", "couvre X par des tests"
  - you create or edit a `*Tests.cs` (backend) or `*.test.tsx` (frontend) file
  - a new entity / handler / service / controller / hook / component / endpoint needs coverage
  Imitate the idioms below and the code the generators emit — do not invent a new style.
allowed-tools: [Read, Glob, Grep]
---

# Test conventions — SmartStack client extension

**Prime directive**: before writing a test, read the `references/` file for its TYPE and
imitate the idioms it describes. These match what the CLI's test generators emit, so a
hand-written test should be indistinguishable from a scaffolded one.

> This is the EXTENSION edition. It deliberately does **not** use the app-internal helpers
> `MockDbSetFactory` / `TestEntityFactory` / `CoreDbContext` (they don't exist in your
> project). Extension entities live in the `extensions` schema behind
> `IExtensionsDbContext` / `ExtensionsDbContext`.

## Which reference to read?

| Type | You test… | Code under test | Tests live in… | Reference |
|------|-----------|-----------------|----------------|-----------|
| **Domain** | entities, value objects, invariants, transitions | `*.Domain/` | `Tests/{Module}/Domain/` | [backend-domain.md](references/backend-domain.md) |
| **Application** | CQRS handlers (delegate to `I{E}Service`), validators | `*.Application/` | `Tests/{Module}/Application/` | [backend-application.md](references/backend-application.md) |
| **Services** | `{E}Service` (EF access) + infra services (JWT, storage, …) | `*.Infrastructure/Services/` | `Tests/{Module}/Application/` (service) | [backend-services.md](references/backend-services.md) |
| **Controllers** | controllers, `[RequirePermission]` | `*.Api/Controllers/` | `Tests/{Module}/Api/` | [backend-controllers.md](references/backend-controllers.md) |
| **Integration** | real HTTP routes on a real DB | the whole API | `Tests/{Module}/Api/` (`[Collection("Integration")]`) | [backend-integration.md](references/backend-integration.md) |
| **Frontend** | hooks, components, pages | `web/src/` | `tests/{module}/` | [frontend-vitest.md](references/frontend-vitest.md) |

## Universal rules (every backend test)

- **Naming** `Method_Scenario_Expected`; **one test = one behaviour**; `#region` per method.
- **Construction** via the factory `Entity.Create(...)` — never `new Entity { … }`.
- **GUIDs** always `Guid.NewGuid()` (frontend: `crypto.randomUUID()`) — never a hardcoded literal.
- **FluentAssertions 8.x**: DateTime → `BeOnOrAfter` / `BeBefore` / `BeCloseTo(expected, TimeSpan.FromSeconds(n))` (**never `BeGreaterThan`**); collections → `HaveCount(n)`; exceptions → `Throw<T>().WithMessage("*keyword*")` (wildcard, never exact) / async `ThrowAsync<T>`.
- **xUnit v3**: pass `TestContext.Current.CancellationToken` (never `CancellationToken.None`).
- **Loggers** `NullLogger<T>.Instance` (unless asserting a log); **options** `Options.Create(new TOptions{…})` (never `Mock<IOptions<T>>`).
- **Zero stubs** — never `Assert.True(true)` / `expect(true).toBe(true)` / leftover `// TODO`.

The `audit-dev-tests` audit flags the last few as advisory `DEV-TEST-004..007`.

## The two-level business contract (extension-specific)

The generated CQRS handler is a **thin delegator** to `I{E}Service`; the **service** holds the
logic + EF access (`IExtensionsDbContext`). So tests split:

- **Handler test** → `Mock<I{E}Service>` + `Verify(...)` the delegation (no DbContext).
- **Service test** → a REAL in-memory `ExtensionsDbContext` built with a
  `FakeCurrentTenantService` (so the tenant query filter runs). EF InMemory honours query
  filters but NOT relational constraints / SQL translation / SqlObjects — those belong to the
  **integration** tests on real SQL Server LocalDB.

See [backend-application.md](references/backend-application.md) and
[backend-services.md](references/backend-services.md).
