初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user