Files
yellowjacket/backend/explore/lrclib_test.go
T
yonluandClaude Opus 4.8 65048401e8 feat: autotag scoring overhaul, dump-based explore index, and lyrics search
Consolidates in-progress work across autotag, explore, and library:

- autotag: beets/Picard-informed scoring engine — ID-first matching, VA
  handling, recommendation tiers, and a merged distance/rank cascade, with
  an eval harness for regression tracking.
- explore: offline MusicBrainz dump import/incremental refresh replaces the
  legacy tier crawl; index-first local search with fuzzy matching and a
  dedicated ranker; disk-free guards for dump downloads.
- library: artist-credit extraction and matching.
- lyrics: owned-library lyric search (FTS) with LRCLIB backfill.

Also: rewrite README to be user-focused, and migrate upstream to
git.ljones.me/yonlu/yellowjacket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:14:20 -04:00

128 lines
2.8 KiB
Go

package explore
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"yellowjacket/backend/database"
)
// newTestLRCLib builds an LRCLIB client whose HTTP calls hit the given
// test server, sharing a real (in-memory) cache so caching behaviour is
// exercised.
func newTestLRCLib(t *testing.T, handler http.HandlerFunc) (*LRCLibClient, *httptest.Server) {
t.Helper()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
db := database.NewTestDB(t)
cache := NewCache(db, testLogger())
c := NewLRCLibClient(cache, testLogger())
// Point the client at the test server instead of the real API by
// overriding its transport to rewrite the host.
c.http = srv.Client()
c.baseURL = srv.URL
return c, srv
}
func TestLRCLibGetLyrics(t *testing.T) {
t.Parallel()
var calls int
c, _ := newTestLRCLib(t, func(w http.ResponseWriter, r *http.Request) {
calls++
if r.URL.Path != "/api/get" {
http.Error(w, "not found", http.StatusNotFound)
return
}
if r.URL.Query().Get("track_name") == "Missing" {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"id": 42,
"trackName": "Yesterday",
"artistName": "The Beatles",
"albumName": "Help!",
"duration": 125,
"instrumental": false,
"plainLyrics": "Yesterday, all my troubles seemed so far away",
"syncedLyrics": "[00:00.00] Yesterday"
}`))
})
t.Run("hit returns plain + synced lyrics", func(t *testing.T) {
lyrics, err := c.GetLyrics(context.Background(), "The Beatles", "Yesterday", "Help!", 125)
if err != nil {
t.Fatalf("GetLyrics: %v", err)
}
if lyrics.Plain == "" || lyrics.Synced == "" {
t.Errorf("expected plain and synced lyrics, got %+v", lyrics)
}
if lyrics.Instrumental {
t.Error("expected non-instrumental")
}
})
t.Run("second identical request is served from cache", func(t *testing.T) {
before := calls
if _, err := c.GetLyrics(
context.Background(),
"The Beatles",
"Yesterday",
"Help!",
125,
); err != nil {
t.Fatalf("GetLyrics: %v", err)
}
if calls != before {
t.Errorf("expected cache hit (no new HTTP call), calls went %d → %d", before, calls)
}
})
t.Run("404 maps to ErrLyricsNotFound", func(t *testing.T) {
_, err := c.GetLyrics(context.Background(), "Nobody", "Missing", "", 0)
if !errors.Is(err, ErrLyricsNotFound) {
t.Errorf("expected ErrLyricsNotFound, got %v", err)
}
})
t.Run("missing artist/title short-circuits without a request", func(t *testing.T) {
before := calls
if _, err := c.GetLyrics(
context.Background(),
"",
"Yesterday",
"",
0,
); !errors.Is(
err,
ErrLyricsNotFound,
) {
t.Errorf("expected ErrLyricsNotFound, got %v", err)
}
if calls != before {
t.Error("expected no HTTP call for empty artist")
}
})
}