Files
miti99bot/internal/modules/alias/fallback_test.go
T
tiennm99 733cd48d64 feat(alias): keep text formatting, name kinds in /aliases, copyable commands
Text and caption formatting now survives an alias. Bold, italic, code, links
and mentions are stored as entities beside the text and sent back with it, on
the /insert, bare-command and inline paths alike. This works because the text
is re-sent byte-identical, so the offsets the entities carry stay valid — the
earlier comment claiming otherwise was wrong. They go back as entities rather
than re-rendered markup, which avoids escaping and re-parsing content the user
never wrote as markup.

/aliases now lists one line per name with what it holds, so the list says what
each will send:

    3 aliases:
    /cheer — sticker
    /clip — video
    /greeting — text

That costs one store read per listed alias, since DocStore has no bulk get and
the kind lives in the document. The reads stop once the message is full, so the
cost is bounded by what fits in one reply rather than by how many aliases exist.

Every reply that names a command or an alias wraps it in <code>, so tapping it
copies something ready to send. The generic usage lines stay plain text: they
contain a literal <name> placeholder that HTML mode would swallow as a tag.

Replying to another bot's message gets its own refusal. Telegram delivers that
reply with the content stripped, so capture finds nothing and the format advice
read as if the wrong kind had been sent. Checked only after capture fails, so
this bot's own messages — which are readable — never reach it.
2026-09-04 13:28:50 +07:00

171 lines
5.8 KiB
Go

package alias_test
import (
"context"
"testing"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/tiennm99/miti99bot/internal/modules"
"github.com/tiennm99/miti99bot/internal/modules/alias"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/testutil"
)
// The headline of path B: a saved name becomes its own command.
func TestFallback_SavedNameWorksAsItsOwnCommand(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("cheer", &models.Message{Sticker: &models.Sticker{FileID: "sticker-id"}}))
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/cheer"))
call, ok := callTo(rb, "sendSticker")
if !ok {
t.Fatalf("no sendSticker call; /cheer did not resolve: %+v", rb.Sent())
}
if got := call.Form["sticker"]; got != "sticker-id" {
t.Errorf("sticker = %q, want the saved file_id", got)
}
}
// Groups send /cmd@botname, and the fallback must normalise that the same way
// the command matcher does.
func TestFallback_ToleratesAtBotnameSuffix(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("cheer", &models.Message{Text: "yay"}))
rb.Reset()
// The fixture builder stops the entity at '@', but real Telegram includes
// the whole "/cheer@miti99bot" in it — which is the case the stripping
// exists for, so the entity is widened here to match the wire format.
upd := testutil.NewPrivateMessage(7, "/cheer@miti99bot")
upd.Message.Entities[0].Length = len(upd.Message.Text)
rb.Bot.ProcessUpdate(context.Background(), upd)
if got := rb.LastSent().Text(); got != "yay" {
t.Errorf("reply = %q, want the alias to resolve despite the @suffix", got)
}
}
// Mobile keyboards autocapitalise; /Cheer must reach the same alias.
func TestFallback_IsCaseInsensitive(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("cheer", &models.Message{Text: "yay"}))
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/CHEER"))
if got := rb.LastSent().Text(); got != "yay" {
t.Errorf("reply = %q, want case-folded resolution", got)
}
}
// Silence on a miss is deliberate: the fallback sees every unrecognised command
// in every chat, so replying would turn typos into noise and would confirm
// which names are taken.
func TestFallback_UnknownNameIsSilent(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/pign"))
if calls := rb.Sent(); len(calls) != 0 {
t.Errorf("unknown command produced output: %+v", calls)
}
}
// The rule the user asked for: behaviour in code wins over an alias, always.
// Registered commands are installed before the fallback, and the bot library
// returns the first matching handler.
func TestFallback_NeverShadowsARegisteredCommand(t *testing.T) {
var realRan bool
realModule := func(_ modules.Deps) modules.Module {
return modules.Module{Commands: []modules.Command{{
Name: "cheer",
Visibility: modules.VisibilityPublic,
Description: "the real thing",
Handler: func(_ context.Context, _ *bot.Bot, _ *models.Update) error {
realRan = true
return nil
},
}}}
}
rb := testutil.NewRecordingBot(t)
reg, err := modules.Build([]string{"alias", "real"},
map[string]modules.Factory{"alias": alias.New, "real": realModule},
storage.NewMemoryProvider(), modules.BuildOptions{})
if err != nil {
t.Fatalf("Build: %v", err)
}
modules.Install(rb.Bot, reg, modules.Auth{})
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/cheer"))
if !realRan {
t.Error("the registered /cheer handler did not run")
}
}
// Refusing at assignment time is the companion to dispatch order: an alias
// named after a real command would only ever be reachable via /insert.
func TestAlias_RefusesNameOfARegisteredCommand(t *testing.T) {
rb := installAlias(t)
// /aliases is one of this module's own commands.
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("aliases", &models.Message{Text: "hijack"}))
rb.AssertSentText(t, "already a command of mine")
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/insert aliases"))
rb.AssertSentText(t, "Nothing is saved")
}
func TestUnalias_DeletesAndThenNameIsFree(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("temp", &models.Message{Text: "content"}))
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/unalias temp"))
rb.AssertSentText(t, "Deleted <code>temp</code>")
// Gone from /insert, from the list, and from the bare-command path.
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/insert temp"))
rb.AssertSentText(t, "Nothing is saved")
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/aliases"))
rb.AssertSentText(t, "No aliases saved yet")
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/temp"))
if calls := rb.Sent(); len(calls) != 0 {
t.Errorf("deleted alias still answered as a command: %+v", calls)
}
}
func TestUnalias_UnknownNameIsReported(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/unalias ghost"))
rb.AssertSentText(t, "Nothing is saved")
}
// Deleting is open to anyone, matching the overwrite rule — the namespace is
// shared, so the permission model is too.
func TestUnalias_AnyoneMayDelete(t *testing.T) {
rb := installAlias(t)
rb.Bot.ProcessUpdate(context.Background(),
aliasCmd("shared", &models.Message{Text: "content"}))
rb.Reset()
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(99, "/unalias shared"))
rb.AssertSentText(t, "Deleted")
}