package teamspeak import ( "encoding/binary" "strings" "testing" "time" "github.com/honeybbq/teamspeak-go/commands" "github.com/honeybbq/teamspeak-go/crypto" "github.com/honeybbq/teamspeak-go/transport" ) func TestHandleInitServer_SetsStatusConnected(t *testing.T) { c := newTestClient(t) c.handleCommand("initserver aclid=7 virtualserver_name=TestServer") c.mu.Lock() status := c.status clid := c.clid c.mu.Unlock() if status != StatusConnected { t.Errorf("expected StatusConnected, got %v", status) } if clid != 7 { t.Errorf("expected clid=7, got %d", clid) } } func TestHandleInitServer_FallsBackToClid(t *testing.T) { c := newTestClient(t) c.handleCommand("initserver clid=42 virtualserver_name=X") c.mu.Lock() clid := c.clid c.mu.Unlock() if clid != 42 { t.Errorf("expected clid=42, got %d", clid) } } func TestHandleInitServer_ClosesConnectedChan(t *testing.T) { c := newTestClient(t) c.handleCommand("initserver aclid=3 virtualserver_name=X") select { case <-c.connectedChan: case <-time.After(time.Second): t.Error("connectedChan was not closed after initserver") } } func TestHandleInitServer_TriggersOnConnected(t *testing.T) { c := newTestClient(t) connected := make(chan struct{}, 1) c.OnConnected(func() { connected <- struct{}{} }) c.handleCommand("initserver aclid=1") select { case <-connected: case <-time.After(time.Second): t.Error("OnConnected not called after initserver") } } func TestHandleInitServer_CalledTwice_StillConnected(t *testing.T) { c := newTestClient(t) c.handleCommand("initserver aclid=1") c.handleCommand("initserver aclid=2") c.mu.Lock() clid := c.clid c.mu.Unlock() if clid != 2 { t.Errorf("expected clid=2 after second initserver, got %d", clid) } } // handleHandshakeInitIV (old path) func TestHandleHandshakeInitIV_InvalidAlpha_NoSendClientInit(t *testing.T) { c := newTestClient(t) // Invalid base64 for alpha: InitCrypto fails, handleHandshakeInitIV returns. // Must not panic. c.handleCommand("clientinitiv alpha=!!! beta=AAAAAAAAAA omega=") } func TestHandleHandshakeInitIV_ValidParams_SendsClientInit(t *testing.T) { c, serverConn := newTestClientWithPipe(t) // Drain the initial Init1 packet from Start(). _ = readFromPipe(t, serverConn) // Wire OnPacket so that the handler routes Init1 packets to handlePacket. c.handler.OnPacket = c.handlePacket c.handler.OnClosed = func(err error) {} // A valid old-style handshake: alpha + beta are 10-byte base64, omega is the // server's public key. We use the test identity's own public key as omega // (InitCrypto only validates format, not that it comes from a server). pubKey := c.crypt.Identity.PublicKeyBase64() cmd := "clientinitiv alpha=AAAAAAAAAA== beta=AAAAAAAAAA== omega=" + pubKey c.handleCommand(cmd) // After successful InitCrypto, sendClientInit is called and a Command packet // (type 2) is sent through the handler. The payload is encrypted. pkt := readFromPipe(t, serverConn) if len(pkt) < 13 { t.Fatalf("expected clientinit packet, got %d bytes", len(pkt)) } // C2S header[4] byte (index 12 in S2C layout) = TypeFlagged; lower nibble = type. pktType := pkt[12] & 0x0F if pktType != 0x02 { t.Errorf("expected PacketTypeCommand (2), got %d", pktType) } } // handleHandshakeExpand2 (new path) func TestHandleHandshakeExpand2_InvalidBeta_NoSendClientInit(t *testing.T) { c := newTestClient(t) // Invalid base64 for beta: DecodeString fails, returns early. c.handleCommand("initivexpand2 l= omega= proof= beta=!!!") } func TestBuildClientInitCommand_DefaultAuthFieldsAreEmpty(t *testing.T) { c := newTestClient(t) cmd := commands.ParseCommand(c.buildClientInitCommand()) if cmd == nil { t.Fatal("expected clientinit command") } if got := cmd.Params["client_default_channel"]; got != "" { t.Errorf("expected empty client_default_channel, got %q", got) } if got := cmd.Params["client_default_channel_password"]; got != "" { t.Errorf("expected empty client_default_channel_password, got %q", got) } if got := cmd.Params["client_server_password"]; got != "" { t.Errorf("expected empty client_server_password, got %q", got) } } func TestBuildClientInitCommand_IncludesConfiguredHandshakeCredentials(t *testing.T) { id, err := crypto.IdentityFromString(testClientIdentity) if err != nil { t.Fatalf("IdentityFromString: %v", err) } c := NewClient( id, "127.0.0.1:9987", "Test Bot", WithServerPassword("server secret"), WithDefaultChannel("Lobby Alpha"), WithDefaultChannelPassword("channel secret"), ) cmd := commands.ParseCommand(c.buildClientInitCommand()) if cmd == nil { t.Fatal("expected clientinit command") } if got := cmd.Params["client_server_password"]; got != prepareClientPassword("server secret") { t.Errorf("expected client_server_password to be %q, got %q", prepareClientPassword("server secret"), got) } if got := cmd.Params["client_default_channel"]; got != "Lobby Alpha" { t.Errorf("expected client_default_channel to be %q, got %q", "Lobby Alpha", got) } if got := cmd.Params["client_default_channel_password"]; got != prepareClientPassword("channel secret") { t.Errorf( "expected client_default_channel_password to be %q, got %q", prepareClientPassword("channel secret"), got, ) } } func TestBuildClientInitCommand_PreservesCredentialFieldOrder(t *testing.T) { id, err := crypto.IdentityFromString(testClientIdentity) if err != nil { t.Fatalf("IdentityFromString: %v", err) } c := NewClient( id, "127.0.0.1:9987", "Test Bot", WithServerPassword("server secret"), WithDefaultChannel("Lobby Alpha"), WithDefaultChannelPassword("channel secret"), ) raw := c.buildClientInitCommand() defaultChannelIndex := indexOfOrFail(t, raw, "client_default_channel=Lobby\\sAlpha") defaultChannelPasswordIndex := indexOfOrFail( t, raw, "client_default_channel_password="+commands.Escape(prepareClientPassword("channel secret")), ) serverPasswordIndex := indexOfOrFail( t, raw, "client_server_password="+commands.Escape(prepareClientPassword("server secret")), ) metaDataIndex := indexOfOrFail(t, raw, "client_meta_data=") if defaultChannelIndex >= defaultChannelPasswordIndex || defaultChannelPasswordIndex >= serverPasswordIndex || serverPasswordIndex >= metaDataIndex { t.Fatalf("unexpected credential field order in %q", raw) } } func TestHandlePacket_Init1_Step0_SendsStep1(t *testing.T) { c, serverConn := newTestClientWithPipe(t) c.handler.OnPacket = c.handlePacket c.handler.OnClosed = func(error) {} // Drain initial C2S Init1 from Start(). _ = readFromPipe(t, serverConn) // Build step-0 server response: data[0]=0x00 step0 := make([]byte, 21) step0[0] = 0x00 binary.LittleEndian.PutUint32(step0[9:13], 0xCAFEBABE) // Wrap as S2C Init1 raw packet: [8 tag][2 ID][1 TypeFlagged][21 payload] pktBytes := make([]byte, 8+3+len(step0)) pktBytes[10] = 0x08 // PacketTypeInit1 copy(pktBytes[11:], step0) _, writeErr := serverConn.Write(pktBytes) if writeErr != nil { t.Fatalf("Write: %v", writeErr) } // handlePacket calls handler.SendPacket(8, step1Response, 0), // which writes through the pipe. resp := readFromPipe(t, serverConn) if len(resp) < 13 { t.Fatalf("expected step-1 response, got %d bytes", len(resp)) } if resp[12]&0x0F != 0x08 { t.Errorf("expected PacketTypeInit1 (8), got %d", resp[12]&0x0F) } // Step-1 payload starts with 0x01 if resp[13] != 0x01 { t.Errorf("expected step 0x01 payload, got 0x%02x", resp[13]) } } func TestHandlePacket_CommandType_RoutedToHandleCommandLines(t *testing.T) { c := newTestClient(t) entered := make(chan struct{}, 1) c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} }) c.rebuildMiddlewareChains() enterLine := "notifycliententerview clid=9 client_nickname=G" + " cid=1 client_type=0 client_servergroups= client_unique_identifier=g" p := &transport.Packet{ TypeFlagged: 0x02, // PacketTypeCommand Data: []byte(enterLine), } c.handlePacket(p) select { case <-entered: case <-time.After(time.Second): t.Error("handlePacket did not route Command to handleCommandLines") } } // readFromPipe reads one datagram from the server-side pipe with a 2s timeout. func readFromPipe(t *testing.T, server *pipePair) []byte { t.Helper() buf := make([]byte, 4096) done := make(chan []byte, 1) go func() { n, readErr := server.Read(buf) if readErr != nil { done <- nil return } cp := make([]byte, n) copy(cp, buf[:n]) done <- cp }() select { case data := <-done: return data case <-time.After(2 * time.Second): t.Fatal("readFromPipe: timed out") return nil } } func indexOfOrFail(t *testing.T, s string, needle string) int { t.Helper() idx := strings.Index(s, needle) if idx < 0 { t.Fatalf("expected %q to contain %q", s, needle) } return idx }