MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m6s
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>
93 lines
2.2 KiB
Go
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")
|
|
}
|
|
}
|