首次推送
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user