Files
glchat/internal/server/push_notify_test.go
T

222 lines
9.7 KiB
Go
Raw Normal View History

package server
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"glchat/internal/push"
"glchat/internal/store"
)
// recordingPush — подмена отправителя: запоминает, что сервер поставил в
// очередь. Реальная доставка идёт в фоне и в тесте не наблюдаема.
type recordingPush struct {
enabled bool
publicKey string
jobs []recordedPush
}
type recordedPush struct {
subscription store.PushSubscription
payload push.Payload
}
func (r *recordingPush) Enabled() bool { return r.enabled }
func (r *recordingPush) PublicKey() string { return r.publicKey }
func (r *recordingPush) Enqueue(subscription store.PushSubscription, payload push.Payload) bool {
r.jobs = append(r.jobs, recordedPush{subscription: subscription, payload: payload})
return true
}
// createTestDM открывает личную беседу между автором и указанным участником
// и возвращает идентификатор комнаты.
func createTestDM(t *testing.T, srv *Server, cookie *http.Cookie, userID string) string {
t.Helper()
rec := doJSON(t, srv, http.MethodPost, "/api/v1/users/@me/channels",
`{"recipient_id":"`+userID+`"}`, cookie)
if rec.Code != http.StatusOK {
t.Fatalf("открыть личную беседу = %d, body = %s", rec.Code, rec.Body.String())
}
payload := decodeResponse[struct {
Channel struct {
ID string `json:"id"`
} `json:"channel"`
}](t, rec)
if payload.Channel.ID == "" {
t.Fatalf("беседа без идентификатора: %s", rec.Body.String())
}
return payload.Channel.ID
}
// savePushSubscription заводит подписку устройства напрямую в БД.
func savePushSubscription(t *testing.T, st *store.Store, userID uint64, endpoint string) store.PushSubscription {
t.Helper()
subscription, err := st.SavePushSubscription(context.Background(), store.SavePushSubscriptionParams{
UserID: userID,
Endpoint: endpoint,
P256dh: "p256dh",
Auth: "auth",
})
if err != nil {
t.Fatalf("save subscription: %v", err)
}
return *subscription
}
// TestPushNotifiesDirectMessage: личное сообщение уходит на устройство
// получателя, если он не в клиенте (AGENT.md 7.16, Фаза 7).
func TestPushNotifiesDirectMessage(t *testing.T) {
srv, st := newPushTestServer(t, true)
recorder := &recordingPush{enabled: true, publicKey: "key"}
srv.push = recorder
authorCookie := registerAndLogin(t, srv, "push_author", "push-author@example.com")
registerAndLogin(t, srv, "push_target", "push-target@example.com")
targetID := mustUserID(t, srv, "push-target@example.com")
savePushSubscription(t, st, targetID, "https://fcm.googleapis.com/fcm/send/target")
channelID := createTestDM(t, srv, authorCookie, formatSnowflake(targetID))
rec := doJSON(t, srv, http.MethodPost, "/api/v1/channels/"+channelID+"/messages",
`{"content":"привет, это личное"}`, authorCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка сообщения = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 1 {
t.Fatalf("поставлено уведомлений %d, ожидалось 1", len(recorder.jobs))
}
job := recorder.jobs[0]
if job.payload.Kind != "direct" || job.payload.Body != "привет, это личное" {
t.Fatalf("неожиданное уведомление: %+v", job.payload)
}
if job.payload.ChannelID != channelID {
t.Fatalf("комната уведомления %q, ожидалась %q", job.payload.ChannelID, channelID)
}
if job.payload.URL != "/app/friends/"+channelID {
t.Fatalf("ссылка = %q", job.payload.URL)
}
if job.subscription.Endpoint != "https://fcm.googleapis.com/fcm/send/target" {
t.Fatalf("подписка = %q", job.subscription.Endpoint)
}
}
// TestPushNotifiesMention: упоминание в комнате сервера уходит получателю;
// обычное сообщение — нет.
func TestPushNotifiesMention(t *testing.T) {
fixture := newMessagingFixture(t)
recorder := &recordingPush{enabled: true, publicKey: "key"}
fixture.srv.push = recorder
savePushSubscription(t, fixture.srv.store, guildIDOf(t, fixture.memberID), "https://fcm.googleapis.com/fcm/send/member")
// Обычное сообщение: упоминаний нет — push не нужен.
rec := doJSON(t, fixture.srv, http.MethodPost, "/api/v1/channels/"+fixture.openChannel+"/messages",
`{"content":"просто сообщение"}`, fixture.ownerCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка сообщения = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 0 {
t.Fatalf("без упоминания поставлено уведомлений %d", len(recorder.jobs))
}
// Упоминание: уведомление уходит со ссылкой на комнату сервера.
rec = doJSON(t, fixture.srv, http.MethodPost, "/api/v1/channels/"+fixture.openChannel+"/messages",
`{"content":"<@`+fixture.memberID+`> посмотри"}`, fixture.ownerCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка упоминания = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 1 {
t.Fatalf("поставлено уведомлений %d, ожидалось 1", len(recorder.jobs))
}
payload := recorder.jobs[0].payload
if payload.Kind != "mention" {
t.Fatalf("вид уведомления = %q", payload.Kind)
}
if !strings.Contains(payload.Body, "посмотри") {
t.Fatalf("тело уведомления = %q", payload.Body)
}
if payload.ChannelID != fixture.openChannel {
t.Fatalf("комната уведомления %q, ожидалась %q", payload.ChannelID, fixture.openChannel)
}
if payload.URL != "/app/"+fixture.guildID+"/"+fixture.openChannel {
t.Fatalf("ссылка = %q", payload.URL)
}
if payload.Title == "" || !strings.Contains(payload.Title, "#") {
t.Fatalf("заголовок уведомления = %q", payload.Title)
}
}
// TestPushSkipsOwnMessages: свои сообщения уведомлений не порождают — как в
// desktopNotifications.ts.
func TestPushSkipsOwnMessages(t *testing.T) {
srv, st := newPushTestServer(t, true)
recorder := &recordingPush{enabled: true, publicKey: "key"}
srv.push = recorder
cookie := registerAndLogin(t, srv, "push_self", "push-self@example.com")
selfID := mustUserID(t, srv, "push-self@example.com")
savePushSubscription(t, st, selfID, "https://fcm.googleapis.com/fcm/send/self")
otherCookie := registerAndLogin(t, srv, "push_other", "push-other@example.com")
otherID := mustUserID(t, srv, "push-other@example.com")
channelID := createTestDM(t, srv, cookie, formatSnowflake(otherID))
// Отправляем другому, но получатель — тоже автор? Нет: подписка только у
// автора, а сообщение уходит второму участнику — уведомлений быть не должно.
rec := doJSON(t, srv, http.MethodPost, "/api/v1/channels/"+channelID+"/messages",
`{"content":"<@`+formatSnowflake(selfID)+`> себе"}`, cookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 0 {
t.Fatalf("своё сообщение дало уведомления: %+v", recorder.jobs)
}
_ = otherCookie
}
// TestPushSkipsOnlineUsers: если получатель в клиенте (есть соединение шлюза),
// уведомление покажет сам клиент — push не отправляем.
func TestPushSkipsOnlineUsers(t *testing.T) {
srv, st := newPushTestServer(t, true)
recorder := &recordingPush{enabled: true, publicKey: "key"}
srv.push = recorder
authorCookie := registerAndLogin(t, srv, "push_online_author", "push-online-author@example.com")
targetCookie := registerAndLogin(t, srv, "push_online_target", "push-online-target@example.com")
targetID := mustUserID(t, srv, "push-online-target@example.com")
savePushSubscription(t, st, targetID, "https://fcm.googleapis.com/fcm/send/online")
channelID := createTestDM(t, srv, authorCookie, formatSnowflake(targetID))
// Настоящее соединение шлюза за получателя: он «в клиенте».
httpServer := httptest.NewServer(srv.Handler())
t.Cleanup(httpServer.Close)
dialGateway(t, httpServer, targetCookie)
rec := doJSON(t, srv, http.MethodPost, "/api/v1/channels/"+channelID+"/messages",
`{"content":"он в клиенте"}`, authorCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 0 {
t.Fatalf("пользователю в клиенте отправлен push: %+v", recorder.jobs)
}
}
// TestPushSkippedWhenDisabled: без VAPID-ключа сервер не трогает подписки.
func TestPushSkippedWhenDisabled(t *testing.T) {
fixture := newMessagingFixture(t)
recorder := &recordingPush{enabled: false}
fixture.srv.push = recorder
savePushSubscription(t, fixture.srv.store, guildIDOf(t, fixture.memberID), "https://fcm.googleapis.com/fcm/send/off")
rec := doJSON(t, fixture.srv, http.MethodPost, "/api/v1/channels/"+fixture.openChannel+"/messages",
`{"content":"<@`+fixture.memberID+`> привет"}`, fixture.ownerCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка = %d, body = %s", rec.Code, rec.Body.String())
}
if len(recorder.jobs) != 0 {
t.Fatalf("выключенный push всё равно отправил: %+v", recorder.jobs)
}
}