package api import ( "bytes" "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/go-chi/chi/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) func TestLeveledStreamToken(t *testing.T) { secret := []byte("leveled-secret") exp := time.Now().Unix() + 3600 g := library.LeveledGain{CentiDB: -550} tok := SignLeveledStreamToken(secret, "track-1", exp, g) if !VerifyLeveledStreamToken(secret, "track-1", exp, g, tok) { t.Fatal("round trip failed") } // The gain is part of what is signed: a speaker URL edited to another // gain, or to switch the limiter on, no longer verifies. if VerifyLeveledStreamToken(secret, "track-1", exp, library.LeveledGain{CentiDB: 1200}, tok) { t.Error("token verified for a different gain") } if VerifyLeveledStreamToken(secret, "track-1", exp, library.LeveledGain{CentiDB: -550, Limiter: true}, tok) { t.Error("token verified with the limiter switched on") } if VerifyLeveledStreamToken(secret, "track-2", exp, g, tok) { t.Error("token verified for another track") } if VerifyLeveledStreamToken(secret, "track-1", time.Now().Unix()-1, g, SignLeveledStreamToken(secret, "track-1", time.Now().Unix()-1, g)) { t.Error("expired token verified") } // Plain and leveled tokens are not interchangeable. plain := SignStreamToken(secret, "track-1", exp) if VerifyLeveledStreamToken(secret, "track-1", exp, library.LeveledGain{}, plain) { t.Error("a plain stream token opened a leveled stream") } if VerifyStreamToken(secret, "track-1", exp, tok) { t.Error("a leveled token opened the plain stream") } } func TestParseLeveledGain(t *testing.T) { for _, c := range []struct { query string want library.LeveledGain ok bool }{ {"g=-550&lim=0", library.LeveledGain{CentiDB: -550}, true}, {"g=320&lim=1", library.LeveledGain{CentiDB: 320, Limiter: true}, true}, {"g=1201&lim=0", library.LeveledGain{}, false}, {"g=abc&lim=0", library.LeveledGain{}, false}, {"g=100&lim=yes", library.LeveledGain{}, false}, {"lim=0", library.LeveledGain{}, false}, } { got, ok := parseLeveledGain(httptest.NewRequest(http.MethodGet, "/x?"+c.query, nil)) if ok != c.ok || (ok && got != c.want) { t.Errorf("%s: got %+v ok=%v, want %+v ok=%v", c.query, got, ok, c.want, c.ok) } } } func TestCastStreamToken_Leveled(t *testing.T) { h, pool := testHandlers(t) h.streamSecret = []byte("leveled-cast-secret") r, err := library.NewLeveledRenderer(t.TempDir(), nil, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatal(err) } h.leveled = r user := seedUser(t, pool, "lev", "pw", false) track, _ := seedTrackForRemoveTest(t, h, "lev", "", "") trackID := uuidToString(track.ID) lufs, peak := float32(-12.5), float32(-1) if err := dbq.New(pool).UpsertTrackLoudness(context.Background(), dbq.UpsertTrackLoudnessParams{ TrackID: track.ID, IntegratedLufs: &lufs, TruePeakDbtp: &peak, AnalysisVersion: 1, }); err != nil { t.Fatalf("seed loudness: %v", err) } mint := func(req castTokenRequest) castTokenResponse { t.Helper() body, _ := json.Marshal(req) w := httptest.NewRecorder() h.handleCastStreamToken(w, withUser(httptest.NewRequest(http.MethodPost, "/api/cast/stream-token", bytes.NewReader(body)), user)) if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } var resp castTokenResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } return resp } // Default preference (auto, -18): the track measures -12.5 LUFS, so it is // cut by 5.5 dB, and the URL and its token say exactly that. resp := mint(castTokenRequest{TrackID: trackID, Level: true}) if !resp.Leveled || resp.MIME != "audio/flac" || !strings.Contains(resp.URL, "/api/tracks/"+trackID+"/leveled.flac?g=-550&lim=0&token=") { t.Fatalf("leveled mint = %+v", resp) } if !VerifyLeveledStreamToken(h.streamSecret, trackID, resp.Exp, library.LeveledGain{CentiDB: -550}, resp.Token) { t.Fatal("leveled token does not verify for the gain in the URL") } // Not asked for: the plain stream, as before. if resp := mint(castTokenRequest{TrackID: trackID}); resp.Leveled || strings.Contains(resp.URL, "leveled") { t.Fatalf("unleveled mint = %+v", resp) } // Leveling switched off: nothing to render, so the plain stream. if _, err := library.SaveNormalizationPrefs(context.Background(), dbq.New(pool), user.ID, library.NormalizationPrefs{Mode: "off", TargetLUFS: -18, Boost: "headroom"}); err != nil { t.Fatal(err) } if resp := mint(castTokenRequest{TrackID: trackID, Level: true}); resp.Leveled { t.Fatalf("mint with leveling off = %+v, want the plain stream", resp) } } func TestGetLeveledStream_RefusesUnsignedGains(t *testing.T) { h, _ := testHandlers(t) h.streamSecret = []byte("leveled-get-secret") router := chi.NewRouter() router.Get("/api/tracks/{id}/leveled.flac", h.handleGetLeveledStream) id := nonExistentTrackUUID exp := time.Now().Unix() + 3600 tok := SignLeveledStreamToken(h.streamSecret, id, exp, library.LeveledGain{CentiDB: -300}) get := func(query string) int { w := httptest.NewRecorder() router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/tracks/"+id+"/leveled.flac?"+query, nil)) return w.Code } e := strconv.FormatInt(exp, 10) for name, q := range map[string]string{ "no token": "g=-300&lim=0", "edited gain": "g=1200&lim=0&token=" + tok + "&exp=" + e, "limiter on": "g=-300&lim=1&token=" + tok + "&exp=" + e, "invalid gain": "g=99999&lim=0&token=" + tok + "&exp=" + e, } { if code := get(q); code != http.StatusUnauthorized { t.Errorf("%s: status %d, want 401", name, code) } } // The signed gain gets past auth to the track lookup. if code := get("g=-300&lim=0&token=" + tok + "&exp=" + e); code == http.StatusUnauthorized { t.Errorf("the signed gain was refused") } }