Rewrites the internal/api comment blocks to rule 32: task, milestone and rule ids gone, the stories behind them restated as the constraint they left, stale references corrected (the stream secret is wired from config, the timezone handler already refreshes the scheduler, the Android app is the native client, the design docs cited are not in the repo). Also rewraps comment lines over 80 columns left by the earlier batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.2 KiB
Go
150 lines
5.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
|
)
|
|
|
|
// These tests pin the wire-format keys for /api/library/sync upserts.
|
|
// Without them, sqlc model field-name drift or accidental
|
|
// `json.Marshal(rawStruct)` regressions would silently break the Android
|
|
// client, which reads these snake_case keys via @SerialName in
|
|
// models/wire/SyncResponseWire.kt. "Silently" is the operative word:
|
|
// kotlinx.serialization skips unknown keys, so a renamed field arrives as
|
|
// a default value rather than an error.
|
|
|
|
// validUUID is a deterministic test UUID — not a real value, just
|
|
// something that pgtype.UUID.Valid will accept.
|
|
var validUUID = pgtype.UUID{
|
|
Bytes: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
|
|
Valid: true,
|
|
}
|
|
|
|
func assertJSONKeys(t *testing.T, label string, b []byte, want []string) {
|
|
t.Helper()
|
|
var m map[string]json.RawMessage
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
t.Fatalf("%s: invalid json: %v\nbody: %s", label, err, b)
|
|
}
|
|
for _, k := range want {
|
|
if _, ok := m[k]; !ok {
|
|
t.Errorf("%s: missing key %q in %s", label, k, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArtistSyncView_WireKeys(t *testing.T) {
|
|
a := dbq.Artist{ID: validUUID, Name: "Aphex Twin", SortName: "Aphex Twin"}
|
|
b, err := json.Marshal(toArtistSyncView(a))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertJSONKeys(t, "artist", b, []string{
|
|
"id", "name", "sort_name", "mbid",
|
|
"artist_thumb_path", "artist_fanart_path",
|
|
})
|
|
}
|
|
|
|
func TestAlbumSyncView_WireKeys(t *testing.T) {
|
|
a := dbq.Album{ID: validUUID, ArtistID: validUUID, Title: "Drukqs", SortTitle: "Drukqs"}
|
|
b, err := json.Marshal(toAlbumSyncView(a, library.ReplayGain{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertJSONKeys(t, "album", b, []string{
|
|
"id", "artist_id", "title", "sort_title",
|
|
"release_date", "cover_art_path", "mbid", "album_gain", "album_peak",
|
|
})
|
|
}
|
|
|
|
func TestTrackSyncView_WireKeys(t *testing.T) {
|
|
tr := dbq.Track{
|
|
ID: validUUID, AlbumID: validUUID, ArtistID: validUUID,
|
|
Title: "Avril 14th", DurationMs: 121_000,
|
|
FilePath: "x", FileFormat: "flac",
|
|
}
|
|
b, err := json.Marshal(toTrackSyncView(tr, library.ReplayGain{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertJSONKeys(t, "track", b, []string{
|
|
"id", "album_id", "artist_id", "title", "duration_ms",
|
|
"track_number", "disc_number", "file_path", "file_format", "genre",
|
|
"missing", "track_gain", "track_peak",
|
|
})
|
|
}
|
|
|
|
// The mark is what the client filters on, so a wrong value here is silent: a
|
|
// present track marked missing vanishes from the client's library, a missing
|
|
// one stays playable and fails at the speaker.
|
|
func TestTrackSyncView_MissingReflectsTheMark(t *testing.T) {
|
|
present := dbq.Track{ID: validUUID, AlbumID: validUUID, ArtistID: validUUID}
|
|
if toTrackSyncView(present, library.ReplayGain{}).Missing {
|
|
t.Error("a track with no missing_since must not be marked missing")
|
|
}
|
|
|
|
gone := present
|
|
gone.MissingSince = pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
|
if !toTrackSyncView(gone, library.ReplayGain{}).Missing {
|
|
t.Error("a track with missing_since must be marked missing")
|
|
}
|
|
}
|
|
|
|
// The gains are what Android levels playback by, offline included:
|
|
// the track's pair rides on the track view and the album's on the album view.
|
|
func TestSyncViews_CarryReplayGain(t *testing.T) {
|
|
gain, peak := float32(-3.25), float32(0.9441)
|
|
g := library.ReplayGain{TrackGain: &gain, TrackPeak: &peak, AlbumGain: &gain, AlbumPeak: &peak}
|
|
tv := toTrackSyncView(dbq.Track{ID: validUUID, AlbumID: validUUID, ArtistID: validUUID}, g)
|
|
if tv.TrackGain == nil || *tv.TrackGain != gain || tv.TrackPeak == nil || *tv.TrackPeak != peak {
|
|
t.Errorf("track view gains = %v/%v, want %v/%v", tv.TrackGain, tv.TrackPeak, gain, peak)
|
|
}
|
|
av := toAlbumSyncView(dbq.Album{ID: validUUID, ArtistID: validUUID}, g)
|
|
if av.AlbumGain == nil || *av.AlbumGain != gain || av.AlbumPeak == nil || *av.AlbumPeak != peak {
|
|
t.Errorf("album view gains = %v/%v, want %v/%v", av.AlbumGain, av.AlbumPeak, gain, peak)
|
|
}
|
|
// Unmeasured: explicit nulls, like every other optional sync field.
|
|
b, err := json.Marshal(toTrackSyncView(dbq.Track{ID: validUUID, AlbumID: validUUID, ArtistID: validUUID}, library.ReplayGain{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, ok := m["track_gain"]; !ok || v != nil {
|
|
t.Errorf("unmeasured track_gain = %v (present %v), want an explicit null", v, ok)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistSyncView_WireKeys(t *testing.T) {
|
|
variant := "discover"
|
|
p := dbq.Playlist{
|
|
ID: validUUID, UserID: validUUID, Name: "Test",
|
|
IsPublic: true, TrackCount: 5, DurationSec: 600,
|
|
SystemVariant: &variant,
|
|
}
|
|
b, err := json.Marshal(toPlaylistSyncView(p))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertJSONKeys(t, "playlist", b, []string{
|
|
"id", "user_id", "name", "description",
|
|
"is_public", "cover_path", "track_count",
|
|
"duration_sec", "system_variant",
|
|
})
|
|
// Sanity: system_variant carries the value, not just present.
|
|
var m map[string]any
|
|
_ = json.Unmarshal(b, &m)
|
|
if got := m["system_variant"]; got != "discover" {
|
|
t.Errorf("system_variant: want 'discover', got %v", got)
|
|
}
|
|
}
|