Files
Musify/docs/development.md
T
Admin 31032bd8e8 Remove YouTube integration from backend and deployment config
Drops the YouTube-backed second catalog source entirely: the
Local/External split on Track/Album collapses into single concrete
entities, Artist/TrackArtist/ExternalTrack/ExternalAlbum and the
YouTube service/download pipeline (yt-dlp, MassTransit consumer,
routing slip) are deleted, and mixes now source discovery only from
local listening history. Includes the RemoveYouTubeIntegration EF
Core migration and matching Worker/deploy/compose cleanup (no more
yt-dlp/python/deno in the Worker image, no thumbnail-cache/YtDlp
config sections).
2026-09-15 21:17:03 +02:00

7.1 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). 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, 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, 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, Workers, StreamGateway / StreamTicket.

Requisitos del host

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