Files
go-telegram/dispatch/groups_test.go
T
lukaszraczylo 9072e9eafb Initial release of go-telegram
A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
2026-05-09 13:09:27 +01:00

210 lines
5.5 KiB
Go

package dispatch
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
"github.com/stretchr/testify/require"
)
// msgUpdate builds a simple private message update.
func msgUpdate(id int64, text string) api.Update {
return api.Update{
UpdateID: id,
Message: &api.Message{
MessageID: id,
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
Text: text,
},
}
}
// cmdUpdate builds a command message update.
func cmdUpdate(id int64, cmd string) api.Update {
return api.Update{
UpdateID: id,
Message: &api.Message{
MessageID: id,
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
Text: cmd,
Entities: []api.MessageEntity{
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len(cmd))},
},
},
}
}
// runSingle fires one update through the router and waits for it to complete.
func runSingle(t *testing.T, r *Router, up api.Update) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_ = r.Run(ctx, newFake(up))
}
// TestGroup_Order verifies group 0 fires before group 1.
func TestGroup_Order(t *testing.T) {
r := New(client.New("t"))
var order []int
r.Group(0).OnText(`.*`, func(c *Context, m *api.Message) error {
order = append(order, 0)
return ErrContinueGroups // let group 1 also run
})
r.Group(1).OnText(`.*`, func(c *Context, m *api.Message) error {
order = append(order, 1)
return nil
})
runSingle(t, r, msgUpdate(1, "hello"))
require.Equal(t, []int{0, 1}, order)
}
// TestGroup_FirstMatchWins verifies group 0 match stops group 1 by default.
func TestGroup_FirstMatchWins(t *testing.T) {
r := New(client.New("t"))
var fired []int
r.Group(0).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 0)
return nil // matched — group 1 must NOT run
})
r.Group(1).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 1)
return nil
})
runSingle(t, r, msgUpdate(1, "hello"))
require.Equal(t, []int{0}, fired)
}
// TestGroup_ErrContinueGroups lets group 1 run when group 0 returns ErrContinueGroups.
func TestGroup_ErrContinueGroups(t *testing.T) {
r := New(client.New("t"))
g1Hit := make(chan struct{}, 1)
r.Group(0).OnText(`.*`, func(c *Context, m *api.Message) error {
return ErrContinueGroups
})
r.Group(1).OnText(`.*`, func(c *Context, m *api.Message) error {
g1Hit <- struct{}{}
return nil
})
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
go func() { _ = r.Run(ctx, newFake(msgUpdate(1, "ping"))) }()
select {
case <-g1Hit:
case <-ctx.Done():
t.Fatal("group 1 handler never fired")
}
}
// TestGroup_ErrEndGroups stops all further groups.
func TestGroup_ErrEndGroups(t *testing.T) {
r := New(client.New("t"))
var fired []int
r.Group(0).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 0)
return ErrEndGroups
})
r.Group(1).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 1)
return nil
})
runSingle(t, r, msgUpdate(1, "hello"))
require.Equal(t, []int{0}, fired)
}
// TestGroup_NonSentinelError propagates error and stops further groups.
func TestGroup_NonSentinelError(t *testing.T) {
r := New(client.New("t"), WithMaxConcurrency(0))
var fired []int
r.Group(0).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 0)
return context.DeadlineExceeded // non-sentinel real error
})
r.Group(1).OnText(`.*`, func(c *Context, m *api.Message) error {
fired = append(fired, 1)
return nil
})
runSingle(t, r, msgUpdate(1, "hello"))
// group 1 must not fire
require.Equal(t, []int{0}, fired)
}
// TestGroup_Command verifies OnCommand in a group works.
func TestGroup_Command(t *testing.T) {
r := New(client.New("t"))
hit := make(chan string, 1)
r.Group(0).OnCommand("/start", func(c *Context, m *api.Message) error {
hit <- "g0-start"
return nil
})
r.Group(1).OnCommand("/start", func(c *Context, m *api.Message) error {
hit <- "g1-start"
return nil
})
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
go func() { _ = r.Run(ctx, newFake(cmdUpdate(1, "/start"))) }()
got := <-hit
require.Equal(t, "g0-start", got)
}
// TestGroup_MessageFilter verifies OnMessageFilter in a group works.
func TestGroup_MessageFilter(t *testing.T) {
r := New(client.New("t"))
hit := make(chan bool, 1)
r.Group(0).OnMessageFilter(
Filter[*api.Message](func(m *api.Message) bool { return m != nil && m.Text == "ok" }),
func(c *Context, m *api.Message) error {
hit <- true
return nil
},
)
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
go func() { _ = r.Run(ctx, newFake(msgUpdate(1, "ok"))) }()
require.True(t, <-hit)
}
// TestGroup_ErrContinueGroups_WithCommand verifies ErrContinueGroups works for commands across groups.
func TestGroup_ErrContinueGroups_WithCommand(t *testing.T) {
r := New(client.New("t"))
var count atomic.Int32
r.Group(0).OnCommand("/ping", func(c *Context, m *api.Message) error {
count.Add(1)
return ErrContinueGroups
})
r.Group(1).OnCommand("/ping", func(c *Context, m *api.Message) error {
count.Add(10)
return nil
})
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
go func() { _ = r.Run(ctx, newFake(cmdUpdate(1, "/ping"))) }()
time.Sleep(100 * time.Millisecond)
cancel()
require.Equal(t, int32(11), count.Load())
}