mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
mockGuildRepoForMail and mockGuildRepoOps each implemented different subsets of the 68-method GuildRepo interface. Adding any new method required updating both mocks. Merged into a single mockGuildRepo with configurable struct fields for error injection and no-op defaults for the rest.
571 lines
16 KiB
Go
571 lines
16 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func newTestMailService(mr MailRepo, gr GuildRepo) *MailService {
|
|
logger, _ := zap.NewDevelopment()
|
|
return NewMailService(mr, gr, logger)
|
|
}
|
|
|
|
func newTestGuildService(gr GuildRepo, mr MailRepo) *GuildService {
|
|
logger, _ := zap.NewDevelopment()
|
|
ms := newTestMailService(mr, gr)
|
|
return NewGuildService(gr, ms, 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 := &mockGuildRepo{
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 := &mockGuildRepo{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 := &mockGuildRepo{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 := &mockGuildRepo{
|
|
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 := &mockGuildRepo{
|
|
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 := &mockGuildRepo{
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|