CI / ci (push) Successful in 8m31s
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
153 lines
7.2 KiB
Go
153 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
|
)
|
|
|
|
const notificationsPath = "/api/v1/settings/notifications"
|
|
|
|
// seedPrefs is the seed notification preferences as GET answers them.
|
|
var seedPrefs = []any{
|
|
map[string]any{"event": "plan_ready", "push": true, "chat": true},
|
|
map[string]any{"event": "list_ready", "push": true, "chat": true},
|
|
map[string]any{"event": "day_of_reminder", "push": true, "chat": false},
|
|
map[string]any{"event": "new_suggestions", "push": true, "chat": false},
|
|
map[string]any{"event": "rotation_review", "push": true, "chat": false},
|
|
}
|
|
|
|
func TestGetNotificationSettings(t *testing.T) {
|
|
t.Parallel()
|
|
router := newTestRouterWith(t, "dev", newTestSettings(t), newTestRunner(t))
|
|
|
|
rec, body := serve(t, router, signedIn(http.MethodGet, notificationsPath, nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body)
|
|
}
|
|
want := map[string]any{"prefs": seedPrefs, "reminder_time": "16:30"}
|
|
if !reflect.DeepEqual(body, want) {
|
|
t.Errorf("body = %v, want %v", body, want)
|
|
}
|
|
}
|
|
|
|
const allChanged = `{"prefs":[
|
|
{"event":"plan_ready","push":false,"chat":true},
|
|
{"event":"list_ready","push":true,"chat":false},
|
|
{"event":"day_of_reminder","push":true,"chat":true},
|
|
{"event":"new_suggestions","push":false,"chat":false},
|
|
{"event":"rotation_review","push":false,"chat":true}
|
|
],"reminder_time":"15:45"}`
|
|
|
|
func TestPutNotificationSettings(t *testing.T) {
|
|
t.Parallel()
|
|
settings := newTestSettings(t)
|
|
router := newTestRouterWith(t, "dev", settings, newTestRunner(t))
|
|
|
|
var notified []household.Settings
|
|
settings.Subscribe(func(s household.Settings) { notified = append(notified, s) })
|
|
|
|
// The events come in any order and are answered in screen order.
|
|
rec, body := serve(t, router, signedIn(http.MethodPut, notificationsPath, strings.NewReader(`{"reminder_time":"15:45","prefs":[
|
|
{"event":"rotation_review","push":false,"chat":true},
|
|
{"event":"new_suggestions","push":false,"chat":false},
|
|
{"event":"day_of_reminder","push":true,"chat":true},
|
|
{"event":"list_ready","push":true,"chat":false},
|
|
{"event":"plan_ready","push":false,"chat":true}
|
|
]}`)))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body)
|
|
}
|
|
want := map[string]any{
|
|
"prefs": []any{
|
|
map[string]any{"event": "plan_ready", "push": false, "chat": true},
|
|
map[string]any{"event": "list_ready", "push": true, "chat": false},
|
|
map[string]any{"event": "day_of_reminder", "push": true, "chat": true},
|
|
map[string]any{"event": "new_suggestions", "push": false, "chat": false},
|
|
map[string]any{"event": "rotation_review", "push": false, "chat": true},
|
|
},
|
|
"reminder_time": "15:45",
|
|
}
|
|
if !reflect.DeepEqual(body, want) {
|
|
t.Errorf("PUT body = %v, want %v", body, want)
|
|
}
|
|
if _, got := serve(t, router, signedIn(http.MethodGet, notificationsPath, nil)); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GET after PUT = %v, want %v", got, want)
|
|
}
|
|
|
|
// reminder_time is the household setting: there is one copy of it.
|
|
_, hs := serve(t, router, signedIn(http.MethodGet, settingsPath, nil))
|
|
if hs["reminder_time"] != "15:45" || hs["updated_by"] != testSession.Subject {
|
|
t.Errorf("household settings after PUT = %v, want reminder_time 15:45 updated by %s", hs, testSession.Subject)
|
|
}
|
|
if len(notified) != 1 || notified[0].ReminderTime != "15:45" {
|
|
t.Errorf("subscriber notified with %+v, want one change to 15:45", notified)
|
|
}
|
|
|
|
// A household settings change to reminder_time is what GET answers.
|
|
if rec, _ := serve(t, router, signedIn(http.MethodPatch, settingsPath, strings.NewReader(`{"reminder_time":"17:05"}`))); rec.Code != http.StatusOK {
|
|
t.Fatalf("PATCH household status = %d: %s", rec.Code, rec.Body)
|
|
}
|
|
if _, got := serve(t, router, signedIn(http.MethodGet, notificationsPath, nil)); got["reminder_time"] != "17:05" {
|
|
t.Errorf("GET reminder_time after PATCH household = %v, want 17:05", got["reminder_time"])
|
|
}
|
|
}
|
|
|
|
func TestPutNotificationSettingsRejected(t *testing.T) {
|
|
t.Parallel()
|
|
settings := newTestSettings(t)
|
|
router := newTestRouterWith(t, "dev", settings, newTestRunner(t))
|
|
|
|
const four = `{"event":"plan_ready","push":true,"chat":true},
|
|
{"event":"list_ready","push":true,"chat":true},
|
|
{"event":"day_of_reminder","push":true,"chat":true},
|
|
{"event":"new_suggestions","push":true,"chat":true}`
|
|
for _, tc := range []struct {
|
|
name, body, code, message string
|
|
}{
|
|
{"bad time", strings.Replace(allChanged, "15:45", "24:00", 1), "invalid_settings", `reminder_time: invalid time "24:00"`},
|
|
{"unpadded time", strings.Replace(allChanged, "15:45", "9:00", 1), "invalid_settings", `reminder_time: invalid time "9:00"`},
|
|
{"missing event", `{"prefs":[` + four + `],"reminder_time":"16:30"}`, "invalid_settings", `prefs: event "rotation_review" is missing`},
|
|
{"unknown event", `{"prefs":[` + four + `,{"event":"rotation_review","push":true,"chat":true},{"event":"weather","push":true,"chat":true}],"reminder_time":"16:30"}`, "invalid_settings", `prefs: unknown event "weather"`},
|
|
{"duplicate event", `{"prefs":[` + four + `,{"event":"plan_ready","push":true,"chat":true}],"reminder_time":"16:30"}`, "invalid_settings", `prefs: event "plan_ready" is listed more than once`},
|
|
{"unknown field", strings.Replace(allChanged, `"reminder_time"`, `"review_time":"09:00","reminder_time"`, 1), "invalid_request", `unknown field "review_time"`},
|
|
{"unknown pref field", strings.Replace(allChanged, `"chat":true}`, `"chat":true,"email":true}`, 1), "invalid_request", `unknown field "email"`},
|
|
{"missing reminder_time", `{"prefs":[]}`, "invalid_request", "prefs and reminder_time are required"},
|
|
{"missing channel", strings.Replace(allChanged, `"push":false,"chat":true}`, `"push":false}`, 1), "invalid_request", "prefs[0]: event, push and chat are required"},
|
|
{"null channel", strings.Replace(allChanged, `"chat":true}`, `"chat":null}`, 1), "invalid_request", "prefs[0]: event, push and chat are required"},
|
|
{"null field", `{"prefs":null,"reminder_time":"16:30"}`, "invalid_request", "prefs: must not be null"},
|
|
{"wrong type", strings.Replace(allChanged, `"push":false`, `"push":"no"`, 1), "invalid_request", "want a JSON bool"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rec, body := serve(t, router, signedIn(http.MethodPut, notificationsPath, strings.NewReader(tc.body)))
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400: %s", rec.Code, rec.Body)
|
|
}
|
|
e, _ := body["error"].(map[string]any)
|
|
if e["code"] != tc.code || !strings.Contains(e["message"].(string), tc.message) {
|
|
t.Errorf("error = %v, want code %s with %q", e, tc.code, tc.message)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Nothing was changed.
|
|
_, got := serve(t, router, signedIn(http.MethodGet, notificationsPath, nil))
|
|
if want := (map[string]any{"prefs": seedPrefs, "reminder_time": "16:30"}); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GET after rejected PUTs = %v, want the seed %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPutNotificationSettingsNeedsCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
router := newTestRouterWith(t, "dev", newTestSettings(t), newTestRunner(t))
|
|
|
|
req := signedIn(http.MethodPut, notificationsPath, strings.NewReader(allChanged))
|
|
req.Header.Del(CSRFHeader)
|
|
if rec, _ := serve(t, router, req); rec.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want 403: %s", rec.Code, rec.Body)
|
|
}
|
|
}
|