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).
153 lines
7.2 KiB
Markdown
153 lines
7.2 KiB
Markdown
# 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](../deploy/README.md).
|
|
|
|
## Arranque
|
|
|
|
```sh
|
|
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](../deploy/README.md).
|
|
|
|
## Aplicaciones
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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
|
|
|
|
```sh
|
|
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](../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.
|