首次推送
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
package teamspeak
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/commands"
|
||||
"github.com/honeybbq/teamspeak-go/transport"
|
||||
)
|
||||
|
||||
func TestHandleCommandLines_Empty(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
// Must not panic.
|
||||
c.handleCommandLines("")
|
||||
}
|
||||
|
||||
func TestHandleCommandLines_NewlineSeparated(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
// Two lines: an error line with a return_code + one notify.
|
||||
// We only care that neither path panics and notify fires.
|
||||
entered := make(chan struct{}, 2)
|
||||
c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
|
||||
c.rebuildMiddlewareChains()
|
||||
|
||||
line1 := "notifycliententerview clid=1 client_nickname=A" +
|
||||
" cid=1 client_type=0 client_servergroups= client_unique_identifier=x"
|
||||
line2 := "notifycliententerview clid=2 client_nickname=B" +
|
||||
" cid=1 client_type=0 client_servergroups= client_unique_identifier=y"
|
||||
c.handleCommandLines(line1 + "\n" + line2)
|
||||
|
||||
for i := range 2 {
|
||||
select {
|
||||
case <-entered:
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("notify not fired for line %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommandLines_NullByteSeparated(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
entered := make(chan struct{}, 1)
|
||||
c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
|
||||
c.rebuildMiddlewareChains()
|
||||
|
||||
line := "notifycliententerview clid=3 client_nickname=C" +
|
||||
" cid=1 client_type=0 client_servergroups= client_unique_identifier=z"
|
||||
c.handleCommandLines(line + "\x00")
|
||||
|
||||
select {
|
||||
case <-entered:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("notify not fired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommand_Notify_Routed(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
entered := make(chan struct{}, 1)
|
||||
c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
|
||||
c.rebuildMiddlewareChains()
|
||||
|
||||
line := "notifycliententerview clid=4 client_nickname=D" +
|
||||
" cid=1 client_type=0 client_servergroups= client_unique_identifier=d"
|
||||
c.handleCommand(line)
|
||||
|
||||
select {
|
||||
case <-entered:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("notification not routed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommand_Error_NoReturnCode_NoResolve(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
rc, ch := c.cmdTrack.register()
|
||||
|
||||
// error without a matching return_code should not resolve our tracker.
|
||||
c.handleCommand("error id=1 msg=fail")
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Error("should not resolve for error without matching return_code")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
c.cmdTrack.unregister(rc)
|
||||
}
|
||||
|
||||
func TestHandleCommand_Error_WithReturnCode_Resolves(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
rc, ch := c.cmdTrack.register()
|
||||
|
||||
c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if result.Err != nil {
|
||||
t.Errorf("expected nil error, got %v", result.Err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("command not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommand_Error_NonZeroID_ReturnsError(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
rc, ch := c.cmdTrack.register()
|
||||
|
||||
c.handleCommand("error id=256 msg=notfound return_code=" + mustUint32Str(rc))
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if result.Err == nil {
|
||||
t.Error("expected non-nil error for id=256")
|
||||
}
|
||||
if !strings.Contains(result.Err.Error(), "notfound") {
|
||||
t.Errorf("unexpected error: %v", result.Err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("command not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommand_DataCollectedBeforeError(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
rc, ch := c.cmdTrack.register()
|
||||
|
||||
// In the real TeamSpeak flow, data rows arrive before the "error" response.
|
||||
c.handleCommand("somedata key=val")
|
||||
c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if len(result.Data) != 1 || result.Data[0]["key"] != "val" {
|
||||
t.Errorf("unexpected data: %v", result.Data)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCommand_UnknownCommand_CollectsAsData(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
rc, ch := c.cmdTrack.register()
|
||||
|
||||
c.handleCommand("unknowncmd foo=bar baz=qux")
|
||||
c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if len(result.Data) != 1 {
|
||||
t.Fatalf("expected 1 data row, got %d", len(result.Data))
|
||||
}
|
||||
if result.Data[0]["foo"] != "bar" {
|
||||
t.Errorf("unexpected foo: %q", result.Data[0]["foo"])
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePacket_VoiceMetadata(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
received := make(chan VoiceDataEvent, 2)
|
||||
c.OnVoiceData(func(evt VoiceDataEvent) { received <- evt })
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
packetType transport.PacketType
|
||||
isWhisper bool
|
||||
}{
|
||||
{name: "channel voice", packetType: transport.PacketTypeVoice},
|
||||
{name: "whisper", packetType: transport.PacketTypeVoiceWhisper, isWhisper: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c.handlePacket(&transport.Packet{
|
||||
TypeFlagged: byte(test.packetType),
|
||||
ID: 0x1234,
|
||||
Data: []byte{0xab, 0xcd, 0x01, 0x02, 4, 0xaa, 0xbb},
|
||||
})
|
||||
|
||||
select {
|
||||
case evt := <-received:
|
||||
if evt.ClientID != 0x0102 || evt.Codec != 4 {
|
||||
t.Errorf("unexpected voice source: clientID=%d codec=%d", evt.ClientID, evt.Codec)
|
||||
}
|
||||
if evt.Sequence != 0xabcd {
|
||||
t.Errorf("sequence = %#x, want payload packet ID %#x", evt.Sequence, uint16(0xabcd))
|
||||
}
|
||||
if evt.IsWhisper != test.isWhisper {
|
||||
t.Errorf("IsWhisper = %t, want %t", evt.IsWhisper, test.isWhisper)
|
||||
}
|
||||
if string(evt.Data) != string([]byte{0xaa, 0xbb}) {
|
||||
t.Errorf("opus data = %x, want aabb", evt.Data)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("voice event not received")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePacket_VoiceSequencesArePerSpeaker(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
received := make(chan VoiceDataEvent, 4)
|
||||
c.OnVoiceData(func(evt VoiceDataEvent) { received <- evt })
|
||||
|
||||
packets := []*transport.Packet{
|
||||
{TypeFlagged: byte(transport.PacketTypeVoice), ID: 700, Data: []byte{0, 100, 0, 1, 4, 0xa0}},
|
||||
{TypeFlagged: byte(transport.PacketTypeVoice), ID: 701, Data: []byte{1, 244, 0, 2, 4, 0xb0}},
|
||||
{TypeFlagged: byte(transport.PacketTypeVoice), ID: 702, Data: []byte{0, 101, 0, 1, 4, 0xa1}},
|
||||
{TypeFlagged: byte(transport.PacketTypeVoice), ID: 703, Data: []byte{1, 245, 0, 2, 4, 0xb1}},
|
||||
}
|
||||
for _, packet := range packets {
|
||||
c.handlePacket(packet)
|
||||
}
|
||||
|
||||
sequences := map[uint16][]uint16{}
|
||||
for range packets {
|
||||
select {
|
||||
case evt := <-received:
|
||||
sequences[evt.ClientID] = append(sequences[evt.ClientID], evt.Sequence)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("voice event not received")
|
||||
}
|
||||
}
|
||||
|
||||
if got := sequences[1]; len(got) != 2 || got[0] != 100 || got[1] != 101 {
|
||||
t.Errorf("client 1 sequences = %v, want [100 101]", got)
|
||||
}
|
||||
if got := sequences[2]; len(got) != 2 || got[0] != 500 || got[1] != 501 {
|
||||
t.Errorf("client 2 sequences = %v, want [500 501]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecCommandWithResponse_Success(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
// The command sent by ExecCommandWithResponse contains "return_code=N".
|
||||
// Simulate server data row then ok error line with matching return_code.
|
||||
c.handleCommandLines("rowdata somekey=someval\nerror id=0 msg=ok return_code=1")
|
||||
}()
|
||||
|
||||
data, err := c.ExecCommandWithResponse("dummycmd", 2*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(data) != 1 || data[0]["somekey"] != "someval" {
|
||||
t.Errorf("unexpected data: %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecCommandWithResponse_Timeout(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
|
||||
start := time.Now()
|
||||
_, err := c.ExecCommandWithResponse("dummycmd", 50*time.Millisecond)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected timeout error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "timeout") {
|
||||
t.Errorf("expected 'timeout' in error, got %v", err)
|
||||
}
|
||||
if elapsed < 40*time.Millisecond || elapsed > 500*time.Millisecond {
|
||||
t.Errorf("unexpected elapsed time: %v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecCommandWithResponse_ServerError(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
c.handleCommandLines("error id=512 msg=invalid_size return_code=1")
|
||||
}()
|
||||
|
||||
_, err := c.ExecCommandWithResponse("dummycmd", 2*time.Second)
|
||||
if err == nil {
|
||||
t.Error("expected error from server")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid_size") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustUint32Str(rc uint32) string {
|
||||
return commands.ParseCommand("x return_code=" + itoa(rc)).Params["return_code"]
|
||||
}
|
||||
|
||||
func itoa(n uint32) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := make([]byte, 0, 10)
|
||||
for n > 0 {
|
||||
buf = append([]byte{byte('0' + n%10)}, buf...)
|
||||
n /= 10
|
||||
}
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
Reference in New Issue
Block a user