初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"oci-portal/internal/model"
)
// telegramCapture 记录假 Telegram 服务收到的 sendMessage 请求。
type telegramCapture struct {
mu sync.Mutex
paths []string
texts []string
}
// snapshot 返回目前收到的全部文本副本。
func (r *telegramCapture) snapshot() []string {
r.mu.Lock()
defer r.mu.Unlock()
return append([]string(nil), r.texts...)
}
// newFakeTelegram 起一个假 Telegram API,固定返回 reply 响应体。
func newFakeTelegram(t *testing.T, reply string) (*httptest.Server, *telegramCapture) {
t.Helper()
rec := &telegramCapture{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Text string `json:"text"`
}
_ = json.NewDecoder(r.Body).Decode(&payload)
rec.mu.Lock()
rec.paths = append(rec.paths, r.URL.Path)
rec.texts = append(rec.texts, payload.Text)
rec.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(reply))
}))
t.Cleanup(srv.Close)
return srv, rec
}
// newTestNotifier 组装指向假 Telegram 服务的 Notifierin 为 nil 表示不预置配置。
func newTestNotifier(t *testing.T, base string, in *UpdateTelegramInput) *Notifier {
t.Helper()
settings, _ := newSettingEnv(t)
if in != nil {
if err := settings.UpdateTelegram(context.Background(), *in); err != nil {
t.Fatalf("update telegram: %v", err)
}
}
n := NewNotifier(settings)
n.base = base
return n
}
func TestNotifierSend(t *testing.T) {
token := "123456:AAfake"
tests := []struct {
name string
setup *UpdateTelegramInput
reply string
wantSent bool
wantErr string // 空串表示期望无错误
}{
{
name: "启用且配置完整时发送",
setup: &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
reply: `{"ok":true}`,
wantSent: true,
},
{
name: "未启用时跳过",
setup: &UpdateTelegramInput{Enabled: false, BotToken: &token, ChatID: "42"},
reply: `{"ok":true}`,
},
{
name: "未配置 token 时跳过",
setup: &UpdateTelegramInput{Enabled: true, ChatID: "42"},
reply: `{"ok":true}`,
},
{
name: "完全未配置时跳过",
reply: `{"ok":true}`,
},
{
name: "telegram 报错时透出 description",
setup: &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
reply: `{"ok":false,"error_code":401,"description":"Unauthorized"}`,
wantSent: true,
wantErr: "Unauthorized",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv, rec := newFakeTelegram(t, tt.reply)
n := newTestNotifier(t, srv.URL, tt.setup)
err := n.Send(context.Background(), "hi <b>&")
if tt.wantErr == "" && err != nil {
t.Fatalf("Send: %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("Send err = %v, want contains %q", err, tt.wantErr)
}
texts := rec.snapshot()
if sent := len(texts) > 0; sent != tt.wantSent {
t.Fatalf("sent = %v (texts %v), want %v", sent, texts, tt.wantSent)
}
if tt.wantSent {
if got, want := rec.paths[0], "/bot"+token+"/sendMessage"; got != want {
t.Errorf("path = %q, want %q", got, want)
}
if got, want := texts[0], "hi &lt;b&gt;&amp;"; got != want {
t.Errorf("text = %q, want HTML 转义后 %q", got, want)
}
}
})
}
}
func TestNotifierTest(t *testing.T) {
token := "123456:AAfake"
tests := []struct {
name string
setup *UpdateTelegramInput
reply string
wantErr string
}{
{
name: "配置有效时发送成功",
setup: &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
reply: `{"ok":true}`,
},
{
name: "未配置时报可读错误",
wantErr: "bot token and chat id are required",
},
{
name: "chat 不存在时透出描述",
setup: &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
reply: `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`,
wantErr: "chat not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv, _ := newFakeTelegram(t, tt.reply)
n := newTestNotifier(t, srv.URL, tt.setup)
err := n.Test(context.Background())
if tt.wantErr == "" && err != nil {
t.Fatalf("Test: %v, want nil", err)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("Test err = %v, want contains %q", err, tt.wantErr)
}
})
}
}
// TestNotifierSendErrorHidesToken 验证网络层失败的错误信息不含 bot token:
// *url.Error 会拼出完整请求 URL(路径含 token),必须剥壳后再向上返回。
func TestNotifierSendErrorHidesToken(t *testing.T) {
token := "123456:AAfake"
srv, _ := newFakeTelegram(t, `{"ok":true}`)
base := srv.URL
srv.Close() // 立刻关闭:请求必然连接失败
n := newTestNotifier(t, base, &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"})
err := n.Send(context.Background(), "hi")
if err == nil {
t.Fatal("Send = nil, want connection error")
}
if strings.Contains(err.Error(), token) {
t.Errorf("err = %v, 错误信息泄漏了 bot token", err)
}
}
func TestNotifyEvents(t *testing.T) {
tests := []struct {
name string
prev taskSnapshot
cur taskSnapshot
want []notifyEvent // Kind 精确匹配,Vars 为需包含的变量键值;空表示不发
}{
{
name: "成功转失败发失败通知",
prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive},
cur: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
want: []notifyEvent{{Kind: notifyTaskFail, Vars: map[string]string{"task_name": "抢机", "error": "Out of host capacity"}}},
},
{
name: "连续失败不重复发",
prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
cur: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
want: nil,
},
{
name: "失败换了错误内容也不重复发",
prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "capacity"},
cur: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "timeout"},
want: nil,
},
{
name: "失败恢复成功发恢复通知",
prev: taskSnapshot{Name: "测活", Status: model.TaskStatusActive, LastError: "boom"},
cur: taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
want: []notifyEvent{{Kind: notifyTaskRecover, Vars: map[string]string{"task_name": "测活"}}},
},
{
name: "持续正常不发",
prev: taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
cur: taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
want: nil,
},
{
name: "非成功转成功发抢机成功",
prev: taskSnapshot{Name: "抢A1", Status: model.TaskStatusActive},
cur: taskSnapshot{Name: "抢A1", Status: model.TaskStatusSucceeded, Message: "created 1: ocid1"},
want: []notifyEvent{{Kind: notifySnatchSuccess, Vars: map[string]string{"task_name": "抢A1", "message": "created 1: ocid1"}}},
},
{
name: "失败后直接抢满只发抢机成功不叠加恢复",
prev: taskSnapshot{Name: "抢A1", Status: model.TaskStatusActive, LastError: "capacity"},
cur: taskSnapshot{Name: "抢A1", Status: model.TaskStatusSucceeded, Message: "created 1: ocid1"},
want: []notifyEvent{{Kind: notifySnatchSuccess, Vars: map[string]string{"task_name": "抢A1", "message": "created 1: ocid1"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := notifyEvents(tt.prev, tt.cur)
if len(got) != len(tt.want) {
t.Fatalf("events = %v (%d 条), want %d 条", got, len(got), len(tt.want))
}
for i, w := range tt.want {
if got[i].Kind != w.Kind {
t.Errorf("event[%d].Kind = %q, want %q", i, got[i].Kind, w.Kind)
}
for k, v := range w.Vars {
if got[i].Vars[k] != v {
t.Errorf("event[%d].Vars[%s] = %q, want %q", i, k, got[i].Vars[k], v)
}
}
}
})
}
}
func TestSameStringSet(t *testing.T) {
tests := []struct {
name string
a, b []string
want bool
}{
{name: "都为空", a: nil, b: nil, want: true},
{name: "顺序不同视为相同", a: []string{"x", "y"}, b: []string{"y", "x"}, want: true},
{name: "长度不同", a: []string{"x"}, b: []string{"x", "y"}, want: false},
{name: "元素不同", a: []string{"x"}, b: []string{"y"}, want: false},
{name: "重复元素次数不同", a: []string{"x", "y"}, b: []string{"x", "x"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := sameStringSet(tt.a, tt.b); got != tt.want {
t.Errorf("sameStringSet(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}
func TestTruncateText(t *testing.T) {
tests := []struct {
name string
text string
limit int
want string
}{
{name: "未超限原样返回", text: "hello", limit: 10, want: "hello"},
{name: "超限截断加省略号", text: "hello", limit: 3, want: "hel…"},
{name: "多字节按 rune 截断", text: "抢机成功", limit: 2, want: "抢机…"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := truncateText(tt.text, tt.limit); got != tt.want {
t.Errorf("truncateText(%q, %d) = %q, want %q", tt.text, tt.limit, got, tt.want)
}
})
}
}