package main import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) // Line breaks are the content here — LRC timestamps are per line — so cleanLyrics must not do what // clean() does to a title. func TestCleanLyrics(t *testing.T) { got := cleanLyrics(" [00:11.74] Rivi yksi\r\n[00:13.99] Rivi\x07 kaksi\r\n\n") want := "[00:11.74] Rivi yksi\n[00:13.99] Rivi kaksi" if got != want { t.Fatalf("cleanLyrics gave %q, want %q", got, want) } if n := len([]rune(cleanLyrics(strings.Repeat("a", maxLyrics+500)))); n != maxLyrics { t.Fatalf("truncated to %d runes, want %d", n, maxLyrics) } } // Plain text must parse to nil: that is the signal to scroll continuously rather than highlight // lines on timings nobody measured. func TestParseLRC(t *testing.T) { if got := parseLRC("Ihan tavallista tekstiä\ntoinen rivi"); got != nil { t.Fatalf("plain text parsed as synced: %v", got) } lines := parseLRC("[00:11.74] Ensimmäinen\n[01:02] Toinen\n[00:05.5] Aikaisempi\nrivi ilman aikaa") if len(lines) != 3 { t.Fatalf("got %d lines, want 3 — untimed lines are dropped", len(lines)) } // Sorted by time, whatever order the file had. if lines[0].At != 5.5 || lines[0].Text != "Aikaisempi" { t.Fatalf("first line is %+v, want 5.5s Aikaisempi", lines[0]) } if lines[1].At != 11.74 || lines[2].At != 62 { t.Fatalf("timestamps parsed as %v and %v, want 11.74 and 62", lines[1].At, lines[2].At) } // A refrain can carry several timestamps on one line, and each is its own occurrence. rep := parseLRC("[00:10.00][01:10.00] Kertosäe") if len(rep) != 2 || rep[0].At != 10 || rep[1].At != 70 { t.Fatalf("repeated stamps gave %+v, want two occurrences", rep) } } // The lookup is a suggestion, so "nothing found" is a normal answer rather than an error, and a // synced hit always beats a plain one. func TestFetchLyrics(t *testing.T) { var lastPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lastPath = r.URL.Path w.Header().Set("Content-Type", "application/json") switch { case r.URL.Path == "/get" && r.URL.Query().Get("track_name") == "Paranoid": w.Write([]byte(`{"trackName":"Paranoid","artistName":"Black Sabbath","duration":168, "plainLyrics":"plain version","syncedLyrics":"[00:11.74] synced version"}`)) case r.URL.Path == "/get": http.Error(w, `{"code":404}`, http.StatusNotFound) case r.URL.Path == "/search" && strings.Contains(r.URL.Query().Get("q"), "Soittorasia"): // An instrumental and a wrong-length take come first: both must be skipped. w.Write([]byte(`[{"trackName":"Soittorasia","duration":200,"instrumental":true, "plainLyrics":"","syncedLyrics":"[00:01.00] should be skipped"}, {"trackName":"Soittorasia","duration":600, "plainLyrics":"wrong length take"}, {"trackName":"Soittorasia","duration":201, "plainLyrics":"right one"}]`)) default: w.Write([]byte(`[]`)) } })) defer srv.Close() old := lrclibBase lrclibBase = srv.URL defer func() { lrclibBase = old }() ctx := context.Background() got, err := fetchLyrics(ctx, "Paranoid", "Black Sabbath", 168) if err != nil { t.Fatal(err) } if got != "[00:11.74] synced version" { t.Fatalf("exact match returned %q, want the synced version", got) } got, err = fetchLyrics(ctx, "Soittorasia", "Joku", 200) if err != nil { t.Fatal(err) } if got != "right one" { t.Fatalf("search fallback returned %q — instrumental and wrong-length takes must be skipped", got) } if lastPath != "/search" { t.Fatalf("last request was %s, want the search fallback", lastPath) } // Nothing found is not an error: the submitter simply types their own. got, err = fetchLyrics(ctx, "Ei olemassa", "Kukaan", 100) if err != nil || got != "" { t.Fatalf("miss returned %q, %v — want empty and no error", got, err) } // No title means nothing to match on, and no request at all. if got, err := fetchLyrics(ctx, "", "Artisti", 100); err != nil || got != "" { t.Fatalf("empty title returned %q, %v", got, err) } }