Files
Musify/docs/projects.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

4.3 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/), con JwtBearerEventsHandler que 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 (Id como long?, RequiredId, nombre, etc.) sin tener que leer ClaimsPrincipal a mano.
  • Emite: presigned URLs de subida (vía IStorageService) y stream-tickets (vía IStreamTicketService, 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 en ResultHttpExtensions.

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.

Tests

backend/Tests/, carpeta /Tests/ en el .slnx: cinco proyectos xUnit — Musify.Domain.Tests, Musify.Application.Tests (SQLite en memoria, sin Docker), Musify.Infrastructure.Tests y Musify.Api.Tests (Testcontainers: Postgres + SeaweedFS reales) y Musify.StreamingGateway.Tests. Detalle en development.md.

Otras carpetas

  • deploy/ — stacks de Docker Compose (dev y prod), autocontenidos. Ver ../deploy/README.md.
  • docs/ — esta documentación.