package web import ( "context" "errors" "net/http" "net/http/httptest" "net/url" "strings" "testing" "music-server/internal/backend" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var soundtracks = []string{ "Call of Duty: Modern Warfare", "Final Fantasy VII", "Joe & Mac", "Kingdom Hearts", "Lemmings (SNES + Mega Drive)", "MG", "Metal Gear Solid 4 - Guns of the Patriots", "Pokémon HeartGold & SoulSilver", "Resident Evil 2 (Remake)", "Ys III - Wanderers from Ys (+The Oath in Felghana)", } func newCache(names []string, alternatives ...map[string][]string) *backend.SearchCache { soundtracks := make([]backend.SearchSoundtrack, len(names)) for i, name := range names { soundtracks[i] = backend.SearchSoundtrack{Name: name} for _, a := range alternatives { soundtracks[i].AlternativeNames = a[name] } } return backend.NewSearchCache(func(context.Context) ([]backend.SearchSoundtrack, error) { return soundtracks, nil }) } // names returns the soundtrack names of results. func names(results []SearchResult) []string { var n []string for _, r := range results { n = append(n, r.Name) } return n } func searchData(t *testing.T) []backend.SoundtrackData { data, err := newCache(soundtracks).Get(context.Background()) require.NoError(t, err) return data } func TestSearch(t *testing.T) { data := searchData(t) tests := []struct { query string want []string }{ // Exact (case-insensitive) substring {"resident", []string{"Resident Evil 2 (Remake)"}}, // Ignoring spaces and special characters {"joeandmac", []string{"Joe & Mac"}}, {"snes+mega", []string{"Lemmings (SNES + Mega Drive)"}}, {"pokemon", []string{"Pokémon HeartGold & SoulSilver"}}, // Letters in order {"codmw", []string{"Call of Duty: Modern Warfare"}}, {"mgs4gotp", []string{"Metal Gear Solid 4 - Guns of the Patriots"}}, {"re2r", []string{"Resident Evil 2 (Remake)"}}, // No match {"zzz", nil}, // Regex special characters are plain text {"(remake)", []string{"Resident Evil 2 (Remake)"}}, {"[", nil}, } for _, tt := range tests { t.Run(tt.query, func(t *testing.T) { assert.Equal(t, tt.want, names(search(tt.query, data))) }) } } func TestSearchEmptyReturnsEverything(t *testing.T) { assert.Equal(t, soundtracks, names(search("", searchData(t)))) } func TestSearchOrdering(t *testing.T) { // "MG" is an exact match, "Metal Gear Solid..." only matches by letters in order. results := names(search("mg", searchData(t))) require.GreaterOrEqual(t, len(results), 2) assert.Equal(t, "MG", results[0]) assert.Contains(t, results, "Metal Gear Solid 4 - Guns of the Patriots") } func TestSearchAlternativeNames(t *testing.T) { data, err := newCache(soundtracks, map[string][]string{ "Final Fantasy VII": {"FF7", "ファイナルファンタジーVII"}, "Kingdom Hearts": {"KH"}, }).Get(context.Background()) require.NoError(t, err) assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII", MatchedName: "FF7"}}, search("ff7", data)) assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII", MatchedName: "ファイナルファンタジーVII"}}, search("ファイナル", data)) assert.Equal(t, []SearchResult{{Name: "Final Fantasy VII"}}, search("fantasy", data), "no matched name when the soundtrack's own name matches") // An exact match on an alternative name comes before a letters-in-order match on a name. results := search("kh", data) require.NotEmpty(t, results) assert.Equal(t, SearchResult{Name: "Kingdom Hearts", MatchedName: "KH"}, results[0]) } func TestFoundSoundtracksShowsMatchedName(t *testing.T) { var out strings.Builder require.NoError(t, FoundSoundtracks([]SearchResult{ {Name: "Final Fantasy VII", MatchedName: "FF7"}, {Name: "Kingdom Hearts"}, }).Render(context.Background(), &out)) assert.Contains(t, out.String(), "Final Fantasy VII") assert.Contains(t, out.String(), "Also known as FF7") assert.Equal(t, 1, strings.Count(out.String(), "Also known as")) } func TestSearchHandler(t *testing.T) { handler := NewSearchHandler(newCache(soundtracks)) req := httptest.NewRequest(http.MethodPost, "/find", strings.NewReader(url.Values{"search_term": {"kingdom"}}.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Contains(t, rec.Body.String(), "Kingdom Hearts") assert.NotContains(t, rec.Body.String(), "Final Fantasy") } func TestSearchHandlerLoadError(t *testing.T) { cache := backend.NewSearchCache(func(context.Context) ([]backend.SearchSoundtrack, error) { return nil, errors.New("db down") }) rec := httptest.NewRecorder() NewSearchHandler(cache).ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/find", nil)) assert.Equal(t, http.StatusInternalServerError, rec.Code) }