Files
MindSift/internal/ws/hub_test.go
T
sandrodachandClaude Opus 5.5 bcc06da3ee
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m6s
fix(realtime): scope events to authorized users, authorize rooms, keep SSE alive
Phase 0 of the collaboration plan:
- Remove BroadcastToAll; task, task list and workspace folder events now
  only reach users who can see the resource. Task list events carry only
  the list ID, clients reload with their own permissions.
- Reject unauthenticated WebSocket/SSE connections (401). Every room join
  (URL room, JOIN_ROOM, SSE rooms) is checked via a room authorizer;
  /events/rooms requires login and only accepts the caller's own sseId.
- Reorder rejects tasks the caller cannot access or edit (IDOR).
- Lift the server WriteTimeout for SSE streams so they are not cut after 30s.
- Fix folder share removal sending DOC_DELETED to the share ID.
- Tests for all of the above; docs and flaw list updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:19:14 +01:00

93 lines
2.2 KiB
Go

package ws
import (
"sync"
"testing"
"time"
)
func TestHubCloseAndGracefulShutdown(t *testing.T) {
hub := NewHub()
// Register multiple dummy SSE clients
var sseClients []*SSEClient
for i := 0; i < 5; i++ {
client := &SSEClient{
id: "sse-test",
userID: "user-1",
send: make(chan []byte, 10),
}
hub.RegisterSSE(client)
sseClients = append(sseClients, client)
}
// Verify broadcast works
hub.BroadcastToUser("user-1", "TEST_EVENT", map[string]string{"msg": "hello"})
// Concurrently invoke Close() and Broadcast() to verify no panics or deadlocks
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
hub.BroadcastToUser("user-1", "SPAM", map[string]int{"i": i})
time.Sleep(1 * time.Millisecond)
}
}()
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond)
hub.Close()
}()
wg.Wait()
// Verify that all SSE client channels are closed
for _, c := range sseClients {
select {
case _, ok := <-c.send:
if ok {
// Drained a message, check next
_, ok = <-c.send
}
default:
}
}
}
func TestRoomJoinsRequireAuthorization(t *testing.T) {
hub := NewHub()
c := &Client{hub: hub, userID: "user-1", send: make(chan []byte, 10)}
if hub.JoinRoom(c, "lst_1") {
t.Fatal("join must be denied without an authorizer")
}
hub.SetRoomAuthorizer(func(userID, roomID string) bool {
return userID == "user-1" && roomID == "lst_allowed"
})
if hub.JoinRoom(c, "lst_other") {
t.Fatal("join to an unauthorized room must be denied")
}
if !hub.JoinRoom(c, "lst_allowed") {
t.Fatal("join to an authorized room must succeed")
}
anon := &Client{hub: hub, userID: "anonymous", send: make(chan []byte, 10)}
if hub.JoinRoom(anon, "lst_allowed") {
t.Fatal("anonymous clients must never join rooms")
}
sse := &SSEClient{id: "sse_1", userID: "user-1", send: make(chan []byte, 10),
subscribedRooms: map[string]bool{"lst_allowed": true, "doc:secret": true}}
hub.RegisterSSE(sse)
if sse.subscribedRooms["doc:secret"] {
t.Fatal("unauthorized initial SSE rooms must be dropped")
}
if hub.JoinSSERoomByID("sse_1", "user-2", "lst_allowed") {
t.Fatal("joining with another user's sseId must fail")
}
}