Files
Musify/docs/development.md
T
Admin 611d9cd436 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`.
2026-09-09 18:29:05 +02:00

4.9 KiB

Entorno de desarrollo

Cómo levantar todo en local y probarlo. El detalle completo del deploy (y el de producción) está en deploy/README.md.

Arranque

docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d

Ese único comando levanta la infraestructura y corre los jobs one-shot de bootstrap: genera las claves RS256 de stream-ticket en deploy/keys/, aplica las migraciones EF Core y provisiona Zitadel (proyecto + apps OIDC), dejando los client ids en deploy/.env. Detalle completo, incluido cómo arrancar las apps en Docker con --profile apps, en deploy/README.md.

Aplicaciones

dotnet run --project backend/Hosts/Musify.Api               # :5111
dotnet run --project backend/Hosts/Musify.StreamingGateway  # :8081
dotnet run --project backend/Hosts/Musify.Worker            # background
npm --prefix web-player run dev                       # :5173

Mapa de puertos

Servicio Puerto
Musify.Api 5111 (/scalar/v1, /openapi/v1.json)
Musify.StreamingGateway 8081 (/health, /media/...)
web-player 5173 (npm run dev) o 3000 (contenedor)
SeaweedFS S3 / filer / master 8333 / 8888 / 9333
Zitadel 8080
PostgreSQL 59000 (→ 5432 en el contenedor)
RabbitMQ AMQP / management 5672 / 15672
Jaeger UI / OTLP 16686 / 4317
pgAdmin (-Tools) 5050

DB: musify_db, usuario postgres/postgres. Bucket S3: webapi-storage (credenciales admin_access_key/admin_secret_key). Todos los valores salen de deploy/.env.

Migraciones

El servicio migrate de deploy/compose.yml las aplica en cada docker compose up -d (no-op si el esquema ya está al día). A mano, tras cambios de esquema, contra la base local:

dotnet ef database update --project backend/Core/Musify.Infrastructure --startup-project backend/Core/Musify.Infrastructure --context Database

Musify.Infrastructure es a la vez el proyecto de migraciones y el de arranque: tiene el IDesignTimeDbContextFactory y el paquete EFCore.Design. La cadena de conexión se lee de sus user-secrets, o de la variable de entorno Database__ConnectionString si no hay user-secrets (así es como la usa el contenedor migrate, ver backend/Musify.Migrator.Dockerfile).

Stream-tickets (claves RS256)

La API firma los stream-tickets con la clave privada y el gateway los valida con la pública. El servicio keys-init de deploy/compose.yml las genera en deploy/keys/ si no existen; a mano:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out deploy/keys/stream_private.pem
openssl rsa -in deploy/keys/stream_private.pem -pubout -out deploy/keys/stream_public.pem
  • API: StreamTicket:PrivateKeyPath.
  • Gateway: StreamTicket:PublicKeyPath.

Tests

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; AppSettings.Development.json tiene los valores reales de dev; en Docker todo se sobreescribe con variables de entorno (Seccion__Clave) desde deploy/compose.yml. Para correr Musify.Api desde el IDE, los valores de Authentication van en user-secrets (no en el repo) — cópialos de deploy/.env una vez que zitadel-init los haya generado (ver deploy/README.md).

Secciones clave: Authentication (Zitadel), Database, MassTransit, InfrastructureStorage (S3), ApplicationStorage (bucket), Track, PlayList, UploadIntent, AudioTranscoder, YtDlp, Workers, StreamGateway / StreamTicket.

Requisitos del host

  • .NET 10 SDK y Node 22.
  • ffmpeg y yt-dlp en el PATH (transcode y descarga de audio del Worker).
  • Docker Desktop.