chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,235 @@
package postgres
import (
"context"
"fmt"
"testing"
"time"
appauth "telesrv/internal/app/auth"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
// TestBotStoreRoundTripPostgres 验证迁移 0090 的 bot 模型BotFather 种子行、
// CreateBotAccount 双行事务、空 phone 不撞唯一索引、token 轮换、对话状态。
func TestBotStoreRoundTripPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
users := NewUserStore(pool)
bots := NewBotStore(pool)
// BotFather 种子users 行 + bots 行 + 不可登录token 为空)。
bf, found, err := users.ByID(ctx, domain.BotFatherUserID)
if err != nil || !found {
t.Fatalf("BotFather user not seeded: found=%v err=%v", found, err)
}
if !bf.Bot || bf.BotInfoVersion < 1 || bf.Username != "BotFather" || !bf.Verified {
t.Fatalf("BotFather user = %+v, want verified bot with bot_info_version>=1", bf)
}
bfProfile, found, err := bots.GetBot(ctx, domain.BotFatherUserID)
if err != nil || !found {
t.Fatalf("BotFather bots row not seeded: found=%v err=%v", found, err)
}
if bfProfile.TokenSecret != "" || len(bfProfile.Commands) == 0 {
t.Fatalf("BotFather profile = %+v, want empty token with seeded commands", bfProfile)
}
// 空 phone 查询不得命中任何行。
if _, found, err := users.ByPhone(ctx, ""); err != nil || found {
t.Fatalf("ByPhone('') found=%v err=%v, want not found", found, err)
}
// owner 用户 + 两个空 phone 的 bot验证部分唯一索引
suffix := time.Now().UnixNano() % 1_000_000_000
ownerPhone := fmt.Sprintf("1666%d", suffix)
owner, err := users.Create(ctx, domain.User{AccessHash: 7, Phone: ownerPhone, FirstName: "BotOwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
makeBot := func(i int) (domain.User, domain.BotProfile) {
u, p, err := bots.CreateBotAccount(ctx, domain.User{
AccessHash: int64(100 + i),
FirstName: fmt.Sprintf("IT Bot %d", i),
Username: fmt.Sprintf("it%d_%d_bot", i, suffix),
}, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: fmt.Sprintf("secret-%d-%d", i, suffix)})
if err != nil {
t.Fatalf("create bot %d: %v", i, err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", u.ID) // bots 行随 FK CASCADE
})
return u, p
}
bot1, profile1 := makeBot(1)
bot2, _ := makeBot(2)
if bot1.Phone != "" || bot2.Phone != "" {
t.Fatalf("bot phones = %q/%q, want empty", bot1.Phone, bot2.Phone)
}
if !bot1.Bot || bot1.BotInfoVersion != 1 {
t.Fatalf("bot1 = %+v, want is_bot with bot_info_version=1", bot1)
}
// username 冲突映射 ErrUsernameOccupied且不留 users 孤儿行。
if _, _, err := bots.CreateBotAccount(ctx, domain.User{
AccessHash: 999,
FirstName: "Dup",
Username: bot1.Username,
}, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: "x"}); err != domain.ErrUsernameOccupied {
t.Fatalf("duplicate username err = %v, want ErrUsernameOccupied", err)
}
if n, err := bots.CountBotsByOwner(ctx, owner.ID); err != nil || n != 2 {
t.Fatalf("CountBotsByOwner = %d err=%v, want 2", n, err)
}
list, err := bots.ListBotsByOwner(ctx, owner.ID)
if err != nil || len(list) != 2 {
t.Fatalf("ListBotsByOwner = %d err=%v, want 2", len(list), err)
}
// token 轮换。
if err := bots.UpdateBotTokenSecret(ctx, bot1.ID, "rotated-secret"); err != nil {
t.Fatalf("rotate token: %v", err)
}
rotated, _, err := bots.GetBot(ctx, bot1.ID)
if err != nil || rotated.TokenSecret != "rotated-secret" {
t.Fatalf("rotated profile = %+v err=%v", rotated, err)
}
if err := bots.UpdateBotTokenSecret(ctx, 424242, "x"); err != domain.ErrBotNotFound {
t.Fatalf("rotate missing bot err = %v, want ErrBotNotFound", err)
}
// 对话状态 upsert/get/delete。
state := domain.BotChatState{
BotUserID: domain.BotFatherUserID,
UserID: owner.ID,
Command: "newbot",
Step: "username",
Draft: map[string]string{"name": "IT Bot"},
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM bot_chat_states WHERE user_id = $1", owner.ID)
})
if err := bots.UpsertBotChatState(ctx, state); err != nil {
t.Fatalf("upsert state: %v", err)
}
got, found, err := bots.GetBotChatState(ctx, domain.BotFatherUserID, owner.ID)
if err != nil || !found {
t.Fatalf("get state: found=%v err=%v", found, err)
}
if got.Command != "newbot" || got.Step != "username" || got.Draft["name"] != "IT Bot" {
t.Fatalf("state = %+v, want round-trip", got)
}
if err := bots.DeleteBotChatState(ctx, domain.BotFatherUserID, owner.ID); err != nil {
t.Fatalf("delete state: %v", err)
}
if _, found, _ := bots.GetBotChatState(ctx, domain.BotFatherUserID, owner.ID); found {
t.Fatal("state still present after delete")
}
// P2 元数据写入 + bot_info_version bumppostgres 单事务)。
uBefore, _, _ := users.ByID(ctx, bot1.ID)
v1, err := bots.UpdateBotCommands(ctx, bot1.ID, []domain.BotCommand{{Command: "start", Description: "begin"}})
if err != nil || v1 <= uBefore.BotInfoVersion {
t.Fatalf("UpdateBotCommands version = %d err=%v, want > %d", v1, err, uBefore.BotInfoVersion)
}
gotBot, _, _ := bots.GetBot(ctx, bot1.ID)
if len(gotBot.Commands) != 1 || gotBot.Commands[0].Command != "start" {
t.Fatalf("commands = %+v, want [start]", gotBot.Commands)
}
v2, err := bots.UpdateBotInfo(ctx, bot1.ID, domain.BotInfoUpdate{
SetName: true, Name: "PG Bot", SetAbout: true, About: "pg about", SetDescription: true, Description: "pg desc",
})
if err != nil || v2 <= v1 {
t.Fatalf("UpdateBotInfo version = %d err=%v, want > %d", v2, err, v1)
}
uAfter, _, _ := users.ByID(ctx, bot1.ID)
if uAfter.FirstName != "PG Bot" || uAfter.About != "pg about" {
t.Fatalf("bot user after setInfo = first_name=%q about=%q", uAfter.FirstName, uAfter.About)
}
if descBot, _, _ := bots.GetBot(ctx, bot1.ID); descBot.Description != "pg desc" {
t.Fatalf("description = %q, want 'pg desc'", descBot.Description)
}
v3, err := bots.UpdateBotMenuButton(ctx, bot1.ID, domain.BotMenuButton{Type: domain.BotMenuButtonWebView, Text: "Open", URL: "https://pg.example/app"})
if err != nil || v3 <= v2 {
t.Fatalf("UpdateBotMenuButton version = %d err=%v, want > %d", v3, err, v2)
}
if mbBot, _, _ := bots.GetBot(ctx, bot1.ID); mbBot.MenuButton.Type != domain.BotMenuButtonWebView || mbBot.MenuButton.URL != "https://pg.example/app" {
t.Fatalf("menu button = %+v", mbBot.MenuButton)
}
v4, err := bots.SetBotInlinePlaceholder(ctx, bot1.ID, "Search PG")
if err != nil || v4 <= v3 {
t.Fatalf("SetBotInlinePlaceholder version = %d err=%v, want > %d", v4, err, v3)
}
if inlineBot, _, _ := bots.GetBot(ctx, bot1.ID); inlineBot.InlinePlaceholder != "Search PG" {
t.Fatalf("inline placeholder = %q, want Search PG", inlineBot.InlinePlaceholder)
}
v5, err := bots.SetBotInlineGeo(ctx, bot1.ID, true)
if err != nil || v5 <= v4 {
t.Fatalf("SetBotInlineGeo version = %d err=%v, want > %d", v5, err, v4)
}
if inlineBot, _, _ := bots.GetBot(ctx, bot1.ID); !inlineBot.InlineGeo {
t.Fatalf("inline geo = false, want true")
}
if _, err := bots.SetBotNochats(ctx, bot1.ID, true); err != nil {
t.Fatalf("SetBotNochats: %v", err)
}
if _, err := bots.SetBotChatHistory(ctx, bot1.ID, true); err != nil {
t.Fatalf("SetBotChatHistory: %v", err)
}
if flagBot, _, _ := bots.GetBot(ctx, bot1.ID); !flagBot.Nochats || !flagBot.ChatHistory {
t.Fatalf("flags = nochats=%v chat_history=%v, want both true", flagBot.Nochats, flagBot.ChatHistory)
}
if can, err := bots.CanBotSendMessage(ctx, bot1.ID, owner.ID); err != nil || can {
t.Fatalf("CanBotSendMessage before allow = %v,%v, want false,nil", can, err)
}
if created, err := bots.AllowBotSendMessage(ctx, bot1.ID, owner.ID, true); err != nil || !created {
t.Fatalf("AllowBotSendMessage first = %v,%v, want true,nil", created, err)
}
if can, err := bots.CanBotSendMessage(ctx, bot1.ID, owner.ID); err != nil || !can {
t.Fatalf("CanBotSendMessage after allow = %v,%v, want true,nil", can, err)
}
if created, err := bots.AllowBotSendMessage(ctx, bot1.ID, owner.ID, true); err != nil || created {
t.Fatalf("AllowBotSendMessage repeat = %v,%v, want false,nil", created, err)
}
// 不存在的 bot → ErrBotNotFound。
if _, err := bots.UpdateBotCommands(ctx, 424243, nil); err != domain.ErrBotNotFound {
t.Fatalf("update missing bot err = %v, want ErrBotNotFound", err)
}
// SignInBotPG 全链路 token 校验 + authorizations 绑定。
authSvc := appauth.NewService(users, NewAuthorizationStore(pool), memory.NewCodeStore(), nil, nil, "12345",
appauth.WithBotLogin(bots))
var authKeyID [8]byte
copy(authKeyID[:], fmt.Sprintf("%08d", suffix%100000000))
if _, err := pool.Exec(ctx, "INSERT INTO auth_keys (auth_key_id, body, server_salt) VALUES ($1, $2, 0) ON CONFLICT DO NOTHING",
authKeyIDToInt64(authKeyID), make([]byte, 256)); err != nil {
t.Fatalf("seed auth key: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM auth_keys WHERE auth_key_id = $1", authKeyIDToInt64(authKeyID))
})
token := domain.FormatBotToken(profile1.BotUserID, "rotated-secret")
u, err := authSvc.SignInBot(ctx, domain.Authorization{AuthKeyID: authKeyID}, token)
if err != nil {
t.Fatalf("SignInBot: %v", err)
}
if u.ID != bot1.ID || !u.Bot {
t.Fatalf("SignInBot user = %+v, want bot %d", u, bot1.ID)
}
if uid, ok, err := authSvc.UserID(ctx, authKeyID); err != nil || !ok || uid != bot1.ID {
t.Fatalf("UserID after SignInBot = %d,%v,%v, want %d", uid, ok, err, bot1.ID)
}
// 旧 token轮换前 secret必须失效。
oldToken := domain.FormatBotToken(profile1.BotUserID, profile1.TokenSecret)
if _, err := authSvc.SignInBot(ctx, domain.Authorization{AuthKeyID: authKeyID}, oldToken); err != domain.ErrBotTokenInvalid {
t.Fatalf("old token err = %v, want ErrBotTokenInvalid", err)
}
// BotFather 空 token 永不可登录。
if _, err := authSvc.SignInBot(ctx, domain.Authorization{AuthKeyID: authKeyID}, domain.FormatBotToken(domain.BotFatherUserID, "")); err != domain.ErrBotTokenInvalid {
t.Fatalf("BotFather token err = %v, want ErrBotTokenInvalid", err)
}
}