package service_test import ( "context" "errors" "fmt" "testing" "time" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/service" "gitea.dwysokinski.me/twhelp/dcbot/internal/service/internal/mock" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/text/language" ) func TestGroup_Create(t *testing.T) { t.Parallel() newParams := func(tb testing.TB) domain.CreateGroupParams { tb.Helper() params, err := domain.NewCreateGroupParams( "592292203234328587", "en", "en113", language.English.String(), "1234", "1235", true, true, ) require.NoError(t, err) return params } t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.CreateCalls(func(_ context.Context, p domain.CreateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: p.ServerID(), ChannelGains: p.ChannelGains(), ChannelLosses: p.ChannelLosses(), Barbarians: p.Barbarians(), Internals: p.Internals(), ServerKey: p.ServerKey(), VersionCode: p.VersionCode(), CreatedAt: time.Now(), }, }, nil }) twhelpSvc := &mock.FakeTWHelpService{} twhelpSvc.GetOpenServerCalls(func(_ context.Context, _ string, server string) (domain.TWServer, error) { return domain.TWServer{ Key: server, URL: fmt.Sprintf("https://%s.tribalwars.net", server), Open: true, }, nil }) params := newParams(t) g, err := service.NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1).Create(context.Background(), params) assert.NoError(t, err) assert.NotEmpty(t, g.ID) assert.Equal(t, params.ServerID(), g.ServerID) assert.Equal(t, params.ServerKey(), g.ServerKey) assert.Equal(t, params.VersionCode(), g.VersionCode) assert.Equal(t, params.ChannelGains(), g.ChannelGains) assert.Equal(t, params.ChannelLosses(), g.ChannelLosses) assert.Equal(t, params.Barbarians(), g.Barbarians) assert.Equal(t, params.Internals(), g.Barbarians) assert.NotEmpty(t, g.CreatedAt) }) t.Run("ERR: group limit has been reached", func(t *testing.T) { t.Parallel() const maxGroupsPerServer = 10 repo := &mock.FakeGroupRepository{} repo.ListReturns(make([]domain.GroupWithMonitors, maxGroupsPerServer), nil) g, err := service.NewGroup(repo, nil, zap.NewNop(), maxGroupsPerServer, 1).Create(context.Background(), newParams(t)) assert.ErrorIs(t, err, domain.GroupLimitReachedError{ Current: maxGroupsPerServer, Limit: maxGroupsPerServer, }) assert.Zero(t, g) }) } func TestGroup_AddTribe(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.AddMonitorCalls(func(ctx context.Context, id string, tribeID int) (domain.GroupWithMonitors, error) { group.Monitors = append(group.Monitors, domain.Monitor{ ID: uuid.NewString(), GroupID: id, TribeID: tribeID, }) return group, nil }) twhelpSvc := &mock.FakeTWHelpService{} tribe := domain.Tribe{ ID: 1234, Tag: "*TAG*", Name: "Name", ProfileURL: "Profile", DeletedAt: time.Time{}, } twhelpSvc.GetExistingTribeByTagReturns(tribe, nil) g, err := service. NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). AddTribe(context.Background(), group.ID, group.ServerID, tribe.Tag) assert.NoError(t, err) require.Len(t, g.Monitors, 1) assert.NotZero(t, g.Monitors[0].ID) assert.Equal(t, group.ID, g.Monitors[0].GroupID) assert.Equal(t, tribe.ID, g.Monitors[0].TribeID) }) t.Run("ERR: group doesn't exist", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", serverID: uuid.NewString(), group: group, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). AddTribe(context.Background(), group.ID, uuid.NewString(), "*TAG*") assert.ErrorIs(t, err, domain.GroupDoesNotExistError{ ID: group.ID, }) assert.Zero(t, g) }) } }) t.Run("ERR: monitor limit has been reached", func(t *testing.T) { t.Parallel() const maxMonitorsPerGroup = 10 repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, Monitors: make([]domain.Monitor, maxMonitorsPerGroup), } repo.GetReturns(group, nil) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, maxMonitorsPerGroup). AddTribe(context.Background(), group.ID, group.ServerID, "*TAG*") assert.ErrorIs(t, err, domain.MonitorLimitReachedError{ Current: len(group.Monitors), Limit: maxMonitorsPerGroup, }) assert.Zero(t, g) }) } func TestGroup_RemoveTribe(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} groupID := uuid.NewString() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: groupID, ServerID: uuid.NewString(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: 1234, }, { ID: uuid.NewString(), TribeID: 1237, }, }, } repo.GetReturns(group, nil) repo.DeleteMonitorsCalls(func(ctx context.Context, id string, monitorIDs ...string) (domain.GroupWithMonitors, error) { monitors := make([]domain.Monitor, 0, len(group.Monitors)-1) for _, m := range group.Monitors { var skip bool for _, monitorID := range monitorIDs { if m.ID == monitorID { skip = true break } } if skip { continue } monitors = append(monitors, m) } return domain.GroupWithMonitors{ Monitors: monitors, }, nil }) twhelpSvc := &mock.FakeTWHelpService{} tribe := domain.Tribe{ ID: group.Monitors[0].TribeID, Tag: "*TAG*", } twhelpSvc.ListTribesByTagReturns([]domain.Tribe{tribe}, nil) g, err := service. NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). RemoveTribe(context.Background(), group.ID, group.ServerID, tribe.Tag) assert.NoError(t, err) assert.Len(t, g.Monitors, len(group.Monitors)-1) assert.NotContains(t, g.Monitors, group.Monitors[0]) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), }, }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). RemoveTribe(context.Background(), group.ID, tt.serverID, group.Monitors[0].ID) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) t.Run("ERR: tribe not found", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} groupID := uuid.NewString() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: groupID, ServerID: uuid.NewString(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: 1234, }, { ID: uuid.NewString(), TribeID: 1237, }, }, } repo.GetReturns(group, nil) repo.DeleteMonitorsCalls(func(ctx context.Context, id string, monitorIDs ...string) (domain.GroupWithMonitors, error) { monitors := make([]domain.Monitor, 0, len(group.Monitors)-1) for _, m := range group.Monitors { var skip bool for _, monitorID := range monitorIDs { if m.ID == monitorID { skip = true break } } if skip { continue } monitors = append(monitors, m) } return domain.GroupWithMonitors{ Monitors: monitors, }, nil }) twhelpSvc := &mock.FakeTWHelpService{} tag := "*X*" g, err := service. NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). RemoveTribe(context.Background(), group.ID, group.ServerID, tag) assert.ErrorIs(t, err, domain.TribeTagNotFoundError{ Tag: tag, }) assert.Zero(t, g) }) } func TestGroup_SetLanguageTag(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: id, LanguageTag: params.LanguageTag.String, }, }, nil }) languageTag := uuid.NewString() g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetLanguageTag(context.Background(), group.ID, group.ServerID, languageTag) assert.NoError(t, err) assert.Equal(t, group.ID, g.ID) assert.Equal(t, languageTag, g.LanguageTag) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetLanguageTag(context.Background(), group.ID, tt.serverID, uuid.NewString()) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_SetChannelGains(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: id, ChannelGains: params.ChannelGains.String, }, }, nil }) channelID := uuid.NewString() g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetChannelGains(context.Background(), group.ID, group.ServerID, channelID) assert.NoError(t, err) assert.Equal(t, group.ID, g.ID) assert.Equal(t, channelID, g.ChannelGains) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetChannelGains(context.Background(), group.ID, tt.serverID, uuid.NewString()) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_SetChannelLosses(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: id, ChannelLosses: params.ChannelLosses.String, }, }, nil }) channelID := uuid.NewString() g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetChannelLosses(context.Background(), group.ID, group.ServerID, channelID) assert.NoError(t, err) assert.Equal(t, group.ID, g.ID) assert.Equal(t, channelID, g.ChannelLosses) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetChannelLosses(context.Background(), group.ID, tt.serverID, uuid.NewString()) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_SetInternals(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: id, Internals: params.Internals.Bool, }, }, nil }) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetInternals(context.Background(), group.ID, group.ServerID, true) assert.NoError(t, err) assert.Equal(t, group.ID, g.ID) assert.True(t, g.Internals) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetInternals(context.Background(), group.ID, tt.serverID, true) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_SetBarbarians(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { return domain.GroupWithMonitors{ Group: domain.Group{ ID: id, Barbarians: params.Barbarians.Bool, }, }, nil }) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetBarbarians(context.Background(), group.ID, group.ServerID, true) assert.NoError(t, err) assert.Equal(t, group.ID, g.ID) assert.True(t, g.Barbarians) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). SetBarbarians(context.Background(), group.ID, tt.serverID, true) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_Get(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). Get(context.Background(), group.ID, group.ServerID) assert.NoError(t, err) assert.Equal(t, group, g) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). Get(context.Background(), group.ID, tt.serverID) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) } func TestGroup_GetWithTribes(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), VersionCode: "pl", ServerKey: "pl181", }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: 115, }, { ID: uuid.NewString(), TribeID: 130, }, { ID: uuid.NewString(), TribeID: 11, }, }, } repo.GetReturns(group, nil) twhelpSvc := &mock.FakeTWHelpService{} twhelpSvc.GetTribeByIDCalls(func(ctx context.Context, versionCode string, serverKey string, id int) (domain.Tribe, error) { return domain.Tribe{ ID: id, Tag: fmt.Sprintf("*%d*", id), Name: fmt.Sprintf("name %d", id), ProfileURL: fmt.Sprintf("profile-%d", id), }, nil }) g, err := service. NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). GetWithTribes(context.Background(), group.ID, group.ServerID) assert.NoError(t, err) assert.Equal(t, group.Group, g.Group) assert.Len(t, g.Monitors, len(group.Monitors)) for _, m := range g.Monitors { assert.NotZero(t, m.Tribe) } }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) g, err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). GetWithTribes(context.Background(), group.ID, tt.serverID) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) assert.Zero(t, g) }) } }) t.Run("ERR: one of requests failed", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), VersionCode: "pl", ServerKey: "pl181", }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: 115, }, { ID: uuid.NewString(), TribeID: 130, }, { ID: uuid.NewString(), TribeID: 11, }, }, } repo.GetReturns(group, nil) twhelpSvc := &mock.FakeTWHelpService{} expectedErr := errors.New("err") twhelpSvc.GetTribeByIDCalls(func(ctx context.Context, versionCode string, serverKey string, id int) (domain.Tribe, error) { if id == 115 { return domain.Tribe{}, expectedErr } <-ctx.Done() return domain.Tribe{}, ctx.Err() }) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() g, err := service. NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). GetWithTribes(ctx, group.ID, group.ServerID) assert.ErrorIs(t, err, expectedErr) assert.Zero(t, g) }) } func TestGroup_Delete(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } repo.GetReturns(group, nil) assert.NoError(t, service. NewGroup(repo, nil, zap.NewNop(), 1, 1). Delete(context.Background(), group.ID, group.ServerID)) }) t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() group := domain.GroupWithMonitors{ Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), }, } tests := []struct { name string group domain.GroupWithMonitors serverID string err error }{ { name: "repository returned error", serverID: group.ServerID, err: domain.GroupNotFoundError{ ID: group.ID, }, }, { name: "incorrect server id", group: group, serverID: uuid.NewString(), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() repo := &mock.FakeGroupRepository{} repo.GetReturns(tt.group, tt.err) err := service. NewGroup(repo, nil, zap.NewNop(), 1, 1). Delete(context.Background(), group.ID, tt.serverID) assert.ErrorIs(t, err, domain.GroupNotFoundError{ ID: group.ID, }) }) } }) } func TestGroup_Execute(t *testing.T) { t.Parallel() twhelpSvc := &mock.FakeTWHelpService{} tribes := map[string][]domain.TribeMeta{ "pl:pl181": { { ID: 1, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 2, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 3, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, }, "en:en130": { { ID: 100, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 101, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, }, "pl:pl180": { { ID: 200, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, }, "de:de200": { { ID: 300, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 301, Name: uuid.NewString(), Tag: uuid.NewString(), ProfileURL: uuid.NewString(), }, }, } players := map[string][]domain.PlayerMeta{ "pl:pl181": { { ID: 1, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["pl:pl181"][0], Valid: true, }, }, { ID: 2, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["pl:pl181"][1], Valid: true, }, }, { ID: 3, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["pl:pl181"][2], Valid: true, }, }, { ID: 4, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{}, }, }, "en:en130": { { ID: 100, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["en:en130"][0], Valid: true, }, }, { ID: 101, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["en:en130"][1], Valid: true, }, }, { ID: 102, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{}, }, }, "pl:pl180": { { ID: 200, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["pl:pl180"][0], Valid: true, }, }, }, "de:de200": { { ID: 300, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["de:de200"][0], Valid: true, }, }, { ID: 301, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["de:de200"][0], Valid: true, }, }, { ID: 302, Name: uuid.NewString(), ProfileURL: uuid.NewString(), Tribe: domain.NullTribeMeta{ Tribe: tribes["de:de200"][1], Valid: true, }, }, }, } villages := map[string][]domain.VillageMeta{ "pl:pl181": { { ID: 1, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["pl:pl181"][1], Valid: true, }, }, { ID: 2, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["pl:pl181"][0], Valid: true, }, }, { ID: 3, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["pl:pl181"][2], Valid: true, }, }, { ID: 4, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 5, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["pl:pl181"][0], Valid: true, }, }, }, "en:en130": { { ID: 100, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 101, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["en:en130"][0], Valid: true, }, }, { ID: 102, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["en:en130"][2], Valid: true, }, }, }, "pl:pl180": { { ID: 200, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), }, }, "de:de200": { { ID: 300, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), }, { ID: 301, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["de:de200"][1], Valid: true, }, }, { ID: 302, FullName: uuid.NewString(), ProfileURL: uuid.NewString(), Player: domain.NullPlayerMeta{ Player: players["de:de200"][2], Valid: true, }, }, }, } ennoblements := map[string][]domain.Ennoblement{ "pl:pl181": { { ID: 1, Village: villages["pl:pl181"][0], NewOwner: players["pl:pl181"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 2, // self conquer, should be skipped Village: villages["pl:pl181"][1], NewOwner: players["pl:pl181"][0], CreatedAt: time.Now().Add(-4 * time.Minute), }, { ID: 3, // internal, should be skipped (internals disabled) Village: villages["pl:pl181"][2], NewOwner: players["pl:pl181"][0], CreatedAt: time.Now().Add(-3 * time.Minute), }, { ID: 4, // barbarian, shouldn't be skipped (barbarians enabled) Village: villages["pl:pl181"][3], NewOwner: players["pl:pl181"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 5, // disabled notifications about gains, should be skipped Village: villages["pl:pl181"][4], NewOwner: players["pl:pl181"][1], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 6, // disabled notifications about losses, should be skipped Village: villages["pl:pl181"][4], NewOwner: players["pl:pl181"][3], CreatedAt: time.Now().Add(-5 * time.Minute), }, }, "en:en130": { { ID: 100, // no monitor for these tribes, should be skipped Village: villages["en:en130"][0], NewOwner: players["en:en130"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 101, // no monitor for these tribes, should be skipped Village: villages["en:en130"][1], NewOwner: players["en:en130"][2], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 102, Village: villages["en:en130"][2], NewOwner: players["en:en130"][1], CreatedAt: time.Now().Add(-5 * time.Minute), }, }, "pl:pl180": { { ID: 200, // api error, should be skipped Village: villages["pl:pl180"][0], NewOwner: players["pl:pl180"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, }, "de:de200": { { ID: 300, // barbarian, should be skipped (barbarians disabled) Village: villages["de:de200"][0], NewOwner: players["de:de200"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 301, // internal, shouldn't be skipped (internals enabled) Village: villages["de:de200"][1], NewOwner: players["de:de200"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, { ID: 302, // internal, shouldn't be skipped (internals enabled) Village: villages["de:de200"][2], NewOwner: players["de:de200"][0], CreatedAt: time.Now().Add(-5 * time.Minute), }, }, } twhelpSvc.ListEnnoblementsSinceCalls( func(ctx context.Context, version string, server string, _ time.Time) ([]domain.Ennoblement, error) { if version == "pl" && server == "pl180" { return nil, errors.New("random error") } return ennoblements[version+":"+server], nil }, ) repo := &mock.FakeGroupRepository{} groups := []domain.GroupWithMonitors{ { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: "", Barbarians: true, ServerKey: "pl181", VersionCode: "pl", CreatedAt: time.Now(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: tribes["pl:pl181"][0].ID, CreatedAt: time.Now(), }, { ID: uuid.NewString(), TribeID: tribes["pl:pl181"][2].ID, CreatedAt: time.Now(), }, }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: "", ChannelLosses: uuid.NewString(), ServerKey: "pl181", VersionCode: "pl", CreatedAt: time.Now(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: tribes["pl:pl181"][1].ID, CreatedAt: time.Now(), }, }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), ServerKey: "en130", VersionCode: "en", CreatedAt: time.Now(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: tribes["en:en130"][1].ID, CreatedAt: time.Now(), }, }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), Barbarians: true, ServerKey: "pl180", VersionCode: "pl", CreatedAt: time.Now(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: tribes["pl:pl180"][0].ID, CreatedAt: time.Now(), }, }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), Barbarians: false, Internals: true, ServerKey: "de200", VersionCode: "de", CreatedAt: time.Now(), }, Monitors: []domain.Monitor{ { ID: uuid.NewString(), TribeID: tribes["de:de200"][0].ID, CreatedAt: time.Now(), }, { ID: uuid.NewString(), TribeID: tribes["de:de200"][1].ID, CreatedAt: time.Now(), }, }, }, } repo.ListReturns(groups, nil) notifications, err := service.NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1). Execute(context.Background()) assert.NoError(t, err) expectedNotifications := []domain.EnnoblementNotification{ newNotification(domain.EnnoblementNotificationTypeGain, groups[0].ServerID, groups[0].ChannelGains, ennoblements["pl:pl181"][0]), newNotification(domain.EnnoblementNotificationTypeGain, groups[0].ServerID, groups[0].ChannelGains, ennoblements["pl:pl181"][3]), newNotification(domain.EnnoblementNotificationTypeLoss, groups[1].ServerID, groups[1].ChannelLosses, ennoblements["pl:pl181"][0]), newNotification(domain.EnnoblementNotificationTypeGain, groups[2].ServerID, groups[2].ChannelGains, ennoblements["en:en130"][2]), newNotification(domain.EnnoblementNotificationTypeGain, groups[4].ServerID, groups[4].ChannelGains, ennoblements["de:de200"][1]), newNotification(domain.EnnoblementNotificationTypeGain, groups[4].ServerID, groups[4].ChannelGains, ennoblements["de:de200"][2]), } assert.Len(t, notifications, len(expectedNotifications)) for _, n := range expectedNotifications { assert.Contains(t, notifications, n) } } func TestGroup_CleanUp(t *testing.T) { t.Parallel() twhelpSvc := &mock.FakeTWHelpService{} versions := []domain.TWVersion{ { Code: "pl", Host: "www.plemiona.pl", Name: "Poland", Timezone: "Europe/Warsaw", }, { Code: "uk", Host: "www.tribalwars.co.uk", Name: "United Kingdom", Timezone: "Europe/London", }, { Code: "tr", Host: "www.klanlar.org", Name: "Turkey", Timezone: "Europe/Istanbul", }, } twhelpSvc.ListVersionsReturns(versions, nil) serversByVersion := map[string][]domain.TWServer{ versions[0].Code: { // pl { Key: "pl181", URL: "https://pl181.plemiona.pl", Open: false, }, }, versions[1].Code: { // uk { Key: "uk51", URL: "https://uk51.tribalwars.co.uk", Open: false, }, { Key: "uk52", URL: "https://uk52.tribalwars.co.uk", Open: false, }, }, } twhelpSvc.ListClosedServersCalls(func(ctx context.Context, version string, _ int) ([]domain.TWServer, error) { return serversByVersion[version], nil }) repo := &mock.FakeGroupRepository{} groupsWithDisabledNotifications := []domain.GroupWithMonitors{ { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: "", ChannelLosses: "", Internals: true, Barbarians: true, ServerKey: "pl181", VersionCode: versions[0].Code, CreatedAt: time.Now().Add(-48 * time.Hour), }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: "", ChannelLosses: "", Internals: true, Barbarians: true, ServerKey: "pl181", VersionCode: versions[0].Code, CreatedAt: time.Now().Add(-45 * time.Hour), }, }, } groupsPL := []domain.GroupWithMonitors{ { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: "", Internals: true, Barbarians: true, ServerKey: "pl181", VersionCode: versions[0].Code, CreatedAt: time.Now().Add(-120 * time.Hour), }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), Internals: true, Barbarians: true, ServerKey: "pl181", VersionCode: versions[0].Code, CreatedAt: time.Now().Add(-11 * time.Hour), }, }, } groupsUK := []domain.GroupWithMonitors{ { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), Internals: true, Barbarians: true, ServerKey: "uk51", VersionCode: versions[1].Code, CreatedAt: time.Now().Add(-150 * time.Hour), }, }, { Group: domain.Group{ ID: uuid.NewString(), ServerID: uuid.NewString(), ChannelGains: uuid.NewString(), ChannelLosses: uuid.NewString(), Internals: true, Barbarians: true, ServerKey: "uk52", VersionCode: versions[1].Code, CreatedAt: time.Now().Add(-200 * time.Hour), }, }, } repo.ListCalls(func(ctx context.Context, params domain.ListGroupsParams) ([]domain.GroupWithMonitors, error) { if params.EnabledNotifications.Valid && !params.EnabledNotifications.Bool { return groupsWithDisabledNotifications, nil } if params.VersionCode.String == versions[0].Code { return groupsPL, nil } if params.VersionCode.String == versions[1].Code { return groupsUK, nil } return nil, errors.New("unexpected call") }) assert.NoError(t, service.NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1).CleanUp(context.Background())) require.Equal(t, 3, repo.DeleteManyCallCount()) for _, tt := range []struct { i int groups []domain.GroupWithMonitors }{ { i: 0, groups: groupsWithDisabledNotifications, }, { i: 1, groups: groupsPL, }, { i: 2, groups: groupsUK, }, } { _, ids := repo.DeleteManyArgsForCall(tt.i) assert.Len(t, ids, len(tt.groups)) for i, id := range ids { assert.Equal(t, tt.groups[i].ID, id) } } } func newNotification(typ domain.EnnoblementNotificationType, serverID, channelID string, e domain.Ennoblement) domain.EnnoblementNotification { return domain.EnnoblementNotification{ Type: typ, ServerID: serverID, ChannelID: channelID, Ennoblement: e, } }