You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user