Files
multica/server/internal/handler/notification_preference_test.go
Jiayuan Zhang 19e52e007c MUL-4798: make Inbox notification preference updates atomic (#5451)
* 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>
2026-07-15 17:52:10 +08:00

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())
}
}