mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 17:43:21 +01:00
refactor(guild): extract disband, resign, leave, and scout logic into GuildService
Move business logic for guild disband, resign leadership, leave, post scout, and answer scout from handlers into GuildService methods. Handlers now delegate to the service layer and handle only protocol concerns (packet parsing, ACK responses, cross-channel notifications). Adds 22 new table-driven service tests and sentinel errors for typed error handling (ErrNoEligibleLeader, ErrAlreadyInvited, etc.). DonateRP left in handler due to Session coupling.
This commit is contained in:
@@ -168,3 +168,397 @@ func TestGuildService_OperateMember(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuildService_Disband(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actorCharID uint32
|
||||
guild *Guild
|
||||
disbandErr error
|
||||
wantSuccess bool
|
||||
wantDisbID uint32
|
||||
}{
|
||||
{
|
||||
name: "leader disbands successfully",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
wantSuccess: true,
|
||||
wantDisbID: 10,
|
||||
},
|
||||
{
|
||||
name: "non-leader cannot disband",
|
||||
actorCharID: 5,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
wantSuccess: false,
|
||||
},
|
||||
{
|
||||
name: "repo error returns failure",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
disbandErr: errors.New("db error"),
|
||||
wantSuccess: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
guildMock := &mockGuildRepoOps{disbandErr: tt.disbandErr}
|
||||
guildMock.guild = tt.guild
|
||||
svc := newTestGuildService(guildMock, &mockMailRepo{})
|
||||
|
||||
result, err := svc.Disband(tt.actorCharID, tt.guild.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if result.Success != tt.wantSuccess {
|
||||
t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
|
||||
}
|
||||
if tt.wantDisbID != 0 && guildMock.disbandedID != tt.wantDisbID {
|
||||
t.Errorf("disbandedID = %d, want %d", guildMock.disbandedID, tt.wantDisbID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuildService_ResignLeadership(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actorCharID uint32
|
||||
guild *Guild
|
||||
members []*GuildMember
|
||||
getMembersErr error
|
||||
wantNewLeader uint32
|
||||
wantErr bool
|
||||
wantSavedCount int
|
||||
wantGuildSaved bool
|
||||
}{
|
||||
{
|
||||
name: "transfers to next eligible member",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
members: []*GuildMember{
|
||||
{CharID: 1, OrderIndex: 1, IsLeader: true},
|
||||
{CharID: 2, OrderIndex: 2, AvoidLeadership: false},
|
||||
},
|
||||
wantNewLeader: 2,
|
||||
wantSavedCount: 2,
|
||||
wantGuildSaved: true,
|
||||
},
|
||||
{
|
||||
name: "skips members avoiding leadership",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
members: []*GuildMember{
|
||||
{CharID: 1, OrderIndex: 1, IsLeader: true},
|
||||
{CharID: 2, OrderIndex: 2, AvoidLeadership: true},
|
||||
{CharID: 3, OrderIndex: 3, AvoidLeadership: false},
|
||||
},
|
||||
wantNewLeader: 3,
|
||||
wantSavedCount: 2,
|
||||
wantGuildSaved: true,
|
||||
},
|
||||
{
|
||||
name: "no eligible successor returns zero",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
members: []*GuildMember{
|
||||
{CharID: 1, OrderIndex: 1, IsLeader: true},
|
||||
{CharID: 2, OrderIndex: 2, AvoidLeadership: true},
|
||||
},
|
||||
wantNewLeader: 0,
|
||||
},
|
||||
{
|
||||
name: "get members error",
|
||||
actorCharID: 1,
|
||||
guild: &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
|
||||
getMembersErr: errors.New("db error"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
guildMock := &mockGuildRepoOps{getMembersErr: tt.getMembersErr}
|
||||
guildMock.guild = tt.guild
|
||||
guildMock.members = tt.members
|
||||
svc := newTestGuildService(guildMock, &mockMailRepo{})
|
||||
|
||||
result, err := svc.ResignLeadership(tt.actorCharID, tt.guild.ID)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if result.NewLeaderCharID != tt.wantNewLeader {
|
||||
t.Errorf("NewLeaderCharID = %d, want %d", result.NewLeaderCharID, tt.wantNewLeader)
|
||||
}
|
||||
if tt.wantSavedCount > 0 && len(guildMock.savedMembers) != tt.wantSavedCount {
|
||||
t.Errorf("savedMembers count = %d, want %d", len(guildMock.savedMembers), tt.wantSavedCount)
|
||||
}
|
||||
if tt.wantGuildSaved && guildMock.savedGuild == nil {
|
||||
t.Error("Guild should be saved")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuildService_Leave(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isApplicant bool
|
||||
rejectErr error
|
||||
removeErr error
|
||||
sendErr error
|
||||
wantSuccess bool
|
||||
wantRejected uint32
|
||||
wantRemoved uint32
|
||||
wantMailCount int
|
||||
}{
|
||||
{
|
||||
name: "member leaves successfully",
|
||||
isApplicant: false,
|
||||
wantSuccess: true,
|
||||
wantRemoved: 1,
|
||||
wantMailCount: 1,
|
||||
},
|
||||
{
|
||||
name: "applicant withdraws via reject",
|
||||
isApplicant: true,
|
||||
wantSuccess: true,
|
||||
wantRejected: 1,
|
||||
wantMailCount: 1,
|
||||
},
|
||||
{
|
||||
name: "remove error returns failure",
|
||||
isApplicant: false,
|
||||
removeErr: errors.New("db error"),
|
||||
wantSuccess: false,
|
||||
},
|
||||
{
|
||||
name: "mail error is best-effort",
|
||||
isApplicant: false,
|
||||
sendErr: errors.New("mail failed"),
|
||||
wantSuccess: true,
|
||||
wantRemoved: 1,
|
||||
wantMailCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
guildMock := &mockGuildRepoOps{
|
||||
rejectErr: tt.rejectErr,
|
||||
removeErr: tt.removeErr,
|
||||
}
|
||||
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
||||
mailMock := &mockMailRepo{sendErr: tt.sendErr}
|
||||
svc := newTestGuildService(guildMock, mailMock)
|
||||
|
||||
result, _ := svc.Leave(1, 10, tt.isApplicant, "TestGuild")
|
||||
if result.Success != tt.wantSuccess {
|
||||
t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
|
||||
}
|
||||
if tt.wantRejected != 0 && guildMock.rejectedCharID != tt.wantRejected {
|
||||
t.Errorf("rejectedCharID = %d, want %d", guildMock.rejectedCharID, tt.wantRejected)
|
||||
}
|
||||
if tt.wantRemoved != 0 && guildMock.removedCharID != tt.wantRemoved {
|
||||
t.Errorf("removedCharID = %d, want %d", guildMock.removedCharID, tt.wantRemoved)
|
||||
}
|
||||
if len(mailMock.sentMails) != tt.wantMailCount {
|
||||
t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuildService_PostScout(t *testing.T) {
|
||||
strings := ScoutInviteStrings{Title: "Invite", Body: "Join 「%s」"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
membership *GuildMember
|
||||
guild *Guild
|
||||
hasApp bool
|
||||
hasAppErr error
|
||||
createAppErr error
|
||||
getMemberErr error
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "successful scout",
|
||||
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
|
||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||
},
|
||||
{
|
||||
name: "already invited",
|
||||
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
|
||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||
hasApp: true,
|
||||
wantErr: ErrAlreadyInvited,
|
||||
},
|
||||
{
|
||||
name: "cannot recruit",
|
||||
membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 10}, // not recruiter, not sub-leader
|
||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||
wantErr: ErrCannotRecruit,
|
||||
},
|
||||
{
|
||||
name: "nil membership",
|
||||
getMemberErr: errors.New("not found"),
|
||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||
wantErr: errors.New("any"), // just check err != nil
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
guildMock := &mockGuildRepoOps{
|
||||
membership: tt.membership,
|
||||
hasAppResult: tt.hasApp,
|
||||
hasAppErr: tt.hasAppErr,
|
||||
createAppErr: tt.createAppErr,
|
||||
getMemberErr: tt.getMemberErr,
|
||||
}
|
||||
guildMock.guild = tt.guild
|
||||
svc := newTestGuildService(guildMock, &mockMailRepo{})
|
||||
|
||||
err := svc.PostScout(1, 42, strings)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
if errors.Is(tt.wantErr, ErrAlreadyInvited) || errors.Is(tt.wantErr, ErrCannotRecruit) {
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Expected %v, got %v", tt.wantErr, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuildService_AnswerScout(t *testing.T) {
|
||||
strings := AnswerScoutStrings{
|
||||
SuccessTitle: "Success!",
|
||||
SuccessBody: "Joined 「%s」.",
|
||||
AcceptedTitle: "Accepted",
|
||||
AcceptedBody: "Accepted invite to 「%s」.",
|
||||
RejectedTitle: "Rejected",
|
||||
RejectedBody: "Rejected invite to 「%s」.",
|
||||
DeclinedTitle: "Declined",
|
||||
DeclinedBody: "Declined invite to 「%s」.",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accept bool
|
||||
guild *Guild
|
||||
application *GuildApplication
|
||||
acceptErr error
|
||||
rejectErr error
|
||||
sendErr error
|
||||
getErr error
|
||||
wantSuccess bool
|
||||
wantErr error
|
||||
wantMailCount int
|
||||
wantAccepted uint32
|
||||
wantRejected uint32
|
||||
}{
|
||||
{
|
||||
name: "accept invitation",
|
||||
accept: true,
|
||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
||||
wantSuccess: true,
|
||||
wantMailCount: 2,
|
||||
wantAccepted: 1,
|
||||
},
|
||||
{
|
||||
name: "decline invitation",
|
||||
accept: false,
|
||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
||||
wantSuccess: true,
|
||||
wantMailCount: 2,
|
||||
wantRejected: 1,
|
||||
},
|
||||
{
|
||||
name: "application missing",
|
||||
accept: true,
|
||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||
application: nil,
|
||||
wantSuccess: false,
|
||||
wantErr: ErrApplicationMissing,
|
||||
},
|
||||
{
|
||||
name: "guild not found",
|
||||
accept: true,
|
||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||
getErr: errors.New("not found"),
|
||||
wantErr: errors.New("any"),
|
||||
},
|
||||
{
|
||||
name: "mail error is best-effort",
|
||||
accept: true,
|
||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
||||
sendErr: errors.New("mail failed"),
|
||||
wantSuccess: true,
|
||||
wantMailCount: 2,
|
||||
wantAccepted: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
guildMock := &mockGuildRepoOps{
|
||||
application: tt.application,
|
||||
acceptErr: tt.acceptErr,
|
||||
rejectErr: tt.rejectErr,
|
||||
}
|
||||
guildMock.guild = tt.guild
|
||||
guildMock.getErr = tt.getErr
|
||||
mailMock := &mockMailRepo{sendErr: tt.sendErr}
|
||||
svc := newTestGuildService(guildMock, mailMock)
|
||||
|
||||
result, err := svc.AnswerScout(1, 50, tt.accept, strings)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
if errors.Is(tt.wantErr, ErrApplicationMissing) && !errors.Is(err, ErrApplicationMissing) {
|
||||
t.Errorf("Expected ErrApplicationMissing, got %v", err)
|
||||
}
|
||||
if result != nil && result.Success {
|
||||
t.Error("Result should not be successful")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if result.Success != tt.wantSuccess {
|
||||
t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
|
||||
}
|
||||
if len(mailMock.sentMails) != tt.wantMailCount {
|
||||
t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
|
||||
}
|
||||
if tt.wantAccepted != 0 && guildMock.acceptedCharID != tt.wantAccepted {
|
||||
t.Errorf("acceptedCharID = %d, want %d", guildMock.acceptedCharID, tt.wantAccepted)
|
||||
}
|
||||
if tt.wantRejected != 0 && guildMock.rejectedCharID != tt.wantRejected {
|
||||
t.Errorf("rejectedCharID = %d, want %d", guildMock.rejectedCharID, tt.wantRejected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user