Files
Musify/docs/development.md
T
Admin a26f4b43fd Add Infrastructure/Api/StreamingGateway test projects
Second pass on backend/Tests/, covering what Domain+Application (the first
pass) deliberately left out — the parts that talk to real infrastructure:

  Musify.Infrastructure.Tests   Database/migrations, StorageService,
                                 StreamTicketService, AuditableEntityInterceptor,
                                 PictureService, SingleFlightCache,
                                 PlayListPresetSeeder.
  Musify.Api.Tests               Real HTTP endpoints via WebApplicationFactory
                                 (routing, auth, ValidationFilter, ErrorOr->HTTP
                                 mapping), plus isolated unit tests for
                                 ErrorOrHttpExtensions, CurrentUser and the
                                 FluentValidation validators.
  Musify.StreamingGateway.Tests  TicketValidator (RS256) and
                                 TicketValidationMiddleware (path traversal,
                                 prefix boundary, ticket extraction).

Musify.Worker gets no test project: its Program.cs is pure DI/host wiring
(Hangfire, MassTransit, jobs); the logic it runs lives in Musify.Infrastructure,
already covered there.

Per the user's direction, the parts that touch real infrastructure use
Testcontainers instead of fakes: a real Postgres (Testcontainers.PostgreSql)
for Infrastructure.Tests and Api.Tests, and — matching deploy/compose.yml
exactly (chrislusf/seaweedfs:latest, same startup command), not MinIO — a
real SeaweedFS container for StorageService. Api.Tests boots the actual
Musify.Api host against that Postgres (real routing/auth/validation) but
substitutes the services that talk over the network (storage, YouTube, the
message bus, stream-ticket signing) with NSubstitute fakes, and swaps the
JwtBearer scheme for a test-only header-based one (FakeAuthenticationHandler)
so tests don't need a reachable Zitadel. Application.Tests' approach — a real
DbContext (SQLite in-memory) rather than mocking IDatabase, since it exposes
DbSet<T> directly and handlers compose real LINQ queries against it — carries
over into Infrastructure.Tests/Api.Tests via real Postgres instead.

Testing against the real backend (not a fake) found two genuine production
bugs, both fixed here:

- StorageService.RemoveFolderAsync threw NullReferenceException against
  SeaweedFS: its DeleteObjects response omits the <Error> list entirely when
  nothing failed, which the AWS SDK deserializes as null rather than an empty
  list (real AWS S3 always returns one). Now treats null as no errors.
- AuditableEntityInterceptor never bumped UpdatedAt on a normal update (load,
  mutate a property, SaveChanges) — only on insert. SavingChanges(Async) fires
  before EF's own automatic change detection for that save, so an entity
  modified through a plain property setter still reported EntityState.Unchanged
  inside the interceptor. Now calls ChangeTracker.DetectChanges() explicitly
  first, which is the documented fix for exactly this interceptor pattern.

Also required to make the Api host testable: Program.cs now ends with `public
partial class Program;` (WebApplicationFactory<Program> needs the top-level-
statements Program class to be public — no behavior change), and
Directory.Packages.props gains Microsoft.AspNetCore.Mvc.Testing, Testcontainers
and Testcontainers.PostgreSql.

249 tests total across all five projects, all green: `dotnet test
backend/Musify.slnx` (needs Docker running for Infrastructure.Tests and
Api.Tests).
2026-09-09 21:39:06 +02:00

7.2 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 cinco proyectos xUnit:

Proyecto Cubre Docker
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í. No
Musify.Application.Tests Los ~45 handlers y servicios de Musify.Application (Albums, Tracks, PlayLists, Users, Mixes, YouTube). No
Musify.Infrastructure.Tests Database/migraciones, StorageService, StreamTicketService, AuditableEntityInterceptor, PictureService, SingleFlightCache, PlayListPresetSeeder. Sí
Musify.Api.Tests Endpoints HTTP reales (routing, auth, ValidationFilter, mapeo ErrorOr→HTTP) vía WebApplicationFactory, más ErrorOrHttpExtensions/CurrentUser/validators FluentValidation en aislado. Sí
Musify.StreamingGateway.Tests TicketValidator (RS256) y TicketValidationMiddleware (traversal, límites de prefijo, extracción de ticket). No

Musify.Worker no tiene proyecto de test: su Program.cs es puro wiring de DI/host (Hangfire, MassTransit, jobs) — la lógica real que ejecuta vive en Musify.Infrastructure, ya cubierta ahí.

Application.Tests: por qué SQLite en memoria y no mocks

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.

Infrastructure.Tests y Api.Tests: Testcontainers

Estos dos sí necesitan Docker corriendo. Levantan un Postgres real (Testcontainers.PostgreSql) y, en Infrastructure.Tests, también un SeaweedFS real (mismo chrislusf/seaweedfs:latest e imagen/comando que deploy/compose.yml, no MinIO) — así StorageService se prueba contra el mismo backend S3-compatible que usa producción, no contra AWS S3. Los contenedores se levantan una vez por corrida (ICollectionFixture), con migraciones EF Core reales aplicadas contra el Postgres efímero.

Api.Tests usa WebApplicationFactory<Program> contra ese mismo Postgres — routing, auth y validación reales — pero sustituye por fakes los servicios que hablan por red (IStorageService, IEventBus, IYouTubeMusicService, IStreamTicketService) y el esquema de autenticación JWT real por un handler de prueba (ver TestSupport/FakeAuthenticationHandler.cs) que autentica según un header, sin necesitar un Zitadel real.

Escribir estos tests contra el backend real encontró y arregló dos bugs reales (no simulados): StorageService.RemoveFolderAsync reventaba con NullReferenceException contra SeaweedFS (su respuesta DeleteObjects omite la lista <Error> cuando no hay errores, a diferencia de AWS S3), y AuditableEntityInterceptor nunca actualizaba UpdatedAt en una actualización normal (carga → modifica → guarda) porque corría antes de que EF detectara los cambios pendientes.

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.