package loldle import ( "context" "encoding/json" "fmt" "math" "net/http" "net/http/httptest" "net/url" "regexp" "strings" "testing" "time" "github.com/tiennm99/tiennm99bot/internal/modules/loldle/web" "github.com/tiennm99/tiennm99bot/internal/modules/util/guessgame" ) func urlUnescape(t *testing.T, s string) string { t.Helper() u, err := url.QueryUnescape(s) if err != nil { t.Fatal(err) } return u } func TestRules_MarksArrowsAndRefusals(t *testing.T) { r := rules{champions: loadChampions()} for guess, want := range map[string]string{"Akali": "cwwwccu", "wukong": "wcwccwc", "kaisa": "cwccwwd", "Ahri": "ccccccc"} { g, err := r.Judge(guess, "Ahri", nil) if err != nil || g.Marks != want { t.Errorf("%s vs Ahri = %+v, %v; want %s", guess, g, err, want) } } if g, _ := r.Judge("jinx", "Kai'Sa", nil); g.Word != "Jinx" || g.Marks != "cpccwcu" { t.Errorf("Jinx vs Kai'Sa = %+v", g) } for input, want := range map[string]error{"zzz": errUnknownChampion, "ka": errAmbiguous, "": errUnknownChampion} { if _, err := r.Judge(input, "Ahri", nil); err != want { t.Errorf("Judge(%q) = %v, want %v", input, err, want) } } if _, err := r.Judge("akali", "Ahri", []guessgame.Guess{{Word: "Akali"}}); err != errDuplicate { t.Errorf("duplicate = %v", err) } if _, err := r.Judge("akali", "Gone", nil); err != errTargetGone { t.Errorf("gone = %v", err) } if got := guessgame.EmojiRow(r, "cpwudcw"); got != "๐ŸŸฉ๐ŸŸจ๐ŸŸฅ๐ŸŸฅ๐ŸŸฅ๐ŸŸฉ๐ŸŸฅ" { t.Errorf("emoji = %q", got) } } func TestChampions_EveryRowHasAnIconID(t *testing.T) { seen := map[string]bool{} idRe := regexp.MustCompile(`^[A-Za-z]+$`) for _, c := range loadChampions() { if !idRe.MatchString(c.ID) || seen[c.ID] || c.Title == "" { t.Errorf("%s: id %q title %q", c.ChampionName, c.ID, c.Title) } seen[c.ID] = true } } // Data Dragon tile file names are case-sensitive and do not always match the // champion key: Fiddlesticks' tile is FiddleSticks_0.jpg. func TestChampions_IconIDsNameTheTileFile(t *testing.T) { ids := map[string]string{} for _, c := range loadChampions() { ids[c.ChampionName] = c.ID } for name, want := range map[string]string{"Fiddlesticks": "FiddleSticks", "Wukong": "MonkeyKing", "Ahri": "Ahri"} { if ids[name] != want { t.Errorf("%s icon id = %q, want %q", name, ids[name], want) } } } func TestAPI_ChampionListCarriesNamesAndIDsOnly(t *testing.T) { h := newHarness(t) rec := httptest.NewRecorder() h.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, routePrefix+"champions.json", nil)) if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), "application/json") { t.Fatalf("champions.json: %d %v", rec.Code, rec.Header()) } var list []map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil || len(list) != len(loadChampions()) { t.Fatalf("list = %d, %v", len(list), err) } for _, c := range list { if len(c) != 2 || c["name"] == "" || c["id"] == "" { t.Fatalf("entry %v", c) } } if list[0]["name"] != "Aatrox" || !strings.Contains(rec.Body.String(), `{"name":"Wukong","id":"MonkeyKing"}`) { t.Fatalf("list starts %v", list[0]) } } func TestAPI_HeadersAllowDataDragonImagesOnly(t *testing.T) { h := newHarness(t) for path, ctype := range map[string]string{"": "text/html", "app.js": "text/javascript", "app.css": "text/css"} { rec := httptest.NewRecorder() h.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, routePrefix+path, nil)) csp := rec.Header().Get("Content-Security-Policy") if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), ctype) || !strings.Contains(csp, "img-src 'self' data: https://ddragon.leagueoflegends.com;") || !strings.Contains(csp, "connect-src 'self'") { t.Errorf("GET %q: %d %v", path, rec.Code, rec.Header()) } } if r := h.state("garbage"); r.code != http.StatusUnauthorized || r.err != "bad_token" { t.Fatalf("bad token = %d %s", r.code, r.body) } } var ( inlineScriptRe = regexp.MustCompile(`(?is)]*)>(.*?)`) urlRe = regexp.MustCompile(`https?://[^'"\s)]+`) inlineHandlerRe = regexp.MustCompile(`\son[a-z]+=`) ) // The page runs under the strict CSP: no inline script, style or handler, // and it reaches no host but its own and Data Dragon's icons. func TestWebAssets_FitTheCSP(t *testing.T) { index, _ := web.FS.ReadFile("index.html") for _, m := range inlineScriptRe.FindAllStringSubmatch(string(index), -1) { if strings.TrimSpace(m[2]) != "" || !strings.Contains(m[1], "src=") { t.Errorf("inline script breaks the CSP: %q", m[0]) } } if strings.Contains(string(index), " style=") || strings.Contains(string(index), "