mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
`new_comment` and `mentioned` shared the `comments` preference group, so
muting comment volume also silenced @-mentions — and because a muted item
is never created, those mentions were lost rather than deferred. In
agent-heavy workspaces the only lever that reduces inbox noise also
removed the signal the inbox exists for.
`mentioned` now maps to its own `mentions` group. Preferences are stored
sparse (a missing key means "all"), so existing `{"comments":"muted"}`
rows default the new group to "all" with no migration: muting comments is
a volume decision, not a decision to become unreachable by name.
The split also aligns the global setting with behavior the platform
already had — per-issue unsubscribe keeps delivering direct mentions, and
mention delivery bypasses the subscriber table entirely.
Closes #6468
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
179 lines
5.3 KiB
Go
179 lines
5.3 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)
|
|
}
|
|
}
|
|
|
|
// TestPatchNotificationPreferencesAcceptsMentions covers the group added by
|
|
// #6468. The whitelist rejects unknown keys, so a client shipped ahead of the
|
|
// server would 400 on this key — the reason the server must deploy first.
|
|
func TestPatchNotificationPreferencesAcceptsMentions(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `
|
|
DELETE FROM notification_preference
|
|
WHERE workspace_id = $1 AND user_id = $2
|
|
`, testWorkspaceID, testUserID)
|
|
})
|
|
|
|
recorder := httptest.NewRecorder()
|
|
testHandler.PatchNotificationPreferences(
|
|
recorder,
|
|
notificationPreferenceRequest(t, http.MethodPatch, map[string]string{
|
|
"mentions": "muted",
|
|
}),
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
|
|
var response struct {
|
|
Preferences map[string]string `json:"preferences"`
|
|
}
|
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if response.Preferences["mentions"] != "muted" {
|
|
t.Fatalf("mentions not persisted: %#v", response.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())
|
|
}
|
|
}
|