Files
tiennm99bot/internal/modules/sticker/sticker_source_test.go
T
tiennm99 be91d2eb41 feat(alias): add a shared alias dictionary invocable as a bare command
/alias <name> saves a replied message under a name and /insert <name> sends it
back; /aliases lists every name and /unalias deletes one. Every Telegram format
is supported — sticker, photo, GIF, video, video note, audio, voice, document,
plain text — and each is kept as the file_id Telegram already issued, so nothing
is downloaded and an alias survives redeploys. The namespace is global and the
last assignment wins, matching the shared sticker pack; /unalias is open to
anyone for the same reason.

A saved name also works as its own command: /cheer rather than /insert cheer.
This needs two new seams in the module contract. Module.Fallback handles a
/command no module registered, and the dispatcher installs it after every
Command — the bot library returns the first matching handler, so code always
beats a name resolved at runtime, including an alias that shares a command
added in a later build. /alias refuses a name already in the registry for the
same reason, since such an alias would only reach /insert. An unknown command
stays silent: the fallback sees every unrecognised /foo in every chat, so
replying would make typos noisy and would confirm which names exist.

Module.Inline answers inline-mode queries — "@botname <prefix>" from any chat,
filtered by prefix and capped at Telegram's 50 results. Each result is a cached
inline type carrying the stored file_id, so the picker renders real previews
without an upload. Video notes are omitted because Telegram defines no
InlineQueryResultCachedVideoNote and substituting a plain video would change
what was saved. Inline mode must be enabled in BotFather before Telegram
delivers these updates.

Both slots are single-occupancy with conflict detection at Build. Auth.Permits
learns the inline sender so a gated inline handler would not deny everyone.
Build's command indexing and slot claiming move into addCommands/addSingletons,
keeping it under the project's cyclomatic cap.

Also restores the sticker module: /addsticker moves back out of util, which has
no store, into internal/modules/sticker as its only command.
2026-09-04 11:36:50 +07:00

128 lines
4.4 KiB
Go

package sticker
import (
"testing"
"github.com/go-telegram/bot/models"
)
// hasMovingSource picks which of the two conversion paths a reply takes, from
// the message alone. Getting it wrong sends a video at the image resampler, or
// an image at ffmpeg.
func TestHasMovingSource(t *testing.T) {
cases := []struct {
name string
replied *models.Message
want bool
}{
{name: "nil reply", replied: nil, want: false},
{name: "animation", replied: &models.Message{Animation: &models.Animation{FileID: "a"}}, want: true},
{name: "video", replied: &models.Message{Video: &models.Video{FileID: "v"}}, want: true},
{name: "video note", replied: &models.Message{VideoNote: &models.VideoNote{FileID: "n"}}, want: true},
{name: "gif document", replied: &models.Message{Document: &models.Document{MimeType: "image/gif"}}, want: true},
{name: "mp4 document", replied: &models.Message{Document: &models.Document{MimeType: "video/mp4"}}, want: true},
{name: "webm document", replied: &models.Message{Document: &models.Document{MimeType: "video/webm"}}, want: true},
{name: "mov document", replied: &models.Message{Document: &models.Document{MimeType: "video/quicktime"}}, want: true},
{name: "png document", replied: &models.Message{Document: &models.Document{MimeType: "image/png"}}, want: false},
{name: "photo", replied: &models.Message{Photo: []models.PhotoSize{{FileID: "p"}}}, want: false},
{name: "sticker", replied: &models.Message{Sticker: &models.Sticker{FileID: "s"}}, want: false},
{name: "pdf document", replied: &models.Message{Document: &models.Document{MimeType: "application/pdf"}}, want: false},
{name: "plain text", replied: &models.Message{Text: "hello"}, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := hasMovingSource(tc.replied); got != tc.want {
t.Errorf("hasMovingSource = %v, want %v", got, tc.want)
}
})
}
}
// Source selection happens before any download, so an oversized clip costs
// nothing.
func TestVideoFileID(t *testing.T) {
cases := []struct {
name string
replied *models.Message
want string
ok bool
}{
{
name: "animation",
replied: &models.Message{Animation: &models.Animation{FileID: "anim", FileSize: 1 << 20}},
want: "anim", ok: true,
},
{
name: "video",
replied: &models.Message{Video: &models.Video{FileID: "vid", FileSize: 1 << 20}},
want: "vid", ok: true,
},
{
name: "video note",
replied: &models.Message{VideoNote: &models.VideoNote{FileID: "note", FileSize: 1 << 20}},
want: "note", ok: true,
},
{
name: "document",
replied: &models.Message{Document: &models.Document{FileID: "doc", MimeType: "video/mp4"}},
want: "doc", ok: true,
},
{
// Animation is checked before Video: Telegram attaches both to a
// GIF-style message, and the animation is the one to convert.
name: "animation wins over video",
replied: &models.Message{
Animation: &models.Animation{FileID: "anim"},
Video: &models.Video{FileID: "vid"},
},
want: "anim", ok: true,
},
{
name: "oversized animation refused",
replied: &models.Message{Animation: &models.Animation{FileID: "anim", FileSize: maxVideoSourceBytes + 1}},
ok: false,
},
{
name: "oversized video refused",
replied: &models.Message{Video: &models.Video{FileID: "vid", FileSize: maxVideoSourceBytes + 1}},
ok: false,
},
{
name: "oversized document refused",
replied: &models.Message{Document: &models.Document{FileID: "doc", MimeType: "video/mp4", FileSize: maxVideoSourceBytes + 1}},
ok: false,
},
{
name: "nothing usable",
replied: &models.Message{Text: "hello"},
ok: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := videoFileID(tc.replied)
if tc.ok {
if err != nil {
t.Fatalf("videoFileID: %v", err)
}
if got != tc.want {
t.Errorf("file_id = %q, want %q", got, tc.want)
}
return
}
if err == nil {
t.Fatalf("videoFileID = %q, want a refusal", got)
}
})
}
}
// The video cap is the one on the source, and it is an order of magnitude above
// the image cap on purpose — the 256 KB sticker limit applies to the transcode
// output, not the input.
func TestVideoSourceCapExceedsImageCap(t *testing.T) {
if maxVideoSourceBytes <= maxSourceBytes {
t.Errorf("maxVideoSourceBytes (%d) must exceed maxSourceBytes (%d)", maxVideoSourceBytes, maxSourceBytes)
}
}