Files
velox-bot/internal/commands/limits_test.go
T
FernandoJVideiraandClaude Sonnet 5.5 d95ebf473e Security: tune the Join Gate, raid mode, anti-nuke and heat with commands
- /security settings, joingate check|account-age, words add|remove|list, raid, antinuke set|limit, heat, alert-dms
- Same rule as the rest of the configuration: owner or trusted, validated in the engine
- schema.sql was missing security_settings.alert_dm, so the bot couldn't read security settings on a database built from it. Fixed, with an ALTER for existing installs
- A test checks every command against Discord's limits

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 02:03:03 +01:00

107 lines
3.3 KiB
Go

package commands
import (
"regexp"
"testing"
"unicode/utf8"
"github.com/bwmarrin/discordgo"
)
// Discord refuses a command that is over its limits, and then the whole
// command silently doesn't exist. These checks catch that in a test instead
// of after a deploy. See
// https://discord.com/developers/docs/interactions/application-commands
var commandName = regexp.MustCompile(`^[-_\p{L}\p{N}]{1,32}$`)
func checkOptions(t *testing.T, path string, options []*discordgo.ApplicationCommandOption, size *int) {
t.Helper()
if len(options) > 25 {
t.Errorf("%s: %d options, at most 25 allowed", path, len(options))
}
seen := map[string]bool{}
sawOptional := false
for _, o := range options {
where := path + " " + o.Name
if !commandName.MatchString(o.Name) || o.Name != lower(o.Name) {
t.Errorf("%s: option names are 1 to 32 lowercase letters, digits, - or _", where)
}
if seen[o.Name] {
t.Errorf("%s: duplicate option name", where)
}
seen[o.Name] = true
if n := utf8.RuneCountInString(o.Description); n < 1 || n > 100 {
t.Errorf("%s: description is %d characters, must be 1 to 100", where, n)
}
*size += utf8.RuneCountInString(o.Name) + utf8.RuneCountInString(o.Description)
if len(o.Choices) > 25 {
t.Errorf("%s: %d choices, at most 25", where, len(o.Choices))
}
for _, c := range o.Choices {
*size += utf8.RuneCountInString(c.Name)
if s, ok := c.Value.(string); ok {
*size += utf8.RuneCountInString(s)
if utf8.RuneCountInString(s) > 100 || utf8.RuneCountInString(c.Name) > 100 {
t.Errorf("%s: a choice is over 100 characters", where)
}
}
}
switch o.Type {
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
if len(o.Options) == 0 && o.Type == discordgo.ApplicationCommandOptionSubCommandGroup {
t.Errorf("%s: a group needs subcommands", where)
}
checkOptions(t, where, o.Options, size)
default:
// Required options come before optional ones.
if o.Required && sawOptional {
t.Errorf("%s: a required option after an optional one", where)
}
if !o.Required {
sawOptional = true
}
}
}
}
func lower(s string) string {
out := []rune(s)
for i, r := range out {
if r >= 'A' && r <= 'Z' {
out[i] = r + 32
}
}
return string(out)
}
func TestCommandsStayWithinDiscordLimits(t *testing.T) {
all := append(append([]*discordgo.ApplicationCommand{}, AllCommands...), BetaCommands...)
names := map[string]bool{}
for _, c := range all {
if names[c.Name] {
t.Errorf("/%s is defined twice", c.Name)
}
names[c.Name] = true
if !commandName.MatchString(c.Name) || c.Name != lower(c.Name) {
t.Errorf("/%s: names are 1 to 32 lowercase letters, digits, - or _", c.Name)
}
if n := utf8.RuneCountInString(c.Description); n < 1 || n > 100 {
t.Errorf("/%s: description is %d characters, must be 1 to 100", c.Name, n)
}
size := utf8.RuneCountInString(c.Name) + utf8.RuneCountInString(c.Description)
checkOptions(t, "/"+c.Name, c.Options, &size)
if size > 8000 {
t.Errorf("/%s: %d characters in total, Discord allows 8000", c.Name, size)
}
if _, ok := handlers[c.Name]; !ok {
t.Errorf("/%s has no handler", c.Name)
}
}
for name := range handlers {
if !names[name] {
t.Errorf("a handler for /%s exists but the command is never registered", name)
}
}
}