mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
706 lines
25 KiB
Go
706 lines
25 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/config"
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
func newBotTestService(t *testing.T) (*repository.Container, *TelegramBotService) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repos := repository.New(db)
|
|
cfg := &config.Config{}
|
|
cfg.Secrets.JWTSecret = "test-secret"
|
|
log := zap.NewNop()
|
|
perms := NewPermissionService(log, repos)
|
|
tokenSvc := NewTokenService(cfg, log, repos)
|
|
auth := NewAuthService(cfg, log, repos, tokenSvc, perms)
|
|
crypto := NewCryptoService("test-secret", log)
|
|
bot := NewTelegramBotService(log, repos, crypto, auth)
|
|
return repos, bot
|
|
}
|
|
|
|
// ── pure logic ──────────────────────────────────────────────────────────────
|
|
|
|
func TestRenewExpiry(t *testing.T) {
|
|
// 永久(0 天)→ nil
|
|
if got := renewExpiry(nil, 0); got != nil {
|
|
t.Fatalf("expected nil for permanent, got %v", got)
|
|
}
|
|
// 从现在起 +30 天(当前为空)
|
|
got := renewExpiry(nil, 30)
|
|
if got == nil || got.Before(time.Now().Add(29*24*time.Hour)) {
|
|
t.Fatalf("expected ~30d expiry, got %v", got)
|
|
}
|
|
// 已有未来到期 → 在原到期基础上叠加
|
|
future := time.Now().Add(10 * 24 * time.Hour)
|
|
got = renewExpiry(&future, 30)
|
|
if got == nil || got.Before(future.Add(29*24*time.Hour)) {
|
|
t.Fatalf("expected stacking on future expiry, got %v", got)
|
|
}
|
|
// 已过期 → 从现在起算
|
|
past := time.Now().Add(-10 * 24 * time.Hour)
|
|
got = renewExpiry(&past, 5)
|
|
if got == nil || got.Before(time.Now().Add(4*24*time.Hour)) {
|
|
t.Fatalf("expected fresh window from now, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestCapacityRemaining(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
c capacityInfo
|
|
want int64
|
|
}{
|
|
{"license only", capacityInfo{UsedUsers: 5, MaxUsers: 20}, 15},
|
|
{"quota tighter", capacityInfo{UsedUsers: 5, MaxUsers: 100, OpenRegLimit: 10, OpenRegUsed: 3}, 7},
|
|
{"license tighter", capacityInfo{UsedUsers: 95, MaxUsers: 100, OpenRegLimit: 50, OpenRegUsed: 0}, 5},
|
|
{"full", capacityInfo{UsedUsers: 20, MaxUsers: 20}, 0},
|
|
{"quota exhausted", capacityInfo{UsedUsers: 1, MaxUsers: 100, OpenRegLimit: 5, OpenRegUsed: 5}, 0},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := tc.c.Remaining(); got != tc.want {
|
|
t.Errorf("%s: Remaining()=%d want %d", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRandomWindowDays(t *testing.T) {
|
|
for i := 0; i < 200; i++ {
|
|
d := randomWindowDays(3, 5)
|
|
if d < 3 || d > 5 {
|
|
t.Fatalf("randomWindowDays(3,5)=%d out of range", d)
|
|
}
|
|
}
|
|
if d := randomWindowDays(4, 4); d != 4 {
|
|
t.Fatalf("randomWindowDays(4,4)=%d want 4", d)
|
|
}
|
|
}
|
|
|
|
func TestFingerprintStability(t *testing.T) {
|
|
a := fingerprint("Infuse", "iPhone")
|
|
b := fingerprint("infuse", " iPhone ")
|
|
if a != b {
|
|
t.Fatalf("fingerprint should be case/space-insensitive: %s != %s", a, b)
|
|
}
|
|
if a == fingerprint("Infuse", "iPad") {
|
|
t.Fatal("different device names must yield different fingerprints")
|
|
}
|
|
}
|
|
|
|
// ── DB-backed ─────────────────────────────────────────────────────────────
|
|
|
|
func TestSignInStreak(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
u := &model.User{Username: "alice", PasswordHash: "x", Role: "user"}
|
|
if err := repos.User.Create(ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
res, err := bot.signIn(ctx, u.ID)
|
|
if err != nil || res.Streak != 1 || res.Total != 1 {
|
|
t.Fatalf("first sign-in: %+v err=%v", res, err)
|
|
}
|
|
// 同日重复签到 → 不增长
|
|
res, _ = bot.signIn(ctx, u.ID)
|
|
if !res.AlreadySigned || res.Streak != 1 {
|
|
t.Fatalf("same-day re-signin should be no-op: %+v", res)
|
|
}
|
|
// 模拟昨天签到 → 连续 +1
|
|
rec, _ := repos.SignIn.Get(ctx, u.ID)
|
|
rec.LastSignIn = time.Now().Add(-24 * time.Hour)
|
|
_ = repos.SignIn.Save(ctx, rec)
|
|
res, _ = bot.signIn(ctx, u.ID)
|
|
if res.Streak != 2 || res.Total != 2 {
|
|
t.Fatalf("consecutive day should bump streak: %+v", res)
|
|
}
|
|
// 中断(前天)→ 重置为 1
|
|
rec, _ = repos.SignIn.Get(ctx, u.ID)
|
|
rec.LastSignIn = time.Now().Add(-72 * time.Hour)
|
|
_ = repos.SignIn.Save(ctx, rec)
|
|
res, _ = bot.signIn(ctx, u.ID)
|
|
if res.Streak != 1 {
|
|
t.Fatalf("broken streak should reset to 1: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestRegistrationCodeRedeemOnce(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
|
|
code, err := bot.generateCode(ctx, model.RegistrationCodeRenew, 30, 0, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 首次校验通过
|
|
rc, msg := bot.lookupRedeemableCode(ctx, code.Code, model.RegistrationCodeRenew)
|
|
if rc == nil {
|
|
t.Fatalf("expected valid code, got msg=%q", msg)
|
|
}
|
|
// 标记使用后不可再用
|
|
if err := repos.RegCode.MarkUsed(ctx, rc.ID, "user-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, msg := bot.lookupRedeemableCode(ctx, code.Code, model.RegistrationCodeRenew); msg == "" {
|
|
t.Fatal("used code must not validate again")
|
|
}
|
|
// 第二次 MarkUsed 应失败(防止双花)
|
|
if err := repos.RegCode.MarkUsed(ctx, rc.ID, "user-2"); err == nil {
|
|
t.Fatal("double-spend should be rejected")
|
|
}
|
|
// 类型不匹配应被拒
|
|
reg, _ := bot.generateCode(ctx, model.RegistrationCodeRegister, 0, 0, "")
|
|
if _, msg := bot.lookupRedeemableCode(ctx, reg.Code, model.RegistrationCodeRenew); msg == "" {
|
|
t.Fatal("register code should not validate as renew")
|
|
}
|
|
}
|
|
|
|
func TestRenewalClearsExpiry(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
past := time.Now().Add(-time.Hour)
|
|
u := &model.User{Username: "bob", PasswordHash: "x", Role: "user", IsActive: false, ExpiredAt: &past}
|
|
if err := repos.User.Create(ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := bot.applyRenewal(ctx, u.ID, 30); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := repos.User.FindByID(ctx, u.ID)
|
|
if !got.IsActive {
|
|
t.Fatal("renewal should re-activate account")
|
|
}
|
|
if got.ExpiredAt == nil || got.ExpiredAt.Before(time.Now()) {
|
|
t.Fatalf("renewal should set future expiry, got %v", got.ExpiredAt)
|
|
}
|
|
}
|
|
|
|
func TestDeviceKickAndConcurrency(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, _ := newBotTestService(t)
|
|
dev := NewDeviceService(zap.NewNop(), repos)
|
|
u := &model.User{Username: "carol", PasswordHash: "x", Role: "user", IsActive: true}
|
|
if err := repos.User.Create(ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dev.RecordLogin(ctx, u.ID, "dev-1", "iPhone", "Infuse", "1.2.3.4")
|
|
dev.RecordPlayback(ctx, u.ID, "dev-1", "iPhone", "Infuse")
|
|
devices, _ := dev.ListDevices(ctx, u.ID)
|
|
if len(devices) != 1 {
|
|
t.Fatalf("expected 1 device, got %d", len(devices))
|
|
}
|
|
|
|
// 踢下线后命中 kicked
|
|
if err := dev.KickDevice(ctx, u.ID, "dev-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !dev.IsDeviceKicked(ctx, u.ID, "dev-1") {
|
|
t.Fatal("device should be kicked")
|
|
}
|
|
// 重新登录清除 kicked
|
|
dev.RecordLogin(ctx, u.ID, "dev-1", "iPhone", "Infuse", "1.2.3.4")
|
|
if dev.IsDeviceKicked(ctx, u.ID, "dev-1") {
|
|
t.Fatal("re-login should clear kicked flag")
|
|
}
|
|
|
|
// 并发播放计数
|
|
now := time.Now()
|
|
for i, id := range []string{"d1", "d2", "d3", "d4"} {
|
|
_ = repos.UserDevice.Create(ctx, &model.UserDevice{
|
|
UserID: u.ID, DeviceID: id, FirstSeenAt: now, LastSeenAt: now, LastPlayAt: &now,
|
|
})
|
|
_ = i
|
|
}
|
|
n, err := repos.UserDevice.CountConcurrentPlaying(ctx, u.ID, now.Add(-time.Minute))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n < 4 {
|
|
t.Fatalf("expected >=4 concurrent playing, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestProtectedAdminNeverViolated(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, _ := newBotTestService(t)
|
|
dev := NewDeviceService(zap.NewNop(), repos)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = repos.Setting.Set(ctx, SettingAntiShareEnabled, "true")
|
|
cfg := loadBotConfig(ctx, repos)
|
|
// 多次违规也不应删除/警告/禁用管理员
|
|
for i := 0; i < 5; i++ {
|
|
dev.registerFingerprintWarning(ctx, admin.ID, "test", cfg)
|
|
}
|
|
got, _ := repos.User.FindByID(ctx, admin.ID)
|
|
if got == nil {
|
|
t.Fatal("admin must never be auto-deleted")
|
|
}
|
|
if !got.IsActive {
|
|
t.Fatal("admin must never be auto-disabled")
|
|
}
|
|
if got.ShareWarnings != 0 {
|
|
t.Fatalf("admin should accrue no warnings, got %d", got.ShareWarnings)
|
|
}
|
|
}
|
|
|
|
func TestBotAdminCommandsManageDevicePolicy(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&model.TelegramBinding{
|
|
TelegramUserID: 9001,
|
|
TelegramName: "@root",
|
|
ChatID: 9001,
|
|
UserID: admin.ID,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/antishare on play=4 login=5 warn=3")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "防共享:<b>已开启</b>") {
|
|
t.Fatalf("expected antishare enabled reply, got %q", reply.Text)
|
|
}
|
|
cfg := loadBotConfig(ctx, repos)
|
|
if !cfg.AntiShareEnabled || cfg.MaxConcurrentPlay != 4 || cfg.MaxLoggedClients != 5 || cfg.WarnThreshold != 3 {
|
|
t.Fatalf("unexpected device policy: %+v", cfg)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup_mode count 2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg = loadBotConfig(ctx, repos)
|
|
if cfg.AccountCleanupKeepMode != "count" || cfg.AccountCleanupRequiredCount != 2 {
|
|
t.Fatalf("unexpected cleanup mode: %+v; reply=%q", cfg, reply.Text)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup_rule add recent_login login_7d 七天内登录 7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg = loadBotConfig(ctx, repos)
|
|
found := false
|
|
for _, rule := range cfg.AccountCleanupRules {
|
|
if rule.ID == "login_7d" && rule.Type == "recent_login" && rule.WindowDaysMax == 7 {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("cleanup rule not added; reply=%q rules=%+v", reply.Text, cfg.AccountCleanupRules)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup_rule add account_age_grace new_7d 7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg = loadBotConfig(ctx, repos)
|
|
found = false
|
|
for _, rule := range cfg.AccountCleanupRules {
|
|
if rule.ID == "new_7d" && rule.Type == "account_age_grace" && rule.MinCount == 7 {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("cleanup shorthand rule not added; reply=%q rules=%+v", reply.Text, cfg.AccountCleanupRules)
|
|
}
|
|
}
|
|
|
|
func TestBotCleanupRulesCanBeDeletedUntilEmpty(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup_rule del watch_3_5d_6h")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := loadBotConfig(ctx, repos)
|
|
if len(cfg.AccountCleanupRules) != 0 {
|
|
t.Fatalf("cleanup rules should stay empty after deleting the last rule; reply=%q rules=%+v", reply.Text, cfg.AccountCleanupRules)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/cleanup_rule list")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "暂无规则") {
|
|
t.Fatalf("expected empty rule list, got %q", reply.Text)
|
|
}
|
|
}
|
|
|
|
func TestBotCleanupRuleListInfersDaysAndHidesDuplicateNames(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Setting.Set(ctx, SettingAccountCleanupRules, `[
|
|
{"id":"login_7d","name":"login_7d","type":"recent_login","enabled":true,"window_days_min":1,"window_days_max":5,"min_count":1},
|
|
{"id":"new_7d","name":"new_7d","type":"account_age_grace","enabled":true,"window_days_min":1,"window_days_max":1,"min_count":1}
|
|
]`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9001, Username: "root"}, Chat: TelegramChat{ID: 9001, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/cleanup_rule list")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, bad := range []string{"login_7d</code> · login_7d", "new_7d</code> · new_7d", "5 天内登录", "新号宽限 1 天"} {
|
|
if strings.Contains(reply.Text, bad) {
|
|
t.Fatalf("rule list still contains bad fragment %q: %s", bad, reply.Text)
|
|
}
|
|
}
|
|
for _, want := range []string{"login_7d", "7 天内登录", "new_7d", "新号宽限 7 天"} {
|
|
if !strings.Contains(reply.Text, want) {
|
|
t.Fatalf("rule list missing %q: %s", want, reply.Text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBotRegistrationCommandUsesOpenRegQuota(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Setting.Set(ctx, SettingOpenRegEnabled, "false"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&model.TelegramBinding{
|
|
TelegramUserID: 9051,
|
|
TelegramName: "@root",
|
|
ChatID: 9051,
|
|
UserID: admin.ID,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9051"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9051, Username: "root"}, Chat: TelegramChat{ID: 9051, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/registration on 2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "2 个名额") {
|
|
t.Fatalf("expected quota feedback, got %q", reply.Text)
|
|
}
|
|
capacity := bot.loadCapacity(ctx)
|
|
if !capacity.OpenRegOn || capacity.OpenRegLimit != 2 || capacity.OpenRegUsed != 0 {
|
|
t.Fatalf("registration command should open quota-aware registration, got %+v", capacity)
|
|
}
|
|
}
|
|
|
|
func TestBotUserCommandsAndAdminGate(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
user := &model.User{Username: "viewer", PasswordHash: "x", Role: "user", IsActive: true}
|
|
if err := repos.User.Create(ctx, user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&model.TelegramBinding{
|
|
TelegramUserID: 9101,
|
|
TelegramName: "@viewer",
|
|
ChatID: 9101,
|
|
UserID: user.ID,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now()
|
|
if err := repos.UserDevice.Create(ctx, &model.UserDevice{
|
|
UserID: user.ID, DeviceID: "dev-1", DeviceName: "iPhone", Client: "Infuse", FirstSeenAt: now, LastSeenAt: now,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9101, Username: "viewer"}, Chat: TelegramChat{ID: 9101, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/antishare on")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "仅管理员") {
|
|
t.Fatalf("regular user should not manage policy, got %q", reply.Text)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/devices")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "我的登录设备") {
|
|
t.Fatalf("expected device list, got %q", reply.Text)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/kick 1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "已踢下线") {
|
|
t.Fatalf("expected kick feedback, got %q", reply.Text)
|
|
}
|
|
if kicked := bot.device; kicked != nil {
|
|
t.Fatal("test should not require wired device service")
|
|
}
|
|
if ok := NewDeviceService(zap.NewNop(), repos).IsDeviceKicked(ctx, user.ID, "dev-1"); !ok {
|
|
t.Fatal("device should be marked kicked")
|
|
}
|
|
}
|
|
|
|
func TestBotRedeemRegisterRequiresAllowedTelegramUser(t *testing.T) {
|
|
ctx := context.Background()
|
|
_, bot := newBotTestService(t)
|
|
code, err := bot.generateCode(ctx, model.RegistrationCodeRegister, 30, 0, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9001"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9201, Username: "outsider"}, Chat: TelegramChat{ID: 9201, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/redeem_register "+code.Code)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "不在管理员配置") {
|
|
t.Fatalf("outsider should not redeem register code, got %q", reply.Text)
|
|
}
|
|
|
|
channel.Config = `{"admin_user_ids":"9201"}`
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/redeem_register "+code.Code)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "兑换成功") {
|
|
t.Fatalf("allowed user should redeem register code, got %q", reply.Text)
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9201); binding == nil {
|
|
t.Fatal("redeemed account should be bound to telegram user")
|
|
}
|
|
}
|
|
|
|
func TestBotRedeemRegisterCodeCreatesOnlyOneAccount(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
code, err := bot.generateCode(ctx, model.RegistrationCodeRegister, 30, 0, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9201,9202"}`}
|
|
|
|
first := &TelegramMessage{From: TelegramUser{ID: 9201, Username: "first"}, Chat: TelegramChat{ID: 9201, Type: "private"}}
|
|
reply, err := bot.executeCommand(ctx, channel, first, "/redeem_register "+code.Code)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "兑换成功") {
|
|
t.Fatalf("first redeem should succeed, got %q", reply.Text)
|
|
}
|
|
|
|
second := &TelegramMessage{From: TelegramUser{ID: 9202, Username: "second"}, Chat: TelegramChat{ID: 9202, Type: "private"}}
|
|
reply, err = bot.executeCommand(ctx, channel, second, "/redeem_register "+code.Code)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "兑换码已被使用") && !strings.Contains(reply.Text, "兑换码刚刚被使用") {
|
|
t.Fatalf("second redeem should be rejected as used, got %q", reply.Text)
|
|
}
|
|
var users int64
|
|
if err := repos.DB.Model(&model.User{}).Count(&users).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if users != 1 {
|
|
t.Fatalf("one register code must create exactly one user, got %d", users)
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9202); binding != nil {
|
|
t.Fatal("second telegram user must not be bound by an already-used register code")
|
|
}
|
|
}
|
|
|
|
func TestBotAdminCodeAndUserCommands(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
user := &model.User{Username: "viewer", PasswordHash: "x", Role: "user", IsActive: true}
|
|
if err := repos.User.Create(ctx, admin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.User.Create(ctx, user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&model.TelegramBinding{
|
|
TelegramUserID: 9301,
|
|
TelegramName: "@root",
|
|
ChatID: 9301,
|
|
UserID: admin.ID,
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9301"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9301, Username: "root"}, Chat: TelegramChat{ID: 9301, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/gencode renew 90 7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "已生成续期码") {
|
|
t.Fatalf("expected generated renew code, got %q", reply.Text)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/renew_user viewer 30")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "viewer") {
|
|
t.Fatalf("renew command should return user actions, got %q", reply.Text)
|
|
}
|
|
updated, _ := repos.User.FindByID(ctx, user.ID)
|
|
if updated.ExpiredAt == nil || updated.ExpiredAt.Before(time.Now()) {
|
|
t.Fatalf("renew_user should set future expiry, got %v", updated.ExpiredAt)
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/delete_user viewer")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "需要确认") {
|
|
t.Fatalf("delete without confirm should be rejected, got %q", reply.Text)
|
|
}
|
|
}
|
|
|
|
func TestBotAdminUnbindMultipleUsers(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true}
|
|
viewer := &model.User{Username: "viewer", PasswordHash: "x", Role: "user", IsActive: true}
|
|
guest := &model.User{Username: "guest", PasswordHash: "x", Role: "user", IsActive: true}
|
|
for _, user := range []*model.User{admin, viewer, guest} {
|
|
if err := repos.User.Create(ctx, user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
bindings := []model.TelegramBinding{
|
|
{TelegramUserID: 9401, TelegramName: "@root", ChatID: 9401, UserID: admin.ID},
|
|
{TelegramUserID: 9402, TelegramName: "@viewer", ChatID: 9402, UserID: viewer.ID},
|
|
{TelegramUserID: 9403, TelegramName: "@guest", ChatID: 9403, UserID: guest.ID},
|
|
}
|
|
for i := range bindings {
|
|
if err := repos.DB.Create(&bindings[i]).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9401"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9401, Username: "root"}, Chat: TelegramChat{ID: 9401, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/unbind viewer,guest missing root")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "已解绑:<b>2</b>") || !strings.Contains(reply.Text, "root(管理员)") || !strings.Contains(reply.Text, "missing") {
|
|
t.Fatalf("unexpected unbind reply: %q", reply.Text)
|
|
}
|
|
for _, user := range []*model.User{viewer, guest} {
|
|
var count int64
|
|
if err := repos.DB.Model(&model.TelegramBinding{}).Where("user_id = ?", user.ID).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("%s binding count = %d, want 0", user.Username, count)
|
|
}
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9401); binding == nil {
|
|
t.Fatal("admin binding should be protected from /unbind by username")
|
|
}
|
|
}
|
|
|
|
func TestBotAdminUnbindInactiveAndInvalidBindings(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, bot := newBotTestService(t)
|
|
oldTime := time.Now().Add(-45 * 24 * time.Hour)
|
|
recentTime := time.Now().Add(-2 * 24 * time.Hour)
|
|
admin := &model.User{Username: "root", PasswordHash: "x", Role: "admin", IsActive: true, LastLoginAt: &oldTime}
|
|
oldUser := &model.User{Username: "old", PasswordHash: "x", Role: "user", IsActive: true, LastLoginAt: &oldTime}
|
|
recentUser := &model.User{Username: "recent", PasswordHash: "x", Role: "user", IsActive: true, LastLoginAt: &recentTime}
|
|
for _, user := range []*model.User{admin, oldUser, recentUser} {
|
|
if err := repos.User.Create(ctx, user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, binding := range []model.TelegramBinding{
|
|
{TelegramUserID: 9501, TelegramName: "@root", ChatID: 9501, UserID: admin.ID},
|
|
{TelegramUserID: 9502, TelegramName: "@old", ChatID: 9502, UserID: oldUser.ID},
|
|
{TelegramUserID: 9503, TelegramName: "@recent", ChatID: 9503, UserID: recentUser.ID},
|
|
{TelegramUserID: 9504, TelegramName: "@ghost", ChatID: 9504, UserID: "missing-user"},
|
|
} {
|
|
row := binding
|
|
if err := repos.DB.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"admin_user_ids":"9501"}`}
|
|
msg := &TelegramMessage{From: TelegramUser{ID: 9501, Username: "root"}, Chat: TelegramChat{ID: 9501, Type: "private"}}
|
|
|
|
reply, err := bot.executeCommand(ctx, channel, msg, "/unbind_inactive 30")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "已解绑:<b>1</b>") || !strings.Contains(reply.Text, "old") {
|
|
t.Fatalf("unexpected inactive unbind reply: %q", reply.Text)
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9502); binding != nil {
|
|
t.Fatal("old user binding should be removed")
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9501); binding == nil {
|
|
t.Fatal("admin binding should be skipped by inactive cleanup")
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9503); binding == nil {
|
|
t.Fatal("recent user binding should remain")
|
|
}
|
|
|
|
reply, err = bot.executeCommand(ctx, channel, msg, "/unbind_duplicates")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(reply.Text, "已解绑:<b>1</b>") || !strings.Contains(reply.Text, "tg:9504") {
|
|
t.Fatalf("unexpected duplicate cleanup reply: %q", reply.Text)
|
|
}
|
|
if binding := bot.telegramBinding(ctx, 9504); binding != nil {
|
|
t.Fatal("invalid binding should be removed")
|
|
}
|
|
}
|