A client starts an upload with the soundtrack's name, sends one MP3 per
request, and finishes it. The server then moves the songs into a new folder
in the music folder and syncs only that folder, so the soundtrack is in the
library, lists and search right away.
- POST /api/v1/soundtracks/uploads, GET/DELETE .../uploads/{id},
PUT .../uploads/{id}/files/{file} and POST .../uploads/{id}/finish.
- Names must work as file names on every system and be free, ignoring case.
File names are plain: clients flatten disc folders.
- Only MP3 files. Size limits from UPLOAD_MAX_FILE_MB (50) and
UPLOAD_MAX_TOTAL_MB (2048).
- Refused while a sync runs, and a sync cannot start while an upload is
finished. Songs equal to an existing soundtrack's are refused, as the sync
would take them for that soundtrack renamed.
- Unfinished uploads live in MUSIC_PATH/.uploads and are removed after a day.
The sync now skips every hidden folder.
Closes #33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
package config
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func env(values map[string]string) func(string) string {
|
|
return func(key string) string { return values[key] }
|
|
}
|
|
|
|
func validEnv() map[string]string {
|
|
return map[string]string{
|
|
"DB_HOST": "localhost",
|
|
"DB_PORT": "5432",
|
|
"DB_USERNAME": "user",
|
|
"DB_PASSWORD": "pass",
|
|
"DB_NAME": "music",
|
|
"MUSIC_PATH": "/music",
|
|
"CHARACTERS_PATH": "/characters",
|
|
}
|
|
}
|
|
|
|
func TestLoadDefaults(t *testing.T) {
|
|
cfg, err := load(env(validEnv()))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8080, cfg.Port)
|
|
assert.Equal(t, "info", cfg.LogLevel)
|
|
assert.Equal(t, "songs", cfg.SoundCheckPath)
|
|
assert.Equal(t, DefaultReleasesURL, cfg.ReleasesURL)
|
|
assert.Empty(t, cfg.TrustedProxies)
|
|
assert.Empty(t, cfg.CORSAllowedOrigins)
|
|
assert.Equal(t, DefaultMetricsPort, cfg.MetricsPort)
|
|
assert.Empty(t, cfg.CoversPath, "covers are off by default")
|
|
assert.Empty(t, cfg.SteamGridDBAPIKey)
|
|
}
|
|
|
|
func TestLoadUploadLimits(t *testing.T) {
|
|
cfg, err := load(env(validEnv()))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(DefaultUploadMaxFileMB<<20), cfg.UploadMaxFileSize)
|
|
assert.Equal(t, int64(DefaultUploadMaxTotalMB<<20), cfg.UploadMaxTotalSize)
|
|
|
|
values := validEnv()
|
|
values["UPLOAD_MAX_FILE_MB"] = "100"
|
|
values["UPLOAD_MAX_TOTAL_MB"] = "500"
|
|
cfg, err = load(env(values))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(100<<20), cfg.UploadMaxFileSize)
|
|
assert.Equal(t, int64(500<<20), cfg.UploadMaxTotalSize)
|
|
|
|
for _, invalid := range []string{"0", "-5", "lots", "1.5"} {
|
|
values["UPLOAD_MAX_FILE_MB"] = invalid
|
|
_, err = load(env(values))
|
|
assert.ErrorContains(t, err, "UPLOAD_MAX_FILE_MB", invalid)
|
|
}
|
|
}
|
|
|
|
func TestLoadCovers(t *testing.T) {
|
|
values := validEnv()
|
|
values["COVERS_PATH"] = "/covers"
|
|
values["STEAMGRIDDB_API_KEY"] = "key"
|
|
cfg, err := load(env(values))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/covers", cfg.CoversPath)
|
|
assert.Equal(t, "key", cfg.SteamGridDBAPIKey)
|
|
}
|
|
|
|
func TestLoadMetricsPort(t *testing.T) {
|
|
values := validEnv()
|
|
values["METRICS_PORT"] = "9100"
|
|
cfg, err := load(env(values))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 9100, cfg.MetricsPort)
|
|
|
|
values["METRICS_PORT"] = "off"
|
|
cfg, err = load(env(values))
|
|
require.NoError(t, err)
|
|
assert.Zero(t, cfg.MetricsPort)
|
|
|
|
for _, invalid := range []string{"abc", "0", "70000", "-1", "OFF"} {
|
|
values["METRICS_PORT"] = invalid
|
|
_, err = load(env(values))
|
|
assert.ErrorContains(t, err, "METRICS_PORT", invalid)
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingRequired(t *testing.T) {
|
|
values := validEnv()
|
|
delete(values, "MUSIC_PATH")
|
|
delete(values, "DB_HOST")
|
|
_, err := load(env(values))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "MUSIC_PATH is required")
|
|
assert.Contains(t, err.Error(), "DB_HOST is required")
|
|
}
|
|
|
|
func TestLoadTrustedProxies(t *testing.T) {
|
|
values := validEnv()
|
|
values["TRUSTED_PROXIES"] = "10.0.0.0/8, 172.16.0.0/12"
|
|
cfg, err := load(env(values))
|
|
require.NoError(t, err)
|
|
require.Len(t, cfg.TrustedProxies, 2)
|
|
assert.Equal(t, "10.0.0.0/8", cfg.TrustedProxies[0].String())
|
|
|
|
values["TRUSTED_PROXIES"] = "not-a-cidr"
|
|
_, err = load(env(values))
|
|
assert.ErrorContains(t, err, "TRUSTED_PROXIES")
|
|
}
|
|
|
|
func TestLoadCORSAllowedOrigins(t *testing.T) {
|
|
values := validEnv()
|
|
values["CORS_ALLOWED_ORIGINS"] = "https://music.example.com, http://localhost:5173,"
|
|
cfg, err := load(env(values))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"https://music.example.com", "http://localhost:5173"}, cfg.CORSAllowedOrigins)
|
|
|
|
for _, invalid := range []string{"music.example.com", "ftp://example.com", "https://example.com/app",
|
|
"https://*.example.com", "https://*", "*", "https://example.com?x=1", "https://user@example.com"} {
|
|
values["CORS_ALLOWED_ORIGINS"] = invalid
|
|
_, err = load(env(values))
|
|
assert.ErrorContains(t, err, "CORS_ALLOWED_ORIGINS", invalid)
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidPort(t *testing.T) {
|
|
values := validEnv()
|
|
values["PORT"] = "abc"
|
|
_, err := load(env(values))
|
|
assert.ErrorContains(t, err, "PORT")
|
|
}
|