Files
OwnCord/Server/ws/hub_channel_meta_test.go
T
J3vbandClaude Opus 5 35acc09121 fix(ws): filter channel metadata broadcasts by READ_MESSAGES (F9)
channel_create and channel_update were handed to BroadcastToAll and enqueued
with channelID 0, so the full channel payload -- name, topic and category of a
channel that channel_overrides hides from the recipient's role -- went to every
connected client and was replayed unconditionally from the ring buffer. Both
now resolve an audience through the same READ_MESSAGES helper the voice path
uses and enqueue under the real channel id, which filters live delivery and
both replay tiers by one mechanism. channel_delete stays unfiltered by design:
the row is already gone, so a check there would strand the channel in the
sidebar of users who saw it via a positive override.

Verified by a panel of agents; a base-revert control fails on both the live
leak and the replay leak, while the pre-existing broadcast tests pass
unmodified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:16:01 +02:00

113 lines
4.3 KiB
Go

package ws_test
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/ws"
)
// countChannelMetaFor counts channel_create / channel_update events for a
// specific channel id in a batch of raw WS frames.
func countChannelMetaFor(msgs [][]byte, channelID int64) int {
n := 0
for _, m := range msgs {
var env struct {
Type string `json:"type"`
Payload struct {
ID int64 `json:"id"`
} `json:"payload"`
}
if json.Unmarshal(m, &env) != nil {
continue
}
if env.Payload.ID != channelID {
continue
}
if env.Type == "channel_create" || env.Type == "channel_update" {
n++
}
}
return n
}
// TestChannelMetadata_NotDeliveredToRolesDeniedRead locks the visibility
// invariant for channel metadata: channel_create / channel_update used to go
// out via BroadcastToAll, so every authenticated client learned the name,
// category and topic of a channel that channel_overrides hides from their role —
// live, and again on reconnect, since an event stored under channelID 0 is
// replayed unconditionally. A role that may READ the channel must still receive
// both, live and on replay.
func TestChannelMetadata_NotDeliveredToRolesDeniedRead(t *testing.T) {
hub, database := newHandlerHub(t)
pubID := seedTestChannel(t, database, "chmeta-general")
privID := seedTestChannel(t, database, "chmeta-leadership")
insider := seedMemberUser(t, database, "chmeta-insider") // role 4: base READ_MESSAGES
outsider := seedModUser(t, database, "chmeta-outsider") // role 3: denied READ below
if err := database.UpsertChannelOverride(
context.Background(), privID, outsider.RoleID, 0, permissions.ReadMessages,
); err != nil {
t.Fatalf("UpsertChannelOverride: %v", err)
}
insiderSend := make(chan []byte, 64)
outsiderSend := make(chan []byte, 64)
hub.Register(ws.NewTestClientWithUser(hub, insider, 0, insiderSend))
hub.Register(ws.NewTestClientWithUser(hub, outsider, 0, outsiderSend))
time.Sleep(30 * time.Millisecond)
pub := &db.Channel{ID: pubID, Name: "chmeta-general", Type: "text", Category: "Text"}
priv := &db.Channel{
ID: privID, Name: "chmeta-leadership", Type: "text",
Category: "Staff", Topic: "acquisition talks",
}
hub.BroadcastChannelCreate(pub)
hub.BroadcastChannelUpdate(pub)
hub.BroadcastChannelCreate(priv)
hub.BroadcastChannelUpdate(priv)
time.Sleep(150 * time.Millisecond)
// ── live delivery ─────────────────────────────────────────────────────────
insiderLive := drainChanTimeout(insiderSend, 200*time.Millisecond)
outsiderLive := drainChanTimeout(outsiderSend, 200*time.Millisecond)
if got := countChannelMetaFor(insiderLive, privID); got != 2 {
t.Errorf("insider received %d channel_create/channel_update for the private channel, want 2", got)
}
// Positive control: the outsider is connected and receiving, so a zero count
// on the private channel is filtering and not a broken delivery path.
if got := countChannelMetaFor(outsiderLive, pubID); got != 2 {
t.Errorf("outsider received %d channel_create/channel_update for the readable channel, want 2", got)
}
if got := countChannelMetaFor(outsiderLive, privID); got != 0 {
t.Errorf("a role denied READ received %d channel metadata events for the private channel, want 0", got)
}
// ── reconnect replay ──────────────────────────────────────────────────────
oldest := hub.ReplayBuffer().OldestSeq()
if oldest == 0 {
t.Fatal("replay buffer recorded no channel events (oldest seq is 0)")
}
replayFor := func(u *db.User) [][]byte {
t.Helper()
allowed, err := hub.ComputeAllowedChannelsForTest(database, u)
if err != nil {
t.Fatalf("ComputeAllowedChannelsForTest: %v", err)
}
return hub.ReplayBuffer().EventsSinceFiltered(oldest+1, allowed)
}
if got := countChannelMetaFor(replayFor(insider), privID); got != 2 {
t.Errorf("insider replay contained %d channel metadata events for the private channel, want 2", got)
}
if got := countChannelMetaFor(replayFor(outsider), privID); got != 0 {
t.Errorf("replay leaked %d channel metadata events for the private channel to a role denied READ, want 0", got)
}
}