Files
Erupe/cmd/protbot/protocol/channel_test.go
Houmgaor 03adb21e99 fix(channelserver): post-RC1 stabilization sprint
Fix rasta_id=0 overwriting NULL in SaveMercenary, which prevented
game state saving for characters without a mercenary (#163).

Also includes:
- CHANGELOG updated with all 10 post-RC1 commits
- Setup wizard fmt.Printf replaced with zap structured logging
- technical-debt.md updated with 6 newly completed items
- Scenario binary format documented (docs/scenario-format.md)
- Tests: alliance nil-guard (#171), handler dispatch table,
  migrations (sorted/SQL/baseline), setup wizard (10 tests),
  protbot protocol sign/entrance/channel (23 tests)
2026-03-05 16:39:15 +01:00

304 lines
7.9 KiB
Go

package protocol
import (
"encoding/binary"
"testing"
"time"
)
func TestHandleAck_SimpleAck(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(1)
waitCh := make(chan *AckResponse, 1)
ch.waiters.Store(ackHandle, waitCh)
// Build simple ACK data (after opcode has been stripped).
// Format: uint32 ackHandle + uint8 isBuffer(0) + uint8 errorCode + uint16 ignored + uint32 data
data := make([]byte, 12)
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 0 // isBuffer = false
data[5] = 0 // errorCode = 0
binary.BigEndian.PutUint16(data[6:8], 0)
binary.BigEndian.PutUint32(data[8:12], 0xDEADBEEF) // simple ACK data
ch.handleAck(data)
select {
case resp := <-waitCh:
if resp.AckHandle != ackHandle {
t.Errorf("AckHandle: got %d, want %d", resp.AckHandle, ackHandle)
}
if resp.IsBufferResponse {
t.Error("IsBufferResponse: got true, want false")
}
if resp.ErrorCode != 0 {
t.Errorf("ErrorCode: got %d, want 0", resp.ErrorCode)
}
if len(resp.Data) != 4 {
t.Fatalf("Data length: got %d, want 4", len(resp.Data))
}
val := binary.BigEndian.Uint32(resp.Data)
if val != 0xDEADBEEF {
t.Errorf("Data value: got 0x%08X, want 0xDEADBEEF", val)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for ACK response")
}
}
func TestHandleAck_BufferAck(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(2)
waitCh := make(chan *AckResponse, 1)
ch.waiters.Store(ackHandle, waitCh)
payload := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
// Build buffer ACK data.
// Format: uint32 ackHandle + uint8 isBuffer(1) + uint8 errorCode + uint16 payloadSize + payload
data := make([]byte, 8+len(payload))
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 1 // isBuffer = true
data[5] = 0 // errorCode = 0
binary.BigEndian.PutUint16(data[6:8], uint16(len(payload)))
copy(data[8:], payload)
ch.handleAck(data)
select {
case resp := <-waitCh:
if resp.AckHandle != ackHandle {
t.Errorf("AckHandle: got %d, want %d", resp.AckHandle, ackHandle)
}
if !resp.IsBufferResponse {
t.Error("IsBufferResponse: got false, want true")
}
if resp.ErrorCode != 0 {
t.Errorf("ErrorCode: got %d, want 0", resp.ErrorCode)
}
if len(resp.Data) != len(payload) {
t.Fatalf("Data length: got %d, want %d", len(resp.Data), len(payload))
}
for i, b := range payload {
if resp.Data[i] != b {
t.Errorf("Data[%d]: got 0x%02X, want 0x%02X", i, resp.Data[i], b)
}
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for ACK response")
}
}
func TestHandleAck_ExtendedBuffer(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(3)
waitCh := make(chan *AckResponse, 1)
ch.waiters.Store(ackHandle, waitCh)
payload := make([]byte, 10)
for i := range payload {
payload[i] = byte(i)
}
// Build extended buffer ACK data (payloadSize == 0xFFFF).
// Format: uint32 ackHandle + uint8 isBuffer(1) + uint8 errorCode + uint16(0xFFFF) + uint32 realSize + payload
data := make([]byte, 12+len(payload))
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 1 // isBuffer = true
data[5] = 0 // errorCode = 0
binary.BigEndian.PutUint16(data[6:8], 0xFFFF)
binary.BigEndian.PutUint32(data[8:12], uint32(len(payload)))
copy(data[12:], payload)
ch.handleAck(data)
select {
case resp := <-waitCh:
if resp.AckHandle != ackHandle {
t.Errorf("AckHandle: got %d, want %d", resp.AckHandle, ackHandle)
}
if !resp.IsBufferResponse {
t.Error("IsBufferResponse: got false, want true")
}
if len(resp.Data) != len(payload) {
t.Fatalf("Data length: got %d, want %d", len(resp.Data), len(payload))
}
for i, b := range payload {
if resp.Data[i] != b {
t.Errorf("Data[%d]: got 0x%02X, want 0x%02X", i, resp.Data[i], b)
}
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for ACK response")
}
}
func TestHandleAck_TooShort(t *testing.T) {
ch := &ChannelConn{}
// Should not panic with data shorter than 8 bytes.
ch.handleAck(nil)
ch.handleAck([]byte{})
ch.handleAck([]byte{0x00, 0x01, 0x02})
ch.handleAck([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
// 7 bytes: still < 8, should return silently.
data := make([]byte, 7)
binary.BigEndian.PutUint32(data[0:4], 99)
ch.handleAck(data)
}
func TestHandleAck_ExtendedBuffer_TooShortForSize(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(4)
waitCh := make(chan *AckResponse, 1)
ch.waiters.Store(ackHandle, waitCh)
// payloadSize=0xFFFF but data too short for the uint32 real size field (only 8 bytes total).
data := make([]byte, 8)
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 1 // isBuffer = true
data[5] = 0
binary.BigEndian.PutUint16(data[6:8], 0xFFFF)
ch.handleAck(data)
// The handler should return early (no payload), but still dispatch the response
// with nil Data since the early return is only for reading payload.
select {
case resp := <-waitCh:
// Response dispatched but with nil data since len(data) < 12.
if resp.Data != nil {
t.Errorf("expected nil Data for truncated extended buffer, got %d bytes", len(resp.Data))
}
case <-time.After(100 * time.Millisecond):
// The handler returns before dispatching if len(data) < 12 for 0xFFFF path.
// This is also acceptable behavior.
}
}
func TestHandleAck_WithErrorCode(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(5)
waitCh := make(chan *AckResponse, 1)
ch.waiters.Store(ackHandle, waitCh)
data := make([]byte, 12)
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 0 // isBuffer = false
data[5] = 42 // errorCode = 42
binary.BigEndian.PutUint16(data[6:8], 0)
binary.BigEndian.PutUint32(data[8:12], 0)
ch.handleAck(data)
select {
case resp := <-waitCh:
if resp.ErrorCode != 42 {
t.Errorf("ErrorCode: got %d, want 42", resp.ErrorCode)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for ACK response")
}
}
func TestHandleAck_NoWaiter(t *testing.T) {
ch := &ChannelConn{}
// No waiter registered for handle 999 — should not panic.
data := make([]byte, 12)
binary.BigEndian.PutUint32(data[0:4], 999)
data[4] = 0
data[5] = 0
binary.BigEndian.PutUint16(data[6:8], 0)
binary.BigEndian.PutUint32(data[8:12], 0)
// This should log but not panic.
ch.handleAck(data)
}
func TestNextAckHandle(t *testing.T) {
ch := &ChannelConn{}
h1 := ch.NextAckHandle()
h2 := ch.NextAckHandle()
h3 := ch.NextAckHandle()
if h1 != 1 {
t.Errorf("first handle: got %d, want 1", h1)
}
if h2 != 2 {
t.Errorf("second handle: got %d, want 2", h2)
}
if h3 != 3 {
t.Errorf("third handle: got %d, want 3", h3)
}
}
func TestNextAckHandle_Concurrent(t *testing.T) {
ch := &ChannelConn{}
const goroutines = 100
results := make(chan uint32, goroutines)
for i := 0; i < goroutines; i++ {
go func() {
results <- ch.NextAckHandle()
}()
}
seen := make(map[uint32]bool)
for i := 0; i < goroutines; i++ {
h := <-results
if seen[h] {
t.Errorf("duplicate handle: %d", h)
}
seen[h] = true
}
if len(seen) != goroutines {
t.Errorf("unique handles: got %d, want %d", len(seen), goroutines)
}
}
func TestWaitForAck_Timeout(t *testing.T) {
ch := &ChannelConn{}
// No handleAck call will be made, so this should time out.
_, err := ch.WaitForAck(999, 50*time.Millisecond)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
}
func TestWaitForAck_Success(t *testing.T) {
ch := &ChannelConn{}
ackHandle := uint32(10)
// Dispatch the ACK from another goroutine after a short delay.
go func() {
time.Sleep(10 * time.Millisecond)
data := make([]byte, 12)
binary.BigEndian.PutUint32(data[0:4], ackHandle)
data[4] = 0 // isBuffer = false
data[5] = 0 // errorCode
binary.BigEndian.PutUint16(data[6:8], 0)
binary.BigEndian.PutUint32(data[8:12], 0x12345678)
ch.handleAck(data)
}()
resp, err := ch.WaitForAck(ackHandle, 1*time.Second)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.AckHandle != ackHandle {
t.Errorf("AckHandle: got %d, want %d", resp.AckHandle, ackHandle)
}
}