mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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)
554 lines
13 KiB
Go
554 lines
13 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")
|
|
}
|
|
}
|
|
|
|
// --- scanAllianceWithGuilds nil guild tests (issue #171) ---
|
|
|
|
func TestInfoJoint_MissingSubGuild1(t *testing.T) {
|
|
// Verify that GetAllianceByID returns an error when sub guild 1 references
|
|
// a non-existent guild (nil return from GetByID). This is the scenario from
|
|
// issue #171 — a deleted guild causes a nil dereference in scanAllianceWithGuilds.
|
|
server := createMockServer()
|
|
guildMock := &mockGuildRepo{
|
|
// GetAllianceByID returns an error for missing guilds because
|
|
// scanAllianceWithGuilds calls GetByID for each sub guild.
|
|
// With guild=nil and SubGuild1ID > 0, GetByID returns nil,
|
|
// and scanAllianceWithGuilds should return an error rather than panic.
|
|
getAllianceErr: errNotFound,
|
|
}
|
|
server.guildRepo = guildMock
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 5}
|
|
handleMsgMhfInfoJoint(session, pkt)
|
|
|
|
// Handler should send a response even on error (not softlock)
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Error("No response packet queued — would softlock the client")
|
|
}
|
|
}
|
|
|
|
func TestInfoJoint_MissingSubGuild2(t *testing.T) {
|
|
server := createMockServer()
|
|
guildMock := &mockGuildRepo{
|
|
getAllianceErr: errNotFound,
|
|
}
|
|
server.guildRepo = guildMock
|
|
session := createMockSession(1, server)
|
|
|
|
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 6}
|
|
handleMsgMhfInfoJoint(session, pkt)
|
|
|
|
select {
|
|
case <-session.sendPackets:
|
|
default:
|
|
t.Error("No response packet queued — would softlock the client")
|
|
}
|
|
}
|