Files
minstrel/internal/api/library_sync_views_test.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
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>
2026-10-23 12:23:58 -04:00

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)
}
}