package help import ( "io" "net/http" "strings" "testing" "unicode/utf8" "github.com/bwmarrin/discordgo" ) // Discord refuses an embed that is over its limits, and then /help would send // nothing. This catches it when a command is added to the text. func TestHelpFitsDiscordsLimits(t *testing.T) { list := embeds(&discordgo.MessageEmbedAuthor{Name: "Velox Bot"}) if len(list) == 0 { t.Fatal("no help") } seen := 0 for n, e := range list { total := utf8.RuneCountInString(e.Title) + utf8.RuneCountInString(e.Description) if len(e.Fields) > 25 { t.Errorf("embed %d has %d fields, at most 25", n, len(e.Fields)) } for _, f := range e.Fields { seen++ if l := utf8.RuneCountInString(f.Value); l == 0 || l > maxFieldChars { t.Errorf("field %q has %d characters, must be 1 to %d", f.Name, l, maxFieldChars) } if l := utf8.RuneCountInString(f.Name); l == 0 || l > 256 { t.Errorf("field name %q has %d characters, at most 256", f.Name, l) } total += fieldSize(f) } if total > 6000 { t.Errorf("embed %d has %d characters, Discord allows 6000", n, total) } } if seen != len(fields()) { t.Errorf("%d fields shown of %d", seen, len(fields())) } } // fakeDiscord answers the calls /help makes and records what the member was // shown. dm decides whether the direct message works. type fakeDiscord struct { dm bool edited []byte callbacks int sent int } func (f *fakeDiscord) RoundTrip(r *http.Request) (*http.Response, error) { reply := func(code int, body string) (*http.Response, error) { return &http.Response{StatusCode: code, Body: io.NopCloser(strings.NewReader(body)), Header: http.Header{"Content-Type": {"application/json"}}, Request: r}, nil } switch { case strings.HasSuffix(r.URL.Path, "/callback"): f.callbacks++ return reply(204, "") case strings.HasSuffix(r.URL.Path, "/users/@me/channels"): if !f.dm { return reply(403, `{"message":"Cannot send messages to this user","code":50007}`) } return reply(200, `{"id":"900","type":1}`) case strings.Contains(r.URL.Path, "/channels/900/messages"): f.sent++ return reply(200, `{"id":"1"}`) case strings.Contains(r.URL.Path, "/messages/@original"): f.edited, _ = io.ReadAll(r.Body) return reply(200, `{"id":"2"}`) } return reply(404, `{"message":"unexpected `+r.URL.Path+`"}`) } func run(t *testing.T, f *fakeDiscord, guild string) { t.Helper() s, err := discordgo.New("Bot x") if err != nil { t.Fatal(err) } s.Client = &http.Client{Transport: f} s.MaxRestRetries = 0 i := &discordgo.InteractionCreate{Interaction: &discordgo.Interaction{ ID: "1", Token: "tok", AppID: "100", GuildID: guild, Member: &discordgo.Member{User: &discordgo.User{ID: "42"}}, }} HelpHandler(s, i) } func TestHelpGoesToDMs(t *testing.T) { f := &fakeDiscord{dm: true} run(t, f, "5") if f.sent == 0 || !strings.Contains(string(f.edited), "Check your DMs") || strings.Contains(string(f.edited), `"embeds"`) { t.Errorf("sent=%d edited=%s", f.sent, f.edited) } } func TestHelpFallsBackToTheChannelWhenDMsAreOff(t *testing.T) { f := &fakeDiscord{dm: false} run(t, f, "5") got := string(f.edited) if f.sent != 0 || !strings.Contains(got, "couldn't DM you") || !strings.Contains(got, `"embeds"`) || !strings.Contains(got, "Velox Bot Help") { t.Errorf("the help should be shown in the reply: sent=%d edited=%s", f.sent, got) } } func TestHelpInADMIsShownDirectly(t *testing.T) { f := &fakeDiscord{dm: true} run(t, f, "") if f.callbacks != 1 || f.sent != 0 || f.edited != nil { t.Errorf("one direct answer, no DM: %+v", f) } } // The help is meant to be wide and short: inline fields, so Discord puts // three side by side, and it should fit in one message. func TestHelpIsCompactAndInline(t *testing.T) { if n := len(embeds(&discordgo.MessageEmbedAuthor{Name: "Velox Bot"})); n != 1 { t.Errorf("the help should fit in one message, it takes %d", n) } inline := 0 for _, f := range fields() { if f.Inline { inline++ } } if inline < len(fields())-2 { t.Errorf("only %d of %d fields are inline", inline, len(fields())) } }