初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -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 服务的 Notifier;in 为 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 <b>&"; 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user