Files
Erupe/server/channelserver/svc_guild_test.go
Houmgaor 2abca9fb23 refactor(guild): introduce service layer for guild member operations
Extract business logic from handleMsgMhfOperateGuildMember into
GuildService.OperateMember, establishing the handler→service→repo
layering pattern. The handler is now ~20 lines of protocol glue
(type-assert, map action, call service, send ACK, notify).

GuildService owns authorization checks, repo coordination, mail
composition, and best-effort mail delivery. It accepts plain Go
types (no mhfpacket or Session imports), making it fully testable
with mock repos. Cross-channel notification stays in the handler
since it requires Session.

Adds 7 table-driven service-level tests covering accept/reject/kick,
authorization, repo errors, mail errors, and unknown actions.
2026-02-23 23:26:46 +01:00

171 lines
5.3 KiB
Go

package channelserver
import (
"errors"
"testing"
"go.uber.org/zap"
)
func newTestGuildService(gr GuildRepo, mr MailRepo) *GuildService {
logger, _ := zap.NewDevelopment()
return NewGuildService(gr, mr, nil, logger)
}
func TestGuildService_OperateMember(t *testing.T) {
tests := []struct {
name string
actorCharID uint32
targetCharID uint32
action GuildMemberAction
guild *Guild
membership *GuildMember
acceptErr error
rejectErr error
removeErr error
sendErr error
wantErr bool
wantErrIs error
wantAccepted uint32
wantRejected uint32
wantRemoved uint32
wantMailCount int
wantRecipient uint32
wantMailSubj string
}{
{
name: "accept application as leader",
actorCharID: 1,
targetCharID: 42,
action: GuildMemberActionAccept,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
wantAccepted: 42,
wantMailCount: 1,
wantRecipient: 42,
wantMailSubj: "Accepted!",
},
{
name: "reject application as sub-leader",
actorCharID: 2,
targetCharID: 42,
action: GuildMemberActionReject,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 2, OrderIndex: 2}, // sub-leader
wantRejected: 42,
wantMailCount: 1,
wantRecipient: 42,
wantMailSubj: "Rejected",
},
{
name: "kick member as leader",
actorCharID: 1,
targetCharID: 42,
action: GuildMemberActionKick,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
wantRemoved: 42,
wantMailCount: 1,
wantRecipient: 42,
wantMailSubj: "Kicked",
},
{
name: "unauthorized - not leader or sub",
actorCharID: 5,
targetCharID: 42,
action: GuildMemberActionAccept,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 5, OrderIndex: 10},
wantErr: true,
wantErrIs: ErrUnauthorized,
},
{
name: "repo error on accept",
actorCharID: 1,
targetCharID: 42,
action: GuildMemberActionAccept,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
acceptErr: errors.New("db error"),
wantErr: true,
},
{
name: "mail error is best-effort",
actorCharID: 1,
targetCharID: 42,
action: GuildMemberActionAccept,
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
sendErr: errors.New("mail failed"),
wantAccepted: 42,
wantMailCount: 1,
wantRecipient: 42,
wantMailSubj: "Accepted!",
},
{
name: "unknown action",
actorCharID: 1,
targetCharID: 42,
action: GuildMemberAction(99),
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
wantErr: true,
wantErrIs: ErrUnknownAction,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
guildMock := &mockGuildRepoOps{
membership: tt.membership,
acceptErr: tt.acceptErr,
rejectErr: tt.rejectErr,
removeErr: tt.removeErr,
}
guildMock.guild = tt.guild
mailMock := &mockMailRepo{sendErr: tt.sendErr}
svc := newTestGuildService(guildMock, mailMock)
result, err := svc.OperateMember(tt.actorCharID, tt.targetCharID, tt.action)
if tt.wantErr {
if err == nil {
t.Fatal("Expected error, got nil")
}
if tt.wantErrIs != nil && !errors.Is(err, tt.wantErrIs) {
t.Errorf("Expected error %v, got %v", tt.wantErrIs, err)
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
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)
}
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.Fatalf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
}
if tt.wantMailCount > 0 {
if mailMock.sentMails[0].recipientID != tt.wantRecipient {
t.Errorf("mail recipientID = %d, want %d", mailMock.sentMails[0].recipientID, tt.wantRecipient)
}
if mailMock.sentMails[0].subject != tt.wantMailSubj {
t.Errorf("mail subject = %q, want %q", mailMock.sentMails[0].subject, tt.wantMailSubj)
}
}
if result.MailRecipientID != tt.targetCharID {
t.Errorf("result.MailRecipientID = %d, want %d", result.MailRecipientID, tt.targetCharID)
}
})
}
}