diff --git a/server/channelserver/handlers_guild_board_test.go b/server/channelserver/handlers_guild_board_test.go new file mode 100644 index 000000000..d7be37af9 --- /dev/null +++ b/server/channelserver/handlers_guild_board_test.go @@ -0,0 +1,241 @@ +package channelserver + +import ( + "testing" + "time" + + "erupe-ce/network/mhfpacket" +) + +// --- handleMsgMhfUpdateGuildMessageBoard tests --- + +func TestUpdateGuildMessageBoard_CreatePost(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateGuildMessageBoard{ + AckHandle: 100, + MessageOp: 0, // Create + PostType: 0, + StampID: 5, + Title: "Test Title", + Body: "Test Body", + } + + handleMsgMhfUpdateGuildMessageBoard(session, pkt) + + if guildMock.createdPost == nil { + t.Fatal("CreatePost should be called") + } + if guildMock.createdPost[0].(uint32) != 10 { + t.Errorf("CreatePost guildID = %d, want 10", guildMock.createdPost[0]) + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestUpdateGuildMessageBoard_DeletePost(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateGuildMessageBoard{ + AckHandle: 100, + MessageOp: 1, // Delete + PostID: 42, + } + + handleMsgMhfUpdateGuildMessageBoard(session, pkt) + + if guildMock.deletedPostID != 42 { + t.Errorf("DeletePost postID = %d, want 42", guildMock.deletedPostID) + } +} + +func TestUpdateGuildMessageBoard_NoGuild(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{} + guildMock.getErr = errNotFound + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateGuildMessageBoard{ + AckHandle: 100, + MessageOp: 0, + } + + handleMsgMhfUpdateGuildMessageBoard(session, pkt) + + // Returns early with empty success + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestUpdateGuildMessageBoard_Applicant(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + hasAppResult: true, // is an applicant + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateGuildMessageBoard{ + AckHandle: 100, + MessageOp: 0, + } + + handleMsgMhfUpdateGuildMessageBoard(session, pkt) + + if guildMock.createdPost != nil { + t.Error("Applicant should not be able to create posts") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestUpdateGuildMessageBoard_HasAppError(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + hasAppErr: errNotFound, // error checking app status + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfUpdateGuildMessageBoard{ + AckHandle: 100, + MessageOp: 0, + Title: "Test", + Body: "Body", + } + + // Should log warning and treat as non-applicant (applicant=false on error) + handleMsgMhfUpdateGuildMessageBoard(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +// --- handleMsgMhfEnumerateGuildMessageBoard tests --- + +func TestEnumerateGuildMessageBoard_NoPosts(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + posts: []*MessageBoardPost{}, + } + guildMock.guild = &Guild{ID: 10} + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateGuildMessageBoard{ + AckHandle: 100, + BoardType: 0, + MaxPosts: 100, + } + + handleMsgMhfEnumerateGuildMessageBoard(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestEnumerateGuildMessageBoard_WithPosts(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + posts: []*MessageBoardPost{ + {ID: 1, AuthorID: 100, StampID: 5, Title: "Hello", Body: "World", Timestamp: time.Now()}, + {ID: 2, AuthorID: 200, StampID: 0, Title: "Test", Body: "Post", Timestamp: time.Now()}, + }, + } + guildMock.guild = &Guild{ID: 10} + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateGuildMessageBoard{ + AckHandle: 100, + BoardType: 0, + MaxPosts: 100, + } + + handleMsgMhfEnumerateGuildMessageBoard(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) < 8 { + t.Errorf("Response too short for 2 posts: %d bytes", len(p.data)) + } + default: + t.Error("No response packet queued") + } +} + +func TestEnumerateGuildMessageBoard_DBError(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + guildMock := &mockGuildRepoOps{ + listPostsErr: errNotFound, + } + guildMock.guild = &Guild{ID: 10} + server.guildRepo = guildMock + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfEnumerateGuildMessageBoard{ + AckHandle: 100, + BoardType: 0, + MaxPosts: 100, + } + + handleMsgMhfEnumerateGuildMessageBoard(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/handlers_guild_ops_test.go b/server/channelserver/handlers_guild_ops_test.go new file mode 100644 index 000000000..102ee0a70 --- /dev/null +++ b/server/channelserver/handlers_guild_ops_test.go @@ -0,0 +1,607 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/common/byteframe" + "erupe-ce/network/mhfpacket" +) + +// --- handleMsgMhfOperateGuild tests --- + +func TestOperateGuild_Disband_Success(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildDisband, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.disbandedID != 10 { + t.Errorf("Disband called with guild %d, want 10", guildMock.disbandedID) + } + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("No response data") + } + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Disband_NotLeader(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 5}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 // different from session charID + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildDisband, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.disbandedID != 0 { + t.Error("Disband should not be called for non-leader") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Disband_RepoError(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + disbandErr: errNotFound, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildDisband, + } + + handleMsgMhfOperateGuild(session, pkt) + + // response=0 when disband fails + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Resign_TransferLeadership(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + guildMock.members = []*GuildMember{ + {CharID: 1, OrderIndex: 1, IsLeader: true}, + {CharID: 2, OrderIndex: 2, AvoidLeadership: false}, + } + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildResign, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.guild.LeaderCharID != 2 { + t.Errorf("Leader should transfer to charID 2, got %d", guildMock.guild.LeaderCharID) + } + if len(guildMock.savedMembers) < 2 { + t.Fatalf("Expected 2 saved members, got %d", len(guildMock.savedMembers)) + } + if guildMock.savedGuild == nil { + t.Error("Guild should be saved after resign") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Resign_SkipsAvoidLeadership(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + guildMock.members = []*GuildMember{ + {CharID: 1, OrderIndex: 1, IsLeader: true}, + {CharID: 2, OrderIndex: 2, AvoidLeadership: true}, + {CharID: 3, OrderIndex: 3, AvoidLeadership: false}, + } + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildResign, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.guild.LeaderCharID != 3 { + t.Errorf("Leader should transfer to charID 3 (skipping 2), got %d", guildMock.guild.LeaderCharID) + } +} + +func TestOperateGuild_Apply_Success(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 5}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildApply, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.createdAppArgs == nil { + t.Fatal("CreateApplication should be called") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Apply_RepoError(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 5}, + createAppErr: errNotFound, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildApply, + } + + handleMsgMhfOperateGuild(session, pkt) + + // Should still succeed with 0 leader ID + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_Leave_AsApplicant(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsApplicant: true, OrderIndex: 5}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildLeave, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.rejectedCharID != 1 { + t.Errorf("RejectApplication should be called for applicant, got rejectedCharID=%d", guildMock.rejectedCharID) + } + if guildMock.removedCharID != 0 { + t.Error("RemoveCharacter should not be called for applicant") + } +} + +func TestOperateGuild_Leave_AsMember(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsApplicant: false, OrderIndex: 5}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildLeave, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.removedCharID != 1 { + t.Errorf("RemoveCharacter should be called with charID 1, got %d", guildMock.removedCharID) + } + if len(mailMock.sentMails) != 1 { + t.Fatalf("Expected 1 withdrawal mail, got %d", len(mailMock.sentMails)) + } + if mailMock.sentMails[0].recipientID != 1 { + t.Errorf("Mail recipientID = %d, want 1", mailMock.sentMails[0].recipientID) + } +} + +func TestOperateGuild_Leave_MailError(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{sendErr: errNotFound} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsApplicant: false, OrderIndex: 5}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildLeave, + } + + // Should not panic; mail error is logged as warning + handleMsgMhfOperateGuild(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_UpdateComment_Success(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildUpdateComment, + Data2: newNullTermBF([]byte("Test\x00")), + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.savedGuild == nil { + t.Error("Guild should be saved after comment update") + } +} + +func TestOperateGuild_UpdateComment_NotLeader(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 10}, // not leader, not sub-leader + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildUpdateComment, + } + + handleMsgMhfOperateGuild(session, pkt) + + // Should return fail ack + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Fatal("Expected fail response") + } + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuild_UpdateMotto_Success(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildUpdateMotto, + Data1: newMottoBF(5, 3), + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.savedGuild == nil { + t.Error("Guild should be saved after motto update") + } + if guildMock.savedGuild.MainMotto != 3 { + t.Errorf("MainMotto = %d, want 3", guildMock.savedGuild.MainMotto) + } + if guildMock.savedGuild.SubMotto != 5 { + t.Errorf("SubMotto = %d, want 5", guildMock.savedGuild.SubMotto) + } +} + +func TestOperateGuild_UpdateMotto_NotLeader(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 10}, + } + guildMock.guild = &Guild{ID: 10} + guildMock.guild.LeaderCharID = 999 + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildUpdateMotto, + } + + handleMsgMhfOperateGuild(session, pkt) + + if guildMock.savedGuild != nil { + t.Error("Guild should not be saved when not leader") + } +} + +func TestOperateGuild_GuildNotFound(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{} + guildMock.getErr = errNotFound + server.guildRepo = guildMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuild{ + AckHandle: 100, + GuildID: 10, + Action: mhfpacket.OperateGuildDisband, + } + + handleMsgMhfOperateGuild(session, pkt) + + // Should return fail ack + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +// --- handleMsgMhfOperateGuildMember tests --- + +func TestOperateGuildMember_Accept(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuildMember{ + AckHandle: 100, + GuildID: 10, + CharID: 42, + Action: mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT, + } + + handleMsgMhfOperateGuildMember(session, pkt) + + if guildMock.acceptedCharID != 42 { + t.Errorf("AcceptApplication charID = %d, want 42", guildMock.acceptedCharID) + } + if len(mailMock.sentMails) != 1 { + t.Fatalf("Expected 1 mail, got %d", len(mailMock.sentMails)) + } + if mailMock.sentMails[0].recipientID != 42 { + t.Errorf("Mail recipientID = %d, want 42", mailMock.sentMails[0].recipientID) + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuildMember_Reject(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuildMember{ + AckHandle: 100, + GuildID: 10, + CharID: 42, + Action: mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT, + } + + handleMsgMhfOperateGuildMember(session, pkt) + + if guildMock.rejectedCharID != 42 { + t.Errorf("RejectApplication charID = %d, want 42", guildMock.rejectedCharID) + } + if len(mailMock.sentMails) != 1 { + t.Fatalf("Expected 1 mail, got %d", len(mailMock.sentMails)) + } +} + +func TestOperateGuildMember_Kick(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuildMember{ + AckHandle: 100, + GuildID: 10, + CharID: 42, + Action: mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK, + } + + handleMsgMhfOperateGuildMember(session, pkt) + + if guildMock.removedCharID != 42 { + t.Errorf("RemoveCharacter charID = %d, want 42", guildMock.removedCharID) + } + if len(mailMock.sentMails) != 1 { + t.Fatalf("Expected 1 mail, got %d", len(mailMock.sentMails)) + } +} + +func TestOperateGuildMember_MailError(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{sendErr: errNotFound} + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 1 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuildMember{ + AckHandle: 100, + GuildID: 10, + CharID: 42, + Action: mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT, + } + + // Should not panic; mail error logged as warning + handleMsgMhfOperateGuildMember(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestOperateGuildMember_NotLeaderOrSub(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{ + membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 10}, // not sub-leader + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 999 // not the session char + server.guildRepo = guildMock + server.mailRepo = &mockMailRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfOperateGuildMember{ + AckHandle: 100, + GuildID: 10, + CharID: 42, + Action: mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT, + } + + handleMsgMhfOperateGuildMember(session, pkt) + + if guildMock.acceptedCharID != 0 { + t.Error("Should not accept when actor lacks permission") + } +} + +// --- byteframe helpers for packet Data fields --- + +func newNullTermBF(data []byte) *byteframe.ByteFrame { + bf := byteframe.NewByteFrame() + bf.WriteBytes(data) + _, _ = bf.Seek(0, 0) + return bf +} + +func newMottoBF(sub, main uint8) *byteframe.ByteFrame { + bf := byteframe.NewByteFrame() + bf.WriteUint16(0) // skipped + bf.WriteUint8(sub) // SubMotto + bf.WriteUint8(main) // MainMotto + _, _ = bf.Seek(0, 0) + return bf +} diff --git a/server/channelserver/handlers_guild_scout_test.go b/server/channelserver/handlers_guild_scout_test.go new file mode 100644 index 000000000..5250fd18a --- /dev/null +++ b/server/channelserver/handlers_guild_scout_test.go @@ -0,0 +1,262 @@ +package channelserver + +import ( + "testing" + + "erupe-ce/network/mhfpacket" +) + +// --- handleMsgMhfAnswerGuildScout tests --- + +func TestAnswerGuildScout_Accept(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + application: &GuildApplication{GuildID: 10, CharID: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 50 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnswerGuildScout{ + AckHandle: 100, + LeaderID: 50, + Answer: true, + } + + handleMsgMhfAnswerGuildScout(session, pkt) + + if guildMock.acceptedCharID != 1 { + t.Errorf("AcceptApplication charID = %d, want 1", guildMock.acceptedCharID) + } + if len(mailMock.sentMails) != 2 { + t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails)) + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestAnswerGuildScout_Decline(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + application: &GuildApplication{GuildID: 10, CharID: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 50 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnswerGuildScout{ + AckHandle: 100, + LeaderID: 50, + Answer: false, + } + + handleMsgMhfAnswerGuildScout(session, pkt) + + if guildMock.rejectedCharID != 1 { + t.Errorf("RejectApplication charID = %d, want 1", guildMock.rejectedCharID) + } + if len(mailMock.sentMails) != 2 { + t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails)) + } +} + +func TestAnswerGuildScout_GuildNotFound(t *testing.T) { + server := createMockServer() + guildMock := &mockGuildRepoOps{} + guildMock.getErr = errNotFound + server.guildRepo = guildMock + server.mailRepo = &mockMailRepo{} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnswerGuildScout{ + AckHandle: 100, + LeaderID: 50, + Answer: true, + } + + handleMsgMhfAnswerGuildScout(session, pkt) + + // Should return fail ack + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestAnswerGuildScout_ApplicationMissing(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{} + guildMock := &mockGuildRepoOps{ + application: nil, // no application found + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 50 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnswerGuildScout{ + AckHandle: 100, + LeaderID: 50, + Answer: true, + } + + handleMsgMhfAnswerGuildScout(session, pkt) + + // No mails should be sent when application is missing + if len(mailMock.sentMails) != 0 { + t.Errorf("Expected 0 mails for missing application, got %d", len(mailMock.sentMails)) + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestAnswerGuildScout_MailError(t *testing.T) { + server := createMockServer() + mailMock := &mockMailRepo{sendErr: errNotFound} + guildMock := &mockGuildRepoOps{ + application: &GuildApplication{GuildID: 10, CharID: 1}, + } + guildMock.guild = &Guild{ID: 10, Name: "TestGuild"} + guildMock.guild.LeaderCharID = 50 + server.guildRepo = guildMock + server.mailRepo = mailMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfAnswerGuildScout{ + AckHandle: 100, + LeaderID: 50, + Answer: true, + } + + // Should not panic; mail errors logged as warnings + handleMsgMhfAnswerGuildScout(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +// --- handleMsgMhfGetRejectGuildScout tests --- + +func TestGetRejectGuildScout_Restricted(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + charMock.bools["restrict_guild_scout"] = true + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRejectGuildScout{AckHandle: 100} + + handleMsgMhfGetRejectGuildScout(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) < 4 { + t.Fatal("Response too short") + } + default: + t.Error("No response packet queued") + } +} + +func TestGetRejectGuildScout_Open(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + charMock.bools["restrict_guild_scout"] = false + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRejectGuildScout{AckHandle: 100} + + handleMsgMhfGetRejectGuildScout(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestGetRejectGuildScout_DBError(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + charMock.readErr = errNotFound + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRejectGuildScout{AckHandle: 100} + + handleMsgMhfGetRejectGuildScout(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +// --- handleMsgMhfSetRejectGuildScout tests --- + +func TestSetRejectGuildScout_Success(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetRejectGuildScout{ + AckHandle: 100, + Reject: true, + } + + handleMsgMhfSetRejectGuildScout(session, pkt) + + if !charMock.bools["restrict_guild_scout"] { + t.Error("restrict_guild_scout should be true") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestSetRejectGuildScout_DBError(t *testing.T) { + server := createMockServer() + charMock := newMockCharacterRepo() + charMock.saveErr = errNotFound + server.charRepo = charMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfSetRejectGuildScout{ + AckHandle: 100, + Reject: true, + } + + handleMsgMhfSetRejectGuildScout(session, pkt) + + // Should return fail ack + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/handlers_items_test.go b/server/channelserver/handlers_items_test.go new file mode 100644 index 000000000..11a569424 --- /dev/null +++ b/server/channelserver/handlers_items_test.go @@ -0,0 +1,364 @@ +package channelserver + +import ( + "testing" + "time" + + "erupe-ce/common/byteframe" + "erupe-ce/common/mhfitem" + "erupe-ce/network/mhfpacket" +) + +// --- userGetItems tests --- + +func TestUserGetItems_NilData(t *testing.T) { + server := createMockServer() + userMock := &mockUserRepoForItems{itemBoxData: nil} + server.userRepo = userMock + session := createMockSession(1, server) + session.userID = 1 + + items := userGetItems(session) + + if len(items) != 0 { + t.Errorf("Expected empty items, got %d", len(items)) + } +} + +func TestUserGetItems_DBError(t *testing.T) { + server := createMockServer() + userMock := &mockUserRepoForItems{itemBoxErr: errNotFound} + server.userRepo = userMock + session := createMockSession(1, server) + session.userID = 1 + + items := userGetItems(session) + + if len(items) != 0 { + t.Errorf("Expected empty items on error, got %d", len(items)) + } +} + +func TestUserGetItems_ParsesData(t *testing.T) { + // Build serialized item box with 1 item + bf := byteframe.NewByteFrame() + bf.WriteUint16(1) // numStacks + bf.WriteUint16(0) // unused + // Item stack: warehouseID(4) + itemID(2) + quantity(2) + unk0(4) = 12 bytes + bf.WriteUint32(100) // warehouseID + bf.WriteUint16(500) // itemID + bf.WriteUint16(3) // quantity + bf.WriteUint32(0) // unk0 + + server := createMockServer() + userMock := &mockUserRepoForItems{itemBoxData: bf.Data()} + server.userRepo = userMock + session := createMockSession(1, server) + session.userID = 1 + + items := userGetItems(session) + + if len(items) != 1 { + t.Fatalf("Expected 1 item, got %d", len(items)) + } + if items[0].Item.ItemID != 500 { + t.Errorf("ItemID = %d, want 500", items[0].Item.ItemID) + } + if items[0].Quantity != 3 { + t.Errorf("Quantity = %d, want 3", items[0].Quantity) + } +} + +// --- handleMsgMhfCheckWeeklyStamp tests --- + +func TestCheckWeeklyStamp_InvalidType(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckWeeklyStamp{ + AckHandle: 100, + StampType: "invalid", + } + + handleMsgMhfCheckWeeklyStamp(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestCheckWeeklyStamp_FirstCheck(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + checkedErr: errNotFound, // no existing record + totals: [2]uint16{0, 0}, + } + server.stampRepo = stampMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckWeeklyStamp{ + AckHandle: 100, + StampType: "hl", + } + + handleMsgMhfCheckWeeklyStamp(session, pkt) + + if !stampMock.initCalled { + t.Error("Init should be called on first check") + } + + select { + case p := <-session.sendPackets: + if len(p.data) < 14 { + t.Errorf("Response too short: %d bytes", len(p.data)) + } + default: + t.Error("No response packet queued") + } +} + +func TestCheckWeeklyStamp_WithinWeek(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + checkedTime: TimeAdjusted(), // checked right now (within this week) + totals: [2]uint16{3, 1}, + } + server.stampRepo = stampMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckWeeklyStamp{ + AckHandle: 100, + StampType: "hl", + } + + handleMsgMhfCheckWeeklyStamp(session, pkt) + + if stampMock.incrementCalled { + t.Error("IncrementTotal should not be called within same week") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestCheckWeeklyStamp_WeekRollover(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + checkedTime: TimeWeekStart().Add(-24 * time.Hour), // before this week + totals: [2]uint16{5, 2}, + } + server.stampRepo = stampMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckWeeklyStamp{ + AckHandle: 100, + StampType: "ex", + } + + handleMsgMhfCheckWeeklyStamp(session, pkt) + + if !stampMock.incrementCalled { + t.Error("IncrementTotal should be called after week rollover") + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestCheckWeeklyStamp_GetTotalsError(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + checkedTime: TimeAdjusted(), + totalsErr: errNotFound, + } + server.stampRepo = stampMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfCheckWeeklyStamp{ + AckHandle: 100, + StampType: "hl", + } + + // Should not panic; logs warning, returns zeros + handleMsgMhfCheckWeeklyStamp(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +// --- handleMsgMhfExchangeWeeklyStamp tests --- + +func TestExchangeWeeklyStamp_InvalidType(t *testing.T) { + server := createMockServer() + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeWeeklyStamp{ + AckHandle: 100, + StampType: "invalid", + } + + handleMsgMhfExchangeWeeklyStamp(session, pkt) + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestExchangeWeeklyStamp_HL(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + exchangeResult: [2]uint16{10, 5}, + } + houseMock := newMockHouseRepoForItems() + server.stampRepo = stampMock + server.houseRepo = houseMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeWeeklyStamp{ + AckHandle: 100, + StampType: "hl", + } + + handleMsgMhfExchangeWeeklyStamp(session, pkt) + + // Verify warehouse gift box was updated (index 10) + if houseMock.setData[10] == nil { + t.Error("Gift box should be updated with ticket item") + } + // Parse the gift box to verify the item + if len(houseMock.setData[10]) > 0 { + bf := byteframe.NewByteFrameFromBytes(houseMock.setData[10]) + count := bf.ReadUint16() + if count != 1 { + t.Errorf("Expected 1 item in gift box, got %d", count) + } + bf.ReadUint16() // unused + item := mhfitem.ReadWarehouseItem(bf) + if item.Item.ItemID != 1630 { + t.Errorf("ItemID = %d, want 1630 (HL ticket)", item.Item.ItemID) + } + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestExchangeWeeklyStamp_EX(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + exchangeResult: [2]uint16{10, 5}, + } + houseMock := newMockHouseRepoForItems() + server.stampRepo = stampMock + server.houseRepo = houseMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeWeeklyStamp{ + AckHandle: 100, + StampType: "ex", + } + + handleMsgMhfExchangeWeeklyStamp(session, pkt) + + if houseMock.setData[10] == nil { + t.Error("Gift box should be updated with ticket item") + } + if len(houseMock.setData[10]) > 0 { + bf := byteframe.NewByteFrameFromBytes(houseMock.setData[10]) + count := bf.ReadUint16() + if count != 1 { + t.Errorf("Expected 1 item in gift box, got %d", count) + } + bf.ReadUint16() // unused + item := mhfitem.ReadWarehouseItem(bf) + if item.Item.ItemID != 1631 { + t.Errorf("ItemID = %d, want 1631 (EX ticket)", item.Item.ItemID) + } + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestExchangeWeeklyStamp_ExchangeError(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + exchangeErr: errNotFound, + } + server.stampRepo = stampMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeWeeklyStamp{ + AckHandle: 100, + StampType: "hl", + } + + handleMsgMhfExchangeWeeklyStamp(session, pkt) + + // Should return fail ack + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} + +func TestExchangeWeeklyStamp_Yearly(t *testing.T) { + server := createMockServer() + stampMock := &mockStampRepoForItems{ + yearlyResult: [2]uint16{20, 10}, + } + houseMock := newMockHouseRepoForItems() + server.stampRepo = stampMock + server.houseRepo = houseMock + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfExchangeWeeklyStamp{ + AckHandle: 100, + StampType: "ex", + ExchangeType: 10, // Yearly + } + + handleMsgMhfExchangeWeeklyStamp(session, pkt) + + if houseMock.setData[10] == nil { + t.Error("Gift box should be updated with yearly ticket") + } + if len(houseMock.setData[10]) > 0 { + bf := byteframe.NewByteFrameFromBytes(houseMock.setData[10]) + count := bf.ReadUint16() + if count != 1 { + t.Errorf("Expected 1 item in gift box, got %d", count) + } + bf.ReadUint16() // unused + item := mhfitem.ReadWarehouseItem(bf) + if item.Item.ItemID != 2210 { + t.Errorf("ItemID = %d, want 2210 (yearly ticket)", item.Item.ItemID) + } + } + + select { + case <-session.sendPackets: + default: + t.Error("No response packet queued") + } +} diff --git a/server/channelserver/repo_mocks_test.go b/server/channelserver/repo_mocks_test.go index 4eab51696..311bef9b2 100644 --- a/server/channelserver/repo_mocks_test.go +++ b/server/channelserver/repo_mocks_test.go @@ -330,3 +330,269 @@ func (m *mockGuildRepoForMail) InsertKillLog(_ uint32, _ int, _ uint8, _ time.Ti func (m *mockGuildRepoForMail) ListInvitedCharacters(_ uint32) ([]*ScoutedCharacter, error) { return nil, nil } func (m *mockGuildRepoForMail) RolloverDailyRP(_ uint32, _ time.Time) error { return nil } func (m *mockGuildRepoForMail) AddWeeklyBonusUsers(_ uint32, _ uint8) error { return nil } + +// --- mockGuildRepoOps (enhanced guild repo for ops/scout/board tests) --- + +type mockGuildRepoOps struct { + mockGuildRepoForMail + + // Configurable errors + saveErr error + saveMemberErr error + disbandErr error + getMembersErr error + acceptErr error + rejectErr error + removeErr error + createAppErr error + getMemberErr error + hasAppResult bool + hasAppErr error + listPostsErr error + createPostErr error + deletePostErr error + + // State tracking + disbandedID uint32 + removedCharID uint32 + acceptedCharID uint32 + rejectedCharID uint32 + savedGuild *Guild + savedMembers []*GuildMember + createdAppArgs []interface{} + createdPost []interface{} + deletedPostID uint32 + + // Data + membership *GuildMember + application *GuildApplication + posts []*MessageBoardPost +} + +func (m *mockGuildRepoOps) GetByID(guildID uint32) (*Guild, error) { + if m.getErr != nil { + return nil, m.getErr + } + if m.guild != nil && m.guild.ID == guildID { + return m.guild, nil + } + return nil, errNotFound +} + +func (m *mockGuildRepoOps) GetByCharID(charID uint32) (*Guild, error) { + if m.getErr != nil { + return nil, m.getErr + } + return m.guild, nil +} + +func (m *mockGuildRepoOps) GetMembers(guildID uint32, applicants bool) ([]*GuildMember, error) { + if m.getMembersErr != nil { + return nil, m.getMembersErr + } + return m.members, nil +} + +func (m *mockGuildRepoOps) GetCharacterMembership(_ uint32) (*GuildMember, error) { + if m.getMemberErr != nil { + return nil, m.getMemberErr + } + return m.membership, nil +} + +func (m *mockGuildRepoOps) Save(guild *Guild) error { + m.savedGuild = guild + return m.saveErr +} + +func (m *mockGuildRepoOps) SaveMember(member *GuildMember) error { + m.savedMembers = append(m.savedMembers, member) + return m.saveMemberErr +} + +func (m *mockGuildRepoOps) Disband(guildID uint32) error { + m.disbandedID = guildID + return m.disbandErr +} + +func (m *mockGuildRepoOps) RemoveCharacter(charID uint32) error { + m.removedCharID = charID + return m.removeErr +} + +func (m *mockGuildRepoOps) AcceptApplication(guildID, charID uint32) error { + m.acceptedCharID = charID + return m.acceptErr +} + +func (m *mockGuildRepoOps) RejectApplication(guildID, charID uint32) error { + m.rejectedCharID = charID + return m.rejectErr +} + +func (m *mockGuildRepoOps) CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType) error { + m.createdAppArgs = []interface{}{guildID, charID, actorID, appType} + return m.createAppErr +} + +func (m *mockGuildRepoOps) HasApplication(guildID, charID uint32) (bool, error) { + return m.hasAppResult, m.hasAppErr +} + +func (m *mockGuildRepoOps) GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error) { + return m.application, nil +} + +func (m *mockGuildRepoOps) ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error) { + if m.listPostsErr != nil { + return nil, m.listPostsErr + } + return m.posts, nil +} + +func (m *mockGuildRepoOps) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error { + m.createdPost = []interface{}{guildID, authorID, stampID, postType, title, body, maxPosts} + return m.createPostErr +} + +func (m *mockGuildRepoOps) DeletePost(postID uint32) error { + m.deletedPostID = postID + return m.deletePostErr +} + +// --- mockUserRepoForItems --- + +type mockUserRepoForItems struct { + itemBoxData []byte + itemBoxErr error + setData []byte +} + +func (m *mockUserRepoForItems) GetItemBox(_ uint32) ([]byte, error) { + return m.itemBoxData, m.itemBoxErr +} + +func (m *mockUserRepoForItems) SetItemBox(_ uint32, data []byte) error { + m.setData = data + return nil +} + +// Stub all other UserRepo methods. +func (m *mockUserRepoForItems) GetGachaPoints(_ uint32) (uint32, uint32, uint32, error) { return 0, 0, 0, nil } +func (m *mockUserRepoForItems) GetTrialCoins(_ uint32) (uint16, error) { return 0, nil } +func (m *mockUserRepoForItems) DeductTrialCoins(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) DeductPremiumCoins(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) AddPremiumCoins(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) AddTrialCoins(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) DeductFrontierPoints(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) AddFrontierPoints(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) AdjustFrontierPointsDeduct(_ uint32, _ int) (uint32, error) { return 0, nil } +func (m *mockUserRepoForItems) AdjustFrontierPointsCredit(_ uint32, _ int) (uint32, error) { return 0, nil } +func (m *mockUserRepoForItems) AddFrontierPointsFromGacha(_ uint32, _ uint32, _ uint8) error { return nil } +func (m *mockUserRepoForItems) GetRights(_ uint32) (uint32, error) { return 0, nil } +func (m *mockUserRepoForItems) SetRights(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) IsOp(_ uint32) (bool, error) { return false, nil } +func (m *mockUserRepoForItems) SetLastCharacter(_ uint32, _ uint32) error { return nil } +func (m *mockUserRepoForItems) GetTimer(_ uint32) (bool, error) { return false, nil } +func (m *mockUserRepoForItems) SetTimer(_ uint32, _ bool) error { return nil } +func (m *mockUserRepoForItems) CountByPSNID(_ string) (int, error) { return 0, nil } +func (m *mockUserRepoForItems) SetPSNID(_ uint32, _ string) error { return nil } +func (m *mockUserRepoForItems) GetDiscordToken(_ uint32) (string, error) { return "", nil } +func (m *mockUserRepoForItems) SetDiscordToken(_ uint32, _ string) error { return nil } +func (m *mockUserRepoForItems) LinkDiscord(_ string, _ string) (string, error) { return "", nil } +func (m *mockUserRepoForItems) SetPasswordByDiscordID(_ string, _ []byte) error { return nil } +func (m *mockUserRepoForItems) GetByIDAndUsername(_ uint32) (uint32, string, error) { return 0, "", nil } +func (m *mockUserRepoForItems) BanUser(_ uint32, _ *time.Time) error { return nil } + +// --- mockStampRepoForItems --- + +type mockStampRepoForItems struct { + checkedTime time.Time + checkedErr error + totals [2]uint16 // total, redeemed + totalsErr error + initCalled bool + incrementCalled bool + setCalled bool + exchangeResult [2]uint16 + exchangeErr error + yearlyResult [2]uint16 + yearlyErr error +} + +func (m *mockStampRepoForItems) GetChecked(_ uint32, _ string) (time.Time, error) { + return m.checkedTime, m.checkedErr +} + +func (m *mockStampRepoForItems) Init(_ uint32, _ time.Time) error { + m.initCalled = true + return nil +} + +func (m *mockStampRepoForItems) SetChecked(_ uint32, _ string, _ time.Time) error { + m.setCalled = true + return nil +} + +func (m *mockStampRepoForItems) IncrementTotal(_ uint32, _ string) error { + m.incrementCalled = true + return nil +} + +func (m *mockStampRepoForItems) GetTotals(_ uint32, _ string) (uint16, uint16, error) { + return m.totals[0], m.totals[1], m.totalsErr +} + +func (m *mockStampRepoForItems) ExchangeYearly(_ uint32) (uint16, uint16, error) { + return m.yearlyResult[0], m.yearlyResult[1], m.yearlyErr +} + +func (m *mockStampRepoForItems) Exchange(_ uint32, _ string) (uint16, uint16, error) { + return m.exchangeResult[0], m.exchangeResult[1], m.exchangeErr +} + +// --- mockHouseRepoForItems --- + +type mockHouseRepoForItems struct { + warehouseItems map[uint8][]byte + setData map[uint8][]byte + setErr error +} + +func newMockHouseRepoForItems() *mockHouseRepoForItems { + return &mockHouseRepoForItems{ + warehouseItems: make(map[uint8][]byte), + setData: make(map[uint8][]byte), + } +} + +func (m *mockHouseRepoForItems) GetWarehouseItemData(_ uint32, index uint8) ([]byte, error) { + return m.warehouseItems[index], nil +} + +func (m *mockHouseRepoForItems) SetWarehouseItemData(_ uint32, index uint8, data []byte) error { + m.setData[index] = data + return m.setErr +} + +func (m *mockHouseRepoForItems) InitializeWarehouse(_ uint32) error { return nil } + +// Stub all other HouseRepo methods. +func (m *mockHouseRepoForItems) UpdateInterior(_ uint32, _ []byte) error { return nil } +func (m *mockHouseRepoForItems) GetHouseByCharID(_ uint32) (HouseData, error) { return HouseData{}, nil } +func (m *mockHouseRepoForItems) SearchHousesByName(_ string) ([]HouseData, error) { return nil, nil } +func (m *mockHouseRepoForItems) UpdateHouseState(_ uint32, _ uint8, _ string) error { return nil } +func (m *mockHouseRepoForItems) GetHouseAccess(_ uint32) (uint8, string, error) { return 0, "", nil } +func (m *mockHouseRepoForItems) GetHouseContents(_ uint32) ([]byte, []byte, []byte, []byte, []byte, []byte, []byte, error) { + return nil, nil, nil, nil, nil, nil, nil, nil +} +func (m *mockHouseRepoForItems) GetMission(_ uint32) ([]byte, error) { return nil, nil } +func (m *mockHouseRepoForItems) UpdateMission(_ uint32, _ []byte) error { return nil } +func (m *mockHouseRepoForItems) GetWarehouseNames(_ uint32) ([10]string, [10]string, error) { + return [10]string{}, [10]string{}, nil +} +func (m *mockHouseRepoForItems) RenameWarehouseBox(_ uint32, _ uint8, _ uint8, _ string) error { return nil } +func (m *mockHouseRepoForItems) GetWarehouseEquipData(_ uint32, _ uint8) ([]byte, error) { return nil, nil } +func (m *mockHouseRepoForItems) SetWarehouseEquipData(_ uint32, _ uint8, _ []byte) error { return nil } +func (m *mockHouseRepoForItems) GetTitles(_ uint32) ([]Title, error) { return nil, nil } +func (m *mockHouseRepoForItems) AcquireTitle(_ uint16, _ uint32) error { return nil }