Files
Erupe/server/channelserver/handlers_guild_alliance_test.go
Houmgaor aee53534a2 fix(guild): add nil guards for alliance guild lookups (#171)
scanAllianceWithGuilds dereferences guild pointers returned by GetByID
without checking for nil. Since GetByID returns (nil, nil) when a guild
is missing, alliances referencing deleted guilds cause nil-pointer
panics. The panic is caught by session recovery but no ACK is sent,
softlocking the client.

Add nil checks in scanAllianceWithGuilds, handleMsgMhfOperateJoint,
handleMsgMhfInfoJoint, and handleMsgMhfInfoGuild so that missing
guilds or alliances produce proper error responses instead of panics.
2026-03-02 19:43:11 +01:00

508 lines
12 KiB
Go

package channelserver
import (
"testing"
"time"
"erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket"
)
// --- handleMsgMhfCreateJoint tests ---
func TestCreateJoint_Success(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfCreateJoint{
AckHandle: 100,
GuildID: 10,
Name: "TestAlliance",
}
handleMsgMhfCreateJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
func TestCreateJoint_Error(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{createAllianceErr: errNotFound}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfCreateJoint{
AckHandle: 100,
GuildID: 10,
Name: "TestAlliance",
}
// Should not panic; error is logged
handleMsgMhfCreateJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
// --- handleMsgMhfOperateJoint tests ---
func TestOperateJoint_Disband_AsOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 10,
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1 // session charID
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DISBAND,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.deletedAllianceID != 5 {
t.Errorf("DeleteAlliance called with %d, want 5", guildMock.deletedAllianceID)
}
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
func TestOperateJoint_Disband_NotOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 99, // different guild
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DISBAND,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.deletedAllianceID != 0 {
t.Error("Should not disband when not alliance owner")
}
}
func TestOperateJoint_Leave_AsLeader(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 99,
SubGuild1ID: 10,
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_LEAVE,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.removedAllyArgs == nil {
t.Fatal("RemoveGuildFromAlliance should be called")
}
if guildMock.removedAllyArgs[1] != 10 {
t.Errorf("Removed guildID = %d, want 10", guildMock.removedAllyArgs[1])
}
}
func TestOperateJoint_Leave_NotLeader(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{ID: 5, ParentGuildID: 99},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 999 // not session char
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_LEAVE,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.removedAllyArgs != nil {
t.Error("Non-leader should not be able to leave alliance")
}
}
func TestOperateJoint_Kick_AsAllianceOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 10,
ParentGuild: Guild{},
SubGuild1ID: 20,
},
}
guildMock.alliance.ParentGuild.LeaderCharID = 1 // session char owns alliance
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
data1 := byteframe.NewByteFrame()
data1.WriteUint32(20) // guildID to kick
_, _ = data1.Seek(0, 0)
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_KICK,
Data1: data1,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.removedAllyArgs == nil {
t.Fatal("RemoveGuildFromAlliance should be called for kick")
}
if guildMock.removedAllyArgs[1] != 20 {
t.Errorf("Kicked guildID = %d, want 20", guildMock.removedAllyArgs[1])
}
}
func TestOperateJoint_Kick_NotOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 99,
ParentGuild: Guild{},
},
}
guildMock.alliance.ParentGuild.LeaderCharID = 999 // not session char
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_KICK,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.removedAllyArgs != nil {
t.Error("Non-owner should not kick from alliance")
}
}
func TestOperateJoint_Allow_AsOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 10,
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_ALLOW,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.allianceRecruitingSet == nil || !*guildMock.allianceRecruitingSet {
t.Error("SetAllianceRecruiting should be called with true")
}
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
func TestOperateJoint_Allow_NotOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 99, // different guild
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_ALLOW,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.allianceRecruitingSet != nil {
t.Error("Non-owner should not toggle alliance recruiting")
}
}
func TestOperateJoint_Deny_AsOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 10,
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DENY,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.allianceRecruitingSet == nil || *guildMock.allianceRecruitingSet {
t.Error("SetAllianceRecruiting should be called with false")
}
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
func TestOperateJoint_Deny_NotOwner(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
ParentGuildID: 99,
},
}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DENY,
}
handleMsgMhfOperateJoint(session, pkt)
if guildMock.allianceRecruitingSet != nil {
t.Error("Non-owner should not toggle alliance recruiting")
}
}
// --- handleMsgMhfInfoJoint tests ---
func TestInfoJoint_Success(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
Name: "TestAlliance",
CreatedAt: time.Now(),
TotalMembers: 15,
ParentGuildID: 10,
ParentGuild: Guild{Name: "ParentGuild", MemberCount: 5},
},
}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 5}
handleMsgMhfInfoJoint(session, pkt)
select {
case p := <-session.sendPackets:
if len(p.data) < 10 {
t.Errorf("Response too short: %d bytes", len(p.data))
}
default:
t.Error("No response packet queued")
}
}
func TestInfoJoint_WithSubGuilds(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{
ID: 5,
Name: "BigAlliance",
CreatedAt: time.Now(),
TotalMembers: 30,
ParentGuildID: 10,
ParentGuild: Guild{Name: "Parent", MemberCount: 10},
SubGuild1ID: 20,
SubGuild1: Guild{Name: "Sub1", MemberCount: 10},
SubGuild2ID: 30,
SubGuild2: Guild{Name: "Sub2", MemberCount: 10},
},
}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 5}
handleMsgMhfInfoJoint(session, pkt)
select {
case p := <-session.sendPackets:
if len(p.data) < 30 {
t.Errorf("Response too short for alliance with sub guilds: %d bytes", len(p.data))
}
default:
t.Error("No response packet queued")
}
}
func TestInfoJoint_NotFound(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{getAllianceErr: errNotFound}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 999}
handleMsgMhfInfoJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued")
}
}
func TestInfoJoint_NilAlliance(t *testing.T) {
server := createMockServer()
// alliance is nil, no error — simulates deleted alliance
guildMock := &mockGuildRepo{}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 999}
handleMsgMhfInfoJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued — would softlock the client")
}
}
func TestOperateJoint_NilGuild(t *testing.T) {
server := createMockServer()
// guild is nil — simulates deleted guild
guildMock := &mockGuildRepo{
alliance: &GuildAlliance{ID: 5, ParentGuildID: 10},
}
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 5,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DISBAND,
}
handleMsgMhfOperateJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued — would softlock the client")
}
}
func TestOperateJoint_NilAlliance(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{}
guildMock.guild = &Guild{ID: 10}
guildMock.guild.LeaderCharID = 1
server.guildRepo = guildMock
session := createMockSession(1, server)
pkt := &mhfpacket.MsgMhfOperateJoint{
AckHandle: 100,
AllianceID: 999,
GuildID: 10,
Action: mhfpacket.OPERATE_JOINT_DISBAND,
}
handleMsgMhfOperateJoint(session, pkt)
select {
case <-session.sendPackets:
default:
t.Error("No response packet queued — would softlock the client")
}
}