Add backend test suite under backend/Tests/

Two xUnit projects, wired into Musify.slnx under a /Tests/ folder that
mirrors Core/ and Hosts/:

  Musify.Domain.Tests        Value-object logic (TrackAudio/TrackPictures'
                              IsProcessed/IsFailed/IsInProgress). Entities are
                              otherwise anemic EF models with no behavior to
                              test.
  Musify.Application.Tests   All ~45 Application handlers and services
                              (Albums, Tracks, PlayLists, Users, Mixes,
                              YouTube, plus TrackStreamIssuer,
                              UploadIntentValidator, YouTubeTrackProvisioner).

IDatabase exposes DbSet<T> directly, so handlers compose real LINQ queries
against it — that can't be expressed with a mock (no IQueryable provider
behind one). TestSupport/TestDatabase.cs is instead a real DbContext on a
private SQLite in-memory connection per test (fresh per test since xUnit
instantiates the test class per test method): fast, no external dependency,
and — unlike EF Core's InMemory provider — SQLite enforces real foreign keys
and supports transactions, which two handlers need. External dependencies
(IStorageService, IEventBus, IYouTubeMusicService, ...) are substituted with
NSubstitute.

TestSupport/TestEntities.cs and TestConfigurations.cs build fully-populated
domain entities and configuration POCOs with sensible defaults for every
required member, so no test method needs to fill in properties it doesn't
care about, and no builder ever hands back null.

Also, needed to make the Application layer itself testable without a real
Postgres or user-secrets:
- DatabaseDesignTimeFactory now falls back to the Database__ConnectionString
  env var, and DesignSettings.json is optional — unblocks the `migrate`
  one-shot container from deploy/compose.yml, which hit the same gap.
- Directory.Packages.props: xunit, xunit.runner.visualstudio, NSubstitute,
  Microsoft.EntityFrameworkCore.Sqlite (pinned to the same 10.0.8 as the rest
  of EF Core), Microsoft.NET.Test.Sdk, coverlet.collector. Also pins
  SQLitePCLRaw.bundle_e_sqlite3 to 2.1.13 — EFCore.Sqlite 10.0.8 pulls 2.1.11
  transitively, which has a known high-severity advisory (GHSA-2m69-gcr7-jv3q).
- backend/.dockerignore excludes Tests/: no application Dockerfile needs it
  in its build context.

162 tests, all green: `dotnet test backend/Musify.slnx`.
This commit is contained in:
2026-09-09 18:29:05 +02:00
parent cf8ac706b6
commit 611d9cd436
61 changed files with 3363 additions and 5 deletions
+25
View File
@@ -72,6 +72,31 @@ openssl rsa -in deploy/keys/stream_private.pem -pubout -out deploy/keys/stream_p
- API: `StreamTicket:PrivateKeyPath`.
- Gateway: `StreamTicket:PublicKeyPath`.
## Tests
```sh
dotnet test backend/Musify.slnx
```
`backend/Tests/` (carpeta `/Tests/` en el `.slnx`) tiene dos proyectos xUnit,
sin Docker ni Postgres real:
| Proyecto | Cubre |
|---|---|
| `Musify.Domain.Tests` | Lógica de los value objects (`TrackAudio`/`TrackPictures`: `IsProcessed`, `IsFailed`, `IsInProgress`). Las entidades son en su mayoría anémicas — sin comportamiento propio, nada más que testear ahí. |
| `Musify.Application.Tests` | Los ~45 handlers y servicios de `Musify.Application` (Albums, Tracks, PlayLists, Users, Mixes, YouTube). |
`IDatabase` expone `DbSet<T>` directamente, así que los handlers arman LINQ
real (`Where`, `Include`, `Select`...) contra él — no se puede mockear eso con
NSubstitute, no hay `IQueryable` detrás de un mock. En su lugar,
`TestSupport/TestDatabase.cs` es un `DbContext` real sobre SQLite en memoria
(una conexión `:memory:` nueva por test, con foreign keys reales — a diferencia
del proveedor InMemory de EF Core, SQLite además soporta transacciones, que dos
handlers usan). `TestSupport/TestEntities.cs` y `TestConfigurations.cs`
construyen entidades y configuración con valores por defecto sensatos.
`IStorageService`, `IEventBus`, `IYouTubeMusicService`, etc. sí se mockean con
NSubstitute, al ser interfaces normales.
## Config (AppSettings)
Convención: `AppSettings.json` es la plantilla con valores vacíos o neutros;
+10 -4
View File
@@ -1,7 +1,7 @@
# Proyectos
Los seis proyectos .NET viven en `backend/` (solución `Musify.slnx`); el cliente
web en `web-player/`.
Los proyectos .NET viven en `backend/` (solución `Musify.slnx`, carpetas
`Core/`, `Hosts/` y `Tests/`); el cliente web en `web-player/`.
## Musify.Api (`:5111`)
@@ -59,8 +59,14 @@ Cliente web (SvelteKit 2 + Svelte 5 + Tailwind 4). La sesión OIDC vive en el
servidor (cookie cifrada); las llamadas al backend salen del servidor con el
access token, y las subidas se hacen contra las URLs prefirmadas de S3.
## Musify.Domain.Tests + Musify.Application.Tests
`backend/Tests/`, carpeta `/Tests/` en el `.slnx`. xUnit, sin Docker ni Postgres
real — `Application.Tests` corre los handlers contra SQLite en memoria (ver
[development.md](development.md#tests)).
## Otras carpetas
- **`deploy/`** — stacks de Docker Compose (dev y prod) y los scripts de
arranque, migraciones y provisioning. Ver [../deploy/README.md](../deploy/README.md).
- **`deploy/`** — stacks de Docker Compose (dev y prod), autocontenidos. Ver
[../deploy/README.md](../deploy/README.md).
- **`docs/`** — esta documentación.