Files

293 lines
9.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})
}
}