feat(notify): web push as a notification channel
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m15s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m15s
- VAPID keys from VAPID_* or generated once (push_keys, migration 9); devices in push_subscriptions, forgotten on 404/410 from the push service - push column for every event (also due tasks) and the daily summary, bundles as one notification; /push/key, /devices, /subscribe, /unsubscribe, /test - settings: switch push on per device, test it, see the devices; the service worker shows notifications and focuses MindSift on click - tests decrypt a sent push like a browser (RFC 8291) and check VAPID Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a49e6b304
commit
38b8c92899
27 files changed
+918
-27
No files matched your search
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
)
|
||||
|
||||
func TestPushSubscribeKeyAndChannel(t *testing.T) {
|
||||
srv, store, _ := setupTestServer(t)
|
||||
srv.cfg.NotifyDigestMinutes = 5
|
||||
router := srv.Router()
|
||||
me, token := newTestUser(t, store, "push@example.com")
|
||||
|
||||
// The key is generated once and stays the same
|
||||
var k1, k2 struct {
|
||||
Enabled bool
|
||||
PublicKey string
|
||||
}
|
||||
_ = json.Unmarshal(sessionCall(router, "GET", "/api/v1/push/key", token, "", "").Body.Bytes(), &k1)
|
||||
_ = json.Unmarshal(sessionCall(router, "GET", "/api/v1/push/key", token, "", "").Body.Bytes(), &k2)
|
||||
if !k1.Enabled || len(k1.PublicKey) < 80 || k1.PublicKey != k2.PublicKey {
|
||||
t.Fatalf("key: %+v / %+v", k1, k2)
|
||||
}
|
||||
|
||||
if rec := sessionCall(router, "POST", "/api/v1/push/subscribe", token, `{"endpoint":"http://intern/x","keys":{"p256dh":"a","auth":"b"}}`, ""); rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("http endpoint must be refused, got %d", rec.Code)
|
||||
}
|
||||
body := `{"endpoint":"https://push.example.com/abc","keys":{"p256dh":"BAAA","auth":"CCCC"},"device":"Firefox auf Linux"}`
|
||||
if rec := sessionCall(router, "POST", "/api/v1/push/subscribe", token, body, ""); rec.Code != http.StatusOK {
|
||||
t.Fatalf("subscribe: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec := sessionCall(router, "GET", "/api/v1/push/devices", token, "", "")
|
||||
if !strings.Contains(rec.Body.String(), "Firefox auf Linux") || strings.Contains(rec.Body.String(), "CCCC") {
|
||||
t.Fatalf("devices must list the device without its keys: %s", rec.Body.String())
|
||||
}
|
||||
|
||||
// Push as a channel for watched tasks
|
||||
var pushed []pushMessage
|
||||
srv.notifyPushHook = func(userID string, msg pushMessage) error {
|
||||
if userID == me.ID {
|
||||
pushed = append(pushed, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
task, _ := store.CreateThoughtWithDetails(me.ID, "Reifen wechseln", "normal", "manual", "", "", "todo", "")
|
||||
_ = store.WatchTask(task.ID, me.ID, true)
|
||||
set := store.NotificationSettingsOf(me.ID)
|
||||
set.IncludeOwnWatched = true
|
||||
set.Events.Watched = domain.ChannelChoice{Push: true}
|
||||
_ = store.SetNotificationSettings(me.ID, set)
|
||||
sessionCall(router, "PATCH", "/api/v1/thoughts/"+task.ID, token, `{"priority":"high"}`, "")
|
||||
srv.flushNotifications(time.Now().Add(10 * time.Minute))
|
||||
if len(pushed) != 1 || !strings.Contains(pushed[0].Body, "Reifen wechseln") {
|
||||
t.Fatalf("expected one push, got %+v", pushed)
|
||||
}
|
||||
|
||||
if rec := sessionCall(router, "POST", "/api/v1/push/unsubscribe", token, `{"endpoint":"https://push.example.com/abc"}`, ""); rec.Code != http.StatusOK {
|
||||
t.Fatalf("unsubscribe: %d", rec.Code)
|
||||
}
|
||||
if rec := sessionCall(router, "POST", "/api/v1/push/test", token, "", ""); rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("test without devices: want 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushDigestText(t *testing.T) {
|
||||
one := pushDigest("Neuigkeiten", []notifyItem{{text: "Anna hat „Einkauf“ erledigt"}}, "")
|
||||
if one.Body != "Anna hat „Einkauf“ erledigt" || one.URL != "/" {
|
||||
t.Fatalf("single: %+v", one)
|
||||
}
|
||||
many := pushDigest("Neuigkeiten", []notifyItem{{text: "a"}, {text: "b"}, {text: "c"}, {text: "d"}, {text: "e"}, {text: "f"}}, "https://x/")
|
||||
if many.Title != "MindSift: Neuigkeiten" || !strings.HasSuffix(many.Body, "… und 2 weitere") {
|
||||
t.Fatalf("many: %+v", many)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user