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>
218 lines
6.1 KiB
Go
218 lines
6.1 KiB
Go
package ws_test
|
|
|
|
// handlers_command_test.go — tests for the chat_command handler and
|
|
// plugin EventSink wiring (Phase C Step 9).
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/owncord/server/plugin"
|
|
"github.com/owncord/server/ws"
|
|
)
|
|
|
|
// compile-time check that SetPluginRegistry is exported.
|
|
var _ = (*ws.Hub)(nil)
|
|
|
|
// ─── chat_command dispatch via HandleMessageForTest ───────────────────────────
|
|
|
|
// TestChatCommand_NoRegistry returns an error when no plugin registry is wired.
|
|
func TestChatCommand_NoRegistry_ReturnsError(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
_ = database
|
|
send := make(chan []byte, 4)
|
|
c := ws.NewTestClient(hub, 1, send)
|
|
hub.Register(c)
|
|
defer hub.Unregister(c)
|
|
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"type": "chat_command",
|
|
"payload": map[string]any{
|
|
"channel_id": int64(1),
|
|
"command": "/hello",
|
|
"args": []string{},
|
|
},
|
|
})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
select {
|
|
case msg := <-send:
|
|
var env map[string]any
|
|
if err := json.Unmarshal(msg, &env); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if env["type"] != "error" {
|
|
t.Fatalf("expected type=error, got %v; raw=%s", env["type"], msg)
|
|
}
|
|
default:
|
|
t.Fatal("expected error message to client")
|
|
}
|
|
}
|
|
|
|
// TestChatCommand_UnknownCommand returns an error when the registry has no
|
|
// plugin owning the command.
|
|
func TestChatCommand_UnknownCommand_ReturnsError(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
send := make(chan []byte, 4)
|
|
c := ws.NewTestClient(hub, 1, send)
|
|
hub.Register(c)
|
|
defer hub.Unregister(c)
|
|
|
|
reg, err := plugin.NewRegistry(plugin.Config{Store: database})
|
|
if err != nil {
|
|
t.Fatalf("NewRegistry: %v", err)
|
|
}
|
|
hub.SetPluginRegistry(reg)
|
|
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"type": "chat_command",
|
|
"payload": map[string]any{
|
|
"channel_id": int64(1),
|
|
"command": "/notexist",
|
|
"args": []string{},
|
|
},
|
|
})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
select {
|
|
case msg := <-send:
|
|
var env map[string]any
|
|
_ = json.Unmarshal(msg, &env)
|
|
if env["type"] != "error" {
|
|
t.Fatalf("expected type=error, got %v", env["type"])
|
|
}
|
|
default:
|
|
t.Fatal("expected error message to client")
|
|
}
|
|
}
|
|
|
|
// TestChatCommand_MalformedPayload returns bad-request when payload is not valid JSON.
|
|
func TestChatCommand_MalformedPayload_ReturnsBadRequest(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
_ = database
|
|
send := make(chan []byte, 4)
|
|
c := ws.NewTestClient(hub, 1, send)
|
|
hub.Register(c)
|
|
defer hub.Unregister(c)
|
|
|
|
raw := []byte(`{"type":"chat_command","payload":"not-an-object"}`)
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
select {
|
|
case msg := <-send:
|
|
var env map[string]any
|
|
_ = json.Unmarshal(msg, &env)
|
|
if env["type"] != "error" {
|
|
t.Fatalf("expected type=error, got %v", env["type"])
|
|
}
|
|
default:
|
|
t.Fatal("expected error message")
|
|
}
|
|
}
|
|
|
|
// TestChatCommand_RateLimited_ReturnsError verifies that chat_command is
|
|
// throttled per-user, same as every other V2 handler (OC-0091): a burst of
|
|
// commands beyond the limit must be rejected with RATE_LIMITED instead of
|
|
// running DispatchCommand (and therefore the plugin's WASM invocation) once
|
|
// per frame with no cap.
|
|
func TestChatCommand_RateLimited_ReturnsError(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
send := make(chan []byte, 32)
|
|
c := ws.NewTestClient(hub, 1, send)
|
|
hub.Register(c)
|
|
defer hub.Unregister(c)
|
|
|
|
reg, err := plugin.NewRegistry(plugin.Config{Store: database})
|
|
if err != nil {
|
|
t.Fatalf("NewRegistry: %v", err)
|
|
}
|
|
hub.SetPluginRegistry(reg)
|
|
|
|
sawRateLimited := false
|
|
for i := range 20 {
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"type": "chat_command",
|
|
"payload": map[string]any{
|
|
"channel_id": int64(1),
|
|
"command": "/notexist",
|
|
"args": []string{},
|
|
},
|
|
})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
select {
|
|
case msg := <-send:
|
|
var env map[string]any
|
|
if err := json.Unmarshal(msg, &env); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
payload, _ := env["payload"].(map[string]any)
|
|
if payload != nil && payload["code"] == "RATE_LIMITED" {
|
|
sawRateLimited = true
|
|
}
|
|
default:
|
|
t.Fatalf("expected a response for message %d", i)
|
|
}
|
|
}
|
|
|
|
if !sawRateLimited {
|
|
t.Fatal("expected at least one RATE_LIMITED response within 20 rapid chat_command frames")
|
|
}
|
|
}
|
|
|
|
// ─── EventSink.Emit ───────────────────────────────────────────────────────────
|
|
|
|
// TestEventSink_Emit_DeliversToBroadcaster verifies that Emit calls the wired
|
|
// broadcaster with the correct channelID and payload.
|
|
func TestEventSink_Emit_DeliversToBroadcaster(t *testing.T) {
|
|
sink := plugin.NewEventSink()
|
|
|
|
var gotChannelID int64
|
|
var gotPayload []byte
|
|
sink.SetBroadcaster(func(channelID int64, payload []byte) {
|
|
gotChannelID = channelID
|
|
gotPayload = payload
|
|
})
|
|
|
|
want := []byte(`{"type":"plugin_event"}`)
|
|
sink.Emit(42, want)
|
|
|
|
if gotChannelID != 42 {
|
|
t.Fatalf("expected channelID=42, got %d", gotChannelID)
|
|
}
|
|
if !bytes.Equal(gotPayload, want) {
|
|
t.Fatalf("expected payload=%s, got %s", want, gotPayload)
|
|
}
|
|
}
|
|
|
|
// TestEventSink_Emit_NilBroadcaster_NoOp verifies that Emit is safe when no
|
|
// broadcaster has been set.
|
|
func TestEventSink_Emit_NilBroadcaster_NoOp(t *testing.T) {
|
|
sink := plugin.NewEventSink()
|
|
sink.Emit(1, []byte(`{"type":"x"}`)) // must not panic
|
|
}
|
|
|
|
// TestEventSink_Emit_NilSink_NoOp verifies Emit is nil-safe.
|
|
func TestEventSink_Emit_NilSink_NoOp(t *testing.T) {
|
|
var sink *plugin.EventSink
|
|
sink.Emit(1, []byte(`{}`)) // must not panic
|
|
}
|
|
|
|
// ─── Hub plugin-sink wiring ───────────────────────────────────────────────────
|
|
|
|
// TestHub_SetPluginEventSink_NoOp verifies that wiring a plugin sink and
|
|
// broadcasting through the hub does not panic (default build no-ops Dispatch).
|
|
func TestHub_SetPluginEventSink_NoOp(t *testing.T) {
|
|
hub, database := newTestHub(t)
|
|
_ = database
|
|
go hub.Run()
|
|
defer hub.Stop()
|
|
|
|
sink := plugin.NewEventSink()
|
|
hub.SetPluginEventSink(sink)
|
|
|
|
// Must not panic.
|
|
hub.BroadcastToAll([]byte(`{"type":"test"}`))
|
|
}
|