Files
Musify/docs/development.md
Admin dd1cb8a5d1 Clean up backend and web-player leftovers
Remove dead scaffold/config cruft and stale references left over from
recent refactors: dotnet's WebApi.http template file, the AppSettings
PascalCase/Dockerfile workaround (renamed to appsettings.json to match
convention), empty directories left behind by the auth-route and
folder-structure moves, a stale ESLint ignore, and the now-fully-applied
frontend refactor plan doc.
2026-09-22 16:08:44 +02:00

7.1 KiB

Development environment

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

Startup

Bring up the separate Infrastructure repository first (Postgres, Zitadel, SeaweedFS, RabbitMQ, Jaeger, see its own README for the exact command), then:

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

That runs Musify's own one-shot bootstrap jobs: it generates the RS256 stream-ticket keys in deploy/keys/, applies the EF Core migrations, creates Musify's storage bucket, 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)

Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not run by this repo. Their ports are documented in the separate Infrastructure repository's own README.

DB: musify_db, user postgres/postgres on Infrastructure's shared Postgres. S3 bucket: webapi-storage (credentials admin_access_key/admin_secret_key, also Infrastructure's). 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.