mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
* fix(notifications): make preference updates atomic Co-authored-by: multica-agent <github@multica.ai> * fix(notifications): serialize preference mutations Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
142 lines
4.1 KiB
Go
142 lines
4.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/multica-ai/multica/server/internal/middleware"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
func notificationPreferenceRequest(
|
|
t *testing.T,
|
|
method string,
|
|
preferences map[string]string,
|
|
) *http.Request {
|
|
t.Helper()
|
|
|
|
member, err := testHandler.Queries.GetMemberByUserAndWorkspace(
|
|
context.Background(),
|
|
db.GetMemberByUserAndWorkspaceParams{
|
|
UserID: parseUUID(testUserID),
|
|
WorkspaceID: parseUUID(testWorkspaceID),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("load test member: %v", err)
|
|
}
|
|
|
|
req := newRequest(method, "/api/notification-preferences", map[string]any{
|
|
"preferences": preferences,
|
|
})
|
|
return req.WithContext(
|
|
middleware.SetMemberContext(req.Context(), testWorkspaceID, member),
|
|
)
|
|
}
|
|
|
|
func TestPatchNotificationPreferencesMergesWithoutReplacing(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if _, err := testPool.Exec(ctx, `
|
|
DELETE FROM notification_preference
|
|
WHERE workspace_id = $1 AND user_id = $2
|
|
`, testWorkspaceID, testUserID); err != nil {
|
|
t.Fatalf("reset notification preference: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `
|
|
DELETE FROM notification_preference
|
|
WHERE workspace_id = $1 AND user_id = $2
|
|
`, testWorkspaceID, testUserID)
|
|
})
|
|
|
|
putRecorder := httptest.NewRecorder()
|
|
testHandler.UpdateNotificationPreferences(
|
|
putRecorder,
|
|
notificationPreferenceRequest(t, http.MethodPut, map[string]string{
|
|
"status_changes": "muted",
|
|
}),
|
|
)
|
|
if putRecorder.Code != http.StatusOK {
|
|
t.Fatalf("seed preference: status=%d body=%s", putRecorder.Code, putRecorder.Body.String())
|
|
}
|
|
|
|
patchRecorder := httptest.NewRecorder()
|
|
testHandler.PatchNotificationPreferences(
|
|
patchRecorder,
|
|
notificationPreferenceRequest(t, http.MethodPatch, map[string]string{
|
|
"comments": "muted",
|
|
}),
|
|
)
|
|
if patchRecorder.Code != http.StatusOK {
|
|
t.Fatalf("patch preference: status=%d body=%s", patchRecorder.Code, patchRecorder.Body.String())
|
|
}
|
|
|
|
var response struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Preferences map[string]string `json:"preferences"`
|
|
}
|
|
if err := json.NewDecoder(patchRecorder.Body).Decode(&response); err != nil {
|
|
t.Fatalf("decode patch response: %v", err)
|
|
}
|
|
if response.WorkspaceID != testWorkspaceID {
|
|
t.Fatalf("workspace_id = %q, want %q", response.WorkspaceID, testWorkspaceID)
|
|
}
|
|
if response.Preferences["status_changes"] != "muted" {
|
|
t.Fatalf("status_changes was replaced: %#v", response.Preferences)
|
|
}
|
|
if response.Preferences["comments"] != "muted" {
|
|
t.Fatalf("comments patch missing: %#v", response.Preferences)
|
|
}
|
|
|
|
enableRecorder := httptest.NewRecorder()
|
|
testHandler.PatchNotificationPreferences(
|
|
enableRecorder,
|
|
notificationPreferenceRequest(t, http.MethodPatch, map[string]string{
|
|
"status_changes": "all",
|
|
}),
|
|
)
|
|
if enableRecorder.Code != http.StatusOK {
|
|
t.Fatalf("enable preference: status=%d body=%s", enableRecorder.Code, enableRecorder.Body.String())
|
|
}
|
|
|
|
var persisted []byte
|
|
if err := testPool.QueryRow(ctx, `
|
|
SELECT preferences
|
|
FROM notification_preference
|
|
WHERE workspace_id = $1 AND user_id = $2
|
|
`, testWorkspaceID, testUserID).Scan(&persisted); err != nil {
|
|
t.Fatalf("load persisted preference: %v", err)
|
|
}
|
|
var preferences map[string]string
|
|
if err := json.Unmarshal(persisted, &preferences); err != nil {
|
|
t.Fatalf("decode persisted preference: %v", err)
|
|
}
|
|
if preferences["status_changes"] != "all" || preferences["comments"] != "muted" {
|
|
t.Fatalf("unexpected persisted preferences: %#v", preferences)
|
|
}
|
|
}
|
|
|
|
func TestPatchNotificationPreferencesRejectsUnknownGroups(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
testHandler.PatchNotificationPreferences(
|
|
recorder,
|
|
notificationPreferenceRequest(t, http.MethodPatch, map[string]string{
|
|
"unknown_group": "muted",
|
|
}),
|
|
)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|