Files
Musify/docs/development.md
T
Admin 23e30d96fc Translate docs to English, rewrite README, drop design handoff folder
Translate all of docs/ from Spanish to English and rewrite the root
README as a short project overview instead of a technical run guide.
Remove web-player/design_handoff_musify_player, an old design-tool
export that was no longer needed.
2026-09-21 18:41:04 +02:00

7.0 KiB

Development environment

How to run everything locally and test it. The full deployment details (dev and prod) live in deploy/README.md.

Startup

docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d

That one command brings up the infrastructure and runs the one-shot bootstrap jobs: generates the RS256 stream-ticket keys in deploy/keys/, applies the EF Core migrations, and provisions Zitadel (project + OIDC apps), writing the client ids into deploy/.env. Full details, including how to run the apps in Docker with --profile apps, are in deploy/README.md.

Applications

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

Port map

Service Port
Musify.Api 5111 (/scalar/v1, /openapi/v1.json)
Musify.StreamingGateway 8081 (/health, /media/...)
web-player 5173 (npm run dev) or 3000 (container)
SeaweedFS S3 / filer / master 8333 / 8888 / 9333
Zitadel 8080
PostgreSQL 59000 (→ 5432 in the container)
RabbitMQ AMQP / management 5672 / 15672
Jaeger UI / OTLP 16686 / 4317
pgAdmin (-Tools) 5050

DB: musify_db, user postgres/postgres. S3 bucket: webapi-storage (credentials admin_access_key/admin_secret_key). All of these come from deploy/.env.

Migrations

The migrate service in deploy/compose.yml applies them on every docker compose up -d (a no-op if the schema is already current). To run them by hand after a schema change, against the local database:

dotnet ef database update --project backend/Core/Musify.Infrastructure --startup-project backend/Core/Musify.Infrastructure --context Database

Musify.Infrastructure is both the migrations project and the startup project: it has the IDesignTimeDbContextFactory and the EFCore.Design package. The connection string is read from its user-secrets, or from the Database__ConnectionString environment variable if there are no user-secrets (which is how the migrate container uses it, see backend/Musify.Migrator.Dockerfile).

Stream tickets (RS256 keys)

The API signs stream tickets with the private key, and the gateway validates them with the public one. The keys-init service in deploy/compose.yml generates them in deploy/keys/ if they don't exist yet; to do it by hand:

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/ (the /Tests/ folder in the .slnx) has five xUnit projects:

Project Covers Docker
Musify.Domain.Tests Value object logic (TrackAudio/TrackPictures: IsProcessed, IsFailed, IsInProgress). Entities are mostly anemic, with no behavior of their own beyond that. No
Musify.Application.Tests The ~45 handlers and services in Musify.Application (Albums, Tracks, PlayLists, Users, Mixes). No
Musify.Infrastructure.Tests Database/migrations, StorageService, StreamTicketService, AuditableEntityInterceptor, PictureService, SingleFlightCache, PlayListPresetSeeder. Yes
Musify.Api.Tests Real HTTP endpoints (routing, auth, ValidationFilter, ErrorOr→HTTP mapping) via WebApplicationFactory, plus ErrorOrHttpExtensions/CurrentUser/FluentValidation validators in isolation. Yes
Musify.StreamingGateway.Tests TicketValidator (RS256) and TicketValidationMiddleware (traversal, prefix limits, ticket extraction). No

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

Application.Tests: why in-memory SQLite instead of mocks

IDatabase exposes DbSet<T> directly, so handlers build real LINQ (Where, Include, Select...) against it. That can't be mocked with NSubstitute, since there's no IQueryable behind a mock. Instead, TestSupport/TestDatabase.cs is a real DbContext on top of in-memory SQLite (a fresh :memory: connection per test, with real foreign keys, unlike EF Core's InMemory provider, SQLite also supports transactions, which two handlers rely on). TestSupport/TestEntities.cs and TestConfigurations.cs build entities and configuration with sensible defaults. IStorageService, IEventBus, etc. are mocked with NSubstitute, since those are plain interfaces.

Infrastructure.Tests and Api.Tests: Testcontainers

These two need Docker running. They spin up a real Postgres (Testcontainers.PostgreSql) and, in Infrastructure.Tests, a real SeaweedFS too (the same chrislusf/seaweedfs:latest image/command as deploy/compose.yml, not MinIO), so StorageService is tested against the same S3-compatible backend production uses, not against AWS S3. Containers are started once per run (ICollectionFixture), with real EF Core migrations applied against the ephemeral Postgres.

Api.Tests uses WebApplicationFactory<Program> against that same Postgres, with real routing, auth, and validation, but replaces the services that talk over the network with fakes (IStorageService, IEventBus, IStreamTicketService), and swaps the real JWT auth scheme for a test handler (see TestSupport/FakeAuthenticationHandler.cs) that authenticates based on a header, with no real Zitadel needed.

Writing these tests against the real backend found and fixed two actual bugs (not staged ones): StorageService.RemoveFolderAsync threw a NullReferenceException against SeaweedFS (its DeleteObjects response omits the <Error> list when there are no errors, unlike AWS S3), and AuditableEntityInterceptor never updated UpdatedAt on a normal update (load → modify → save) because it ran before EF had detected the pending changes.

Config (AppSettings)

Convention: AppSettings.json is the template, with empty or neutral values; AppSettings.Development.json has the real dev values; in Docker everything is overridden with environment variables (Section__Key) from deploy/compose.yml. To run Musify.Api from the IDE, the Authentication values go into user-secrets (not the repo). Copy them from deploy/.env once zitadel-init has generated them (see deploy/README.md).

Key sections: Authentication (Zitadel), Database, MassTransit, InfrastructureStorage (S3), ApplicationStorage (bucket), Track, PlayList, UploadIntent, AudioTranscoder, Workers, StreamGateway / StreamTicket.

Host requirements

  • .NET 10 SDK and Node 22.
  • ffmpeg on the PATH (for the Worker's audio transcoding).
  • Docker Desktop.