mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-29 20:31:54 +00:00
Keep notification counts and stopwatches in sync when tabs open or reconnect, closing the gap behind [this flake](https://github.com/go-gitea/gitea/actions/runs/31312525323/job/93242429394). 1. Send current notification counts and stopwatch state on each WebSocket connection, replacing the separate state fetches tabs made on every connect. 2. Tabs joining an already open connection fetch current state once, since its snapshot went out before they attached. 3. Send events queued during the handshake before the snapshot, so older events can't roll it back. 4. Wait for the Redis SUBSCRIBE acknowledgement, so the snapshot read after subscribing can't miss an event.
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
activities_model "gitea.dev/models/activities"
|
|
"gitea.dev/models/unittest"
|
|
"gitea.dev/modules/json"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/test"
|
|
"gitea.dev/services/pubsub"
|
|
"gitea.dev/tests"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type staleBacklogBroker struct {
|
|
pubsub.Broker
|
|
}
|
|
|
|
func (b *staleBacklogBroker) Subscribe(topic string) (<-chan []byte, func()) {
|
|
ch, cancel := b.Broker.Subscribe(topic)
|
|
for range 9 {
|
|
b.Publish(topic, countEvent(-1))
|
|
}
|
|
return ch, cancel
|
|
}
|
|
|
|
func countEvent(count int) []byte {
|
|
return fmt.Appendf(nil, "\n"+`{"eventType":"notification-count","eventData":{"count":%d}}`, count)
|
|
}
|
|
|
|
func TestWebSocketSnapshotFollowsBacklog(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
defer test.MockVariableValue(&pubsub.DefaultBroker, pubsub.Broker(&staleBacklogBroker{pubsub.NewMemoryBroker()}))()
|
|
session := loginUser(t, "user2")
|
|
server := httptest.NewServer(testWebRoutes)
|
|
defer server.Close()
|
|
|
|
header := http.Header{}
|
|
header.Set("Cookie", session.GetRawCookie(setting.SessionConfig.CookieName).String())
|
|
conn, _, err := websocket.Dial(t.Context(), strings.Replace(server.URL, "http", "ws", 1)+"/-/ws", &websocket.DialOptions{HTTPHeader: header}) //nolint:bodyclose // coder/websocket owns the body, it is nil after the upgrade
|
|
require.NoError(t, err)
|
|
defer conn.CloseNow()
|
|
|
|
const sentinelCount = -2
|
|
lastCount := 0
|
|
for {
|
|
_, payload, err := conn.Read(t.Context())
|
|
require.NoError(t, err)
|
|
var event struct {
|
|
EventType string `json:"eventType"`
|
|
EventData json.Value `json:"eventData"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(payload, &event))
|
|
if event.EventType == "stopwatches" {
|
|
pubsub.DefaultBroker.Publish(pubsub.UserTopic(2), countEvent(sentinelCount))
|
|
continue
|
|
}
|
|
var data struct {
|
|
Count int `json:"count"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(event.EventData, &data))
|
|
if data.Count == sentinelCount {
|
|
break
|
|
}
|
|
lastCount = data.Count
|
|
}
|
|
assert.Equal(t, unittest.GetCount(t, &activities_model.Notification{UserID: 2, Status: activities_model.NotificationStatusUnread}), lastCount)
|
|
}
|