303 lines
9.0 KiB
Go
303 lines
9.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"oci-portal/internal/crypto"
|
|
"oci-portal/internal/model"
|
|
)
|
|
|
|
// newSettingEnv 建一个只含 Setting 表的内存库环境。
|
|
func newSettingEnv(t *testing.T) (*SettingService, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("open in-memory sqlite: %v", err)
|
|
}
|
|
// :memory: 库每个连接彼此独立,SendAsync 等后台 goroutine 读库需复用同一连接
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("db handle: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
if err := db.AutoMigrate(&model.Setting{}); err != nil {
|
|
t.Fatalf("auto migrate: %v", err)
|
|
}
|
|
cipher, err := crypto.NewCipher("test-data-key")
|
|
if err != nil {
|
|
t.Fatalf("new cipher: %v", err)
|
|
}
|
|
return NewSettingService(db, cipher), db
|
|
}
|
|
|
|
func strPtr(s string) *string { return &s }
|
|
|
|
func intPtr(n int) *int { return &n }
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|
|
|
|
func TestUpdateTelegramTokenHandling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initial *string // 先保存一次的 token;nil 表示未配置过
|
|
update *string // 第二次更新携带的 botToken
|
|
wantToken string // 最终 TelegramConfig 应解出的 token
|
|
}{
|
|
{name: "nil 沿用旧 token", initial: strPtr("tok-1234567890"), update: nil, wantToken: "tok-1234567890"},
|
|
{name: "非 nil 覆盖旧 token", initial: strPtr("tok-1234567890"), update: strPtr("tok-abcdefghij"), wantToken: "tok-abcdefghij"},
|
|
{name: "空串清除已存 token", initial: strPtr("tok-1234567890"), update: strPtr(""), wantToken: ""},
|
|
{name: "首次设置", initial: nil, update: strPtr("tok-first99999"), wantToken: "tok-first99999"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, _ := newSettingEnv(t)
|
|
ctx := context.Background()
|
|
if tt.initial != nil {
|
|
in := UpdateTelegramInput{Enabled: true, BotToken: tt.initial, ChatID: "42"}
|
|
if err := svc.UpdateTelegram(ctx, in); err != nil {
|
|
t.Fatalf("save initial: %v", err)
|
|
}
|
|
}
|
|
in := UpdateTelegramInput{Enabled: true, BotToken: tt.update, ChatID: "42"}
|
|
if err := svc.UpdateTelegram(ctx, in); err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
cfg, err := svc.TelegramConfig(ctx)
|
|
if err != nil {
|
|
t.Fatalf("TelegramConfig: %v", err)
|
|
}
|
|
if cfg.BotToken != tt.wantToken {
|
|
t.Errorf("token = %q, want %q", cfg.BotToken, tt.wantToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTelegramViewNeverReturnsPlaintext(t *testing.T) {
|
|
svc, db := newSettingEnv(t)
|
|
ctx := context.Background()
|
|
token := "123456789:AAtestTokenValue"
|
|
in := UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "-100200"}
|
|
if err := svc.UpdateTelegram(ctx, in); err != nil {
|
|
t.Fatalf("UpdateTelegram: %v", err)
|
|
}
|
|
|
|
view, err := svc.TelegramView(ctx)
|
|
if err != nil {
|
|
t.Fatalf("TelegramView: %v", err)
|
|
}
|
|
want := TelegramView{Enabled: true, ChatID: "-100200", TokenSet: true, TokenTail: "alue"}
|
|
if view != want {
|
|
t.Errorf("view = %+v, want %+v", view, want)
|
|
}
|
|
|
|
// 落库必须是密文:存储值不含 token 原文,但能解密还原
|
|
var st model.Setting
|
|
if err := db.First(&st, "key = ?", settingTelegramBotToken).Error; err != nil {
|
|
t.Fatalf("load setting row: %v", err)
|
|
}
|
|
if strings.Contains(st.Value, token) {
|
|
t.Errorf("token stored in plaintext: %q", st.Value)
|
|
}
|
|
cfg, err := svc.TelegramConfig(ctx)
|
|
if err != nil || cfg.BotToken != token {
|
|
t.Errorf("decrypted token = %q (%v), want %q", cfg.BotToken, err, token)
|
|
}
|
|
}
|
|
|
|
func TestTokenTail(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
want string
|
|
}{
|
|
{name: "常规 token 取尾 4 位", token: "123456789:AAtestTokenValue", want: "alue"},
|
|
{name: "过短不展示", token: "abc", want: ""},
|
|
{name: "空串", token: "", want: ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tokenTail(tt.token); got != tt.want {
|
|
t.Errorf("tokenTail(%q) = %q, want %q", tt.token, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNotifyEventsReadWrite(t *testing.T) {
|
|
allOn := NotifyEventsView{
|
|
TaskFail: true, TaskRecover: true, SnatchSuccess: true, TenantDead: true,
|
|
TaskStop: true, LoginLock: true, ModelDeprecated: true,
|
|
LogEventInstance: true, LogEventIdentity: true, LogEventPolicy: true,
|
|
LogEventRegion: true, LogEventLogin: true,
|
|
}
|
|
offTaskFailStop := allOn
|
|
offTaskFailStop.TaskFail, offTaskFailStop.TaskStop = false, false
|
|
offTenantDead := allOn
|
|
offTenantDead.TenantDead = false
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, svc *SettingService)
|
|
want NotifyEventsView
|
|
}{
|
|
{
|
|
name: "键全部缺省视为全开(兼容存量部署)",
|
|
setup: func(t *testing.T, svc *SettingService) {},
|
|
want: allOn,
|
|
},
|
|
{
|
|
name: "写 0 后读回 false",
|
|
setup: func(t *testing.T, svc *SettingService) {
|
|
in := allOn
|
|
in.TaskFail = false
|
|
in.TaskStop = false
|
|
if err := svc.UpdateNotifyEvents(context.Background(), in); err != nil {
|
|
t.Fatalf("UpdateNotifyEvents: %v", err)
|
|
}
|
|
},
|
|
want: offTaskFailStop,
|
|
},
|
|
{
|
|
name: "部分键缺失时只关显式为 0 的项",
|
|
setup: func(t *testing.T, svc *SettingService) {
|
|
if err := svc.set(context.Background(), settingNotifyEventTenantDead, "0"); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
},
|
|
want: offTenantDead,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, _ := newSettingEnv(t)
|
|
tt.setup(t, svc)
|
|
got, err := svc.NotifyEvents(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("NotifyEvents: %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("view = %+v, want %+v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNotifyEventEnabled(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value *string // 键值;nil 表示键不存在
|
|
want bool
|
|
}{
|
|
{name: "键缺失视为开启", value: nil, want: true},
|
|
{name: "显式 0 关闭", value: strPtr("0"), want: false},
|
|
{name: "显式 1 开启", value: strPtr("1"), want: true},
|
|
{name: "脏数据非 0 视为开启", value: strPtr("maybe"), want: true},
|
|
{name: "空值视为开启", value: strPtr(""), want: true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, _ := newSettingEnv(t)
|
|
ctx := context.Background()
|
|
if tt.value != nil {
|
|
if err := svc.set(ctx, settingNotifyEventTaskFail, *tt.value); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
}
|
|
if got := svc.NotifyEventEnabled(ctx, "task_fail"); got != tt.want {
|
|
t.Errorf("NotifyEventEnabled = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// saveTaskLimit 返回把抢机熔断阈值写入设置的 setup 步骤,供 TestTaskSettings 复用。
|
|
func saveTaskLimit(limit int) func(t *testing.T, svc *SettingService) {
|
|
return func(t *testing.T, svc *SettingService) {
|
|
t.Helper()
|
|
in := TaskSettingsView{SnatchAuthFailLimit: limit}
|
|
if err := svc.UpdateTaskSettings(context.Background(), in); err != nil {
|
|
t.Fatalf("UpdateTaskSettings(%d): %v", limit, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTaskSettings(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, svc *SettingService)
|
|
want int
|
|
}{
|
|
{name: "键缺省回退默认 3", setup: func(t *testing.T, svc *SettingService) {}, want: 3},
|
|
{name: "写 5 读回 5", setup: saveTaskLimit(5), want: 5},
|
|
{name: "合法下界写 1 读回 1", setup: saveTaskLimit(1), want: 1},
|
|
{name: "合法上界写 10 读回 10", setup: saveTaskLimit(10), want: 10},
|
|
{
|
|
name: "脏数据非整数回退 3",
|
|
setup: func(t *testing.T, svc *SettingService) {
|
|
if err := svc.set(context.Background(), settingSnatchAuthFailLimit, "abc"); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
},
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "脏数据越界回退 3",
|
|
setup: func(t *testing.T, svc *SettingService) {
|
|
if err := svc.set(context.Background(), settingSnatchAuthFailLimit, "99"); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
},
|
|
want: 3,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, _ := newSettingEnv(t)
|
|
tt.setup(t, svc)
|
|
got, err := svc.TaskSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TaskSettings: %v", err)
|
|
}
|
|
if got.SnatchAuthFailLimit != tt.want {
|
|
t.Errorf("limit = %d, want %d", got.SnatchAuthFailLimit, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateTaskSettingsRejectsOutOfRange(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
limit int
|
|
}{
|
|
{name: "低于下界 0", limit: 0},
|
|
{name: "高于上界 11", limit: 11},
|
|
{name: "负值", limit: -3},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, _ := newSettingEnv(t)
|
|
err := svc.UpdateTaskSettings(context.Background(), TaskSettingsView{SnatchAuthFailLimit: tt.limit})
|
|
if !errors.Is(err, ErrInvalidAuthFailLimit) {
|
|
t.Fatalf("err = %v, want ErrInvalidAuthFailLimit", err)
|
|
}
|
|
got, err := svc.TaskSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TaskSettings: %v", err)
|
|
}
|
|
if got.SnatchAuthFailLimit != 3 {
|
|
t.Errorf("拒绝后读回 = %d, want 保持默认 3", got.SnatchAuthFailLimit)
|
|
}
|
|
})
|
|
}
|
|
}
|