package bot import ( "errors" "fmt" "reflect" "strconv" "strings" "testing" "time" "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/arikawa/v3/state" "github.com/diamondburned/arikawa/v3/state/store" "github.com/diamondburned/arikawa/v3/utils/handler" ) type testc struct { Ctx *Context Return chan interface{} Counter uint64 Typed int8 } func (t *testc) Setup(sub *Subcommand) { sub.AddMiddleware([]string{"*", "GetCounter"}, func(v interface{}) { t.Counter++ }) sub.AddMiddleware("*", func(*gateway.MessageCreateEvent) { t.Counter++ }) // stub middleware for testing sub.AddMiddleware(t.OnTyping, func(*gateway.TypingStartEvent) { t.Typed = 2 }) sub.Hide(t.Hidden) } func (t *testc) Hidden(*gateway.MessageCreateEvent) {} func (t *testc) Noop(*gateway.MessageCreateEvent) {} func (t *testc) GetCounter(*gateway.MessageCreateEvent) { t.Return <- strconv.FormatUint(t.Counter, 10) } func (t *testc) Send(_ *gateway.MessageCreateEvent, args ...string) error { t.Return <- args return errors.New("oh no") } func (t *testc) Custom(_ *gateway.MessageCreateEvent, c *ArgumentParts) { t.Return <- []string(*c) } func (t *testc) Variadic(_ *gateway.MessageCreateEvent, c ...*customParsed) { t.Return <- c[len(c)-1] } func (t *testc) TrailCustom(_ *gateway.MessageCreateEvent, _ string, c ArgumentParts) { t.Return <- c } func (t *testc) Content(_ *gateway.MessageCreateEvent, c RawArguments) { t.Return <- c } func (t *testc) NoArgs(*gateway.MessageCreateEvent) error { return errors.New("passed") } func (t *testc) OnTyping(*gateway.TypingStartEvent) { t.Typed-- } func TestNewContext(t *testing.T) { var s = &state.State{ Cabinet: store.NoopCabinet, } c, err := New(s, &testc{}) if err != nil { t.Fatal("Failed to create new context:", err) } if !reflect.DeepEqual(c.Subcommands(), c.subcommands) { t.Fatal("Subcommands mismatch.") } } func TestContext(t *testing.T) { var given = &testc{} var s = &state.State{ Cabinet: store.NoopCabinet, Handler: handler.New(), } sub, err := NewSubcommand(given) if err != nil { t.Fatal("Failed to create subcommand:", err) } var ctx = &Context{ Name: "arikawa/bot test", Description: "Just a test.", Subcommand: sub, State: s, ParseArgs: DefaultArgsParser, } t.Run("init commands", func(t *testing.T) { if err := ctx.Subcommand.InitCommands(ctx); err != nil { t.Fatal("Failed to init commands:", err) } if given.Ctx != ctx { t.Fatal("given Context field has invalid pointer") } }) t.Run("find commands", func(t *testing.T) { cmd := ctx.FindCommand("", "NoArgs") if cmd == nil { t.Fatal("Failed to find NoArgs") } }) t.Run("middleware", func(t *testing.T) { ctx.HasPrefix = NewPrefix("pls do ") // This should trigger the middleware first. if err := expect(ctx, given, "3", "pls do getCounter"); err != nil { t.Fatal("Unexpected error:", err) } }) t.Run("derive intents", func(t *testing.T) { intents := ctx.DeriveIntents() assertIntents := func(target gateway.Intents, name string) { if !intents.Has(target) { t.Error("Derived intents do not have", name) } } assertIntents(gateway.IntentGuildMessages, "guild messages") assertIntents(gateway.IntentDirectMessages, "direct messages") assertIntents(gateway.IntentGuildMessageTyping, "guild typing") assertIntents(gateway.IntentDirectMessageTyping, "direct message typing") }) t.Run("typing event", func(t *testing.T) { typing := &gateway.TypingStartEvent{} if err := ctx.callCmd(typing); err != nil { t.Fatal("Failed to call with TypingStart:", err) } // -1 none ran if given.Typed != 1 { t.Fatal("Typed bool is false") } }) t.Run("call command", func(t *testing.T) { // Set a custom prefix ctx.HasPrefix = NewPrefix("~") var ( send = "hacka doll no. 3" expects = []string{"hacka", "doll", "no.", "3"} ) if err := expect(ctx, given, expects, "~send "+send); err.Error() != "oh no" { t.Fatal("Unexpected error:", err) } }) t.Run("call command rawarguments", func(t *testing.T) { ctx.HasPrefix = NewPrefix("!") expects := RawArguments("just things") if err := expect(ctx, given, expects, "!content just things"); err != nil { t.Fatal("Unexpected call error:", err) } }) t.Run("call command custom manual parser", func(t *testing.T) { ctx.HasPrefix = NewPrefix("!") expects := []string{"arg1", ":)"} if err := expect(ctx, given, expects, "!custom arg1 :)"); err != nil { t.Fatal("Unexpected call error:", err) } }) t.Run("call command custom variadic parser", func(t *testing.T) { ctx.HasPrefix = NewPrefix("!") expects := &customParsed{true} if err := expect(ctx, given, expects, "!variadic bruh moment"); err != nil { t.Fatal("Unexpected call error:", err) } }) t.Run("call command custom trailing manual parser", func(t *testing.T) { ctx.HasPrefix = NewPrefix("!") expects := ArgumentParts{"arikawa"} if err := sendMsg(ctx, given, &expects, "!trailCustom hime arikawa"); err != nil { t.Fatal("Unexpected call error:", err) } if expects.Length() != 1 { t.Fatal("Unexpected ArgumentParts length.") } if expects.After(1)+expects.After(2)+expects.After(-1) != "" { t.Fatal("Unexpected ArgumentsParts after.") } if expects.String() != "arikawa" { t.Fatal("Unexpected ArgumentsParts string.") } if expects.Arg(0) != "arikawa" { t.Fatal("Unexpected ArgumentParts arg 0") } if expects.Arg(1) != "" { t.Fatal("Unexpected ArgumentParts arg 1") } }) testMessage := func(content string) error { // Mock a messageCreate event m := &gateway.MessageCreateEvent{ Message: discord.Message{ Content: content, }, } return ctx.callCmd(m) } t.Run("call command without args", func(t *testing.T) { ctx.HasPrefix = NewPrefix("") if err := testMessage("noArgs"); err.Error() != "passed" { t.Fatal("unexpected error:", err) } }) // Test error cases t.Run("call unknown command", func(t *testing.T) { ctx.HasPrefix = NewPrefix("joe pls ") err := testMessage("joe pls no") if err == nil || !strings.HasPrefix(err.Error(), "unknown command:") { t.Fatal("unexpected error:", err) } }) // Test subcommands t.Run("register subcommand", func(t *testing.T) { ctx.HasPrefix = NewPrefix("run ") sub := &testc{} ctx.MustRegisterSubcommand(sub) if err := testMessage("run testc noop"); err != nil { t.Fatal("Unexpected error:", err) } expects := RawArguments("hackadoll no. 3") if err := expect(ctx, sub, expects, "run testc content hackadoll no. 3"); err != nil { t.Fatal("Unexpected call error:", err) } if cmd := ctx.FindCommand("testc", "Noop"); cmd == nil { t.Fatal("Failed to find subcommand Noop") } }) t.Run("register subcommand custom", func(t *testing.T) { ctx.MustRegisterSubcommand(&testc{}, "arikawa", "a") }) t.Run("duplicate subcommand", func(t *testing.T) { _, err := ctx.RegisterSubcommand(&testc{}, "arikawa") if err := err.Error(); !strings.Contains(err, "duplicate") { t.Fatal("Unexpected error:", err) } _, err = ctx.RegisterSubcommand(&testc{}, "a") if err := err.Error(); !strings.Contains(err, "duplicate") { t.Fatal("Unexpected error:", err) } }) t.Run("help", func(t *testing.T) { ctx.MustRegisterSubcommand(&testc{}, "helper") h := ctx.Help() if h == "" { t.Fatal("Empty help?") } if strings.Contains(h, "hidden") { t.Fatal("Hidden command shown in help.") } if !strings.Contains(h, "arikawa/bot test") { t.Fatal("Name not found.") } if !strings.Contains(h, "Just a test.") { t.Fatal("Description not found.") } if !strings.Contains(h, "**a**") { t.Fatal("arikawa alias `a' not found.") } }) t.Run("start", func(t *testing.T) { cancel := ctx.Start() defer cancel() ctx.HasPrefix = NewPrefix("!") given.Return = make(chan interface{}) ctx.Handler.Call(&gateway.MessageCreateEvent{ Message: discord.Message{ Content: "!content hime arikawa best trap", }, }) if c := (<-given.Return).(RawArguments); c != "hime arikawa best trap" { t.Fatal("Unexpected content:", c) } }) } func expect(ctx *Context, given *testc, expects interface{}, content string) (call error) { var v interface{} if call = sendMsg(ctx, given, &v, content); call != nil { return } if !reflect.DeepEqual(v, expects) { return fmt.Errorf("returned argument is invalid: %v", v) } return nil } func sendMsg(ctx *Context, given *testc, into interface{}, content string) (call error) { // Return channel for testing ret := make(chan interface{}) given.Return = ret // Mock a messageCreate event m := &gateway.MessageCreateEvent{ Message: discord.Message{ Content: content, }, } var callCh = make(chan error) go func() { callCh <- ctx.Call(m) }() select { case arg := <-ret: call = <-callCh reflect.ValueOf(into).Elem().Set(reflect.ValueOf(arg)) return case call = <-callCh: return fmt.Errorf("expected return before error: %w", call) case <-time.After(time.Second): return errors.New("timed out while waiting") } } func BenchmarkConstructor(b *testing.B) { var s = &state.State{ Cabinet: store.NoopCabinet, } for i := 0; i < b.N; i++ { _, _ = New(s, &testc{}) } } func BenchmarkCall(b *testing.B) { var given = &testc{} var s = &state.State{ Cabinet: store.NoopCabinet, } sub, _ := NewSubcommand(given) var ctx = &Context{ Subcommand: sub, State: s, HasPrefix: NewPrefix("~"), ParseArgs: DefaultArgsParser, } m := &gateway.MessageCreateEvent{ Message: discord.Message{ Content: "~noop", }, } b.ResetTimer() for i := 0; i < b.N; i++ { ctx.callCmd(m) } } func BenchmarkHelp(b *testing.B) { var given = &testc{} var s = &state.State{ Cabinet: store.NoopCabinet, } sub, _ := NewSubcommand(given) var ctx = &Context{ Subcommand: sub, State: s, HasPrefix: NewPrefix("~"), ParseArgs: DefaultArgsParser, } b.ResetTimer() for i := 0; i < b.N; i++ { _ = ctx.Help() } }