owpengram-server/internal/app/bots/mybots_botpic_test.go
Astra 5afafc60c4 /mybots: Edit Bot summary screen, return-to-menu, clickable @mentions
- Edit Bot now shows the current value of every field (Name/About/
  Description/Botpic/Commands) like BotFather, with real botpic status
  via a new PeerHasAvatar port method.
- After editing a field the dialog lands back on a fresh Edit Bot menu
  (working "Back to bot" / "Bots list" buttons) instead of ending, so a
  follow-up button press no longer reports the button as expired.
- Service-bot messages now render @username as a tappable mention entity.
2026-09-14 11:53:43 +01:00

128 lines
4.6 KiB
Go

package bots
import (
"context"
"strings"
"testing"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
type fakeBotAvatar struct {
ownerType domain.PeerType
ownerID int64
sourcePhotoID int64
calls int
err error
hasAvatar bool
}
func (f *fakeBotAvatar) PeerHasAvatar(_ context.Context, _ domain.PeerType, _ int64) (bool, error) {
return f.hasAvatar, nil
}
func (f *fakeBotAvatar) SetAvatarFromExistingPhoto(_ context.Context, ownerType domain.PeerType, ownerID, sourcePhotoID int64, _ int) (domain.Photo, error) {
f.calls++
f.ownerType, f.ownerID, f.sourcePhotoID = ownerType, ownerID, sourcePhotoID
if f.err != nil {
return domain.Photo{}, f.err
}
return domain.Photo{ID: 90210}, nil
}
func botFatherPhotoMsg(userID, photoID int64) domain.Message {
return domain.Message{
From: domain.Peer{Type: domain.PeerTypeUser, ID: userID},
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID},
Media: &domain.MessageMedia{
Kind: domain.MessageMediaKindPhoto,
Photo: &domain.Photo{ID: photoID},
},
}
}
func openBotpicPrompt(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User) {
t.Helper()
sendToBotFather(t, svc, messages, owner, "/mybots")
pressBotFather(t, svc, messages, owner.ID, "@pic_mb_bot")
pressBotFather(t, svc, messages, owner.ID, "Edit Bot")
_, prompt := pressBotFather(t, svc, messages, owner.ID, "Edit Botpic")
if !strings.Contains(prompt.Body, "profile picture") {
t.Fatalf("botpic prompt = %q", prompt.Body)
}
}
func TestMyBotsEditBotpicSetsPhoto(t *testing.T) {
users := memory.NewUserStore()
bots := memory.NewBotStore(users)
messages := memory.NewMessageStore(memory.NewDialogStore())
avatar := &fakeBotAvatar{}
svc := NewService(users, bots, messages, WithBotAvatarStore(avatar))
owner := newOwner(t, users, "+2100")
created, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot")
if err != nil {
t.Fatalf("create bot: %v", err)
}
openBotpicPrompt(t, svc, messages, owner)
// A non-photo message keeps the step and asks again.
if reply := sendToBotFather(t, svc, messages, owner, "here you go"); !strings.Contains(reply, "send a photo") {
t.Fatalf("text-instead-of-photo reply = %q", reply)
}
if avatar.calls != 0 {
t.Fatalf("avatar setter called for a non-photo message")
}
svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777))
reply := botFatherUserReply(t, messages, owner.ID)
if !strings.Contains(reply.Body, "Profile picture updated") {
t.Fatalf("botpic success reply = %q", reply.Body)
}
if avatar.calls != 1 || avatar.ownerID != created.ID || avatar.sourcePhotoID != 7777 || avatar.ownerType != domain.PeerTypeUser {
t.Fatalf("avatar setter got (calls=%d owner=%d photo=%d type=%s)", avatar.calls, avatar.ownerID, avatar.sourcePhotoID, avatar.ownerType)
}
// The dialog is done: a stray message no longer lands on the botpic step.
if reply := sendToBotFather(t, svc, messages, owner, "anything"); strings.Contains(reply, "profile picture") {
t.Fatalf("botpic step still active after success: %q", reply)
}
}
func TestMyBotsEditBotpicRejectsBadImage(t *testing.T) {
users := memory.NewUserStore()
bots := memory.NewBotStore(users)
messages := memory.NewMessageStore(memory.NewDialogStore())
avatar := &fakeBotAvatar{err: domain.ErrPhotoInvalid}
svc := NewService(users, bots, messages, WithBotAvatarStore(avatar))
owner := newOwner(t, users, "+2101")
if _, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot"); err != nil {
t.Fatalf("create bot: %v", err)
}
openBotpicPrompt(t, svc, messages, owner)
svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777))
reply := botFatherUserReply(t, messages, owner.ID)
if !strings.Contains(reply.Body, "couldn't use that image") {
t.Fatalf("bad image reply = %q", reply.Body)
}
// Step is kept so the user can send another photo.
if reply := sendToBotFather(t, svc, messages, owner, "x"); !strings.Contains(reply, "send a photo") {
t.Fatalf("after bad image, step not kept: %q", reply)
}
}
func TestMyBotsEditBotpicUnsupported(t *testing.T) {
svc, users, _, messages := newTestService(t) // no WithBotAvatarStore
owner := newOwner(t, users, "+2102")
if _, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot"); err != nil {
t.Fatalf("create bot: %v", err)
}
openBotpicPrompt(t, svc, messages, owner)
svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777))
reply := botFatherUserReply(t, messages, owner.ID)
if !strings.Contains(reply.Body, "isn't available on this server") {
t.Fatalf("unsupported reply = %q", reply.Body)
}
}