You've already forked agentic-coding-workflow
178 lines
5.8 KiB
Go
178 lines
5.8 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// reverseDecryptor 是测试用 Decryptor:把密文逐字节取反作为 "解密",与
|
|
// reverseEncrypt 配对,确保 IMNotifier 走解密路径而非裸读明文。
|
|
type reverseDecryptor struct{ err error }
|
|
|
|
func reverseBytes(b []byte) []byte {
|
|
out := make([]byte, len(b))
|
|
for i, c := range b {
|
|
out[i] = ^c
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (d reverseDecryptor) Decrypt(ct []byte) ([]byte, error) {
|
|
if d.err != nil {
|
|
return nil, d.err
|
|
}
|
|
return reverseBytes(ct), nil
|
|
}
|
|
|
|
// captureDoer 记录最后一次 HTTP 请求并返回固定响应码。
|
|
type captureDoer struct {
|
|
mu sync.Mutex
|
|
calls int
|
|
url string
|
|
body []byte
|
|
ctype string
|
|
status int
|
|
err error
|
|
}
|
|
|
|
func (c *captureDoer) Do(req *http.Request) (*http.Response, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.err != nil {
|
|
return nil, c.err
|
|
}
|
|
c.calls++
|
|
c.url = req.URL.String()
|
|
c.ctype = req.Header.Get("Content-Type")
|
|
if req.Body != nil {
|
|
c.body, _ = io.ReadAll(req.Body)
|
|
}
|
|
st := c.status
|
|
if st == 0 {
|
|
st = http.StatusOK
|
|
}
|
|
return &http.Response{StatusCode: st, Body: io.NopCloser(strings.NewReader("ok"))}, nil
|
|
}
|
|
|
|
func imPrefs(uid uuid.UUID, enabled bool, sev Severity, webhookEnc []byte) Prefs {
|
|
return Prefs{UserID: uid, IMEnabled: enabled, MinSeverity: sev, ImWebhookURLEnc: webhookEnc}
|
|
}
|
|
|
|
func TestIMNotifier_PostsDecryptedWebhookWithSlackPayload(t *testing.T) {
|
|
uid := uuid.New()
|
|
enc := reverseBytes([]byte("https://hooks.slack.test/abc"))
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityWarning, enc))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
err := n.Send(context.Background(), Message{
|
|
UserID: uid, Topic: "acp.permission_pending", Severity: SeverityWarning,
|
|
Title: "Approval needed", Body: "run rm", Link: "/approvals",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, doer.calls)
|
|
require.Equal(t, "https://hooks.slack.test/abc", doer.url, "应 POST 到解密后的 URL")
|
|
require.Equal(t, "application/json", doer.ctype)
|
|
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal(doer.body, &payload))
|
|
require.Equal(t, "acp.permission_pending", payload["topic"])
|
|
require.Equal(t, "warning", payload["severity"])
|
|
require.Equal(t, "Approval needed", payload["title"])
|
|
text, _ := payload["text"].(string)
|
|
require.Contains(t, text, "[WARNING] Approval needed")
|
|
require.Contains(t, text, "run rm")
|
|
require.Contains(t, text, "/approvals")
|
|
}
|
|
|
|
func TestIMNotifier_DisabledChannelSkipped(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, false, SeverityInfo, reverseBytes([]byte("https://x"))))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
require.Equal(t, 0, doer.calls)
|
|
}
|
|
|
|
func TestIMNotifier_NoWebhookSkipped(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityInfo, nil))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
require.Equal(t, 0, doer.calls, "未配置 webhook 应跳过")
|
|
}
|
|
|
|
func TestIMNotifier_BelowMinSeveritySkipped(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityError, reverseBytes([]byte("https://x"))))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityWarning}))
|
|
require.Equal(t, 0, doer.calls)
|
|
}
|
|
|
|
func TestIMNotifier_GloballyDisabledSkipped(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: false}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
require.Equal(t, 0, doer.calls)
|
|
}
|
|
|
|
func TestIMNotifier_DecryptErrorReturnsErr(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityInfo, []byte("garbage")))
|
|
doer := &captureDoer{}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{err: errors.New("bad key")}, doer, newTestLogger())
|
|
|
|
require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
require.Equal(t, 0, doer.calls)
|
|
}
|
|
|
|
func TestIMNotifier_Non2xxReturnsErr(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
|
|
doer := &captureDoer{status: http.StatusInternalServerError}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
require.Equal(t, 1, doer.calls)
|
|
}
|
|
|
|
func TestIMNotifier_HTTPErrorReturnsErr(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
|
|
doer := &captureDoer{err: errors.New("dial fail")}
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())
|
|
|
|
require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
|
|
}
|
|
|
|
func TestIMNotifier_NameIsIMChannel(t *testing.T) {
|
|
n := NewIMNotifier(IMConfig{Enabled: true}, newFakePrefs(), reverseDecryptor{}, &captureDoer{}, newTestLogger())
|
|
require.Equal(t, ChannelIM, n.Name())
|
|
}
|