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`.
4.2 KiB
Proyectos
Los proyectos .NET viven en backend/ (solución Musify.slnx, carpetas
Core/, Hosts/ y Tests/); el cliente web en web-player/.
Musify.Api (:5111)
API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema.
- Endpoints:
UserEndpoints,TrackEndpoints,PlayListEndpoints. - Autenticación: JWT Bearer contra Zitadel (
Authentication/), conJwtBearerEventsHandlerque sincroniza el usuario en cada token validado. CurrentUser: abstracción que se enlaza como parámetro de endpoint (IBindableFromHttpContext) y expone los claims básicos (Idcomolong?,RequiredId, nombre, etc.) sin tener que leerClaimsPrincipala mano.- Emite: presigned URLs de subida (vía
IStorageService) y stream-tickets (víaIStreamTicketService, RS256). - OpenAPI + Scalar para documentación/prueba; OAuth2 (PKCE) configurado para login desde Scalar.
- CORS totalmente abierto solo en Development (para el front local).
- Traduce
ErrorOr→ HTTP enResultHttpExtensions.
Propósito/porqué: separar la cara pública (autorización + metadatos) del procesado y del servido de bytes.
Musify.Domain + Musify.Application + Musify.Infrastructure
El núcleo, en tres proyectos (clean architecture):
Domain
Entidades (Entities/) y value objects/enums (ValueObjects/: ProcessingStatus, LifeCycleStatus, UploadIntentStatus, …). Sin dependencias de infraestructura.
Application
Casos de uso (CQRS con Mediator):
Tracks/,PlayLists/,Users/→Commands,Queries,Handlers,Responses.Contracts/→ interfaces que implementa la infraestructura (IDatabase,IStorageService,IAudioTranscoderService,IPictureService,IEventBus,IStreamTicketService).Configuration/→ opciones tipadas (Track, PlayList, ApplicationStorage, UploadIntent…) bindeadas y validadas al arranque.Shared/→ helpers transversales (StorageKey.Combine,ImageSize).Pagination/→PaginatedResponse<T>.
Porqué: la lógica de negocio no conoce EF, S3 ni RabbitMQ; solo interfaces. Reutilizable por la API y el Worker.
Infrastructure
Implementaciones concretas:
Persistence/→Database(EF Core/Npgsql), migraciones.Services/→StorageService(AWSSDK.S3 contra SeaweedFS),AudioTranscoderService(ffmpeg →.m4a),PictureService(ImageSharp),StreamTicketService(firma RS256).MassTransit/→ consumers, activities y routing slips (ver media-processing.md).Jobs/→UploadIntentExpirationJob,TemporalUploadsCleanUpJob.
Musify.StreamingGateway (:8081)
Reverse proxy (ASP.NET + YARP) que sirve el audio (.m4a) desde SeaweedFS. Valida el stream-ticket (RS256, clave pública) y, si el objeto pedido cae bajo el prefijo autorizado, reenvía al filer.
Porqué un proyecto aparte: escala con el tráfico de bytes independientemente de la API, tiene superficie mínima (solo sabe validar un JWT y proxiar) y permite que SeaweedFS quede en red privada. Ver streaming.md.
Musify.Worker
Worker Service (host de fondo) que consume los eventos de MassTransit y ejecuta el procesado pesado: transcode de audio a .m4a (ffmpeg), generación de miniaturas (ImageSharp), transferencias de ficheros en el bucket, y los jobs de expiración/limpieza de upload intents.
Porqué aparte: el trabajo CPU/IO no debe ocurrir dentro de un request HTTP; se escala y reinicia por separado. (Antes se llamaba Musify.PictureWorker; se renombró porque hace más que imágenes.)
web-player (:5173 en dev, :3000 en contenedor)
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).
Otras carpetas
deploy/— stacks de Docker Compose (dev y prod), autocontenidos. Ver ../deploy/README.md.docs/— esta documentación.