27f123f7d9
Two fixes in one commit because they're entangled — the systemVariant work would have been theater otherwise. ## The wire-format bug /api/library/sync was emitting PascalCase JSON for artist / album / track / playlist upserts (raw json.Marshal of sqlc-generated structs with no JSON tags — sqlc.yaml: emit_json_tags=false). Flutter's sync_controller _*FromJson reads snake_case keys, so all metadata sync rows landed in drift with empty strings / zero ints. The like_track / like_album / like_artist / playlist_track entities work because they're hand-built `map[string]string` payloads with snake_case keys — they sidestepped the bug. The 4 raw-marshal entities did not. Existing sync test caught zero of this — it asserts on len(upserts) not field shape. Fix: server-side view structs in library_sync_views.go with proper JSON tags + pgtype-flattening (UUID → 8-4-4-4-12 hex string, Date → "2006-01-02"). Mirrors the playlistRowView pattern from /api/playlists. New library_sync_views_test.go pins the wire keys so future field-name drift breaks loud. ## systemVariant column (closes #357 plan C v1 limitation) playlistSyncView now carries `system_variant` server → wire. Flutter drift schema bumped from 1 → 2 with onUpgrade adding the `systemVariant TEXT NULL` column to cached_playlists. Cursor reset to 0 in the migration so existing rows refresh with the new field on the next sync. playlistsListProvider now filters locally by systemVariant: - kind='user' → systemVariant IS NULL (the add-to-playlist sheet's intent) - kind='system' → systemVariant IS NOT NULL - kind='all' → no filter Closes the documented v1 limitation where the add-to-playlist sheet showed system playlists alongside user-created ones. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
// 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
|
|
// Flutter client (which reads snake_case keys via _*FromJson helpers
|
|
// in flutter_client/lib/cache/sync_controller.dart).
|
|
|
|
// 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))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertJSONKeys(t, "album", b, []string{
|
|
"id", "artist_id", "title", "sort_title",
|
|
"release_date", "cover_art_path", "mbid",
|
|
})
|
|
}
|
|
|
|
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))
|
|
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",
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|