From a11ee6d9eb03f18f17d12df3bedbcf0228b01c84 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Thu, 5 Feb 2026 08:48:05 +0100 Subject: [PATCH] test(channelserver): add tests for guild member and gacha functions Add comprehensive tests for pure logic functions: - GuildMember.CanRecruit() and IsSubLeader() methods - getRandomEntries() for gacha weighted/box selection All targeted functions now have 100% coverage. --- .../handlers_guild_member_test.go | 209 ++++++++++++++++++ .../channelserver/handlers_shop_gacha_test.go | 189 ++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 server/channelserver/handlers_guild_member_test.go diff --git a/server/channelserver/handlers_guild_member_test.go b/server/channelserver/handlers_guild_member_test.go new file mode 100644 index 000000000..b8e7b8c85 --- /dev/null +++ b/server/channelserver/handlers_guild_member_test.go @@ -0,0 +1,209 @@ +package channelserver + +import ( + "testing" +) + +func TestGuildMember_CanRecruit(t *testing.T) { + tests := []struct { + name string + member GuildMember + expected bool + }{ + { + name: "recruiter flag true", + member: GuildMember{ + Recruiter: true, + OrderIndex: 10, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 1", + member: GuildMember{ + Recruiter: false, + OrderIndex: 1, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 2", + member: GuildMember{ + Recruiter: false, + OrderIndex: 2, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 3", + member: GuildMember{ + Recruiter: false, + OrderIndex: 3, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 0 (sub-leader)", + member: GuildMember{ + Recruiter: false, + OrderIndex: 0, + IsLeader: false, + }, + expected: true, + }, + { + name: "order index 4 cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 4, + IsLeader: false, + }, + expected: false, + }, + { + name: "order index 5 cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 5, + IsLeader: false, + }, + expected: false, + }, + { + name: "is leader can recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 100, + IsLeader: true, + }, + expected: true, + }, + { + name: "regular member cannot recruit", + member: GuildMember{ + Recruiter: false, + OrderIndex: 10, + IsLeader: false, + }, + expected: false, + }, + { + name: "all flags true", + member: GuildMember{ + Recruiter: true, + OrderIndex: 1, + IsLeader: true, + }, + expected: true, + }, + { + name: "high order index with leader", + member: GuildMember{ + Recruiter: false, + OrderIndex: 255, + IsLeader: true, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.member.CanRecruit() + if result != tt.expected { + t.Errorf("CanRecruit() = %v, expected %v (Recruiter=%v, OrderIndex=%d, IsLeader=%v)", + result, tt.expected, tt.member.Recruiter, tt.member.OrderIndex, tt.member.IsLeader) + } + }) + } +} + +func TestGuildMember_IsSubLeader(t *testing.T) { + tests := []struct { + name string + orderIndex uint8 + expected bool + }{ + { + name: "order index 0", + orderIndex: 0, + expected: true, + }, + { + name: "order index 1", + orderIndex: 1, + expected: true, + }, + { + name: "order index 2", + orderIndex: 2, + expected: true, + }, + { + name: "order index 3", + orderIndex: 3, + expected: true, + }, + { + name: "order index 4", + orderIndex: 4, + expected: false, + }, + { + name: "order index 5", + orderIndex: 5, + expected: false, + }, + { + name: "order index 100", + orderIndex: 100, + expected: false, + }, + { + name: "order index 255", + orderIndex: 255, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + member := GuildMember{OrderIndex: tt.orderIndex} + result := member.IsSubLeader() + if result != tt.expected { + t.Errorf("IsSubLeader() with OrderIndex=%d = %v, expected %v", + tt.orderIndex, result, tt.expected) + } + }) + } +} + +func TestGuildMember_CanRecruit_Priority(t *testing.T) { + // Test that Recruiter flag takes priority (short-circuit) + member := GuildMember{ + Recruiter: true, + OrderIndex: 100, // Would fail OrderIndex check + IsLeader: false, + } + + if !member.CanRecruit() { + t.Error("Recruiter flag should allow recruiting regardless of OrderIndex") + } +} + +func TestGuildMember_CanRecruit_OrderIndexBoundary(t *testing.T) { + // Test the exact boundary at OrderIndex == 3 vs 4 + member3 := GuildMember{Recruiter: false, OrderIndex: 3, IsLeader: false} + member4 := GuildMember{Recruiter: false, OrderIndex: 4, IsLeader: false} + + if !member3.CanRecruit() { + t.Error("OrderIndex 3 should be able to recruit") + } + if member4.CanRecruit() { + t.Error("OrderIndex 4 should NOT be able to recruit") + } +} diff --git a/server/channelserver/handlers_shop_gacha_test.go b/server/channelserver/handlers_shop_gacha_test.go index a6301804b..bb7c9de76 100644 --- a/server/channelserver/handlers_shop_gacha_test.go +++ b/server/channelserver/handlers_shop_gacha_test.go @@ -230,3 +230,192 @@ func TestGachaItemStruct(t *testing.T) { t.Errorf("Quantity = %d, want 20", item.Quantity) } } + +func TestGetRandomEntries_EmptyEntriesZeroRolls(t *testing.T) { + // Note: getRandomEntries with empty entries and rolls > 0 causes infinite loop. + // Only test the valid case of 0 rolls with empty entries. + entries := []GachaEntry{} + result, err := getRandomEntries(entries, 0, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 0 { + t.Errorf("expected empty result, got %d entries", len(result)) + } +} + +func TestGetRandomEntries_ZeroRolls(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 0, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 0 { + t.Errorf("expected 0 results, got %d", len(result)) + } +} + +func TestGetRandomEntries_SingleEntryNonBox(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0, ItemNumber: 100}, + } + result, err := getRandomEntries(entries, 3, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 3 { + t.Errorf("expected 3 results, got %d", len(result)) + } + for i, r := range result { + if r.ID != 1 { + t.Errorf("result[%d].ID = %d, expected 1", i, r.ID) + } + } +} + +func TestGetRandomEntries_NonBoxAllowsDuplicates(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 5, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 5 { + t.Errorf("expected 5 results, got %d", len(result)) + } + // All should be the same since there's only one entry + for i, r := range result { + if r.ID != 1 { + t.Errorf("result[%d].ID = %d, expected 1", i, r.ID) + } + } +} + +func TestGetRandomEntries_BoxModeRemovesSelected(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + {ID: 3, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 3, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 3 { + t.Errorf("expected 3 results, got %d", len(result)) + } + + // In box mode, all entries should be unique + seen := make(map[uint32]bool) + for _, r := range result { + if seen[r.ID] { + t.Errorf("duplicate entry in box mode: ID=%d", r.ID) + } + seen[r.ID] = true + } +} + +func TestGetRandomEntries_BoxModeMatchingCount(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 2, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 2 { + t.Errorf("expected 2 results, got %d", len(result)) + } + + // Should contain both entries exactly once + seen := make(map[uint32]bool) + for _, r := range result { + seen[r.ID] = true + } + if !seen[1] || !seen[2] { + t.Errorf("box mode should return all entries when rolls == len(entries)") + } +} + +func TestGetRandomEntries_WeightedSelectionBias(t *testing.T) { + // Test that weighted selection respects weights + entries := []GachaEntry{ + {ID: 1, Weight: 100.0}, // Very high weight + {ID: 2, Weight: 0.001}, // Very low weight + } + + // Run many iterations + counts := make(map[uint32]int) + for i := 0; i < 1000; i++ { + result, _ := getRandomEntries(entries, 1, false) + if len(result) > 0 { + counts[result[0].ID]++ + } + } + + // ID 1 should be selected much more often + if counts[1] <= counts[2] { + t.Errorf("weighted selection not working: high weight count=%d, low weight count=%d", + counts[1], counts[2]) + } +} + +func TestGetRandomEntries_MultipleEntriesMultipleRolls(t *testing.T) { + entries := []GachaEntry{ + {ID: 1, Weight: 1.0}, + {ID: 2, Weight: 1.0}, + {ID: 3, Weight: 1.0}, + } + result, err := getRandomEntries(entries, 10, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 10 { + t.Errorf("expected 10 results, got %d", len(result)) + } + + // All results should have valid IDs + for i, r := range result { + if r.ID < 1 || r.ID > 3 { + t.Errorf("result[%d].ID = %d, expected 1, 2, or 3", i, r.ID) + } + } +} + +func TestGetRandomEntries_PreservesEntryData(t *testing.T) { + entries := []GachaEntry{ + { + ID: 1, + Weight: 1.0, + ItemNumber: 100, + ItemQuantity: 5, + Rarity: 3, + FrontierPoints: 500, + }, + } + result, err := getRandomEntries(entries, 1, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(result) != 1 { + t.Fatalf("expected 1 result, got %d", len(result)) + } + + r := result[0] + if r.ItemNumber != 100 { + t.Errorf("ItemNumber = %d, expected 100", r.ItemNumber) + } + if r.ItemQuantity != 5 { + t.Errorf("ItemQuantity = %d, expected 5", r.ItemQuantity) + } + if r.Rarity != 3 { + t.Errorf("Rarity = %d, expected 3", r.Rarity) + } + if r.FrontierPoints != 500 { + t.Errorf("FrontierPoints = %d, expected 500", r.FrontierPoints) + } +}