package teamspeak import ( "testing" "time" "github.com/honeybbq/teamspeak-go/commands" ) func TestHandleClientEnterView_AddsToClients(t *testing.T) { c := newTestClient(t) const enterCmd = "notifycliententerview clid=5 client_nickname=Alice" + " cid=10 client_type=0 client_servergroups= client_unique_identifier=uid123" c.handleClientEnterView(commands.ParseCommand(enterCmd)) c.mu.Lock() info, ok := c.clients[5] c.mu.Unlock() if !ok { t.Fatal("expected client 5 to be in clients map") } if info.Nickname != "Alice" { t.Errorf("expected nickname 'Alice', got %q", info.Nickname) } if info.ChannelID != 10 { t.Errorf("expected channelID 10, got %d", info.ChannelID) } if info.UID != "uid123" { t.Errorf("expected uid 'uid123', got %q", info.UID) } } func TestHandleClientEnterView_TriggersCallback(t *testing.T) { c := newTestClient(t) entered := make(chan ClientInfo, 1) c.OnClientEnter(func(ci ClientInfo) { entered <- ci }) c.rebuildMiddlewareChains() const enterCmd2 = "notifycliententerview clid=5 client_nickname=Alice" + " cid=10 client_type=0 client_servergroups= client_unique_identifier=uid123" c.handleClientEnterView(commands.ParseCommand(enterCmd2)) select { case ci := <-entered: if ci.ID != 5 { t.Errorf("expected ID 5, got %d", ci.ID) } case <-time.After(time.Second): t.Error("OnClientEnter callback not called") } } func TestHandleClientEnterView_SetsOwnClidOnNicknameMatch(t *testing.T) { c := newTestClient(t) const enterSelf = "notifycliententerview clid=7 client_nickname=TestBot" + " cid=1 client_type=0 client_servergroups= client_unique_identifier=x" c.handleClientEnterView(commands.ParseCommand(enterSelf)) c.mu.Lock() clid := c.clid c.mu.Unlock() if clid != 7 { t.Errorf("expected own clid=7 after nickname match, got %d", clid) } } func TestHandleClientEnterView_InvalidClidIgnored(t *testing.T) { c := newTestClient(t) const enterZero = "notifycliententerview clid=0 client_nickname=X" + " cid=1 client_type=0 client_servergroups= client_unique_identifier=x" c.handleClientEnterView(commands.ParseCommand(enterZero)) c.mu.Lock() _, ok := c.clients[0] c.mu.Unlock() if ok { t.Error("client with clid=0 should be ignored") } } func TestHandleClientLeftView_RemovesFromClients(t *testing.T) { c := newTestClient(t) c.clients[5] = ClientInfo{ID: 5, Nickname: "Alice"} cmd := commands.ParseCommand("notifyclientleftview clid=5 reasonid=8 reasonmsg=") c.handleClientLeftView(cmd) c.mu.Lock() _, ok := c.clients[5] c.mu.Unlock() if ok { t.Error("expected client 5 to be removed") } } func TestHandleClientLeftView_KickSelf_TriggersKicked(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clid = 5 c.clients[5] = ClientInfo{ID: 5} c.mu.Unlock() kicked := make(chan string, 1) c.OnKicked(func(msg string) { kicked <- msg }) cmd := commands.ParseCommand("notifyclientleftview clid=5 reasonid=5 reasonmsg=banned") c.handleClientLeftView(cmd) select { case msg := <-kicked: if msg != "banned" { t.Errorf("expected 'banned', got %q", msg) } case <-time.After(time.Second): t.Error("OnKicked not called") } } func TestHandleClientLeftView_NonKickReasonid_NoKickedCallback(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clid = 5 c.clients[5] = ClientInfo{ID: 5} c.mu.Unlock() kicked := make(chan string, 1) c.OnKicked(func(msg string) { kicked <- msg }) // reasonid=8 = normal leave, not a kick cmd := commands.ParseCommand("notifyclientleftview clid=5 reasonid=8 reasonmsg=") c.handleClientLeftView(cmd) select { case <-kicked: t.Error("OnKicked should not be called for normal leave") case <-time.After(50 * time.Millisecond): } } func TestHandleClientMoved_UpdatesChannelID(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clients[3] = ClientInfo{ID: 3, ChannelID: 10} c.mu.Unlock() const moveCmd = "notifyclientmoved clid=3 ctid=20 reasonid=0" + " invokerid=1 invokername=Admin invokeruid=admin" c.handleClientMoved(commands.ParseCommand(moveCmd)) c.mu.Lock() info := c.clients[3] c.mu.Unlock() if info.ChannelID != 20 { t.Errorf("expected channelID=20, got %d", info.ChannelID) } } func TestHandleClientMoved_TriggersCallback(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clients[3] = ClientInfo{ID: 3} c.mu.Unlock() moved := make(chan ClientMovedEvent, 1) c.OnClientMoved(func(e ClientMovedEvent) { moved <- e }) c.rebuildMiddlewareChains() const moveCmd2 = "notifyclientmoved clid=3 ctid=20 reasonid=0" + " invokerid=1 invokername=Admin invokeruid=admin" c.handleClientMoved(commands.ParseCommand(moveCmd2)) select { case e := <-moved: if e.ID != 3 || e.TargetChannelID != 20 { t.Errorf("unexpected event: %+v", e) } case <-time.After(time.Second): t.Error("OnClientMoved callback not called") } } func TestHandleTextMessage_CallsCallback(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clients[2] = ClientInfo{ID: 2, UID: "invokeruid"} c.mu.Unlock() msgs := make(chan TextMessage, 1) c.OnTextMessage(func(m TextMessage) { msgs <- m }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand( "notifytextmessage targetmode=1 target=7 invokerid=2 " + "invokername=Bob invokeruid=notifyuid msg=hello", ) c.handleTextMessage(cmd) select { case m := <-msgs: if m.Message != "hello" { t.Errorf("expected 'hello', got %q", m.Message) } if m.TargetID != 7 { t.Errorf("expected TargetID 7, got %d", m.TargetID) } if m.InvokerUID != "notifyuid" { t.Errorf("expected InvokerUID from notify payload, got %q", m.InvokerUID) } case <-time.After(time.Second): t.Error("OnTextMessage not called") } } func TestHandleTextMessage_FallbackToClientCacheUID(t *testing.T) { c := newTestClient(t) c.mu.Lock() c.clients[99] = ClientInfo{ID: 99, UID: "cacheduid"} c.mu.Unlock() msgs := make(chan TextMessage, 1) c.OnTextMessage(func(m TextMessage) { msgs <- m }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand("notifytextmessage targetmode=2 invokerid=99 invokername=Unknown msg=hi") c.handleTextMessage(cmd) select { case m := <-msgs: if m.InvokerUID != "cacheduid" { t.Errorf("expected cached UID fallback, got %q", m.InvokerUID) } if m.TargetID != 0 { t.Errorf("expected zero TargetID when target missing, got %d", m.TargetID) } case <-time.After(time.Second): t.Error("OnTextMessage not called") } } func TestHandleTextMessage_MissingInvoker_NoUID(t *testing.T) { c := newTestClient(t) msgs := make(chan TextMessage, 1) c.OnTextMessage(func(m TextMessage) { msgs <- m }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand("notifytextmessage targetmode=2 invokerid=99 invokername=Unknown msg=hi") c.handleTextMessage(cmd) select { case m := <-msgs: if m.InvokerUID != "" { t.Errorf("expected empty UID for unknown invoker, got %q", m.InvokerUID) } case <-time.After(time.Second): t.Error("OnTextMessage not called") } } func TestHandleClientPoke_TriggersCallback(t *testing.T) { c := newTestClient(t) poked := make(chan PokeEvent, 1) c.OnPoked(func(e PokeEvent) { poked <- e }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand( "notifyclientpoke invokerid=5 invokername=Alice invokeruid=uid123 msg=hello", ) c.handleClientPoke(cmd) select { case e := <-poked: if e.InvokerID != 5 { t.Errorf("expected InvokerID 5, got %d", e.InvokerID) } if e.InvokerName != "Alice" { t.Errorf("expected InvokerName 'Alice', got %q", e.InvokerName) } if e.InvokerUID != "uid123" { t.Errorf("expected InvokerUID 'uid123', got %q", e.InvokerUID) } if e.Message != "hello" { t.Errorf("expected Message 'hello', got %q", e.Message) } case <-time.After(time.Second): t.Error("OnPoked callback not called") } } func TestHandleClientPoke_EmptyMessage(t *testing.T) { c := newTestClient(t) poked := make(chan PokeEvent, 1) c.OnPoked(func(e PokeEvent) { poked <- e }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand( "notifyclientpoke invokerid=3 invokername=Bob invokeruid=uid456 msg=", ) c.handleClientPoke(cmd) select { case e := <-poked: if e.Message != "" { t.Errorf("expected empty message, got %q", e.Message) } case <-time.After(time.Second): t.Error("OnPoked callback not called") } } func TestHandleNotification_DispatchesPoke(t *testing.T) { c := newTestClient(t) poked := make(chan PokeEvent, 1) c.OnPoked(func(e PokeEvent) { poked <- e }) c.rebuildMiddlewareChains() cmd := commands.ParseCommand( "notifyclientpoke invokerid=5 invokername=Alice invokeruid=uid123 msg=hey", ) c.handleNotification(cmd) select { case e := <-poked: if e.Message != "hey" { t.Errorf("expected 'hey', got %q", e.Message) } case <-time.After(time.Second): t.Error("poke not dispatched through handleNotification") } } func TestHandleNotification_UnknownNotification_NoError(t *testing.T) { c := newTestClient(t) cmd := commands.ParseCommand("notifyunknowncommand foo=bar") c.handleNotification(cmd) } func TestHandleNotification_FileTransfer_StartUpload(t *testing.T) { c := newTestClient(t) ch := make(chan any, 1) c.ftTrack.register() cmd := commands.ParseCommand("notifystartupload clientftfid=1 serverftfid=100 ftkey=abc123 port=30033 seekpos=0") c.handleNotification(cmd) close(ch) }