feat: server snapshots (#49)
Some checks failed
ci/woodpecker/push/govulncheck Pipeline failed
ci/woodpecker/push/test Pipeline failed

Reviewed-on: #49
This commit is contained in:
Dawid Wysokiński 2024-05-09 07:49:04 +00:00
parent e3fa23d0c4
commit 08a7f0c504
39 changed files with 2760 additions and 50 deletions

View File

@ -509,7 +509,7 @@ issues:
text: add-constant
- path: bun/migrations
linters:
- init
- gochecknoinits
- linters:
- lll
source: "^//go:generate "

View File

@ -6,7 +6,7 @@ repos:
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
- repo: https://github.com/golangci/golangci-lint
rev: v1.58.0
rev: v1.58.1
hooks:
- id: golangci-lint
- repo: https://github.com/hadolint/hadolint

View File

@ -16,7 +16,7 @@ install-git-hooks:
.PHONY: install-golangci-lint
install-golangci-lint:
@echo "Installing github.com/golangci/golangci-lint..."
@(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.58.0
@(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.58.1
.PHONY: install-oapi-codegen
install-oapi-codegen:

View File

@ -49,18 +49,32 @@ var cmdConsumer = &cli.Command{
c.String(rmqFlagTopicSyncServersCmd.Name),
c.String(rmqFlagTopicServerSyncedEvent.Name),
)
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
marshaler,
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
"",
)
twSvc, err := newTWServiceFromFlags(c)
if err != nil {
return err
}
serverSvc := app.NewServerService(adapter.NewServerBunRepository(db), twSvc, serverPublisher)
serverSnapshotSvc := app.NewServerSnapshotService(
adapter.NewServerSnapshotBunRepository(db),
serverSvc,
serverSnapshotPublisher,
)
consumer := port.NewServerWatermillConsumer(
app.NewServerService(adapter.NewServerBunRepository(db), twSvc, serverPublisher),
serverSvc,
serverSnapshotSvc,
subscriber,
logger,
marshaler,
c.String(rmqFlagTopicSyncServersCmd.Name),
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
c.String(rmqFlagTopicServerSyncedEvent.Name),
c.String(rmqFlagTopicTribesSyncedEvent.Name),
c.String(rmqFlagTopicPlayersSyncedEvent.Name),

View File

@ -171,13 +171,18 @@ var (
}
defer closeBunDB(bunDB, logger)
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
"",
)
tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
c.String(rmqFlagTopicCreateTribeSnapshotsCmd.Name),
c.String(rmqFlagTopicTribeSnapshotsCreatedEvent.Name),
)
playerSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
@ -187,7 +192,13 @@ var (
versionSvc := app.NewVersionService(adapter.NewVersionBunRepository(bunDB))
serverSvc := app.NewServerService(adapter.NewServerBunRepository(bunDB), nil, nil)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher, playerSnapshotPublisher)
snapshotSvc := app.NewSnapshotService(
versionSvc,
serverSvc,
serverSnapshotPublisher,
tribeSnapshotPublisher,
playerSnapshotPublisher,
)
shutdownSignalCtx, stop := newShutdownSignalContext(c.Context)
defer stop()

View File

@ -31,6 +31,11 @@ var (
Value: "tribes.event.synced",
EnvVars: []string{"RABBITMQ_TOPIC_TRIBES_SYNCED_EVENT"},
}
rmqFlagTopicCreateServerSnapshotCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createServerSnapshotCmd",
Value: "servers.cmd.create_snapshot",
EnvVars: []string{"RABBITMQ_TOPIC_CREATE_SERVER_SNAPSHOT_CMD"},
}
rmqFlagTopicCreateTribeSnapshotsCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createTribeSnapshotsCmd",
Value: "tribes.cmd.create_snapshots",
@ -79,6 +84,7 @@ var (
rmqFlags = []cli.Flag{
rmqFlagConnectionString,
rmqFlagTopicSyncServersCmd,
rmqFlagTopicCreateServerSnapshotCmd,
rmqFlagTopicServerSyncedEvent,
rmqFlagTopicTribesSyncedEvent,
rmqFlagTopicCreateTribeSnapshotsCmd,

View File

@ -111,7 +111,7 @@ func (repo *EnnoblementBunRepository) ListWithRelations(
func (repo *EnnoblementBunRepository) Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error {
if _, err := repo.db.NewDelete().
Model(&bunmodel.Ennoblement{}).
Model((*bunmodel.Ennoblement)(nil)).
Where("server_key = ?", serverKey).
Where("created_at <= ?", createdAtLTE).
Returning("NULL").

View File

@ -109,7 +109,7 @@ func (repo *PlayerSnapshotBunRepository) ListWithRelations(
func (repo *PlayerSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete().
Model(&bunmodel.PlayerSnapshot{}).
Model((*bunmodel.PlayerSnapshot)(nil)).
Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE).
Returning("NULL").

View File

@ -182,6 +182,10 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("ennoblement_data_synced_at = ?", ennoblementDataSyncedAt.V)
}
if snapshotCreatedAt := a.params.SnapshotCreatedAt(); snapshotCreatedAt.Valid {
q = q.Set("snapshot_created_at = ?", snapshotCreatedAt.V)
}
if tribeSnapshotsCreatedAt := a.params.TribeSnapshotsCreatedAt(); tribeSnapshotsCreatedAt.Valid {
q = q.Set("tribe_snapshots_created_at = ?", tribeSnapshotsCreatedAt.V)
}
@ -228,6 +232,13 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
)
}
if snapshotCreatedAt := a.params.SnapshotCreatedAtLT(); snapshotCreatedAt.Valid {
q = q.Where(
"server.snapshot_created_at < ? OR server.snapshot_created_at is null",
snapshotCreatedAt.V,
)
}
for _, s := range a.params.Sort() {
column, dir, err := a.sortToColumnAndDirection(s)
if err != nil {

View File

@ -0,0 +1,179 @@
package adapter
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/uptrace/bun"
)
type ServerSnapshotBunRepository struct {
db bun.IDB
}
func NewServerSnapshotBunRepository(db bun.IDB) *ServerSnapshotBunRepository {
return &ServerSnapshotBunRepository{db: db}
}
func (repo *ServerSnapshotBunRepository) Create(
ctx context.Context,
params ...domain.CreateServerSnapshotParams,
) error {
if len(params) == 0 {
return nil
}
now := time.Now()
snapshots := make(bunmodel.ServerSnapshots, 0, len(params))
for _, p := range params {
snapshots = append(snapshots, bunmodel.ServerSnapshot{
ServerKey: p.ServerKey(),
NumPlayers: p.NumPlayers(),
NumActivePlayers: p.NumActivePlayers(),
NumInactivePlayers: p.NumInactivePlayers(),
NumTribes: p.NumTribes(),
NumActiveTribes: p.NumActiveTribes(),
NumInactiveTribes: p.NumInactiveTribes(),
NumVillages: p.NumVillages(),
NumPlayerVillages: p.NumPlayerVillages(),
NumBarbarianVillages: p.NumBarbarianVillages(),
NumBonusVillages: p.NumBonusVillages(),
Date: p.Date(),
CreatedAt: now,
})
}
if _, err := repo.db.NewInsert().
Model(&snapshots).
Ignore().
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("something went wrong while inserting server snapshots into the db: %w", err)
}
return nil
}
func (repo *ServerSnapshotBunRepository) List(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsResult, error) {
var serverSnapshots bunmodel.ServerSnapshots
if err := repo.db.NewSelect().
Model(&serverSnapshots).
Apply(listServerSnapshotsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServerSnapshotsResult{}, fmt.Errorf("couldn't select server snapshots from the db: %w", err)
}
converted, err := serverSnapshots.ToDomain()
if err != nil {
return domain.ListServerSnapshotsResult{}, err
}
return domain.NewListServerSnapshotsResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *ServerSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete().
Model((*bunmodel.ServerSnapshot)(nil)).
Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE).
Returning("NULL").
Exec(ctx); err != nil {
return fmt.Errorf("couldn't delete server snapshots: %w", err)
}
return nil
}
type listServerSnapshotsParamsApplier struct {
params domain.ListServerSnapshotsParams
}
func (a listServerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("ss.server_key IN (?)", bun.In(serverKeys))
}
for _, s := range a.params.Sort() {
column, dir, err := a.sortToColumnAndDirection(s)
if err != nil {
return q.Err(err)
}
q.OrderExpr("? ?", column, dir.Bun())
}
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listServerSnapshotsParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
cursor := a.params.Cursor()
if cursor.IsZero() {
return q
}
sort := a.params.Sort()
cursorApplier := cursorPaginationApplier{
data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
}
for _, s := range sort {
var err error
var el cursorPaginationApplierDataElement
el.column, el.direction, err = a.sortToColumnAndDirection(s)
if err != nil {
return q.Err(err)
}
switch s {
case domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC:
el.value = cursor.ID()
el.unique = true
case domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortServerKeyDESC:
el.value = cursor.ServerKey()
case domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC:
el.value = cursor.Date()
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
}
func (a listServerSnapshotsParamsApplier) sortToColumnAndDirection(
s domain.ServerSnapshotSort,
) (bun.Safe, sortDirection, error) {
switch s {
case domain.ServerSnapshotSortDateASC:
return "ss.date", sortDirectionASC, nil
case domain.ServerSnapshotSortDateDESC:
return "ss.date", sortDirectionDESC, nil
case domain.ServerSnapshotSortIDASC:
return "ss.id", sortDirectionASC, nil
case domain.ServerSnapshotSortIDDESC:
return "ss.id", sortDirectionDESC, nil
case domain.ServerSnapshotSortServerKeyASC:
return "ss.server_key", sortDirectionASC, nil
case domain.ServerSnapshotSortServerKeyDESC:
return "ss.server_key", sortDirectionDESC, nil
default:
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
}
}

View File

@ -0,0 +1,29 @@
package adapter_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/bun/buntest"
)
func TestServerSnapshotBunRepository_Postgres(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
testServerSnapshotRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, postgres.NewDB(t))
})
}
func TestServerSnapshotBunRepository_SQLite(t *testing.T) {
t.Parallel()
testServerSnapshotRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, buntest.NewSQLiteDB(t))
})
}

View File

@ -105,7 +105,7 @@ func (repo *TribeSnapshotBunRepository) ListWithRelations(
func (repo *TribeSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete().
Model(&bunmodel.TribeSnapshot{}).
Model((*bunmodel.TribeSnapshot)(nil)).
Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE).
Returning("NULL").

View File

@ -71,12 +71,14 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.PlayerID(), p.Date().Format(dateFormat))
for i, ps := range playerSnapshots {
if ps.ServerKey() == p.ServerKey() && ps.PlayerID() == p.PlayerID() && ps.Date().Equal(p.Date()) {
//nolint:lll
if ps.ServerKey() == p.ServerKey() && ps.PlayerID() == p.PlayerID() && ps.Date().Format(dateFormat) == p.Date().Format(dateFormat) {
m[key] = append(m[key], i)
}
}
}
assert.NotEmpty(t, m)
for key, indexes := range m {
assert.Len(t, indexes, 1, key)
}

View File

@ -0,0 +1,445 @@
package adapter_test
import (
"cmp"
"context"
"slices"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testServerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
ctx := context.Background()
t.Run("Create", func(t *testing.T) {
t.Parallel()
const dateFormat = "2006-01-02"
repos := newRepos(t)
assertCreated := func(t *testing.T, params domain.CreateServerSnapshotParams) {
t.Helper()
require.NotEmpty(t, params)
listParams := domain.NewListServerSnapshotsParams()
require.NoError(t, listParams.SetServerKeys([]string{params.ServerKey()}))
res, err := repos.serverSnapshot.List(ctx, listParams)
serverSnapshots := res.ServerSnapshots()
require.NoError(t, err)
date := params.Date().Format(dateFormat)
idx := slices.IndexFunc(serverSnapshots, func(ss domain.ServerSnapshot) bool {
return ss.ServerKey() == params.ServerKey() &&
ss.Date().Format(dateFormat) == date
})
require.GreaterOrEqual(t, idx, 0)
serverSnapshot := serverSnapshots[idx]
assert.Equal(t, params.ServerKey(), serverSnapshot.ServerKey())
assert.Equal(t, params.NumPlayers(), serverSnapshot.NumPlayers())
assert.Equal(t, params.NumActivePlayers(), serverSnapshot.NumActivePlayers())
assert.Equal(t, params.NumInactivePlayers(), serverSnapshot.NumInactivePlayers())
assert.Equal(t, params.NumTribes(), serverSnapshot.NumTribes())
assert.Equal(t, params.NumActiveTribes(), serverSnapshot.NumActiveTribes())
assert.Equal(t, params.NumInactiveTribes(), serverSnapshot.NumInactiveTribes())
assert.Equal(t, params.NumVillages(), serverSnapshot.NumVillages())
assert.Equal(t, params.NumPlayerVillages(), serverSnapshot.NumPlayerVillages())
assert.Equal(t, params.NumBarbarianVillages(), serverSnapshot.NumBarbarianVillages())
assert.Equal(t, params.NumBonusVillages(), serverSnapshot.NumBonusVillages())
assert.Equal(t, date, serverSnapshot.Date().Format(dateFormat))
assert.WithinDuration(t, time.Now(), serverSnapshot.CreatedAt(), time.Minute)
}
assertNoDuplicates := func(t *testing.T, params domain.CreateServerSnapshotParams) {
t.Helper()
listParams := domain.NewListServerSnapshotsParams()
require.NoError(t, listParams.SetServerKeys([]string{params.ServerKey()}))
res, err := repos.serverSnapshot.List(ctx, listParams)
require.NoError(t, err)
serverSnapshots := res.ServerSnapshots()
var indexes []int
for i, ss := range serverSnapshots {
if ss.ServerKey() == params.ServerKey() && ss.Date().Format(dateFormat) == params.Date().Format(dateFormat) {
indexes = append(indexes, i)
}
}
assert.Len(t, indexes, 1)
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetOpen(domain.NullBool{
V: true,
Valid: true,
}))
require.NoError(t, listServersParams.SetLimit(1))
res, err := repos.server.List(ctx, listServersParams)
require.NoError(t, err)
servers := res.Servers()
require.NotEmpty(t, servers)
date := time.Now()
createParams, err := domain.NewCreateServerSnapshotParams(servers[0], date)
require.NoError(t, err)
require.NoError(t, repos.serverSnapshot.Create(ctx, createParams))
assertCreated(t, createParams)
require.NoError(t, repos.serverSnapshot.Create(ctx, createParams))
assertNoDuplicates(t, createParams)
})
t.Run("OK: len(params) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.serverSnapshot.Create(ctx))
})
})
t.Run("List", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.ListServerSnapshotsParams
assertResult func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.NewListServerSnapshotsParams()
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
a.Date().Compare(b.Date()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
},
{
name: "OK: sort=[serverKey DESC, date DESC, id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyDESC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortIDDESC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
a.Date().Compare(b.Date()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
},
},
{
name: "OK: sort=[id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: sort=[id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDDESC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID()) * -1
}))
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.ServerSnapshots())
randServerSnapshot := res.ServerSnapshots()[0]
require.NoError(t, params.SetServerKeys([]string{randServerSnapshot.ServerKey()}))
return params
},
assertResult: func(
t *testing.T,
params domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverKeys := params.ServerKeys()
serverSnapshots := res.ServerSnapshots()
assert.NotZero(t, serverSnapshots)
for _, ss := range serverSnapshots {
assert.True(t, slices.Contains(serverKeys, ss.ServerKey()))
}
},
},
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.ServerSnapshots()[1].ServerKey()}))
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverKeys := params.ServerKeys()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
for _, ss := range serverSnapshots {
assert.GreaterOrEqual(t, ss.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, ss.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, serverSnapshots[0].ID(), params.Cursor().ID())
for _, ss := range serverSnapshots {
assert.GreaterOrEqual(t, ss.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyDESC,
domain.ServerSnapshotSortIDDESC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, serverSnapshots[0].ID(), params.Cursor().ID())
for _, ss := range serverSnapshots {
assert.LessOrEqual(t, ss.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(
t *testing.T,
params domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
assert.Len(t, res.ServerSnapshots(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assertError := tt.assertError
if assertError == nil {
assertError = func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}
}
params := tt.params(t)
res, err := repos.serverSnapshot.List(ctx, params)
assertError(t, err)
tt.assertResult(t, params, res)
})
}
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.ServerSnapshots())
randSnapshot := res.ServerSnapshots()[0]
require.NoError(t, repos.serverSnapshot.Delete(ctx, randSnapshot.ServerKey(), randSnapshot.Date()))
require.NoError(t, params.SetServerKeys([]string{randSnapshot.ServerKey()}))
res, err = repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
assert.NotEmpty(t, res.ServerSnapshots())
for _, ss := range res.ServerSnapshots() {
assert.True(t, ss.Date().After(randSnapshot.Date()))
}
})
})
}

View File

@ -281,6 +281,26 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}
},
},
{
name: "OK: snapshotCreatedAt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetSnapshotCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT,
Valid: true,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, servers)
for _, s := range servers {
assert.True(t, s.SnapshotCreatedAt().Before(snapshotsCreatedAtLT))
}
},
},
{
name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
params: func(t *testing.T) domain.ListServersParams {
@ -550,6 +570,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: time.Now(),
Valid: true,
}))
require.NoError(t, updateParams.SetSnapshotCreatedAt(domain.NullTime{
V: time.Now(),
Valid: true,
}))
require.NoError(t, updateParams.SetTribeSnapshotsCreatedAt(domain.NullTime{
V: time.Now(),
Valid: true,
@ -606,6 +630,12 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serverAfterUpdate.EnnoblementDataSyncedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.SnapshotCreatedAt().V,
serverAfterUpdate.SnapshotCreatedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.TribeSnapshotsCreatedAt().V,

View File

@ -67,6 +67,12 @@ type tribeChangeRepository interface {
) (domain.ListTribeChangesWithRelationsResult, error)
}
type serverSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreateServerSnapshotParams) error
List(ctx context.Context, params domain.ListServerSnapshotsParams) (domain.ListServerSnapshotsResult, error)
Delete(ctx context.Context, serverKey string, dateLTE time.Time) error
}
type tribeSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreateTribeSnapshotParams) error
List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.ListTribeSnapshotsResult, error)
@ -95,6 +101,7 @@ type repositories struct {
village villageRepository
ennoblement ennoblementRepository
tribeChange tribeChangeRepository
serverSnapshot serverSnapshotRepository
tribeSnapshot tribeSnapshotRepository
playerSnapshot playerSnapshotRepository
}
@ -112,6 +119,7 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
village: adapter.NewVillageBunRepository(bunDB),
ennoblement: adapter.NewEnnoblementBunRepository(bunDB),
tribeChange: adapter.NewTribeChangeBunRepository(bunDB),
serverSnapshot: adapter.NewServerSnapshotBunRepository(bunDB),
tribeSnapshot: adapter.NewTribeSnapshotBunRepository(bunDB),
playerSnapshot: adapter.NewPlayerSnapshotBunRepository(bunDB),
}

View File

@ -73,12 +73,14 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.TribeID(), p.Date().Format(dateFormat))
for i, ts := range tribeSnapshots {
if ts.ServerKey() == p.ServerKey() && ts.TribeID() == p.TribeID() && ts.Date().Equal(p.Date()) {
//nolint:lll
if ts.ServerKey() == p.ServerKey() && ts.TribeID() == p.TribeID() && ts.Date().Format(dateFormat) == p.Date().Format(dateFormat) {
m[key] = append(m[key], i)
}
}
}
assert.NotEmpty(t, m)
for key, indexes := range m {
assert.Len(t, indexes, 1, key)
}

View File

@ -12,6 +12,7 @@
num_barbarian_villages: 1180
num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z
snapshot_created_at: 2022-03-19T12:00:54.000Z
player_data_synced_at: 2022-03-19T12:00:54.000Z
player_snapshots_created_at: 2022-03-19T12:00:54.000Z
tribe_data_synced_at: 2022-03-19T12:00:54.000Z
@ -31,6 +32,7 @@
num_barbarian_villages: 700
num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z
snapshot_created_at: 2021-04-02T16:01:25.000Z
player_data_synced_at: 2021-04-02T16:01:25.000Z
player_snapshots_created_at: 2021-04-02T16:01:25.000Z
tribe_data_synced_at: 2021-04-02T16:01:25.000Z
@ -50,6 +52,7 @@
num_barbarian_villages: 1682
num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z
snapshot_created_at: 2022-03-19T12:00:04.000Z
player_data_synced_at: 2022-03-19T12:00:04.000Z
player_snapshots_created_at: 2022-03-19T12:00:04.000Z
tribe_data_synced_at: 2022-03-19T12:00:04.000Z
@ -69,6 +72,7 @@
num_barbarian_villages: 1574
num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z
snapshot_created_at: 2022-03-19T12:01:39.000Z
player_data_synced_at: 2022-03-19T12:01:39.000Z
player_snapshots_created_at: 2022-03-19T12:01:39.000Z
tribe_data_synced_at: 2022-03-19T12:01:39.000Z
@ -7374,6 +7378,64 @@
new_tribe_id: 2
server_key: pl169
created_at: 2021-09-10T20:01:11.000Z
- model: ServerSnapshot
rows:
- id: 10000
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-01T05:15:25.154992Z
created_at: 2024-05-01T05:15:25.154994Z
- id: 10001
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-02T05:15:25.154992Z
created_at: 2024-05-02T05:15:25.154994Z
- id: 20000
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-03T05:15:25.154992Z
created_at: 2024-05-03T05:15:25.154994Z
- id: 20001
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-04T05:15:25.154992Z
created_at: 2024-05-04T05:15:25.154994Z
- model: TribeSnapshot
rows:
- rank_att: 1

View File

@ -298,6 +298,21 @@ func (svc *ServerService) UpdateEnnoblementDataSyncedAt(
return svc.repo.Update(ctx, key, updateParams)
}
func (svc *ServerService) UpdateSnapshotCreatedAt(
ctx context.Context,
key string,
) error {
var updateParams domain.UpdateServerParams
if err := updateParams.SetSnapshotCreatedAt(domain.NullTime{
V: time.Now(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
return svc.repo.Update(ctx, key, updateParams)
}
func (svc *ServerService) UpdateTribeSnapshotsCreatedAt(
ctx context.Context,
payload domain.SnapshotsCreatedEventPayload,

View File

@ -0,0 +1,62 @@
package app
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
type ServerSnapshotRepository interface {
// Create persists tribe snapshots in a store (e.g. Postgres).
// Duplicates are ignored.
Create(ctx context.Context, params ...domain.CreateServerSnapshotParams) error
}
type ServerSnapshotService struct {
repo ServerSnapshotRepository
serverSvc *ServerService
pub SnapshotPublisher
}
func NewServerSnapshotService(
repo ServerSnapshotRepository,
serverSvc *ServerService,
pub SnapshotPublisher,
) *ServerSnapshotService {
return &ServerSnapshotService{repo: repo, serverSvc: serverSvc, pub: pub}
}
//nolint:gocyclo
func (svc *ServerSnapshotService) Create(
ctx context.Context,
createSnapshotsCmdPayload domain.CreateSnapshotsCmdPayload,
) error {
versionCode := createSnapshotsCmdPayload.VersionCode()
serverKey := createSnapshotsCmdPayload.ServerKey()
date := createSnapshotsCmdPayload.Date()
server, err := svc.serverSvc.GetNormalByVersionCodeAndServerKey(ctx, versionCode, serverKey)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if !server.Open() {
return nil
}
params, err := domain.NewCreateServerSnapshotParams(server, date)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err = svc.repo.Create(ctx, params); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err = svc.serverSvc.UpdateSnapshotCreatedAt(ctx, serverKey); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
return nil
}

View File

@ -12,6 +12,7 @@ import (
type SnapshotService struct {
versionSvc *VersionService
serverSvc *ServerService
serverSnapshotPub SnapshotPublisher
tribeSnapshotPub SnapshotPublisher
playerSnapshotPub SnapshotPublisher
}
@ -19,14 +20,16 @@ type SnapshotService struct {
func NewSnapshotService(
versionSvc *VersionService,
serverSvc *ServerService,
tribeSnapshotPublisher SnapshotPublisher,
playerSnapshotPublisher SnapshotPublisher,
serverSnapshotPub SnapshotPublisher,
tribeSnapshotPub SnapshotPublisher,
playerSnapshotPub SnapshotPublisher,
) *SnapshotService {
return &SnapshotService{
versionSvc: versionSvc,
serverSvc: serverSvc,
tribeSnapshotPub: tribeSnapshotPublisher,
playerSnapshotPub: playerSnapshotPublisher,
serverSnapshotPub: serverSnapshotPub,
tribeSnapshotPub: tribeSnapshotPub,
playerSnapshotPub: playerSnapshotPub,
}
}
@ -48,6 +51,7 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
if loopErr = errors.Join(
svc.publishServer(ctx, v, snapshotsCreatedAtLT, date),
svc.publishTribe(ctx, v, snapshotsCreatedAtLT, date),
svc.publishPlayer(ctx, v, snapshotsCreatedAtLT, date),
); loopErr != nil {
@ -58,23 +62,47 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
return nil
}
func (svc *SnapshotService) publishServer(
ctx context.Context,
v domain.Version,
snapshotCreatedAtLT time.Time,
date time.Time,
) error {
params, err := svc.baseParams(v)
if err != nil {
return err
}
if err = params.SetSnapshotCreatedAtLT(domain.NullTime{
V: snapshotCreatedAtLT,
Valid: true,
}); err != nil {
return err
}
servers, err := svc.serverSvc.ListAll(ctx, params)
if err != nil {
return err
}
payloads, err := svc.toPayload(v, servers, date)
if err != nil {
return err
}
return svc.serverSnapshotPub.CmdCreate(ctx, payloads...)
}
func (svc *SnapshotService) publishTribe(
ctx context.Context,
v domain.Version,
snapshotsCreatedAtLT time.Time,
date time.Time,
) error {
params := domain.NewListServersParams()
if err := params.SetVersionCodes([]string{v.Code()}); err != nil {
params, err := svc.baseParams(v)
if err != nil {
return err
}
if err := params.SetOpen(domain.NullBool{
V: true,
Valid: true,
}); err != nil {
return err
}
if err := params.SetTribeSnapshotsCreatedAtLT(domain.NullTime{
if err = params.SetTribeSnapshotsCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT,
Valid: true,
}); err != nil {
@ -100,17 +128,11 @@ func (svc *SnapshotService) publishPlayer(
snapshotsCreatedAtLT time.Time,
date time.Time,
) error {
params := domain.NewListServersParams()
if err := params.SetVersionCodes([]string{v.Code()}); err != nil {
params, err := svc.baseParams(v)
if err != nil {
return err
}
if err := params.SetOpen(domain.NullBool{
V: true,
Valid: true,
}); err != nil {
return err
}
if err := params.SetPlayerSnapshotsCreatedAtLT(domain.NullTime{
if err = params.SetPlayerSnapshotsCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT,
Valid: true,
}); err != nil {
@ -130,6 +152,20 @@ func (svc *SnapshotService) publishPlayer(
return svc.playerSnapshotPub.CmdCreate(ctx, payloads...)
}
func (svc *SnapshotService) baseParams(v domain.Version) (domain.ListServersParams, error) {
params := domain.NewListServersParams()
if err := params.SetVersionCodes([]string{v.Code()}); err != nil {
return domain.ListServersParams{}, err
}
if err := params.SetOpen(domain.NullBool{
V: true,
Valid: true,
}); err != nil {
return domain.ListServersParams{}, nil
}
return params, nil
}
func (svc *SnapshotService) toPayload(
v domain.Version,
servers domain.Servers,

View File

@ -30,6 +30,7 @@ type Server struct {
BuildingInfo BuildingInfo `bun:"building_info"`
UnitInfo UnitInfo `bun:"unit_info"`
CreatedAt time.Time `bun:"created_at,nullzero"`
SnapshotCreatedAt time.Time `bun:"snapshot_created_at,nullzero"`
PlayerDataUpdatedAt time.Time `bun:"player_data_synced_at,nullzero"`
PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"`
TribeDataUpdatedAt time.Time `bun:"tribe_data_synced_at,nullzero"`
@ -75,6 +76,7 @@ func (s Server) ToDomain() (domain.Server, error) {
buildingInfo,
unitInfo,
s.CreatedAt,
s.SnapshotCreatedAt,
s.PlayerDataUpdatedAt,
s.PlayerSnapshotsCreatedAt,
s.TribeDataUpdatedAt,

View File

@ -0,0 +1,62 @@
package bunmodel
import (
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/uptrace/bun"
)
type ServerSnapshot struct {
bun.BaseModel `bun:"table:server_snapshots,alias:ss"`
ID int `bun:"id,pk,autoincrement,identity"`
ServerKey string `bun:"server_key,nullzero"`
NumPlayers int `bun:"num_players"`
NumActivePlayers int `bun:"num_active_players"`
NumInactivePlayers int `bun:"num_inactive_players"`
NumTribes int `bun:"num_tribes"`
NumActiveTribes int `bun:"num_active_tribes"`
NumInactiveTribes int `bun:"num_inactive_tribes"`
NumVillages int `bun:"num_villages"`
NumPlayerVillages int `bun:"num_player_villages"`
NumBarbarianVillages int `bun:"num_barbarian_villages"`
NumBonusVillages int `bun:"num_bonus_villages"`
Date time.Time `bun:"date,nullzero"`
CreatedAt time.Time `bun:"created_at,nullzero"`
}
func (ss ServerSnapshot) ToDomain() (domain.ServerSnapshot, error) {
converted, err := domain.UnmarshalServerSnapshotFromDatabase(
ss.ID,
ss.ServerKey,
ss.NumPlayers,
ss.NumActivePlayers,
ss.NumInactivePlayers,
ss.NumTribes,
ss.NumActiveTribes,
ss.NumInactiveTribes,
ss.NumVillages,
ss.NumPlayerVillages,
ss.NumBarbarianVillages,
ss.NumBonusVillages,
ss.Date,
ss.CreatedAt,
)
if err != nil {
return domain.ServerSnapshot{}, fmt.Errorf(
"couldn't construct domain.ServerSnapshot (id=%d): %w",
ss.ID,
err,
)
}
return converted, nil
}
type ServerSnapshots []ServerSnapshot
func (sss ServerSnapshots) ToDomain() (domain.ServerSnapshots, error) {
return sliceToDomain(sss)
}

View File

@ -23,6 +23,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
(*bunmodel.Village)(nil),
(*bunmodel.Ennoblement)(nil),
(*bunmodel.TribeChange)(nil),
(*bunmodel.ServerSnapshot)(nil),
(*bunmodel.TribeSnapshot)(nil),
(*bunmodel.PlayerSnapshot)(nil),
)

View File

@ -12,7 +12,11 @@ func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
var servers bunmodel.Servers
if err := db.NewSelect().Model(&servers).Where("special = false").Scan(ctx); err != nil {
if err := db.NewSelect().
Model(&servers).
ExcludeColumn("snapshot_created_at").
Where("special = false").
Scan(ctx); err != nil {
return fmt.Errorf("couldn't select servers from the db: %w", err)
}

View File

@ -0,0 +1,17 @@
package migrations
import (
"context"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE servers ADD snapshot_created_at timestamp with time zone")
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE servers DROP COLUMN snapshot_created_at")
return err
})
}

View File

@ -0,0 +1,37 @@
package migrations
import (
"context"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, `
create table if not exists server_snapshots
(
?ID_COL,
server_key varchar(100) not null
references servers,
date date not null,
created_at timestamp with time zone default CURRENT_TIMESTAMP not null,
num_players bigint default 0,
num_active_players bigint default 0,
num_inactive_players bigint default 0,
num_tribes bigint default 0,
num_active_tribes bigint default 0,
num_inactive_tribes bigint default 0,
num_villages bigint default 0,
num_player_villages bigint default 0,
num_barbarian_villages bigint default 0,
num_bonus_villages bigint default 0,
unique (server_key, date)
);
`)
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "drop table if exists server_snapshots cascade;")
return err
})
}

View File

@ -97,6 +97,7 @@ func NewServer(tb TestingTB, opts ...func(cfg *ServerConfig)) domain.Server {
NewBuildingInfo(tb),
NewUnitInfo(tb),
time.Now(),
time.Now(),
cfg.PlayerDataSyncedAt,
cfg.PlayerSnapshotsCreatedAt,
cfg.TribeDataSyncedAt,

View File

@ -0,0 +1,102 @@
package domaintest
import (
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/require"
)
type ServerSnapshotCursorConfig struct {
ID int
ServerKey string
Date time.Time
}
func NewServerSnapshotCursor(tb TestingTB, opts ...func(cfg *ServerSnapshotCursorConfig)) domain.ServerSnapshotCursor {
tb.Helper()
cfg := &ServerSnapshotCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
Date: gofakeit.Date(),
}
for _, opt := range opts {
opt(cfg)
}
ssc, err := domain.NewServerSnapshotCursor(
cfg.ID,
cfg.ServerKey,
cfg.Date,
)
require.NoError(tb, err)
return ssc
}
type ServerSnapshotConfig struct {
ID int
ServerKey string
NumPlayers int
NumActivePlayers int
NumInactivePlayers int
NumTribes int
NumActiveTribes int
NumInactiveTribes int
NumVillages int
NumPlayerVillages int
NumBarbarianVillages int
NumBonusVillages int
Date time.Time
CreatedAt time.Time
}
func NewServerSnapshot(tb TestingTB, opts ...func(cfg *ServerSnapshotConfig)) domain.ServerSnapshot {
tb.Helper()
now := time.Now()
cfg := &ServerSnapshotConfig{
ID: RandID(),
ServerKey: RandServerKey(),
NumPlayers: gofakeit.IntRange(1, 10000),
NumActivePlayers: gofakeit.IntRange(1, 10000),
NumInactivePlayers: gofakeit.IntRange(1, 10000),
NumTribes: gofakeit.IntRange(1, 10000),
NumActiveTribes: gofakeit.IntRange(1, 10000),
NumInactiveTribes: gofakeit.IntRange(1, 10000),
NumVillages: gofakeit.IntRange(1, 10000),
NumPlayerVillages: gofakeit.IntRange(1, 10000),
NumBarbarianVillages: gofakeit.IntRange(1, 10000),
NumBonusVillages: gofakeit.IntRange(1, 10000),
Date: now,
CreatedAt: now,
}
for _, opt := range opts {
opt(cfg)
}
ss, err := domain.UnmarshalServerSnapshotFromDatabase(
cfg.ID,
cfg.ServerKey,
cfg.NumPlayers,
cfg.NumActivePlayers,
cfg.NumInactivePlayers,
cfg.NumTribes,
cfg.NumActiveTribes,
cfg.NumInactiveTribes,
cfg.NumVillages,
cfg.NumPlayerVillages,
cfg.NumBarbarianVillages,
cfg.NumBonusVillages,
cfg.Date,
cfg.CreatedAt,
)
require.NoError(tb, err)
return ss
}

View File

@ -27,14 +27,14 @@ func NewTribeSnapshotCursor(tb TestingTB, opts ...func(cfg *TribeSnapshotCursorC
opt(cfg)
}
psc, err := domain.NewTribeSnapshotCursor(
tsc, err := domain.NewTribeSnapshotCursor(
cfg.ID,
cfg.ServerKey,
cfg.Date,
)
require.NoError(tb, err)
return psc
return tsc
}
type TribeSnapshotConfig struct {

View File

@ -28,6 +28,7 @@ type Server struct {
buildingInfo BuildingInfo
unitInfo UnitInfo
createdAt time.Time
snapshotCreatedAt time.Time
playerDataSyncedAt time.Time
playerSnapshotsCreatedAt time.Time
tribeDataSyncedAt time.Time
@ -62,6 +63,7 @@ func UnmarshalServerFromDatabase(
buildingInfo BuildingInfo,
unitInfo UnitInfo,
createdAt time.Time,
snapshotCreatedAt time.Time,
playerDataSyncedAt time.Time,
playerSnapshotsCreatedAt time.Time,
tribeDataSyncedAt time.Time,
@ -114,6 +116,7 @@ func UnmarshalServerFromDatabase(
buildingInfo: buildingInfo,
unitInfo: unitInfo,
createdAt: createdAt,
snapshotCreatedAt: snapshotCreatedAt,
playerDataSyncedAt: playerDataSyncedAt,
playerSnapshotsCreatedAt: playerSnapshotsCreatedAt,
tribeDataSyncedAt: tribeDataSyncedAt,
@ -199,6 +202,10 @@ func (s Server) CreatedAt() time.Time {
return s.createdAt
}
func (s Server) SnapshotCreatedAt() time.Time {
return s.snapshotCreatedAt
}
func (s Server) PlayerDataSyncedAt() time.Time {
return s.playerDataSyncedAt
}
@ -419,6 +426,7 @@ type UpdateServerParams struct {
numBonusVillages NullInt
villageDataSyncedAt NullTime
ennoblementDataSyncedAt NullTime
snapshotCreatedAt NullTime
tribeSnapshotsCreatedAt NullTime
playerSnapshotsCreatedAt NullTime
}
@ -688,6 +696,15 @@ func (params *UpdateServerParams) SetEnnoblementDataSyncedAt(ennoblementDataSync
return nil
}
func (params *UpdateServerParams) SnapshotCreatedAt() NullTime {
return params.snapshotCreatedAt
}
func (params *UpdateServerParams) SetSnapshotCreatedAt(snapshotCreatedAt NullTime) error {
params.snapshotCreatedAt = snapshotCreatedAt
return nil
}
func (params *UpdateServerParams) TribeSnapshotsCreatedAt() NullTime {
return params.tribeSnapshotsCreatedAt
}
@ -721,6 +738,7 @@ func (params *UpdateServerParams) IsZero() bool {
!params.numBonusVillages.Valid &&
!params.villageDataSyncedAt.Valid &&
!params.ennoblementDataSyncedAt.Valid &&
!params.snapshotCreatedAt.Valid &&
!params.tribeSnapshotsCreatedAt.Valid &&
!params.playerSnapshotsCreatedAt.Valid
}
@ -825,6 +843,7 @@ type ListServersParams struct {
versionCodes []string
open NullBool
special NullBool
snapshotCreatedAtLT NullTime
tribeSnapshotsCreatedAtLT NullTime
playerSnapshotsCreatedAtLT NullTime
sort []ServerSort
@ -908,6 +927,15 @@ func (params *ListServersParams) SetSpecial(special NullBool) error {
return nil
}
func (params *ListServersParams) SnapshotCreatedAtLT() NullTime {
return params.snapshotCreatedAtLT
}
func (params *ListServersParams) SetSnapshotCreatedAtLT(snapshotCreatedAtLT NullTime) error {
params.snapshotCreatedAtLT = snapshotCreatedAtLT
return nil
}
func (params *ListServersParams) TribeSnapshotsCreatedAtLT() NullTime {
return params.tribeSnapshotsCreatedAtLT
}

View File

@ -0,0 +1,578 @@
package domain
import (
"errors"
"math"
"time"
)
type ServerSnapshot struct {
id int
serverKey string
numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
numBonusVillages int
date time.Time
createdAt time.Time
}
const serverSnapshotModelName = "ServerSnapshot"
// UnmarshalServerSnapshotFromDatabase unmarshals ServerSnapshot from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalServerSnapshotFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalServerSnapshotFromDatabase(
id int,
serverKey string,
numPlayers int,
numActivePlayers int,
numInactivePlayers int,
numTribes int,
numActiveTribes int,
numInactiveTribes int,
numVillages int,
numPlayerVillages int,
numBarbarianVillages int,
numBonusVillages int,
date time.Time,
createdAt time.Time,
) (ServerSnapshot, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return ServerSnapshot{}, ValidationError{
Model: serverSnapshotModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return ServerSnapshot{}, ValidationError{
Model: serverSnapshotModelName,
Field: "serverKey",
Err: err,
}
}
return ServerSnapshot{
id: id,
serverKey: serverKey,
numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
numInactivePlayers: numInactivePlayers,
numTribes: numTribes,
numActiveTribes: numActiveTribes,
numInactiveTribes: numInactiveTribes,
numVillages: numVillages,
numPlayerVillages: numPlayerVillages,
numBarbarianVillages: numBarbarianVillages,
numBonusVillages: numBonusVillages,
date: date,
createdAt: createdAt,
}, nil
}
func (ss ServerSnapshot) ID() int {
return ss.id
}
func (ss ServerSnapshot) ServerKey() string {
return ss.serverKey
}
func (ss ServerSnapshot) NumPlayers() int {
return ss.numPlayers
}
func (ss ServerSnapshot) NumActivePlayers() int {
return ss.numActivePlayers
}
func (ss ServerSnapshot) NumInactivePlayers() int {
return ss.numInactivePlayers
}
func (ss ServerSnapshot) NumTribes() int {
return ss.numTribes
}
func (ss ServerSnapshot) NumActiveTribes() int {
return ss.numActiveTribes
}
func (ss ServerSnapshot) NumInactiveTribes() int {
return ss.numInactiveTribes
}
func (ss ServerSnapshot) NumVillages() int {
return ss.numVillages
}
func (ss ServerSnapshot) NumPlayerVillages() int {
return ss.numPlayerVillages
}
func (ss ServerSnapshot) NumBarbarianVillages() int {
return ss.numBarbarianVillages
}
func (ss ServerSnapshot) NumBonusVillages() int {
return ss.numBonusVillages
}
func (ss ServerSnapshot) Date() time.Time {
return ss.date
}
func (ss ServerSnapshot) CreatedAt() time.Time {
return ss.createdAt
}
func (ss ServerSnapshot) ToCursor() (ServerSnapshotCursor, error) {
return NewServerSnapshotCursor(ss.id, ss.serverKey, ss.date)
}
func (ss ServerSnapshot) IsZero() bool {
return ss == ServerSnapshot{}
}
type ServerSnapshots []ServerSnapshot
type CreateServerSnapshotParams struct {
serverKey string
numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
numBonusVillages int
date time.Time
}
func NewCreateServerSnapshotParams(server Server, date time.Time) (CreateServerSnapshotParams, error) {
if server.IsZero() {
return CreateServerSnapshotParams{}, errors.New("given server is an empty struct")
}
if !server.Open() {
return CreateServerSnapshotParams{}, errors.New("given server is closed")
}
return CreateServerSnapshotParams{
serverKey: server.Key(),
numPlayers: server.NumPlayers(),
numActivePlayers: server.NumActivePlayers(),
numInactivePlayers: server.NumInactivePlayers(),
numTribes: server.NumTribes(),
numActiveTribes: server.NumActiveTribes(),
numInactiveTribes: server.NumInactivePlayers(),
numVillages: server.NumVillages(),
numPlayerVillages: server.NumPlayerVillages(),
numBarbarianVillages: server.NumBarbarianVillages(),
numBonusVillages: server.NumBonusVillages(),
date: date,
}, nil
}
func (params CreateServerSnapshotParams) ServerKey() string {
return params.serverKey
}
func (params CreateServerSnapshotParams) NumPlayers() int {
return params.numPlayers
}
func (params CreateServerSnapshotParams) NumActivePlayers() int {
return params.numActivePlayers
}
func (params CreateServerSnapshotParams) NumInactivePlayers() int {
return params.numInactivePlayers
}
func (params CreateServerSnapshotParams) NumTribes() int {
return params.numTribes
}
func (params CreateServerSnapshotParams) NumActiveTribes() int {
return params.numActiveTribes
}
func (params CreateServerSnapshotParams) NumInactiveTribes() int {
return params.numInactiveTribes
}
func (params CreateServerSnapshotParams) NumVillages() int {
return params.numVillages
}
func (params CreateServerSnapshotParams) NumPlayerVillages() int {
return params.numPlayerVillages
}
func (params CreateServerSnapshotParams) NumBarbarianVillages() int {
return params.numBarbarianVillages
}
func (params CreateServerSnapshotParams) NumBonusVillages() int {
return params.numBonusVillages
}
func (params CreateServerSnapshotParams) Date() time.Time {
return params.date
}
type ServerSnapshotSort uint8
const (
ServerSnapshotSortDateASC ServerSnapshotSort = iota + 1
ServerSnapshotSortDateDESC
ServerSnapshotSortIDASC
ServerSnapshotSortIDDESC
ServerSnapshotSortServerKeyASC
ServerSnapshotSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together
// (e.g. ServerSnapshotSortIDASC and ServerSnapshotSortIDDESC).
func (s ServerSnapshotSort) IsInConflict(s2 ServerSnapshotSort) bool {
return isSortInConflict(s, s2)
}
//nolint:gocyclo
func (s ServerSnapshotSort) String() string {
switch s {
case ServerSnapshotSortDateASC:
return "date:ASC"
case ServerSnapshotSortDateDESC:
return "date:DESC"
case ServerSnapshotSortIDASC:
return "id:ASC"
case ServerSnapshotSortIDDESC:
return "id:DESC"
case ServerSnapshotSortServerKeyASC:
return "serverKey:ASC"
case ServerSnapshotSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown server snapshot sort"
}
}
type ServerSnapshotCursor struct {
id int
serverKey string
date time.Time
}
const serverSnapshotCursorModelName = "ServerSnapshotCursor"
func NewServerSnapshotCursor(id int, serverKey string, date time.Time) (ServerSnapshotCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return ServerSnapshotCursor{}, ValidationError{
Model: serverSnapshotCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return ServerSnapshotCursor{}, ValidationError{
Model: serverSnapshotCursorModelName,
Field: "serverKey",
Err: err,
}
}
return ServerSnapshotCursor{
id: id,
serverKey: serverKey,
date: date,
}, nil
}
func decodeServerSnapshotCursor(encoded string) (ServerSnapshotCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return ServerSnapshotCursor{}, err
}
id, err := m.int("id")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
date, err := m.time("date")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
tsc, err := NewServerSnapshotCursor(
id,
serverKey,
date,
)
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
return tsc, nil
}
func (ssc ServerSnapshotCursor) ID() int {
return ssc.id
}
func (ssc ServerSnapshotCursor) ServerKey() string {
return ssc.serverKey
}
func (ssc ServerSnapshotCursor) Date() time.Time {
return ssc.date
}
func (ssc ServerSnapshotCursor) IsZero() bool {
return ssc == ServerSnapshotCursor{}
}
func (ssc ServerSnapshotCursor) Encode() string {
if ssc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", ssc.id},
{"serverKey", ssc.serverKey},
{"date", ssc.date},
})
}
type ListServerSnapshotsParams struct {
serverKeys []string
sort []ServerSnapshotSort
cursor ServerSnapshotCursor
limit int
}
const (
ServerSnapshotListMaxLimit = 500
listServerSnapshotsParamsModelName = "ListServerSnapshotsParams"
)
func NewListServerSnapshotsParams() ListServerSnapshotsParams {
return ListServerSnapshotsParams{
sort: []ServerSnapshotSort{
ServerSnapshotSortServerKeyASC,
ServerSnapshotSortDateASC,
ServerSnapshotSortIDASC,
},
limit: ServerSnapshotListMaxLimit,
}
}
func (params *ListServerSnapshotsParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListServerSnapshotsParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
func (params *ListServerSnapshotsParams) Sort() []ServerSnapshotSort {
return params.sort
}
const (
serverSnapshotSortMinLength = 0
serverSnapshotSortMaxLength = 3
)
func (params *ListServerSnapshotsParams) SetSort(sort []ServerSnapshotSort) error {
if err := validateSort(sort, serverSnapshotSortMinLength, serverSnapshotSortMaxLength); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListServerSnapshotsParams) PrependSort(sort []ServerSnapshotSort) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(serverSnapshotSortMaxLength-len(params.sort), 0)); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
return params.SetSort(append(sort, params.sort...))
}
func (params *ListServerSnapshotsParams) PrependSortString(
sort []string,
allowed []ServerSnapshotSort,
maxLength int,
) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(min(serverSnapshotSortMaxLength-len(params.sort), maxLength), 0)); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
toPrepend := make([]ServerSnapshotSort, 0, len(sort))
for i, s := range sort {
converted, err := newSortFromString(s, allowed...)
if err != nil {
return SliceElementValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
toPrepend = append(toPrepend, converted)
}
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListServerSnapshotsParams) Cursor() ServerSnapshotCursor {
return params.cursor
}
func (params *ListServerSnapshotsParams) SetCursor(cursor ServerSnapshotCursor) error {
params.cursor = cursor
return nil
}
func (params *ListServerSnapshotsParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeServerSnapshotCursor(encoded)
if err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListServerSnapshotsParams) Limit() int {
return params.limit
}
func (params *ListServerSnapshotsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, ServerSnapshotListMaxLimit); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
type ListServerSnapshotsResult struct {
snapshots ServerSnapshots
self ServerSnapshotCursor
next ServerSnapshotCursor
}
const listServerSnapshotsResultModelName = "ListServerSnapshotsResult"
func NewListServerSnapshotsResult(
snapshots ServerSnapshots,
next ServerSnapshot,
) (ListServerSnapshotsResult, error) {
var err error
res := ListServerSnapshotsResult{
snapshots: snapshots,
}
if len(snapshots) > 0 {
res.self, err = snapshots[0].ToCursor()
if err != nil {
return ListServerSnapshotsResult{}, ValidationError{
Model: listServerSnapshotsResultModelName,
Field: "self",
Err: err,
}
}
}
if !next.IsZero() {
res.next, err = next.ToCursor()
if err != nil {
return ListServerSnapshotsResult{}, ValidationError{
Model: listServerSnapshotsResultModelName,
Field: "next",
Err: err,
}
}
}
return res, nil
}
func (res ListServerSnapshotsResult) ServerSnapshots() ServerSnapshots {
return res.snapshots
}
func (res ListServerSnapshotsResult) Self() ServerSnapshotCursor {
return res.self
}
func (res ListServerSnapshotsResult) Next() ServerSnapshotCursor {
return res.next
}

View File

@ -0,0 +1,854 @@
package domain_test
import (
"fmt"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCreateServerSnapshotParams(t *testing.T) {
t.Parallel()
server := domaintest.NewServer(t)
date := time.Now()
params, err := domain.NewCreateServerSnapshotParams(server, date)
require.NoError(t, err)
assert.Equal(t, server.Key(), params.ServerKey())
assert.Equal(t, server.NumPlayers(), params.NumPlayers())
assert.Equal(t, server.NumActivePlayers(), params.NumActivePlayers())
assert.Equal(t, server.NumInactivePlayers(), params.NumInactivePlayers())
assert.Equal(t, server.NumTribes(), params.NumTribes())
assert.Equal(t, server.NumActiveTribes(), params.NumActiveTribes())
assert.Equal(t, server.NumInactiveTribes(), params.NumInactiveTribes())
assert.Equal(t, server.NumVillages(), params.NumVillages())
assert.Equal(t, server.NumPlayerVillages(), params.NumPlayerVillages())
assert.Equal(t, server.NumBarbarianVillages(), params.NumBarbarianVillages())
assert.Equal(t, server.NumBonusVillages(), params.NumBonusVillages())
assert.Equal(t, date, params.Date())
}
func TestServerSnapshotSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.ServerSnapshotSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDDESC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: date:ASC date:DESC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortDateASC, domain.ServerSnapshotSortDateDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortServerKeyDESC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewServerSnapshotCursor(t *testing.T) {
t.Parallel()
validServerSnapshotCursor := domaintest.NewServerSnapshotCursor(t)
type args struct {
id int
serverKey string
date time.Time
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validServerSnapshotCursor.ID(),
serverKey: validServerSnapshotCursor.ServerKey(),
date: validServerSnapshotCursor.Date(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validServerSnapshotCursor.ServerKey(),
date: validServerSnapshotCursor.Date(),
},
expectedErr: domain.ValidationError{
Model: "ServerSnapshotCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validServerSnapshotCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "ServerSnapshotCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ssc, err := domain.NewServerSnapshotCursor(
tt.args.id,
tt.args.serverKey,
tt.args.date,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, ssc.ID())
assert.Equal(t, tt.args.serverKey, ssc.ServerKey())
assert.Equal(t, tt.args.date, ssc.Date())
assert.NotEmpty(t, ssc.Encode())
})
}
}
func TestListServerSnapshotsParams_SetServerKeys(t *testing.T) {
t.Parallel()
type args struct {
serverKeys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
serverKeys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
serverKeys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServerSnapshotsParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}
func TestListServerSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
type args struct {
sort []domain.ServerSnapshotSort
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
},
{
name: "OK: empty slice",
args: args{
sort: nil,
},
},
{
name: "ERR: len(sort) > 3",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 3,
Current: 4,
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortIDASC.String(), domain.ServerSnapshotSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.sort, params.Sort())
})
}
}
func TestListServerSnapshotsParams_PrependSort(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.ListServerSnapshotsParams{}
}
type args struct {
sort []domain.ServerSnapshotSort
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListServerSnapshotsParams
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
},
},
},
{
name: "OK: custom params",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
},
},
},
{
name: "OK: empty slice",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: nil,
},
},
{
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: len(sort) > 3",
newParams: defaultNewParams,
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 3,
Current: 4,
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortDateASC.String(), domain.ServerSnapshotSortDateDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
expectedSort := params.Sort()
require.ErrorIs(t, params.PrependSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, append(tt.args.sort, expectedSort...), params.Sort())
})
}
}
func TestListServerSnapshotsParams_PrependSortString(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.ListServerSnapshotsParams{}
}
defaultAllowed := []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortServerKeyDESC,
}
defaultMaxLength := 3
type args struct {
sort []string
allowed []domain.ServerSnapshotSort
maxLength int
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListServerSnapshotsParams
args args
expectedSort []domain.ServerSnapshotSort
expectedErr error
}{
{
name: "OK: [id:ASC, date:ASC, serverKey:ASC]",
args: args{
sort: []string{
"id:ASC",
"date:ASC",
"serverKey:ASC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
{
name: "OK: [id:DESC, date:DESC, serverKey:DESC]",
args: args{
sort: []string{
"id:DESC",
"date:DESC",
"serverKey:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDDESC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortServerKeyDESC,
},
},
{
name: "OK: custom params",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []string{
"date:ASC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
{
name: "OK: empty slice",
args: args{
sort: nil,
},
},
{
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
}))
return params
},
args: args{
sort: []string{
"date:ASC",
"date:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: len(sort) > maxLength",
newParams: defaultNewParams,
args: args{
sort: []string{
"serverKey:ASC",
"date:ASC",
},
allowed: defaultAllowed,
maxLength: 1,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: unsupported sort string",
newParams: defaultNewParams,
args: args{
sort: []string{
"date:",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Index: 0,
Err: domain.UnsupportedSortStringError{
Sort: "date:",
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"date:ASC",
"date:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortDateASC.String(), domain.ServerSnapshotSortDateDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
require.ErrorIs(t, params.PrependSortString(tt.args.sort, tt.args.allowed, tt.args.maxLength), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedSort, params.Sort())
})
}
}
func TestListServerSnapshotsParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewServerSnapshotCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.ServerSnapshotCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 0,
},
},
},
{
name: "ERR: len(cursor) > 1000",
args: args{
cursor: gofakeit.LetterN(1001),
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListServerSnapshotsParams_SetLimit(t *testing.T) {
t.Parallel()
type args struct {
limit int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
limit: domain.ServerSnapshotListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.ServerSnapshotListMaxLimit),
args: args{
limit: domain.ServerSnapshotListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.ServerSnapshotListMaxLimit,
Current: domain.ServerSnapshotListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestNewListServerSnapshotsResult(t *testing.T) {
t.Parallel()
snapshots := domain.ServerSnapshots{
domaintest.NewServerSnapshot(t),
domaintest.NewServerSnapshot(t),
domaintest.NewServerSnapshot(t),
}
next := domaintest.NewServerSnapshot(t)
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(snapshots, next)
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.Date(), res.Next().Date())
})
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(snapshots, domain.ServerSnapshot{})
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 snapshots", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(nil, domain.ServerSnapshot{})
require.NoError(t, err)
assert.Zero(t, res.ServerSnapshots())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -318,7 +318,6 @@ func NewTribeSnapshotCursor(id int, serverKey string, date time.Time) (TribeSnap
}, nil
}
//nolint:gocyclo
func decodeTribeSnapshotCursor(encoded string) (TribeSnapshotCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {

View File

@ -176,7 +176,7 @@ func TestNewTribeSnapshotCursor(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
psc, err := domain.NewTribeSnapshotCursor(
tsc, err := domain.NewTribeSnapshotCursor(
tt.args.id,
tt.args.serverKey,
tt.args.date,
@ -185,10 +185,10 @@ func TestNewTribeSnapshotCursor(t *testing.T) {
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, psc.ID())
assert.Equal(t, tt.args.serverKey, psc.ServerKey())
assert.Equal(t, tt.args.date, psc.Date())
assert.NotEmpty(t, psc.Encode())
assert.Equal(t, tt.args.id, tsc.ID())
assert.Equal(t, tt.args.serverKey, tsc.ServerKey())
assert.Equal(t, tt.args.date, tsc.Date())
assert.NotEmpty(t, tsc.Encode())
})
}
}

View File

@ -158,10 +158,12 @@ func TestDataSync(t *testing.T) {
ctx,
port.NewServerWatermillConsumer(
serverSvc,
nil,
serverSub,
nopLogger,
marshaler,
serverCmdSync,
"",
serverEventSynced,
tribeEventSynced,
playerEventSynced,

View File

@ -138,6 +138,7 @@ func TestEnnoblementSync(t *testing.T) {
ctx,
port.NewServerWatermillConsumer(
serverSvc,
nil,
serverSub,
nopLogger,
marshaler,
@ -146,6 +147,7 @@ func TestEnnoblementSync(t *testing.T) {
"",
"",
"",
"",
ennoblementEventSynced,
"",
"",

View File

@ -70,6 +70,7 @@ func TestSnapshotCreation(t *testing.T) {
)
// events/commands
serverSnapshotCmdCreate := gofakeit.UUID()
tribeSnapshotCmdCreate := gofakeit.UUID()
tribeSnapshotEventCreated := gofakeit.UUID()
playerSnapshotCmdCreate := gofakeit.UUID()
@ -80,8 +81,15 @@ func TestSnapshotCreation(t *testing.T) {
serverRepo := adapter.NewServerBunRepository(db)
tribeRepo := adapter.NewTribeBunRepository(db)
playerRepo := adapter.NewPlayerBunRepository(db)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(db)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(db)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(db)
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
tribePub,
marshaler,
serverSnapshotCmdCreate,
"",
)
tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
tribePub,
marshaler,
@ -100,19 +108,28 @@ func TestSnapshotCreation(t *testing.T) {
serverSvc := app.NewServerService(serverRepo, nil, nil)
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
serverSnapshotSvc := app.NewServerSnapshotService(serverSnapshotRepo, serverSvc, serverSnapshotPublisher)
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, tribeSnapshotPublisher)
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, playerSnapshotPublisher)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher, playerSnapshotPublisher)
snapshotSvc := app.NewSnapshotService(
versionSvc,
serverSvc,
serverSnapshotPublisher,
tribeSnapshotPublisher,
playerSnapshotPublisher,
)
watermilltest.RunRouterWithContext(
t,
ctx,
port.NewServerWatermillConsumer(
serverSvc,
serverSnapshotSvc,
serverSub,
nopLogger,
marshaler,
"",
serverSnapshotCmdCreate,
"",
"",
"",
@ -155,31 +172,85 @@ func TestSnapshotCreation(t *testing.T) {
assert.EventuallyWithTf(t, func(collect *assert.CollectT) {
require.NoError(collect, ctx.Err())
listParams := domain.NewListServersParams()
require.NoError(collect, listParams.SetSort([]domain.ServerSort{
listServersParams := domain.NewListServersParams()
require.NoError(collect, listServersParams.SetSort([]domain.ServerSort{
domain.ServerSortKeyASC,
}))
require.NoError(collect, listParams.SetSpecial(domain.NullBool{
require.NoError(collect, listServersParams.SetSpecial(domain.NullBool{
V: false,
Valid: true,
}))
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit))
require.NoError(collect, listServersParams.SetLimit(domain.ServerListMaxLimit))
var allServers domain.Servers
for {
res, err := serverRepo.List(ctx, listParams)
res, err := serverRepo.List(ctx, listServersParams)
require.NoError(collect, err)
for _, s := range res.Servers() {
assert.WithinDuration(collect, time.Now(), s.SnapshotCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key())
}
allServers = append(allServers, res.Servers()...)
if res.Next().IsZero() {
return
break
}
require.NoError(collect, listParams.SetCursor(res.Next()))
require.NoError(collect, listServersParams.SetCursor(res.Next()))
}
listSnapshotsParams := domain.NewListServerSnapshotsParams()
require.NoError(collect, listSnapshotsParams.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
}))
require.NoError(collect, listSnapshotsParams.SetLimit(domain.ServerSnapshotListMaxLimit))
cnt := 0
for {
res, err := serverSnapshotRepo.List(ctx, listSnapshotsParams)
require.NoError(collect, err)
for _, ss := range res.ServerSnapshots() {
cnt++
msg := fmt.Sprintf("ServerKey=%s", ss.ServerKey())
idx := slices.IndexFunc(allServers, func(s domain.Server) bool {
return s.Key() == ss.ServerKey()
})
if !assert.GreaterOrEqual(
collect,
idx,
0,
msg,
) {
continue
}
server := allServers[idx]
assert.NotZero(collect, ss.ID(), msg)
assert.Equal(collect, server.Key(), ss.ServerKey(), msg)
assert.Equal(collect, server.NumVillages(), ss.NumVillages(), msg)
assert.WithinDuration(collect, time.Now(), ss.CreatedAt(), time.Minute, msg)
assert.WithinDuration(collect, time.Now(), ss.Date(), 24*time.Hour, msg)
}
if res.Next().IsZero() {
break
}
require.NoError(collect, listSnapshotsParams.SetCursor(res.Next()))
}
//nolint:testifylint
assert.Equal(collect, len(allServers), cnt)
}, 30*time.Second, 500*time.Millisecond, "servers")
}()

View File

@ -10,10 +10,12 @@ import (
type ServerWatermillConsumer struct {
svc *app.ServerService
snapshotSvc *app.ServerSnapshotService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
cmdSyncTopic string
cmdCreateSnapshotsTopic string
eventServerSyncedTopic string
eventTribesSyncedTopic string
eventPlayersSyncedTopic string
@ -25,10 +27,12 @@ type ServerWatermillConsumer struct {
func NewServerWatermillConsumer(
svc *app.ServerService,
snapshotSvc *app.ServerSnapshotService,
subscriber message.Subscriber,
logger watermill.LoggerAdapter,
marshaler watermillmsg.Marshaler,
cmdSyncTopic string,
cmdCreateSnapshotsTopic string,
eventServerSyncedTopic string,
eventTribesSyncedTopic string,
eventPlayersSyncedTopic string,
@ -39,10 +43,12 @@ func NewServerWatermillConsumer(
) *ServerWatermillConsumer {
return &ServerWatermillConsumer{
svc: svc,
snapshotSvc: snapshotSvc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic,
cmdCreateSnapshotsTopic: cmdCreateSnapshotsTopic,
eventServerSyncedTopic: eventServerSyncedTopic,
eventTribesSyncedTopic: eventTribesSyncedTopic,
eventPlayersSyncedTopic: eventPlayersSyncedTopic,
@ -55,6 +61,12 @@ func NewServerWatermillConsumer(
func (c *ServerWatermillConsumer) Register(router *message.Router) {
router.AddNoPublisherHandler("ServerConsumer.sync", c.cmdSyncTopic, c.subscriber, c.sync)
router.AddNoPublisherHandler(
"ServerConsumer.createSnapshots",
c.cmdCreateSnapshotsTopic,
c.subscriber,
c.createSnapshots,
)
router.AddNoPublisherHandler(
"ServerConsumer.syncConfigAndInfo",
c.eventServerSyncedTopic,
@ -141,6 +153,32 @@ func (c *ServerWatermillConsumer) syncConfigAndInfo(msg *message.Message) error
return c.svc.SyncConfigAndInfo(msg.Context(), payload)
}
func (c *ServerWatermillConsumer) createSnapshots(msg *message.Message) error {
var rawPayload watermillmsg.CreateSnapshotsCmdPayload
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
payload, err := domain.NewCreateSnapshotsCmdPayload(
rawPayload.ServerKey,
rawPayload.VersionCode,
rawPayload.VersionTimezone,
rawPayload.Date,
)
if err != nil {
c.logger.Error("couldn't construct domain.CreateSnapshotsCmdPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
return c.snapshotSvc.Create(msg.Context(), payload)
}
func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error {
var rawPayload watermillmsg.TribesSyncedEventPayload