From dbc3b21827ce309508a6e9ddd645511b953393f4 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 2 Feb 2026 16:48:57 +0100 Subject: [PATCH] test: increase test coverage across multiple packages Add comprehensive tests for: - network: CryptConn encryption connection tests - signserver: character and member struct validation - entranceserver: encryption roundtrip, server config tests - channelserver: stage creation, object IDs, quest membership All tests pass with race detector enabled. --- network/crypt_conn_test.go | 179 +++++++++++ server/channelserver/sys_stage_test.go | 227 ++++++++++++- server/entranceserver/entrance_server_test.go | 232 ++++++++++++++ server/signserver/dbutils_test.go | 298 ++++++++++++++++++ 4 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 network/crypt_conn_test.go create mode 100644 server/signserver/dbutils_test.go diff --git a/network/crypt_conn_test.go b/network/crypt_conn_test.go new file mode 100644 index 000000000..e76bb9a46 --- /dev/null +++ b/network/crypt_conn_test.go @@ -0,0 +1,179 @@ +package network + +import ( + "testing" +) + +func TestNewCryptConn(t *testing.T) { + // NewCryptConn with nil should not panic + cc := NewCryptConn(nil) + + if cc == nil { + t.Fatal("NewCryptConn() returned nil") + } + + // Verify default key rotation values + if cc.readKeyRot != 995117 { + t.Errorf("readKeyRot = %d, want 995117", cc.readKeyRot) + } + if cc.sendKeyRot != 995117 { + t.Errorf("sendKeyRot = %d, want 995117", cc.sendKeyRot) + } + if cc.sentPackets != 0 { + t.Errorf("sentPackets = %d, want 0", cc.sentPackets) + } + if cc.prevRecvPacketCombinedCheck != 0 { + t.Errorf("prevRecvPacketCombinedCheck = %d, want 0", cc.prevRecvPacketCombinedCheck) + } + if cc.prevSendPacketCombinedCheck != 0 { + t.Errorf("prevSendPacketCombinedCheck = %d, want 0", cc.prevSendPacketCombinedCheck) + } +} + +func TestCryptConnInitialState(t *testing.T) { + cc := &CryptConn{} + + // Zero value should have all zeros + if cc.readKeyRot != 0 { + t.Errorf("zero value readKeyRot = %d, want 0", cc.readKeyRot) + } + if cc.sendKeyRot != 0 { + t.Errorf("zero value sendKeyRot = %d, want 0", cc.sendKeyRot) + } + if cc.conn != nil { + t.Error("zero value conn should be nil") + } +} + +func TestCryptConnDefaultKeyRotation(t *testing.T) { + // The magic number 995117 is the default key rotation value + const defaultKeyRot = 995117 + + cc := NewCryptConn(nil) + + if cc.readKeyRot != defaultKeyRot { + t.Errorf("default readKeyRot = %d, want %d", cc.readKeyRot, defaultKeyRot) + } + if cc.sendKeyRot != defaultKeyRot { + t.Errorf("default sendKeyRot = %d, want %d", cc.sendKeyRot, defaultKeyRot) + } +} + +func TestCryptConnStructFields(t *testing.T) { + cc := &CryptConn{ + readKeyRot: 123456, + sendKeyRot: 654321, + sentPackets: 10, + prevRecvPacketCombinedCheck: 0x1234, + prevSendPacketCombinedCheck: 0x5678, + } + + if cc.readKeyRot != 123456 { + t.Errorf("readKeyRot = %d, want 123456", cc.readKeyRot) + } + if cc.sendKeyRot != 654321 { + t.Errorf("sendKeyRot = %d, want 654321", cc.sendKeyRot) + } + if cc.sentPackets != 10 { + t.Errorf("sentPackets = %d, want 10", cc.sentPackets) + } + if cc.prevRecvPacketCombinedCheck != 0x1234 { + t.Errorf("prevRecvPacketCombinedCheck = 0x%X, want 0x1234", cc.prevRecvPacketCombinedCheck) + } + if cc.prevSendPacketCombinedCheck != 0x5678 { + t.Errorf("prevSendPacketCombinedCheck = 0x%X, want 0x5678", cc.prevSendPacketCombinedCheck) + } +} + +func TestCryptConnKeyRotationType(t *testing.T) { + // Verify key rotation uses uint32 + cc := NewCryptConn(nil) + + // Simulate key rotation + keyRotDelta := byte(3) + cc.sendKeyRot = (uint32(keyRotDelta) * (cc.sendKeyRot + 1)) + + // Should not overflow or behave unexpectedly + if cc.sendKeyRot == 0 { + t.Error("sendKeyRot should not be 0 after rotation") + } +} + +func TestCryptConnSentPacketsCounter(t *testing.T) { + cc := NewCryptConn(nil) + + if cc.sentPackets != 0 { + t.Errorf("initial sentPackets = %d, want 0", cc.sentPackets) + } + + // Simulate incrementing sent packets + cc.sentPackets++ + if cc.sentPackets != 1 { + t.Errorf("sentPackets after increment = %d, want 1", cc.sentPackets) + } + + // Verify it's int32 + cc.sentPackets = 0x7FFFFFFF // Max int32 + if cc.sentPackets != 0x7FFFFFFF { + t.Errorf("sentPackets max value = %d, want %d", cc.sentPackets, 0x7FFFFFFF) + } +} + +func TestCryptConnCombinedCheckStorage(t *testing.T) { + cc := NewCryptConn(nil) + + // Test combined check storage + cc.prevRecvPacketCombinedCheck = 0xABCD + cc.prevSendPacketCombinedCheck = 0xDCBA + + if cc.prevRecvPacketCombinedCheck != 0xABCD { + t.Errorf("prevRecvPacketCombinedCheck = 0x%X, want 0xABCD", cc.prevRecvPacketCombinedCheck) + } + if cc.prevSendPacketCombinedCheck != 0xDCBA { + t.Errorf("prevSendPacketCombinedCheck = 0x%X, want 0xDCBA", cc.prevSendPacketCombinedCheck) + } +} + +func TestCryptConnKeyRotationFormula(t *testing.T) { + // Test the key rotation formula: (keyRotDelta * (keyRot + 1)) + tests := []struct { + name string + initialKey uint32 + keyRotDelta byte + expectedKey uint32 + }{ + {"delta 1", 995117, 1, 995118}, + {"delta 3 default", 995117, 3, 2985354}, + {"delta 0", 995117, 0, 0}, + {"zero initial", 0, 3, 3}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newKey := uint32(tt.keyRotDelta) * (tt.initialKey + 1) + if newKey != tt.expectedKey { + t.Errorf("key rotation = %d, want %d", newKey, tt.expectedKey) + } + }) + } +} + +func TestCryptPacketHeaderLengthConstant(t *testing.T) { + // CryptPacketHeaderLength should always be 14 + if CryptPacketHeaderLength != 14 { + t.Errorf("CryptPacketHeaderLength = %d, want 14", CryptPacketHeaderLength) + } +} + +func TestMultipleCryptConnInstances(t *testing.T) { + // Multiple instances should be independent + cc1 := NewCryptConn(nil) + cc2 := NewCryptConn(nil) + + cc1.sendKeyRot = 12345 + cc2.sendKeyRot = 54321 + + if cc1.sendKeyRot == cc2.sendKeyRot { + t.Error("CryptConn instances should be independent") + } +} diff --git a/server/channelserver/sys_stage_test.go b/server/channelserver/sys_stage_test.go index c5100210d..23691ddd5 100644 --- a/server/channelserver/sys_stage_test.go +++ b/server/channelserver/sys_stage_test.go @@ -190,11 +190,13 @@ func TestStageBroadcastMHF_RaceDetectorWithLock(t *testing.T) { var wg sync.WaitGroup // Goroutine 1: Continuously broadcast - wg.Go(func() { + wg.Add(1) + go func() { + defer wg.Done() for i := 0; i < 1000; i++ { stage.BroadcastMHF(pkt, nil) } - }) + }() // Goroutine 2: Add and remove sessions WITH proper locking // This simulates the fixed logoutPlayer behavior @@ -245,3 +247,224 @@ func TestStageBroadcastMHF_NilClientContextSkipped(t *testing.T) { t.Error("session1 did not receive data") } } + +// TestNewStageBasic verifies Stage creation +func TestNewStageBasic(t *testing.T) { + stageID := "test_stage_001" + stage := NewStage(stageID) + + if stage == nil { + t.Fatal("NewStage() returned nil") + } + if stage.id != stageID { + t.Errorf("stage.id = %s, want %s", stage.id, stageID) + } + if stage.clients == nil { + t.Error("stage.clients should not be nil") + } + if stage.reservedClientSlots == nil { + t.Error("stage.reservedClientSlots should not be nil") + } + if stage.objects == nil { + t.Error("stage.objects should not be nil") + } +} + +// TestStageClientCount tests client counting +func TestStageClientCount(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + if len(stage.clients) != 0 { + t.Errorf("initial client count = %d, want 0", len(stage.clients)) + } + + // Add clients + session1 := createMockSession(1, server) + session2 := createMockSession(2, server) + + stage.clients[session1] = session1.charID + if len(stage.clients) != 1 { + t.Errorf("client count after 1 add = %d, want 1", len(stage.clients)) + } + + stage.clients[session2] = session2.charID + if len(stage.clients) != 2 { + t.Errorf("client count after 2 adds = %d, want 2", len(stage.clients)) + } + + // Remove a client + delete(stage.clients, session1) + if len(stage.clients) != 1 { + t.Errorf("client count after 1 remove = %d, want 1", len(stage.clients)) + } +} + +// TestStageReservation tests stage reservation +func TestStageReservation(t *testing.T) { + stage := NewStage("test_stage") + + if len(stage.reservedClientSlots) != 0 { + t.Errorf("initial reservations = %d, want 0", len(stage.reservedClientSlots)) + } + + // Reserve a slot using character ID + stage.reservedClientSlots[12345] = true + if len(stage.reservedClientSlots) != 1 { + t.Errorf("reservations after 1 add = %d, want 1", len(stage.reservedClientSlots)) + } +} + +// TestStageBinaryData tests setting and getting stage binary data +func TestStageBinaryData(t *testing.T) { + stage := NewStage("test_stage") + + // rawBinaryData is initialized by NewStage + if stage.rawBinaryData == nil { + t.Error("rawBinaryData should not be nil after NewStage") + } + + // Set binary data + key := stageBinaryKey{id0: 1, id1: 2} + testData := []byte{0x01, 0x02, 0x03, 0x04} + stage.rawBinaryData[key] = testData + + if len(stage.rawBinaryData) != 1 { + t.Errorf("rawBinaryData length = %d, want 1", len(stage.rawBinaryData)) + } +} + +// TestStageLockUnlock tests stage locking +func TestStageLockUnlock(t *testing.T) { + stage := NewStage("test_stage") + + // Test lock/unlock without deadlock + stage.Lock() + stage.password = "test" + stage.Unlock() + + stage.RLock() + password := stage.password + stage.RUnlock() + + if password != "test" { + t.Error("stage password should be 'test'") + } +} + +// TestStageHostSession tests host session tracking +func TestStageHostSession(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + session := createMockSession(1, server) + + if stage.host != nil { + t.Error("initial host should be nil") + } + + stage.host = session + if stage.host == nil { + t.Error("host should not be nil after setting") + } + if stage.host.charID != 1 { + t.Errorf("host.charID = %d, want 1", stage.host.charID) + } +} + +// TestStageMultipleClients tests stage with multiple clients +func TestStageMultipleClients(t *testing.T) { + stage := NewStage("test_stage") + server := createMockServer() + + // Add many clients + sessions := make([]*Session, 10) + for i := range sessions { + sessions[i] = createMockSession(uint32(i+1), server) + stage.clients[sessions[i]] = sessions[i].charID + } + + if len(stage.clients) != 10 { + t.Errorf("client count = %d, want 10", len(stage.clients)) + } + + // Verify each client is tracked + for _, s := range sessions { + if _, ok := stage.clients[s]; !ok { + t.Errorf("session with charID %d not found in stage", s.charID) + } + } +} + +// TestStageNewMaxPlayers tests default max players +func TestStageNewMaxPlayers(t *testing.T) { + stage := NewStage("test_stage") + + // Default max players is 4 + if stage.maxPlayers != 4 { + t.Errorf("initial maxPlayers = %d, want 4", stage.maxPlayers) + } +} + +// TestNextObjectID tests object ID generation +func TestNextObjectID(t *testing.T) { + stage := NewStage("test_stage") + + // Generate several object IDs + ids := make(map[uint32]bool) + for i := 0; i < 10; i++ { + id := stage.NextObjectID() + if ids[id] { + t.Errorf("duplicate object ID generated: %d", id) + } + ids[id] = true + } +} + +// TestNextObjectIDWrap tests that object ID wraps at 127 +func TestNextObjectIDWrap(t *testing.T) { + stage := NewStage("test_stage") + stage.objectIndex = 125 + + // Generate IDs to trigger wrap + stage.NextObjectID() // 126 + stage.NextObjectID() // should wrap to 1 + + // After wrap, objectIndex should be 1 + if stage.objectIndex != 1 { + t.Errorf("objectIndex after wrap = %d, want 1", stage.objectIndex) + } +} + +// TestIsCharInQuestByID tests character quest membership check +func TestIsCharInQuestByID(t *testing.T) { + stage := NewStage("test_stage") + + // No reservations - should return false + if stage.isCharInQuestByID(12345) { + t.Error("should return false when no reservations exist") + } + + // Add reservation + stage.reservedClientSlots[12345] = true + + if !stage.isCharInQuestByID(12345) { + t.Error("should return true when character is reserved") + } +} + +// TestIsQuest tests quest detection +func TestIsQuest(t *testing.T) { + stage := NewStage("test_stage") + + // No reservations - not a quest + if stage.isQuest() { + t.Error("should return false when no reservations exist") + } + + // Add reservation - becomes a quest + stage.reservedClientSlots[12345] = true + + if !stage.isQuest() { + t.Error("should return true when reservations exist") + } +} diff --git a/server/entranceserver/entrance_server_test.go b/server/entranceserver/entrance_server_test.go index e0e5a53f4..0bb260c1b 100644 --- a/server/entranceserver/entrance_server_test.go +++ b/server/entranceserver/entrance_server_test.go @@ -60,3 +60,235 @@ func TestConfigFields(t *testing.T) { t.Error("Config ErupeConfig should be nil") } } + +func TestServerShutdownFlag(t *testing.T) { + cfg := &Config{ + ErupeConfig: &config.Config{}, + } + s := NewServer(cfg) + + // Initially not shutting down + if s.isShuttingDown { + t.Error("New server should not be shutting down") + } + + // Simulate setting shutdown flag + s.Lock() + s.isShuttingDown = true + s.Unlock() + + if !s.isShuttingDown { + t.Error("Server should be shutting down after flag is set") + } +} + +func TestServerConfigStorage(t *testing.T) { + erupeConfig := &config.Config{ + Host: "192.168.1.100", + DevMode: true, + Entrance: config.Entrance{ + Enabled: true, + Port: 53310, + Entries: []config.EntranceServerInfo{ + { + Name: "Test Server", + IP: "127.0.0.1", + Type: 1, + }, + }, + }, + } + + cfg := &Config{ + ErupeConfig: erupeConfig, + } + + s := NewServer(cfg) + + if s.erupeConfig.Host != "192.168.1.100" { + t.Errorf("Host = %s, want 192.168.1.100", s.erupeConfig.Host) + } + if s.erupeConfig.DevMode != true { + t.Error("DevMode should be true") + } + if s.erupeConfig.Entrance.Port != 53310 { + t.Errorf("Entrance.Port = %d, want 53310", s.erupeConfig.Entrance.Port) + } +} + +func TestServerEntranceEntries(t *testing.T) { + entries := []config.EntranceServerInfo{ + { + Name: "World 1", + IP: "10.0.0.1", + Type: 1, + Recommended: 1, + Channels: []config.EntranceChannelInfo{ + {Port: 54001, MaxPlayers: 100}, + {Port: 54002, MaxPlayers: 100}, + }, + }, + { + Name: "World 2", + IP: "10.0.0.2", + Type: 2, + Recommended: 0, + Channels: []config.EntranceChannelInfo{ + {Port: 54003, MaxPlayers: 50}, + }, + }, + } + + erupeConfig := &config.Config{ + Entrance: config.Entrance{ + Enabled: true, + Port: 53310, + Entries: entries, + }, + } + + cfg := &Config{ErupeConfig: erupeConfig} + s := NewServer(cfg) + + if len(s.erupeConfig.Entrance.Entries) != 2 { + t.Errorf("Entries count = %d, want 2", len(s.erupeConfig.Entrance.Entries)) + } + + if s.erupeConfig.Entrance.Entries[0].Name != "World 1" { + t.Errorf("First entry name = %s, want World 1", s.erupeConfig.Entrance.Entries[0].Name) + } + + if len(s.erupeConfig.Entrance.Entries[0].Channels) != 2 { + t.Errorf("First entry channels = %d, want 2", len(s.erupeConfig.Entrance.Entries[0].Channels)) + } +} + +func TestEncryptDecryptRoundTrip(t *testing.T) { + tests := []struct { + name string + data []byte + key byte + }{ + {"empty", []byte{}, 0x00}, + {"single byte", []byte{0x42}, 0x00}, + {"multiple bytes", []byte{0x01, 0x02, 0x03, 0x04}, 0x00}, + {"with key", []byte{0xDE, 0xAD, 0xBE, 0xEF}, 0x55}, + {"max key", []byte{0x01, 0x02}, 0xFF}, + {"long data", make([]byte, 100), 0x42}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypted := EncryptBin8(tt.data, tt.key) + decrypted := DecryptBin8(encrypted, tt.key) + + if len(decrypted) != len(tt.data) { + t.Errorf("decrypted length = %d, want %d", len(decrypted), len(tt.data)) + return + } + + for i := range tt.data { + if decrypted[i] != tt.data[i] { + t.Errorf("decrypted[%d] = 0x%X, want 0x%X", i, decrypted[i], tt.data[i]) + } + } + }) + } +} + +func TestCalcSum32Deterministic(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + + sum1 := CalcSum32(data) + sum2 := CalcSum32(data) + + if sum1 != sum2 { + t.Errorf("CalcSum32 not deterministic: got 0x%X and 0x%X", sum1, sum2) + } +} + +func TestCalcSum32DifferentInputs(t *testing.T) { + data1 := []byte{0x01, 0x02, 0x03} + data2 := []byte{0x01, 0x02, 0x04} + + sum1 := CalcSum32(data1) + sum2 := CalcSum32(data2) + + if sum1 == sum2 { + t.Error("Different inputs should produce different checksums") + } +} + +func TestEncryptBin8KeyVariation(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04} + + enc1 := EncryptBin8(data, 0x00) + enc2 := EncryptBin8(data, 0x01) + enc3 := EncryptBin8(data, 0xFF) + + if bytesEqual(enc1, enc2) { + t.Error("Different keys should produce different encrypted data (0x00 vs 0x01)") + } + if bytesEqual(enc2, enc3) { + t.Error("Different keys should produce different encrypted data (0x01 vs 0xFF)") + } +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestEncryptBin8LengthPreservation(t *testing.T) { + lengths := []int{0, 1, 7, 8, 9, 100, 1000} + + for _, length := range lengths { + data := make([]byte, length) + for i := range data { + data[i] = byte(i % 256) + } + + encrypted := EncryptBin8(data, 0x42) + if len(encrypted) != length { + t.Errorf("EncryptBin8 length %d changed to %d", length, len(encrypted)) + } + } +} + +func TestCalcSum32LargeInput(t *testing.T) { + data := make([]byte, 10000) + for i := range data { + data[i] = byte(i % 256) + } + + sum := CalcSum32(data) + sum2 := CalcSum32(data) + if sum != sum2 { + t.Errorf("CalcSum32 inconsistent for large input: 0x%X vs 0x%X", sum, sum2) + } +} + +func TestServerMutexLocking(t *testing.T) { + cfg := &Config{ErupeConfig: &config.Config{}} + s := NewServer(cfg) + + // Test that locking/unlocking works without deadlock + s.Lock() + s.isShuttingDown = true + s.Unlock() + + s.Lock() + result := s.isShuttingDown + s.Unlock() + + if !result { + t.Error("Mutex should protect isShuttingDown flag") + } +} diff --git a/server/signserver/dbutils_test.go b/server/signserver/dbutils_test.go new file mode 100644 index 000000000..c93b6031c --- /dev/null +++ b/server/signserver/dbutils_test.go @@ -0,0 +1,298 @@ +package signserver + +import ( + "testing" +) + +func TestCharacterStruct(t *testing.T) { + c := character{ + ID: 12345, + IsFemale: true, + IsNewCharacter: false, + Name: "TestHunter", + UnkDescString: "Test description", + HRP: 999, + GR: 300, + WeaponType: 5, + LastLogin: 1700000000, + } + + if c.ID != 12345 { + t.Errorf("ID = %d, want 12345", c.ID) + } + if c.IsFemale != true { + t.Error("IsFemale should be true") + } + if c.IsNewCharacter != false { + t.Error("IsNewCharacter should be false") + } + if c.Name != "TestHunter" { + t.Errorf("Name = %s, want TestHunter", c.Name) + } + if c.UnkDescString != "Test description" { + t.Errorf("UnkDescString = %s, want Test description", c.UnkDescString) + } + if c.HRP != 999 { + t.Errorf("HRP = %d, want 999", c.HRP) + } + if c.GR != 300 { + t.Errorf("GR = %d, want 300", c.GR) + } + if c.WeaponType != 5 { + t.Errorf("WeaponType = %d, want 5", c.WeaponType) + } + if c.LastLogin != 1700000000 { + t.Errorf("LastLogin = %d, want 1700000000", c.LastLogin) + } +} + +func TestCharacterStructDefaults(t *testing.T) { + c := character{} + + if c.ID != 0 { + t.Errorf("default ID = %d, want 0", c.ID) + } + if c.IsFemale != false { + t.Error("default IsFemale should be false") + } + if c.IsNewCharacter != false { + t.Error("default IsNewCharacter should be false") + } + if c.Name != "" { + t.Errorf("default Name = %s, want empty", c.Name) + } + if c.HRP != 0 { + t.Errorf("default HRP = %d, want 0", c.HRP) + } + if c.GR != 0 { + t.Errorf("default GR = %d, want 0", c.GR) + } + if c.WeaponType != 0 { + t.Errorf("default WeaponType = %d, want 0", c.WeaponType) + } +} + +func TestMembersStruct(t *testing.T) { + m := members{ + CID: 100, + ID: 200, + Name: "FriendName", + } + + if m.CID != 100 { + t.Errorf("CID = %d, want 100", m.CID) + } + if m.ID != 200 { + t.Errorf("ID = %d, want 200", m.ID) + } + if m.Name != "FriendName" { + t.Errorf("Name = %s, want FriendName", m.Name) + } +} + +func TestMembersStructDefaults(t *testing.T) { + m := members{} + + if m.CID != 0 { + t.Errorf("default CID = %d, want 0", m.CID) + } + if m.ID != 0 { + t.Errorf("default ID = %d, want 0", m.ID) + } + if m.Name != "" { + t.Errorf("default Name = %s, want empty", m.Name) + } +} + +func TestCharacterWeaponTypes(t *testing.T) { + // Test all weapon type values are valid + weaponTypes := []uint16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} + + for _, wt := range weaponTypes { + c := character{WeaponType: wt} + if c.WeaponType != wt { + t.Errorf("WeaponType = %d, want %d", c.WeaponType, wt) + } + } +} + +func TestCharacterHRPRange(t *testing.T) { + tests := []struct { + name string + hrp uint16 + }{ + {"min", 0}, + {"beginner", 1}, + {"hr30", 30}, + {"hr50", 50}, + {"hr99", 99}, + {"hr299", 299}, + {"hr998", 998}, + {"hr999", 999}, + {"max uint16", 65535}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{HRP: tt.hrp} + if c.HRP != tt.hrp { + t.Errorf("HRP = %d, want %d", c.HRP, tt.hrp) + } + }) + } +} + +func TestCharacterGRRange(t *testing.T) { + tests := []struct { + name string + gr uint16 + }{ + {"min", 0}, + {"gr1", 1}, + {"gr100", 100}, + {"gr300", 300}, + {"gr999", 999}, + {"max uint16", 65535}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{GR: tt.gr} + if c.GR != tt.gr { + t.Errorf("GR = %d, want %d", c.GR, tt.gr) + } + }) + } +} + +func TestCharacterIDRange(t *testing.T) { + tests := []struct { + name string + id uint32 + }{ + {"min", 0}, + {"small", 1}, + {"medium", 1000000}, + {"large", 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{ID: tt.id} + if c.ID != tt.id { + t.Errorf("ID = %d, want %d", c.ID, tt.id) + } + }) + } +} + +func TestCharacterGender(t *testing.T) { + // Male character + male := character{IsFemale: false} + if male.IsFemale != false { + t.Error("Male character should have IsFemale = false") + } + + // Female character + female := character{IsFemale: true} + if female.IsFemale != true { + t.Error("Female character should have IsFemale = true") + } +} + +func TestCharacterNewStatus(t *testing.T) { + // New character + newChar := character{IsNewCharacter: true} + if newChar.IsNewCharacter != true { + t.Error("New character should have IsNewCharacter = true") + } + + // Existing character + existingChar := character{IsNewCharacter: false} + if existingChar.IsNewCharacter != false { + t.Error("Existing character should have IsNewCharacter = false") + } +} + +func TestCharacterNameLength(t *testing.T) { + // Test various name lengths + names := []string{ + "", // Empty + "A", // Single char + "Hunter", // Normal + "LongHunterName123", // Longer + } + + for _, name := range names { + c := character{Name: name} + if c.Name != name { + t.Errorf("Name = %s, want %s", c.Name, name) + } + } +} + +func TestCharacterLastLogin(t *testing.T) { + tests := []struct { + name string + lastLogin uint32 + }{ + {"zero", 0}, + {"epoch", 0}, + {"past", 1600000000}, + {"present", 1700000000}, + {"future", 1800000000}, + {"max", 0xFFFFFFFF}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := character{LastLogin: tt.lastLogin} + if c.LastLogin != tt.lastLogin { + t.Errorf("LastLogin = %d, want %d", c.LastLogin, tt.lastLogin) + } + }) + } +} + +func TestMembersCIDAssignment(t *testing.T) { + // CID is the local character ID that references this member + m := members{CID: 12345} + if m.CID != 12345 { + t.Errorf("CID = %d, want 12345", m.CID) + } +} + +func TestMultipleCharacters(t *testing.T) { + // Test creating multiple character instances + chars := []character{ + {ID: 1, Name: "Char1", HRP: 100}, + {ID: 2, Name: "Char2", HRP: 200}, + {ID: 3, Name: "Char3", HRP: 300}, + } + + for i, c := range chars { + expectedID := uint32(i + 1) + if c.ID != expectedID { + t.Errorf("chars[%d].ID = %d, want %d", i, c.ID, expectedID) + } + } +} + +func TestMultipleMembers(t *testing.T) { + // Test creating multiple member instances + membersList := []members{ + {CID: 1, ID: 10, Name: "Friend1"}, + {CID: 1, ID: 20, Name: "Friend2"}, + {CID: 2, ID: 30, Name: "Friend3"}, + } + + // First two should share the same CID + if membersList[0].CID != membersList[1].CID { + t.Error("First two members should share the same CID") + } + + // Third should have different CID + if membersList[1].CID == membersList[2].CID { + t.Error("Third member should have different CID") + } +}