mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* fix(client): 3 defect(s) (OC-0037, OC-0063, OC-0116) Route the tray Status submenu through saveUserStatus() (mapping the legacy "offline" to "invisible") so notifications, autoIdle, and reconnect presence restore all agree with the tray's choice; build the connected overlay from the auth_ok payload instead of a pre-dispatch authStore snapshot; keep the TOTP overlay open across a rejected verify (totpPending latch) and retain the partial token for the retry instead of clearing it in finally. Hand-applied combined cluster preserved from the previous fix run's overlap-guard block (both clusters edit main.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(voice): 2 defect(s) (OC-0010, OC-0011) * fix(ws): 1 defect(s) (OC-0050) * fix(db): 1 defect(s) (OC-0052) * fix(client): 1 defect(s) (OC-0054) * fix(client): 1 defect(s) (OC-0059) * fix(auth): 1 defect(s) (OC-0061) * fix(ws): 1 defect(s) (OC-0062) * fix(client): 1 defect(s) (OC-0064) * fix(service): 1 defect(s) (OC-0070) * fix(ws): 1 defect(s) (OC-0073) * fix(service): 2 defect(s) (OC-0075, OC-0120) * fix(admin): 1 defect(s) (OC-0076) * fix(voice): 1 defect(s) (OC-0084) * fix(client): 2 defect(s) (OC-0085, OC-0094) Scope collapsed-category persistence to the connected host instead of the server display name, and stop the DM back button from jumping to the first text channel when DM mode was entered without recording channelBeforeDm. * fix(service): 1 defect(s) (OC-0087) * fix(client): 1 defect(s) (OC-0089) * fix(ws): 1 defect(s) (OC-0091) * fix(api): 1 defect(s) (OC-0093) * fix(identity): 1 defect(s) (OC-0118) * fix(dm): 1 defect(s) (OC-0119) * fix(voice): 1 defect(s) (OC-0135) * fix(api): 1 defect(s) (OC-0137) * fix(client): 1 defect(s) (OC-0142) * fix(client): 1 defect(s) (OC-0144) * fix(admin): 1 defect(s) (OC-0145) * fix(updater): 1 defect(s) (OC-0146) * fix(client): 1 defect(s) (OC-0150) * fix(mentions): 1 defect(s) (OC-0131) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
200 lines
6.3 KiB
Go
200 lines
6.3 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/owncord/server/db"
|
|
"github.com/owncord/server/ws"
|
|
)
|
|
|
|
// BroadcastUserUpdate, BroadcastDropCount and SetEventPersister had no
|
|
// coverage. The first is what propagates a profile or identity-key change to
|
|
// every connected client — an identity key that fails to propagate silently
|
|
// breaks E2EE key agreement for everyone already online.
|
|
|
|
// awaitMessage reads one message from ch, failing if none arrives.
|
|
func awaitMessage(t *testing.T, ch chan []byte) map[string]any {
|
|
t.Helper()
|
|
select {
|
|
case raw := <-ch:
|
|
var msg map[string]any
|
|
if err := json.Unmarshal(raw, &msg); err != nil {
|
|
t.Fatalf("unmarshal %q: %v", raw, err)
|
|
}
|
|
return msg
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("no message received")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func TestHub_BroadcastUserUpdate(t *testing.T) {
|
|
hub, _ := newTestHub(t)
|
|
go hub.Run()
|
|
t.Cleanup(hub.Stop)
|
|
|
|
send := make(chan []byte, 8)
|
|
client := ws.NewTestClient(hub, 1, send)
|
|
hub.RegisterNowForTest(client)
|
|
|
|
avatar := "avatar.png"
|
|
identityKey := "pubkey-abc"
|
|
hub.BroadcastUserUpdate(ws.UserUpdate{UserID: 42, Username: "renamed", Avatar: &avatar, IdentityPublicKey: &identityKey})
|
|
|
|
msg := awaitMessage(t, send)
|
|
if msg["type"] != "user_update" {
|
|
t.Fatalf("type = %v, want user_update", msg["type"])
|
|
}
|
|
payload, ok := msg["payload"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("payload is not an object: %v", msg["payload"])
|
|
}
|
|
if payload["user_id"] != float64(42) {
|
|
t.Errorf("user_id = %v, want 42", payload["user_id"])
|
|
}
|
|
if payload["username"] != "renamed" {
|
|
t.Errorf("username = %v, want renamed", payload["username"])
|
|
}
|
|
if payload["avatar"] != "avatar.png" {
|
|
t.Errorf("avatar = %v, want avatar.png", payload["avatar"])
|
|
}
|
|
// The identity key is the E2EE handshake input; dropping it here would
|
|
// leave peers unable to derive a session with this user.
|
|
if payload["identity_public_key"] != "pubkey-abc" {
|
|
t.Errorf("identity_public_key = %v, want pubkey-abc", payload["identity_public_key"])
|
|
}
|
|
}
|
|
|
|
func TestHub_BroadcastUserUpdate_NilOptionalFields(t *testing.T) {
|
|
hub, _ := newTestHub(t)
|
|
go hub.Run()
|
|
t.Cleanup(hub.Stop)
|
|
|
|
send := make(chan []byte, 8)
|
|
hub.RegisterNowForTest(ws.NewTestClient(hub, 1, send))
|
|
|
|
hub.BroadcastUserUpdate(ws.UserUpdate{UserID: 42, Username: "noextras"})
|
|
|
|
msg := awaitMessage(t, send)
|
|
payload, ok := msg["payload"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("payload is not an object: %v", msg["payload"])
|
|
}
|
|
if payload["username"] != "noextras" {
|
|
t.Errorf("username = %v, want noextras", payload["username"])
|
|
}
|
|
// A user with no avatar / no published key must serialize as null rather
|
|
// than an empty string, so clients can tell "unset" from "cleared".
|
|
if v, present := payload["avatar"]; present && v != nil {
|
|
t.Errorf("avatar = %v, want null", v)
|
|
}
|
|
if v, present := payload["identity_public_key"]; present && v != nil {
|
|
t.Errorf("identity_public_key = %v, want null", v)
|
|
}
|
|
}
|
|
|
|
func TestHub_BroadcastUserUpdate_ReachesEveryClient(t *testing.T) {
|
|
hub, _ := newTestHub(t)
|
|
go hub.Run()
|
|
t.Cleanup(hub.Stop)
|
|
|
|
a := make(chan []byte, 8)
|
|
b := make(chan []byte, 8)
|
|
hub.RegisterNowForTest(ws.NewTestClient(hub, 1, a))
|
|
hub.RegisterNowForTest(ws.NewTestClient(hub, 2, b))
|
|
|
|
hub.BroadcastUserUpdate(ws.UserUpdate{UserID: 7, Username: "everyone"})
|
|
|
|
for i, ch := range []chan []byte{a, b} {
|
|
msg := awaitMessage(t, ch)
|
|
if msg["type"] != "user_update" {
|
|
t.Errorf("client %d got type %v, want user_update", i, msg["type"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHub_BroadcastDropCount(t *testing.T) {
|
|
hub, _ := newTestHub(t)
|
|
|
|
// A hub that has broadcast nothing has dropped nothing. The admin
|
|
// diagnostics endpoint reads this counter, so a nonzero baseline would
|
|
// read as backpressure that never happened.
|
|
if got := hub.BroadcastDropCount(); got != 0 {
|
|
t.Errorf("BroadcastDropCount = %d on a fresh hub, want 0", got)
|
|
}
|
|
|
|
go hub.Run()
|
|
t.Cleanup(hub.Stop)
|
|
|
|
send := make(chan []byte, 8)
|
|
hub.RegisterNowForTest(ws.NewTestClient(hub, 1, send))
|
|
hub.BroadcastUserUpdate(ws.UserUpdate{UserID: 1, Username: "u"})
|
|
awaitMessage(t, send)
|
|
|
|
// A single delivered broadcast must not increment the drop counter.
|
|
if got := hub.BroadcastDropCount(); got != 0 {
|
|
t.Errorf("BroadcastDropCount = %d after one delivered broadcast, want 0", got)
|
|
}
|
|
}
|
|
|
|
// TestHub_ChannelReadAudience_ExcludesArchivedChannel is OC-0073:
|
|
// channelReadAudience (shared by BroadcastChannelCreate/Update and the voice
|
|
// event / CleanupVoiceForChannel fan-outs) never checked ch.Archived, unlike
|
|
// its sibling RefreshChannelVisibility which treats an archived channel as
|
|
// invisible to every role. A Member has base READ_MESSAGES with no override,
|
|
// so before archiving they are a legitimate audience member for this channel;
|
|
// archiving it must remove them from channelReadAudience even though their
|
|
// role's READ_MESSAGES grant never changed.
|
|
func TestHub_ChannelReadAudience_ExcludesArchivedChannel(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
go hub.Run()
|
|
t.Cleanup(hub.Stop)
|
|
|
|
member := seedMemberUser(t, database, "archived-audience-member")
|
|
send := make(chan []byte, 8)
|
|
hub.RegisterNowForTest(ws.NewTestClient(hub, member.ID, send))
|
|
|
|
chID := seedTestChannel(t, database, "will-be-archived")
|
|
|
|
ch, err := database.GetChannel(context.Background(), chID)
|
|
if err != nil || ch == nil {
|
|
t.Fatalf("GetChannel: %v", err)
|
|
}
|
|
if err := database.AdminUpdateChannel(context.Background(), chID, db.ChannelUpdate{
|
|
Name: ch.Name,
|
|
Topic: ch.Topic,
|
|
Category: ch.Category,
|
|
SlowMode: ch.SlowMode,
|
|
Position: ch.Position,
|
|
Archived: true,
|
|
}); err != nil {
|
|
t.Fatalf("AdminUpdateChannel: %v", err)
|
|
}
|
|
archived, err := database.GetChannel(context.Background(), chID)
|
|
if err != nil || archived == nil {
|
|
t.Fatalf("GetChannel after archive: %v", err)
|
|
}
|
|
if !archived.Archived {
|
|
t.Fatalf("channel not archived after AdminUpdateChannel")
|
|
}
|
|
|
|
hub.BroadcastChannelUpdate(archived)
|
|
|
|
assertNotReceived(t, send, "member with base READ_MESSAGES on an archived channel")
|
|
}
|
|
|
|
func TestHub_SetEventPersister(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
|
|
persister := ws.NewEventPersister(database, 16, 4, 10*time.Millisecond)
|
|
|
|
// Setting and clearing must both be safe — SetEventPersister is called at
|
|
// startup and again on shutdown/reconfiguration.
|
|
hub.SetEventPersister(persister)
|
|
hub.SetEventPersister(nil)
|
|
hub.SetEventPersister(persister)
|
|
}
|