You've already forked agentic-coding-workflow
169 lines
5.9 KiB
Go
169 lines
5.9 KiB
Go
package notify
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type prefsFakeResolver struct{ uid uuid.UUID }
|
|
|
|
func (f *prefsFakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) {
|
|
if token == "" {
|
|
return uuid.Nil, false, nil
|
|
}
|
|
return f.uid, true, nil
|
|
}
|
|
|
|
// reverseEncryptor 是测试用 Encryptor:取反作为 "加密",与 reverseDecryptor 配对。
|
|
type reverseEncryptor struct{}
|
|
|
|
func (reverseEncryptor) Encrypt(pt []byte) ([]byte, error) { return reverseBytes(pt), nil }
|
|
|
|
func mountPrefs(t *testing.T, uid uuid.UUID, prefs PrefsRepository) http.Handler {
|
|
t.Helper()
|
|
r := chi.NewRouter()
|
|
NewHandler(&noopRepo{}, &prefsFakeResolver{uid: uid}).
|
|
WithPrefs(prefs, reverseEncryptor{}).Mount(r)
|
|
return r
|
|
}
|
|
|
|
// noopRepo 满足 inbox Repository(prefs 测试不触达 inbox 端点)。
|
|
type noopRepo struct{}
|
|
|
|
func (noopRepo) Insert(context.Context, Message) error { return nil }
|
|
func (noopRepo) List(context.Context, uuid.UUID, bool, int) ([]Message, error) {
|
|
return nil, nil
|
|
}
|
|
func (noopRepo) CountUnread(context.Context, uuid.UUID) (int, error) { return 0, nil }
|
|
func (noopRepo) MarkRead(context.Context, uuid.UUID, uuid.UUID) error { return nil }
|
|
func (noopRepo) MarkAllRead(context.Context, uuid.UUID) error { return nil }
|
|
|
|
func TestPrefsHandler_GetReturnsDefaultsForNewUser(t *testing.T) {
|
|
uid := uuid.New()
|
|
h := mountPrefs(t, uid, newFakePrefs())
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil)
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
var dto prefsDTO
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &dto))
|
|
require.False(t, dto.EmailEnabled)
|
|
require.False(t, dto.IMEnabled)
|
|
require.False(t, dto.IMWebhookURLSet)
|
|
require.Equal(t, "warning", dto.MinSeverity)
|
|
}
|
|
|
|
func TestPrefsHandler_GetRequiresAuth(t *testing.T) {
|
|
h := mountPrefs(t, uuid.New(), newFakePrefs())
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil) // no token
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
|
}
|
|
|
|
func TestPrefsHandler_PutPersistsAndEncryptsWebhook(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
h := mountPrefs(t, uid, prefs)
|
|
|
|
body, _ := json.Marshal(prefsUpdateReq{
|
|
EmailEnabled: boolPtr(true),
|
|
IMEnabled: boolPtr(true),
|
|
IMWebhookURL: strPtr("https://hooks.slack.test/xyz"),
|
|
MinSeverity: strPtr("error"),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
var dto prefsDTO
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &dto))
|
|
require.True(t, dto.EmailEnabled)
|
|
require.True(t, dto.IMEnabled)
|
|
require.True(t, dto.IMWebhookURLSet)
|
|
require.Equal(t, "error", dto.MinSeverity)
|
|
|
|
// 落库的 webhook 应为密文(取反),解回应得回原 URL;明文绝不直接存。
|
|
saved, err := prefs.Get(context.Background(), uid)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, saved.ImWebhookURLEnc)
|
|
require.NotEqual(t, []byte("https://hooks.slack.test/xyz"), saved.ImWebhookURLEnc)
|
|
require.Equal(t, "https://hooks.slack.test/xyz", string(reverseBytes(saved.ImWebhookURLEnc)))
|
|
}
|
|
|
|
func TestPrefsHandler_PutPartialUpdatePreservesWebhook(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(Prefs{UserID: uid, IMEnabled: true, MinSeverity: SeverityWarning,
|
|
ImWebhookURLEnc: reverseBytes([]byte("https://existing"))})
|
|
h := mountPrefs(t, uid, prefs)
|
|
|
|
// 仅改 email_enabled,省略 im_webhook_url -> 保留原密文。
|
|
body, _ := json.Marshal(prefsUpdateReq{EmailEnabled: boolPtr(true)})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
saved, _ := prefs.Get(context.Background(), uid)
|
|
require.Equal(t, "https://existing", string(reverseBytes(saved.ImWebhookURLEnc)))
|
|
require.True(t, saved.EmailEnabled)
|
|
}
|
|
|
|
func TestPrefsHandler_PutEmptyWebhookClearsIt(t *testing.T) {
|
|
uid := uuid.New()
|
|
prefs := newFakePrefs()
|
|
prefs.set(Prefs{UserID: uid, IMEnabled: true, MinSeverity: SeverityWarning,
|
|
ImWebhookURLEnc: reverseBytes([]byte("https://existing"))})
|
|
h := mountPrefs(t, uid, prefs)
|
|
|
|
body, _ := json.Marshal(prefsUpdateReq{IMWebhookURL: strPtr("")})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
saved, _ := prefs.Get(context.Background(), uid)
|
|
require.Empty(t, saved.ImWebhookURLEnc)
|
|
}
|
|
|
|
func TestPrefsHandler_PutRejectsBadSeverity(t *testing.T) {
|
|
uid := uuid.New()
|
|
h := mountPrefs(t, uid, newFakePrefs())
|
|
body, _ := json.Marshal(prefsUpdateReq{MinSeverity: strPtr("catastrophic")})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/prefs", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func TestPrefsHandler_NotMountedWithoutEncryptor(t *testing.T) {
|
|
uid := uuid.New()
|
|
r := chi.NewRouter()
|
|
// WithPrefs 未调用 -> /prefs 不应被挂载。
|
|
NewHandler(&noopRepo{}, &prefsFakeResolver{uid: uid}).Mount(r)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/prefs", nil)
|
|
req.Header.Set("Authorization", "Bearer tok")
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|
|
func strPtr(s string) *string { return &s }
|