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.
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.