Files
2026-09-22 16:22:29 +08:00

211 lines
5.8 KiB
Go

package service
import (
"context"
"strings"
"testing"
"time"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
)
func newTelegramTestService(t *testing.T) (*TelegramService, *repository.Container) {
t.Helper()
repos := repository.New(newServiceTestDB(t))
return NewTelegramService(zap.NewNop(), repos), repos
}
func seedTelegramTestUser(t *testing.T, repos *repository.Container, id, chatID string) {
t.Helper()
if err := repos.User.Create(context.Background(), &model.User{
Base: model.Base{ID: id},
Username: "tg-" + id,
PasswordHash: "x",
Role: "user",
IsActive: true,
TelegramChatID: chatID,
}); err != nil {
t.Fatal(err)
}
}
func TestStartBindReturnsSixCharCode(t *testing.T) {
s, _ := newTelegramTestService(t)
code, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
if len(code) != 6 {
t.Fatalf("code = %q, want 6 chars", code)
}
for _, r := range code {
if !strings.ContainsRune(telegramBindAlphabet, r) {
t.Fatalf("code %q contains unexpected rune %q", code, r)
}
}
}
// 同一用户重复申请绑定码时,旧码必须立即失效,避免多个有效码并存。
func TestStartBindReplacesPreviousCode(t *testing.T) {
s, _ := newTelegramTestService(t)
first, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
second, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
if first == second {
t.Skip("random collision, rerun")
}
if err := s.CompleteBind(context.Background(), first, "111"); err == nil {
t.Fatal("expected the superseded code to be rejected")
}
}
func TestCompleteBindRejectsExpiredCode(t *testing.T) {
s, _ := newTelegramTestService(t)
code, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
s.mu.Lock()
s.pending[code] = telegramBindPending{UserID: "u1", ExpiresAt: time.Now().Add(-time.Second)}
s.mu.Unlock()
if err := s.CompleteBind(context.Background(), code, "999"); err == nil {
t.Fatal("expected expired code to be rejected")
}
}
func TestCompleteBindWritesChatID(t *testing.T) {
s, repos := newTelegramTestService(t)
seedTelegramTestUser(t, repos, "u1", "")
code, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
if err := s.CompleteBind(context.Background(), code, "987654321"); err != nil {
t.Fatal(err)
}
u, err := repos.User.FindByID(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
if u.TelegramChatID != "987654321" {
t.Fatalf("chat id = %q, want 987654321", u.TelegramChatID)
}
// 绑定成功后码必须被消费,不能重复使用。
if err := s.CompleteBind(context.Background(), code, "987654321"); err == nil {
t.Fatal("expected the consumed code to be rejected")
}
}
func TestStatusMasksChatID(t *testing.T) {
s, repos := newTelegramTestService(t)
seedTelegramTestUser(t, repos, "u1", "1234567890")
bound, masked := s.Status(context.Background(), "u1")
if !bound {
t.Fatal("expected bound user")
}
if strings.Contains(masked, "1234567890") {
t.Fatalf("chat id must not be exposed verbatim, got %q", masked)
}
if !strings.Contains(masked, "***") {
t.Fatalf("masked value should carry a *** marker, got %q", masked)
}
}
func TestUnbindClearsChatID(t *testing.T) {
s, repos := newTelegramTestService(t)
seedTelegramTestUser(t, repos, "u1", "555")
if err := s.Unbind(context.Background(), "u1"); err != nil {
t.Fatal(err)
}
u, err := repos.User.FindByID(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
if u.TelegramChatID != "" {
t.Fatalf("chat id = %q, want empty", u.TelegramChatID)
}
}
// 未启用 / 未配置 token 时,发送必须静默返回,绝不能发起网络请求或以 panic 收场。
func TestSendNoopWhenDisabled(t *testing.T) {
s, repos := newTelegramTestService(t)
seedTelegramTestUser(t, repos, "u1", "1234567890")
s.SendToUser(context.Background(), "u1", "hello")
s.SendToAdmin(context.Background(), "hello")
}
// 未绑定 Telegram 的用户仍可能触发通知(例如首次登录),此时必须静默跳过。
func TestSendNoopWhenUserUnbound(t *testing.T) {
s, _ := newTelegramTestService(t)
s.SendToUser(context.Background(), "missing-user", "hello")
}
// 数据库写入失败时绑定码必须保留,让用户可以重试。
func TestCompleteBindKeepsPendingOnDBFailure(t *testing.T) {
db := newServiceTestDB(t)
repos := repository.New(db)
s := NewTelegramService(zap.NewNop(), repos)
code, err := s.StartBind(context.Background(), "u1")
if err != nil {
t.Fatal(err)
}
// 关闭底层数据库连接,强制 UpdateFields 失败。
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
sqlDB.Close()
bindErr := s.CompleteBind(context.Background(), code, "999")
if bindErr == nil {
t.Fatal("expected an error from closed DB")
}
// 码必须还在 pending 里,以便重试。
s.mu.Lock()
_, stillPresent := s.pending[code]
s.mu.Unlock()
if !stillPresent {
t.Fatal("bind code must be retained when DB write fails, so the user can retry")
}
}
// pollLoop 在 Telegram 禁用状态下不应退出,而应持续等待配置开启。
func TestPollLoopContinuesWhenDisabled(t *testing.T) {
repos := repository.New(newServiceTestDB(t))
s := NewTelegramService(zap.NewNop(), repos)
// 使用一个立即取消的 context 验证 goroutine 能正常退出(不阻塞)。
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
s.pollLoop(ctx)
}()
// 给 goroutine 启动时间,然后取消 context。
time.Sleep(20 * time.Millisecond)
cancel()
select {
case <-done:
// ok: goroutine 正常退出
case <-time.After(2 * time.Second):
t.Fatal("pollLoop did not exit after ctx cancellation")
}
}