Compare commits

..

18 Commits

Author SHA1 Message Date
Dawid Wysokiński 9d5987f74c feat: new endpoint GET /api/v2/versions/{versionCode}/servers/{serverKey}/snapshots (#52)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
Reviewed-on: #52
2024-05-13 05:12:05 +00:00
Dawid Wysokiński 4ca2ea9361
chore: update prod Dockerfile
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-12 09:47:46 +02:00
Dawid Wysokiński 6e2c54679e
feat: postgres - set application name
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-05-11 17:30:06 +02:00
Dawid Wysokiński 7de443c652
fix: server_snapshots - incorrect num_inactive_tribes
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-11 17:23:40 +02:00
Dawid Wysokiński c6d7ad8965
chore: update prod/Dockerfile
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-10 07:04:36 +02:00
Dawid Wysokiński 98361f3ae0 fix: ci/cd - test step timeouts (#51)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #51
2024-05-10 04:57:35 +00:00
Dawid Wysokiński 08a7f0c504 feat: server snapshots (#49)
ci/woodpecker/push/govulncheck Pipeline failed Details
ci/woodpecker/push/test Pipeline failed Details
Reviewed-on: #49
2024-05-09 07:49:04 +00:00
Renovate e3fa23d0c4 chore(deps): update module github.com/brianvoe/gofakeit/v7 to v7.0.3 (#48)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #48
Co-authored-by: Renovate <renovate@dwysokinski.me>
Co-committed-by: Renovate <renovate@dwysokinski.me>
2024-05-08 04:31:00 +00:00
Dawid Wysokiński f8c9bdb321 feat: add more stats to the Server model (#47)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #47
2024-05-07 06:15:37 +00:00
Dawid Wysokiński 4d6c1c8982 refactor: server - rename num_tribes and num_players (#46)
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/govulncheck Pipeline was successful Details
Reviewed-on: #46
2024-05-06 04:50:24 +00:00
Dawid Wysokiński 1c1758ce7d refactor: server - rename *_updated_at columns (#45)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
Reviewed-on: #45
2024-05-04 05:26:22 +00:00
Dawid Wysokiński d05d7aaacb
chore: bump golangci-lint to v1.58.0
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-04 06:57:03 +02:00
Dawid Wysokiński 68b715306d
refactor: versionModelName should be const, not var
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-04-09 07:51:20 +02:00
Dawid Wysokiński 21e530aa0f
refactor: cmd_serve - rename flag variables
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 07:12:08 +02:00
Dawid Wysokiński be824acaec
refactor: runConsumer - minor changes to the initialization/cleanup sequence
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 07:08:11 +02:00
Dawid Wysokiński 3fe539db19
refactor: move logger.Debug call to waitForShutdownSignal
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 06:41:51 +02:00
Renovate 24b8ffd32c chore(deps): update module github.com/getkin/kin-openapi to v0.124.0 (#42)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #42
Co-authored-by: Renovate <renovate@dwysokinski.me>
Co-committed-by: Renovate <renovate@dwysokinski.me>
2024-04-07 04:09:36 +00:00
Dawid Wysokiński 03e0e4149b
fix: incorrect appName
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-04-06 07:22:49 +02:00
88 changed files with 5183 additions and 329 deletions

View File

@ -57,10 +57,12 @@ linters:
- inamedparam
- sloglint
- revive
- gomnd
- mnd
- forbidigo
- copyloopvar
- intrange
- fatcontext
- canonicalheader
linters-settings:
gocyclo:
@ -123,7 +125,7 @@ linters-settings:
enable-all: true
sloglint:
attr-only: true
gomnd:
mnd:
ignored-functions:
- strconv.FormatInt
- strconv.ParseInt

View File

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

View File

@ -39,10 +39,10 @@ steps:
TESTS_RABBITMQ_CONNECTION_STRING:
amqp://twhelp:twhelp@rmq:5672/
commands:
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- go test -parallel 2 -race -coverprofile=coverage.txt -covermode=atomic ./...
lint:
image: golangci/golangci-lint:v1.57
image: golangci/golangci-lint:v1.58
pull: true
depends_on:
- generate

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.57.2
@(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

@ -471,6 +471,25 @@ paths:
$ref: "#/components/responses/ListTribeChangesResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/snapshots:
get:
operationId: listServerServerSnapshots
tags:
- versions
- servers
- snapshots
description: List the given server's snapshots
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/ServerSnapshotSortQueryParam"
responses:
200:
$ref: "#/components/responses/ListServerSnapshotsResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/snapshots:
get:
operationId: listTribeTribeSnapshots
@ -584,7 +603,11 @@ components:
- open
- url
- numPlayers
- numActivePlayers
- numInactivePlayers
- numTribes
- numActiveTribes
- numInactiveTribes
- numVillages
- numBarbarianVillages
- numBonusVillages
@ -601,11 +624,21 @@ components:
example: https://en138.tribalwars.net
numPlayers:
type: integer
description: numActivePlayers+numInactivePlayers
numActivePlayers:
type: integer
numInactivePlayers:
type: integer
playerDataSyncedAt:
type: string
format: date-time
numTribes:
type: integer
description: numActiveTribes+numInactiveTribes
numActiveTribes:
type: integer
numInactiveTribes:
type: integer
tribeDataSyncedAt:
type: string
format: date-time
@ -1580,6 +1613,52 @@ components:
createdAt:
type: string
format: date-time
ServerSnapshot:
type: object
required:
- id
- server
- numPlayers
- numActivePlayers
- numInactivePlayers
- numTribes
- numActiveTribes
- numInactiveTribes
- numVillages
- numBarbarianVillages
- numBonusVillages
- numPlayerVillages
- date
properties:
id:
$ref: "#/components/schemas/IntId"
server:
$ref: "#/components/schemas/ServerMeta"
numPlayers:
type: integer
description: numActivePlayers+numInactivePlayers
numActivePlayers:
type: integer
numInactivePlayers:
type: integer
numTribes:
type: integer
description: numActiveTribes+numInactiveTribes
numActiveTribes:
type: integer
numInactiveTribes:
type: integer
numVillages:
type: integer
numBarbarianVillages:
type: integer
numBonusVillages:
type: integer
numPlayerVillages:
type: integer
date:
type: string
format: date
TribeSnapshot:
type: object
required:
@ -1661,7 +1740,7 @@ components:
x-go-type-skip-optional-pointer: true
allOf:
- $ref: "#/components/schemas/CursorString"
PaginationResponse:
Pagination:
type: object
properties:
cursor:
@ -1826,6 +1905,20 @@ components:
- date:ASC
- date:DESC
maxItems: 1
ServerSnapshotSortQueryParam:
name: sort
in: query
description: Order matters!
schema:
type: array
default:
- date:ASC
items:
type: string
enum:
- date:ASC
- date:DESC
maxItems: 1
PlayerSnapshotSortQueryParam:
name: sort
in: query
@ -1891,7 +1984,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -1917,7 +2010,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -1976,7 +2069,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2002,7 +2095,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2017,7 +2110,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2043,7 +2136,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2069,7 +2162,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2084,7 +2177,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2093,13 +2186,28 @@ components:
type: array
items:
$ref: "#/components/schemas/TribeChange"
ListServerSnapshotsResponse:
description: ""
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
properties:
data:
type: array
items:
$ref: "#/components/schemas/ServerSnapshot"
ListTribeSnapshotsResponse:
description: ""
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2114,7 +2222,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data

View File

@ -1,9 +1,9 @@
FROM --platform=$BUILDPLATFORM alpine:3.19
FROM alpine:3.19
LABEL maintainer="contact@twhelp.app"
ARG TARGETOS="linux"
ARG TARGETARCH="amd64"
ARG CI_COMMIT_TAG="v3.0.0"
ARG TARGETOS
ARG TARGETARCH
ARG CI_COMMIT_TAG
RUN apk --no-cache add ca-certificates tzdata wget && \
wget --progress=dot:giga -O /usr/bin/twhelp https://gitea.dwysokinski.me/twhelp/core/releases/download/${CI_COMMIT_TAG}/core_${CI_COMMIT_TAG##v}_${TARGETOS}_${TARGETARCH} && \

View File

@ -23,30 +23,30 @@ var (
}
dbFlagMaxIdleConns = &cli.IntFlag{
Name: "db.maxIdleConns",
Value: 2, //nolint:gomnd
Value: 2, //nolint:mnd
EnvVars: []string{"DB_MAX_IDLE_CONNS"},
Usage: "https://pkg.go.dev/database/sql#DB.SetMaxIdleConns",
}
dbFlagMaxOpenConns = &cli.IntFlag{
Name: "db.maxOpenConns",
Value: runtime.NumCPU() * 4, //nolint:gomnd
Value: runtime.NumCPU() * 4, //nolint:mnd
EnvVars: []string{"DB_MAX_OPEN_CONNS"},
Usage: "https://pkg.go.dev/database/sql#DB.SetMaxOpenConns",
}
dbFlagConnMaxLifetime = &cli.DurationFlag{
Name: "db.connMaxLifetime",
Value: 30 * time.Minute, //nolint:gomnd
Value: 30 * time.Minute, //nolint:mnd
EnvVars: []string{"DB_CONN_MAX_LIFETIME"},
Usage: "https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime",
}
dbFlagReadTimeout = &cli.DurationFlag{
Name: "db.readTimeout",
Value: 10 * time.Second, //nolint:gomnd
Value: 10 * time.Second, //nolint:mnd
EnvVars: []string{"DB_READ_TIMEOUT"},
}
dbFlagWriteTimeout = &cli.DurationFlag{
Name: "db.writeTimeout",
Value: 5 * time.Second, //nolint:gomnd
Value: 5 * time.Second, //nolint:mnd
EnvVars: []string{"DB_WRITE_TIMEOUT"},
}
dbFlags = []cli.Flag{
@ -59,8 +59,9 @@ var (
}
)
func newBunDBFromFlags(c *cli.Context) (*bun.DB, error) {
func newBunDBFromFlags(c *cli.Context, applicationName string) (*bun.DB, error) {
return newBunDB(bundDBConfig{
applicationName: applicationName,
connectionString: c.String(dbFlagConnectionString.Name),
maxOpenConns: c.Int(dbFlagMaxOpenConns.Name),
maxIdleConns: c.Int(dbFlagMaxIdleConns.Name),
@ -72,6 +73,7 @@ func newBunDBFromFlags(c *cli.Context) (*bun.DB, error) {
type bundDBConfig struct {
connectionString string
applicationName string
maxOpenConns int
maxIdleConns int
connMaxLifetime time.Duration
@ -99,6 +101,7 @@ func newSQLDB(cfg bundDBConfig) *sql.DB {
pgdriver.WithDSN(cfg.connectionString),
pgdriver.WithReadTimeout(cfg.readTimeout),
pgdriver.WithWriteTimeout(cfg.writeTimeout),
pgdriver.WithApplicationName(cfg.applicationName),
))
db.SetMaxOpenConns(cfg.maxOpenConns)
db.SetMaxIdleConns(cfg.maxIdleConns)

View File

@ -18,6 +18,7 @@ import (
"github.com/ThreeDotsLabs/watermill-amqp/v2/pkg/amqp"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
"github.com/ettle/strcase"
"github.com/uptrace/bun"
"github.com/urfave/cli/v2"
)
@ -49,18 +50,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),
@ -299,18 +314,9 @@ type registerConsumerHandlersFunc func(
//nolint:gocyclo
func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerHandlersFunc) error {
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
logger := loggerFromCtx(ctx)
logger := loggerFromCtx(c.Context)
watermillLogger := newWatermillLogger(logger)
var wg sync.WaitGroup
defer func() {
// it's required for the graceful shutdown
wg.Wait()
}()
amqpConn, err := newAMQPConnectionFromFlags(c, watermillLogger)
if err != nil {
return err
@ -341,19 +347,12 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
}
}()
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, strcase.ToSnake(name))
if err != nil {
return err
}
defer closeBunDB(bunDB, logger)
healthObserver := healthfile.LiveObserver(health.New(), "/tmp/live")
defer func() {
if closeErr := healthObserver.Close(); closeErr != nil {
logger.Warn("couldn't close health observer", slog.Any("error", closeErr))
}
}()
router, err := newWatermillRouter(watermillLogger)
if err != nil {
return err
@ -371,6 +370,22 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
return err
}
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
var wg sync.WaitGroup
defer func() {
// it's required for the graceful shutdown
wg.Wait()
}()
healthObserver := healthfile.LiveObserver(health.New(), "/tmp/live")
defer func() {
if closeErr := healthObserver.Close(); closeErr != nil {
logger.Warn("couldn't close health observer", slog.Any("error", closeErr))
}
}()
wg.Add(1)
go func() {
defer wg.Done()
@ -391,7 +406,7 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
logger.Info("consumer is up and running", slog.String("name", name))
}
waitForShutdownSignal(ctx)
waitForShutdownSignal(ctx, logger)
if closeErr := router.Close(); closeErr != nil {
logger.Warn("couldn't close router", slog.Any("error", err))

View File

@ -45,7 +45,7 @@ var cmdDB = &cli.Command{
logger := loggerFromCtx(c.Context)
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "migrations")
if err != nil {
return err
}
@ -94,7 +94,7 @@ var cmdDB = &cli.Command{
logger := loggerFromCtx(c.Context)
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "migrations")
if err != nil {
return err
}

View File

@ -47,7 +47,7 @@ var (
}
}()
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "data_sync_job")
if err != nil {
return err
}
@ -103,7 +103,7 @@ var (
}
}()
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "ennoblement_sync_job")
if err != nil {
return err
}
@ -165,19 +165,24 @@ var (
}
}()
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "snapshot_creation_job")
if err != nil {
return err
}
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()
@ -231,7 +242,7 @@ var (
}
}()
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "cleanup_job")
if err != nil {
return err
}

View File

@ -23,66 +23,66 @@ import (
)
var (
apiServerPortFlag = &cli.UintFlag{
apiServerFlagPort = &cli.UintFlag{
Name: "api.port",
EnvVars: []string{"API_PORT"},
Value: 9234, //nolint:gomnd
Value: 9234, //nolint:mnd
}
apiServerHandlerTimeoutFlag = &cli.DurationFlag{
apiServerFlagHandlerTimeout = &cli.DurationFlag{
Name: "api.handlerTimeout",
EnvVars: []string{"API_HANDLER_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd
Value: 5 * time.Second, //nolint:mnd
Usage: "https://pkg.go.dev/net/http#TimeoutHandler",
}
apiServerReadTimeoutFlag = &cli.DurationFlag{
apiServerFlagReadTimeout = &cli.DurationFlag{
Name: "api.readTimeout",
EnvVars: []string{"API_READ_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd
Value: 5 * time.Second, //nolint:mnd
}
apiServerReadHeaderTimeoutFlag = &cli.DurationFlag{
apiServerFlagReadHeaderTimeout = &cli.DurationFlag{
Name: "api.readHeaderTimeout",
EnvVars: []string{"API_READ_HEADER_TIMEOUT"},
Value: time.Second,
}
apiServerWriteTimeoutFlag = &cli.DurationFlag{
apiServerFlagWriteTimeout = &cli.DurationFlag{
Name: "api.writeTimeout",
EnvVars: []string{"API_WRITE_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
Value: 10 * time.Second, //nolint:mnd
}
apiServerIdleTimeoutFlag = &cli.DurationFlag{
apiServerFlagIdleTimeout = &cli.DurationFlag{
Name: "api.idleTimeout",
EnvVars: []string{"API_IDLE_TIMEOUT"},
Value: 180 * time.Second, //nolint:gomnd
Value: 180 * time.Second, //nolint:mnd
}
apiServerShutdownTimeoutFlag = &cli.DurationFlag{
apiServerFlagShutdownTimeout = &cli.DurationFlag{
Name: "api.shutdownTimeout",
EnvVars: []string{"API_SHUTDOWN_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
Value: 10 * time.Second, //nolint:mnd
}
apiServerOpenAPIEnabledFlag = &cli.BoolFlag{
apiServerFlagOpenAPIEnabled = &cli.BoolFlag{
Name: "api.openApi.enabled",
EnvVars: []string{"API_OPENAPI_ENABLED"},
Value: true,
}
apiServerOpenAPISwaggerEnabledFlag = &cli.BoolFlag{
apiServerFlagOpenAPISwaggerEnabled = &cli.BoolFlag{
Name: "api.openApi.swaggerEnabled",
EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"},
Value: true,
}
apiServerOpenAPIServersFlag = &cli.StringSliceFlag{
apiServerFlagOpenAPIServers = &cli.StringSliceFlag{
Name: "api.openApi.servers",
EnvVars: []string{"API_OPENAPI_SERVERS"},
}
apiServerFlags = []cli.Flag{
apiServerPortFlag,
apiServerHandlerTimeoutFlag,
apiServerReadTimeoutFlag,
apiServerReadHeaderTimeoutFlag,
apiServerWriteTimeoutFlag,
apiServerIdleTimeoutFlag,
apiServerOpenAPIEnabledFlag,
apiServerOpenAPISwaggerEnabledFlag,
apiServerOpenAPIServersFlag,
apiServerFlagPort,
apiServerFlagHandlerTimeout,
apiServerFlagReadTimeout,
apiServerFlagReadHeaderTimeout,
apiServerFlagWriteTimeout,
apiServerFlagIdleTimeout,
apiServerFlagOpenAPIEnabled,
apiServerFlagOpenAPISwaggerEnabled,
apiServerFlagOpenAPIServers,
}
)
@ -99,7 +99,7 @@ var cmdServe = &cli.Command{
logger := loggerFromCtx(c.Context)
// deps
bunDB, err := newBunDBFromFlags(c)
bunDB, err := newBunDBFromFlags(c, "api")
if err != nil {
return err
}
@ -113,6 +113,7 @@ var cmdServe = &cli.Command{
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(bunDB)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
@ -124,6 +125,7 @@ var cmdServe = &cli.Command{
playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil)
villageSvc := app.NewVillageService(villageRepo, nil, nil)
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
serverSnapshotSvc := app.NewServerSnapshotService(serverSnapshotRepo, serverSvc, nil)
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil)
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil)
@ -132,11 +134,11 @@ var cmdServe = &cli.Command{
server, err := newHTTPServer(
httpServerConfig{
port: c.Uint(apiServerPortFlag.Name),
readTimeout: c.Duration(apiServerReadTimeoutFlag.Name),
readHeaderTimeout: c.Duration(apiServerReadHeaderTimeoutFlag.Name),
writeTimeout: c.Duration(apiServerWriteTimeoutFlag.Name),
idleTimeout: c.Duration(apiServerIdleTimeoutFlag.Name),
port: c.Uint(apiServerFlagPort.Name),
readTimeout: c.Duration(apiServerFlagReadTimeout.Name),
readHeaderTimeout: c.Duration(apiServerFlagReadHeaderTimeout.Name),
writeTimeout: c.Duration(apiServerFlagWriteTimeout.Name),
idleTimeout: c.Duration(apiServerFlagIdleTimeout.Name),
},
func(r chi.Router) error {
oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c)
@ -160,6 +162,7 @@ var cmdServe = &cli.Command{
villageSvc,
ennoblementSvc,
tribeChangeSvc,
serverSnapshotSvc,
tribeSnapshotSvc,
playerSnapshotSvc,
port.WithOpenAPIConfig(oapiCfg),
@ -175,11 +178,9 @@ var cmdServe = &cli.Command{
idleConnsClosed := make(chan struct{})
go func() {
waitForShutdownSignal(c.Context)
waitForShutdownSignal(c.Context, logger)
logger.Debug("received shutdown signal")
ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerShutdownTimeoutFlag.Name))
ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerFlagShutdownTimeout.Name))
defer cancel()
if err := server.Shutdown(ctx); err != nil {
@ -202,7 +203,7 @@ var cmdServe = &cli.Command{
}
func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) {
rawURLs := c.StringSlice(apiServerOpenAPIServersFlag.Name)
rawURLs := c.StringSlice(apiServerFlagOpenAPIServers.Name)
servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs))
@ -218,8 +219,8 @@ func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) {
}
return port.OpenAPIConfig{
Enabled: c.Bool(apiServerOpenAPIEnabledFlag.Name),
SwaggerEnabled: c.Bool(apiServerOpenAPISwaggerEnabledFlag.Name),
Enabled: c.Bool(apiServerFlagOpenAPIEnabled.Name),
SwaggerEnabled: c.Bool(apiServerFlagOpenAPISwaggerEnabled.Name),
BasePath: apiBasePath,
Servers: servers,
}, nil
@ -239,7 +240,7 @@ func newAPIMiddlewares(c *cli.Context, logger *slog.Logger) chi.Middlewares {
})),
middleware.Recoverer,
func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, c.Duration(apiServerHandlerTimeoutFlag.Name), "Timeout")
return http.TimeoutHandler(next, c.Duration(apiServerFlagHandlerTimeout.Name), "Timeout")
},
}
}

View File

@ -5,7 +5,7 @@ import (
"os"
)
const appName = "wendia"
const appName = "twhelp"
// this flag will be set by the build flags
var version = "development"

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

@ -20,7 +20,7 @@ var (
}
twSvcFlagTimeout = &cli.DurationFlag{
Name: "tw.timeout",
Value: 10 * time.Second, //nolint:gomnd
Value: 10 * time.Second, //nolint:mnd
EnvVars: []string{"TW_TIMEOUT"},
Usage: "https://pkg.go.dev/net/http#Client.Timeout",
}

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
@ -15,8 +16,9 @@ func newShutdownSignalContext(parent context.Context) (context.Context, context.
return signal.NotifyContext(parent, shutdownSignals...)
}
func waitForShutdownSignal(ctx context.Context) {
func waitForShutdownSignal(ctx context.Context, logger *slog.Logger) {
ctx, cancel := newShutdownSignalContext(ctx)
defer cancel()
<-ctx.Done()
logger.Debug("received shutdown signal")
}

10
go.mod
View File

@ -6,11 +6,11 @@ require (
gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0
github.com/ThreeDotsLabs/watermill v1.3.5
github.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.1
github.com/brianvoe/gofakeit/v7 v7.0.2
github.com/brianvoe/gofakeit/v7 v7.0.3
github.com/cenkalti/backoff/v4 v4.3.0
github.com/elliotchance/phpserialize v1.4.0
github.com/ettle/strcase v0.2.0
github.com/getkin/kin-openapi v0.123.0
github.com/getkin/kin-openapi v0.124.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/render v1.0.3
github.com/google/go-cmp v0.6.0
@ -26,7 +26,7 @@ require (
github.com/uptrace/bun/driver/pgdriver v1.2.1
github.com/uptrace/bun/driver/sqliteshim v1.2.1
github.com/uptrace/bun/extra/bundebug v1.2.1
github.com/urfave/cli/v2 v2.27.1
github.com/urfave/cli/v2 v2.27.2
go.uber.org/automaxprocs v1.5.3
golang.org/x/sync v0.7.0
gopkg.in/yaml.v3 v3.0.1
@ -40,7 +40,7 @@ require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect
@ -86,7 +86,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect

20
go.sum
View File

@ -17,8 +17,8 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/brianvoe/gofakeit/v7 v7.0.2 h1:jzYT7Ge3RDHw7J1CM1kwu0OQywV9vbf2qSGxBS72TCY=
github.com/brianvoe/gofakeit/v7 v7.0.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo=
github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@ -30,8 +30,8 @@ github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvA
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
@ -55,8 +55,8 @@ github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
@ -206,8 +206,8 @@ github.com/uptrace/bun/driver/sqliteshim v1.2.1/go.mod h1:oJtOPSCDdDHgNw/0jwIGr+
github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus=
github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
@ -220,8 +220,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=

View File

@ -35,10 +35,11 @@ func (pub *PlayerWatermillPublisher) EventSynced(
for _, p := range payloads {
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.PlayersSyncedEventPayload{
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumPlayers: p.NumPlayers(),
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumPlayers: p.NumPlayers(),
NumActivePlayers: p.NumActivePlayers(),
})
if err != nil {
return fmt.Errorf("%s: couldn't marshal PlayersSyncedEventPayload: %w", p.ServerKey(), err)

View File

@ -35,10 +35,11 @@ func (pub *TribeWatermillPublisher) EventSynced(
for _, p := range payloads {
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.TribesSyncedEventPayload{
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumTribes: p.NumTribes(),
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumTribes: p.NumTribes(),
NumActiveTribes: p.NumActiveTribes(),
})
if err != nil {
return fmt.Errorf("%s: couldn't marshal TribesSyncedEventPayload: %w", p.ServerKey(), err)

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

@ -131,6 +131,17 @@ func (repo *PlayerBunRepository) ListWithRelations(
return domain.NewListPlayersWithRelationsResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *PlayerBunRepository) Count(ctx context.Context, params domain.CountPlayersParams) (int, error) {
cnt, err := repo.db.NewSelect().
Model((*bunmodel.Player)(nil)).
Apply(countPlayersParamsApplier{params: params}.apply).
Count(ctx)
if err != nil {
return 0, fmt.Errorf("couldnt count players: %w", err)
}
return cnt, nil
}
func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
if len(ids) == 0 {
return nil
@ -323,3 +334,15 @@ func (a listPlayersParamsApplier) sortToColumnAndDirection(
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
}
}
type countPlayersParamsApplier struct {
params domain.CountPlayersParams
}
func (a countPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
}
return q
}

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

@ -130,18 +130,32 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("num_tribes = ?", numTribes.V)
}
if numActiveTribes := a.params.NumActiveTribes(); numActiveTribes.Valid {
q = q.Set("num_active_tribes = ?", numActiveTribes.V)
}
if numInactiveTribes := a.params.NumInactiveTribes(); numInactiveTribes.Valid {
q = q.Set("num_inactive_tribes = ?", numInactiveTribes.V)
}
if tribeDataSyncedAt := a.params.TribeDataSyncedAt(); tribeDataSyncedAt.Valid {
// TODO: rename this column to tribe_data_synced_at
q = q.Set("tribe_data_updated_at = ?", tribeDataSyncedAt.V)
q = q.Set("tribe_data_synced_at = ?", tribeDataSyncedAt.V)
}
if numPlayers := a.params.NumPlayers(); numPlayers.Valid {
q = q.Set("num_players = ?", numPlayers.V)
}
if numActivePlayers := a.params.NumActivePlayers(); numActivePlayers.Valid {
q = q.Set("num_active_players = ?", numActivePlayers.V)
}
if numInactivePlayers := a.params.NumInactivePlayers(); numInactivePlayers.Valid {
q = q.Set("num_inactive_players = ?", numInactivePlayers.V)
}
if playerDataSyncedAt := a.params.PlayerDataSyncedAt(); playerDataSyncedAt.Valid {
// TODO: rename this column to player_data_synced_at
q = q.Set("player_data_updated_at = ?", playerDataSyncedAt.V)
q = q.Set("player_data_synced_at = ?", playerDataSyncedAt.V)
}
if numVillages := a.params.NumVillages(); numVillages.Valid {
@ -161,13 +175,15 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
}
if villageDataSyncedAt := a.params.VillageDataSyncedAt(); villageDataSyncedAt.Valid {
// TODO: rename this column to village_data_synced_at
q = q.Set("village_data_updated_at = ?", villageDataSyncedAt.V)
q = q.Set("village_data_synced_at = ?", villageDataSyncedAt.V)
}
if ennoblementDataSyncedAt := a.params.EnnoblementDataSyncedAt(); ennoblementDataSyncedAt.Valid {
// TODO: rename this column to ennoblement_data_synced_at
q = q.Set("ennoblement_data_updated_at = ?", ennoblementDataSyncedAt.V)
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 {
@ -216,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,206 @@
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) ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error) {
var serverSnapshots bunmodel.ServerSnapshots
if err := repo.db.NewSelect().
Model(&serverSnapshots).
Apply(listServerSnapshotsParamsApplier{params: params}.apply).
Relation("Server", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Column(bunmodel.ServerMetaColumns...)
}).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServerSnapshotsWithRelationsResult{}, fmt.Errorf(
"couldn't select server snapshots from the db: %w",
err,
)
}
converted, err := serverSnapshots.ToDomainWithRelations()
if err != nil {
return domain.ListServerSnapshotsWithRelationsResult{}, err
}
return domain.NewListServerSnapshotsWithRelationsResult(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

@ -132,6 +132,17 @@ func (repo *TribeBunRepository) List(
return domain.NewListTribesResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *TribeBunRepository) Count(ctx context.Context, params domain.CountTribesParams) (int, error) {
cnt, err := repo.db.NewSelect().
Model((*bunmodel.Tribe)(nil)).
Apply(countTribesParamsApplier{params: params}.apply).
Count(ctx)
if err != nil {
return 0, fmt.Errorf("couldnt count tribes: %w", err)
}
return cnt, nil
}
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
if len(ids) == 0 {
return nil
@ -287,3 +298,15 @@ func (a listTribesParamsApplier) sortToColumnAndDirection(
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
}
}
type countTribesParamsApplier struct {
params domain.CountTribesParams
}
func (a countTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("tribe.server_key IN (?)", bun.In(serverKeys))
}
return q
}

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

@ -664,6 +664,78 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}
})
t.Run("Count", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.CountPlayersParams
assertResult func(t *testing.T, params domain.CountPlayersParams, count int)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.CountPlayersParams {
t.Helper()
return domain.NewCountPlayersParams()
},
assertResult: func(t *testing.T, _ domain.CountPlayersParams, count int) {
t.Helper()
res, err := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, err)
assert.Len(t, res.Players(), count)
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.CountPlayersParams {
t.Helper()
res, err := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, err)
params := domain.NewCountPlayersParams()
require.NoError(t, params.SetServerKeys([]string{res.Players()[0].ServerKey()}))
return params
},
assertResult: func(t *testing.T, params domain.CountPlayersParams, count int) {
t.Helper()
listParams := domain.NewListPlayersParams()
require.NoError(t, listParams.SetServerKeys(params.ServerKeys()))
res, err := repos.player.List(ctx, listParams)
require.NoError(t, err)
assert.Len(t, res.Players(), count)
},
},
}
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)
cnt, err := repos.player.Count(ctx, params)
assertError(t, err)
tt.assertResult(t, params, cnt)
})
}
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()

View File

@ -0,0 +1,453 @@
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 & ListWithRelations", 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)
resWithRelations, err := repos.serverSnapshot.ListWithRelations(ctx, params)
assertError(t, err)
require.Len(t, resWithRelations.ServerSnapshots(), len(res.ServerSnapshots()))
for i, ss := range resWithRelations.ServerSnapshots() {
assert.Equal(t, res.ServerSnapshots()[i], ss.ServerSnapshot())
assert.Equal(t, ss.ServerSnapshot().ServerKey(), ss.Server().Key())
}
})
}
})
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 {
@ -498,6 +518,14 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumActiveTribes(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumInactiveTribes(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetTribeDataSyncedAt(domain.NullTime{
V: time.Now(),
Valid: true,
@ -506,6 +534,14 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumActivePlayers(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumInactivePlayers(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetPlayerDataSyncedAt(domain.NullTime{
V: time.Now(),
Valid: true,
@ -534,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,
@ -557,6 +597,8 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assert.Equal(t, updateParams.UnitInfo().V, serverAfterUpdate.UnitInfo())
assert.Equal(t, updateParams.BuildingInfo().V, serverAfterUpdate.BuildingInfo())
assert.Equal(t, updateParams.NumTribes().V, serverAfterUpdate.NumTribes())
assert.Equal(t, updateParams.NumActiveTribes().V, serverAfterUpdate.NumActiveTribes())
assert.Equal(t, updateParams.NumInactiveTribes().V, serverAfterUpdate.NumInactiveTribes())
assert.WithinDuration(
t,
updateParams.TribeDataSyncedAt().V,
@ -564,6 +606,8 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
time.Minute,
)
assert.Equal(t, updateParams.NumPlayers().V, serverAfterUpdate.NumPlayers())
assert.Equal(t, updateParams.NumActivePlayers().V, serverAfterUpdate.NumActivePlayers())
assert.Equal(t, updateParams.NumInactivePlayers().V, serverAfterUpdate.NumInactivePlayers())
assert.WithinDuration(
t,
updateParams.PlayerDataSyncedAt().V,
@ -586,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

@ -26,6 +26,7 @@ type tribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
Count(ctx context.Context, params domain.CountTribesParams) (int, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}
@ -33,6 +34,7 @@ type playerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error)
Count(ctx context.Context, params domain.CountPlayersParams) (int, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}
@ -65,6 +67,16 @@ 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)
ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, 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)
@ -93,6 +105,7 @@ type repositories struct {
village villageRepository
ennoblement ennoblementRepository
tribeChange tribeChangeRepository
serverSnapshot serverSnapshotRepository
tribeSnapshot tribeSnapshotRepository
playerSnapshot playerSnapshotRepository
}
@ -110,6 +123,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

@ -718,6 +718,78 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}
})
t.Run("Count", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.CountTribesParams
assertResult func(t *testing.T, params domain.CountTribesParams, count int)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.CountTribesParams {
t.Helper()
return domain.NewCountTribesParams()
},
assertResult: func(t *testing.T, _ domain.CountTribesParams, count int) {
t.Helper()
res, err := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, err)
assert.Len(t, res.Tribes(), count)
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.CountTribesParams {
t.Helper()
res, err := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, err)
params := domain.NewCountTribesParams()
require.NoError(t, params.SetServerKeys([]string{res.Tribes()[0].ServerKey()}))
return params
},
assertResult: func(t *testing.T, params domain.CountTribesParams, count int) {
t.Helper()
listParams := domain.NewListTribesParams()
require.NoError(t, listParams.SetServerKeys(params.ServerKeys()))
res, err := repos.tribe.List(ctx, listParams)
require.NoError(t, err)
assert.Len(t, res.Tribes(), count)
},
},
}
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)
cnt, err := repos.tribe.Count(ctx, params)
assertError(t, err)
tt.assertResult(t, params, cnt)
})
}
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()

View File

@ -5,76 +5,80 @@
url: https://de188.die-staemme.de
open: true
special: false
num_players: 180
num_tribes: 76
num_active_players: 180
num_active_tribes: 76
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z
player_data_updated_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_updated_at: 2022-03-19T12:00:54.000Z
tribe_data_synced_at: 2022-03-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z
village_data_updated_at: 2022-03-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:54.000Z
village_data_synced_at: 2022-03-19T12:00:54.000Z
ennoblement_data_synced_at: 2022-03-19T12:00:54.000Z
version_code: de
- _id: en113
key: en113
url: https://en113.tribalwars.net
open: false
special: false
num_players: 251
num_tribes: 57
num_active_players: 251
num_active_tribes: 57
num_villages: 41700
num_player_villages: 41000
num_barbarian_villages: 700
num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z
player_data_updated_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_updated_at: 2021-04-02T16:01:25.000Z
tribe_data_synced_at: 2021-04-02T16:01:25.000Z
tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z
village_data_updated_at: 2021-04-02T16:01:25.000Z
ennoblement_data_updated_at: 2021-04-02T16:01:25.000Z
village_data_synced_at: 2021-04-02T16:01:25.000Z
ennoblement_data_synced_at: 2021-04-02T16:01:25.000Z
version_code: en
- _id: it70
key: it70
url: https://it70.tribals.it
open: true
special: false
num_players: 1883
num_tribes: 101
num_active_players: 1883
num_active_tribes: 101
num_villages: 4882
num_player_villages: 3200
num_barbarian_villages: 1682
num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z
player_data_updated_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_updated_at: 2022-03-19T12:00:04.000Z
tribe_data_synced_at: 2022-03-19T12:00:04.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z
village_data_updated_at: 2022-03-19T12:00:04.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:04.000Z
village_data_synced_at: 2022-03-19T12:00:04.000Z
ennoblement_data_synced_at: 2022-03-19T12:00:04.000Z
version_code: it
- _id: pl169
key: pl169
url: https://pl169.plemiona.pl
open: true
special: false
num_players: 2001
num_tribes: 214
num_active_players: 2001
num_active_tribes: 214
num_villages: 49074
num_player_villages: 48500
num_barbarian_villages: 1574
num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z
player_data_updated_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_updated_at: 2022-03-19T12:01:39.000Z
tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z
village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl
- model: Tribe
rows:
@ -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

@ -11,6 +11,7 @@ type PlayerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error)
Count(ctx context.Context, params domain.CountPlayersParams) (int, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
// In addition, Delete sets TribeID to null.
//
@ -51,10 +52,21 @@ func (svc *PlayerService) Sync(ctx context.Context, serverSyncedPayload domain.S
return fmt.Errorf("%s: couldn't delete players: %w", serverKey, err)
}
countParams := domain.NewCountPlayersParams()
if err = countParams.SetServerKeys([]string{serverKey}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
numPlayers, err := svc.repo.Count(ctx, countParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
payload, err := domain.NewPlayersSyncedEventPayload(
serverKey,
serverURL,
serverSyncedPayload.VersionCode(),
numPlayers,
len(players),
)
if err != nil {
@ -101,7 +113,7 @@ func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey str
if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
return err
}
if err := listParams.SetLimit(domain.PlayerListMaxLimit); err != nil {
if err := listParams.SetLimit(len(ids)); err != nil {
return err
}

View File

@ -189,6 +189,18 @@ func (svc *ServerService) UpdateNumTribes(ctx context.Context, payload domain.Tr
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumActiveTribes(domain.NullInt{
V: payload.NumActiveTribes(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumInactiveTribes(domain.NullInt{
V: payload.NumInactiveTribes(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetTribeDataSyncedAt(domain.NullTime{
V: time.Now(),
Valid: true,
@ -209,6 +221,18 @@ func (svc *ServerService) UpdateNumPlayers(ctx context.Context, payload domain.P
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumActivePlayers(domain.NullInt{
V: payload.NumActivePlayers(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumInactivePlayers(domain.NullInt{
V: payload.NumInactivePlayers(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetPlayerDataSyncedAt(domain.NullTime{
V: time.Now(),
Valid: true,
@ -274,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,72 @@
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
ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, 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}
}
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
}
func (svc *ServerSnapshotService) ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error) {
return svc.repo.ListWithRelations(ctx, params)
}

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

@ -11,6 +11,7 @@ type TribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
Count(ctx context.Context, params domain.CountTribesParams) (int, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
//
// https://en.wiktionary.org/wiki/soft_deletion
@ -44,10 +45,21 @@ func (svc *TribeService) Sync(ctx context.Context, serverSyncedPayload domain.Se
return fmt.Errorf("%s: couldn't delete tribes: %w", serverKey, err)
}
countParams := domain.NewCountTribesParams()
if err = countParams.SetServerKeys([]string{serverKey}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
numTribes, err := svc.repo.Count(ctx, countParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
payload, err := domain.NewTribesSyncedEventPayload(
serverKey,
serverURL,
serverSyncedPayload.VersionCode(),
numTribes,
len(tribes),
)
if err != nil {
@ -94,7 +106,7 @@ func (svc *TribeService) createOrUpdateChunk(ctx context.Context, serverKey stri
if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil {
return err
}
if err := listParams.SetLimit(domain.TribeListMaxLimit); err != nil {
if err := listParams.SetLimit(len(ids)); err != nil {
return err
}

View File

@ -11,32 +11,33 @@ import (
var ServerMetaColumns = []string{"key", "url", "open"}
type Server struct {
bun.BaseModel `bun:"table:servers,alias:server"`
Key string `bun:"key,nullzero,pk"`
URL string `bun:"url,nullzero"`
Open bool `bun:"open"`
Special bool `bun:"special"`
NumPlayers int `bun:"num_players"`
NumTribes int `bun:"num_tribes"`
NumVillages int `bun:"num_villages"`
NumPlayerVillages int `bun:"num_player_villages"`
NumBarbarianVillages int `bun:"num_barbarian_villages"`
NumBonusVillages int `bun:"num_bonus_villages"`
Config ServerConfig `bun:"config"`
BuildingInfo BuildingInfo `bun:"building_info"`
UnitInfo UnitInfo `bun:"unit_info"`
CreatedAt time.Time `bun:"created_at,nullzero"`
// TODO: rename this column to player_data_synced_at
PlayerDataUpdatedAt time.Time `bun:"player_data_updated_at,nullzero"`
PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"`
// TODO: rename this column to tribe_data_synced_at
TribeDataUpdatedAt time.Time `bun:"tribe_data_updated_at,nullzero"`
TribeSnapshotsCreatedAt time.Time `bun:"tribe_snapshots_created_at,nullzero"`
// TODO: rename this column to village_data_synced_at
VillageDataUpdatedAt time.Time `bun:"village_data_updated_at,nullzero"`
// TODO: rename this column to ennoblement_data_synced_at
EnnoblementDataUpdatedAt time.Time `bun:"ennoblement_data_updated_at,nullzero"`
VersionCode string `bun:"version_code,nullzero"`
bun.BaseModel `bun:"table:servers,alias:server"`
Key string `bun:"key,nullzero,pk"`
URL string `bun:"url,nullzero"`
Open bool `bun:"open"`
Special bool `bun:"special"`
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"`
Config ServerConfig `bun:"config"`
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"`
TribeSnapshotsCreatedAt time.Time `bun:"tribe_snapshots_created_at,nullzero"`
VillageDataUpdatedAt time.Time `bun:"village_data_synced_at,nullzero"`
EnnoblementDataUpdatedAt time.Time `bun:"ennoblement_data_synced_at,nullzero"`
VersionCode string `bun:"version_code,nullzero"`
}
func (s Server) ToDomain() (domain.Server, error) {
@ -62,7 +63,11 @@ func (s Server) ToDomain() (domain.Server, error) {
s.Open,
s.Special,
s.NumPlayers,
s.NumActivePlayers,
s.NumInactivePlayers,
s.NumTribes,
s.NumActiveTribes,
s.NumInactiveTribes,
s.NumVillages,
s.NumPlayerVillages,
s.NumBarbarianVillages,
@ -71,6 +76,7 @@ func (s Server) ToDomain() (domain.Server, error) {
buildingInfo,
unitInfo,
s.CreatedAt,
s.SnapshotCreatedAt,
s.PlayerDataUpdatedAt,
s.PlayerSnapshotsCreatedAt,
s.TribeDataUpdatedAt,
@ -88,7 +94,7 @@ func (s Server) ToDomain() (domain.Server, error) {
func (s Server) ToMeta() (domain.ServerMeta, error) {
converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open)
if err != nil {
return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.ServerMeta (key=%s): %w", s.Key, err)
}
return converted, nil
}

View File

@ -0,0 +1,81 @@
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"`
Server Server `bun:"server,rel:belongs-to,join:server_key=key"`
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
}
func (ss ServerSnapshot) ToDomainWithRelations() (domain.ServerSnapshotWithRelations, error) {
converted, err := ss.ToDomain()
if err != nil {
return domain.ServerSnapshotWithRelations{}, err
}
server, err := ss.Server.ToMeta()
if err != nil {
return domain.ServerSnapshotWithRelations{}, err
}
return converted.WithRelations(server), nil
}
type ServerSnapshots []ServerSnapshot
func (sss ServerSnapshots) ToDomain() (domain.ServerSnapshots, error) {
return sliceToDomain(sss)
}
func (sss ServerSnapshots) ToDomainWithRelations() (domain.ServerSnapshotsWithRelations, error) {
return sliceToDomainWithRelations(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

@ -0,0 +1,44 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240504051104() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(oldColumn), bun.Safe(newColumn))
if err != nil {
return fmt.Errorf("%s: %w", oldColumn, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240504051104() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(newColumn), bun.Safe(oldColumn))
if err != nil {
return fmt.Errorf("%s: %w", newColumn, err)
}
}
return nil
})
})
}
func getColumnsToRename20240504051104() map[string]string {
return map[string]string{
"player_data_updated_at": "player_data_synced_at",
"tribe_data_updated_at": "tribe_data_synced_at",
"village_data_updated_at": "village_data_synced_at",
"ennoblement_data_updated_at": "ennoblement_data_synced_at",
}
}

View File

@ -0,0 +1,42 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240506043013() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(oldColumn), bun.Safe(newColumn))
if err != nil {
return fmt.Errorf("%s: %w", oldColumn, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240506043013() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(newColumn), bun.Safe(oldColumn))
if err != nil {
return fmt.Errorf("%s: %w", newColumn, err)
}
}
return nil
})
})
}
func getColumnsToRename20240506043013() map[string]string {
return map[string]string{
"num_players": "num_active_players",
"num_tribes": "num_active_tribes",
}
}

View File

@ -0,0 +1,44 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for _, column := range getColumnsToAdd20240506051128() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers ADD ? bigint DEFAULT 0", bun.Safe(column))
if err != nil {
return fmt.Errorf("%s: %w", column, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for _, column := range getColumnsToAdd20240506051128() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers DROP COLUMN ?", bun.Safe(column))
if err != nil {
return fmt.Errorf("%s: %w", column, err)
}
}
return nil
})
})
}
func getColumnsToAdd20240506051128() []string {
return []string{
"num_players",
"num_inactive_players",
"num_tribes",
"num_inactive_tribes",
}
}

View File

@ -0,0 +1,52 @@
package migrations
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
var servers bunmodel.Servers
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)
}
for _, s := range servers {
numPlayers, err := db.NewSelect().Model((*bunmodel.Player)(nil)).Where("server_key = ?", s.Key).Count(ctx)
if err != nil {
return fmt.Errorf("%s: numPlayers: %w", s.Key, err)
}
numTribes, err := db.NewSelect().Model((*bunmodel.Tribe)(nil)).Where("server_key = ?", s.Key).Count(ctx)
if err != nil {
return fmt.Errorf("%s: numTribes: %w", s.Key, err)
}
_, err = db.NewUpdate().
Model((*bunmodel.Server)(nil)).
Returning("NULL").
Where("key = ?", s.Key).
Set("num_players = ?", numPlayers).
Set("num_inactive_players = ?", numPlayers-s.NumActivePlayers).
Set("num_tribes = ?", numTribes).
Set("num_inactive_tribes = ?", numTribes-s.NumActiveTribes).
Exec(ctx)
if err != nil {
return fmt.Errorf("%s: %w", s.Key, err)
}
}
return nil
}, func(_ context.Context, _ *bun.DB) error {
return nil
})
}

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

@ -47,7 +47,7 @@ func Logger(logger Slogger, opts ...Option) func(next http.Handler) http.Handler
slog.String("httpRequest.ip", cfg.ipExtractor(r)),
slog.Int("httpResponse.status", status),
slog.Int("httpResponse.bytes", ww.BytesWritten()),
//nolint:gomnd
//nolint:mnd
slog.Float64("httpResponse.duration", float64(end.Sub(start).Nanoseconds())/1000000.0), // in milliseconds
)
})

View File

@ -83,16 +83,21 @@ func NewServer(tb TestingTB, opts ...func(cfg *ServerConfig)) domain.Server {
cfg.URL.String(),
cfg.Open,
cfg.Special,
0,
0,
0,
0,
0,
0,
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
NewServerConfig(tb),
NewBuildingInfo(tb),
NewUnitInfo(tb),
time.Now(),
time.Now(),
cfg.PlayerDataSyncedAt,
cfg.PlayerSnapshotsCreatedAt,
cfg.TribeDataSyncedAt,

View File

@ -0,0 +1,137 @@
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
}
type ServerSnapshotWithRelationsConfig struct {
ServerSnapshotOptions []func(cfg *ServerSnapshotConfig)
ServerOptions []func(cfg *ServerConfig)
}
func NewServerSnapshotWithRelations(
tb TestingTB,
opts ...func(cfg *ServerSnapshotWithRelationsConfig),
) domain.ServerSnapshotWithRelations {
tb.Helper()
cfg := &ServerSnapshotWithRelationsConfig{}
for _, opt := range opts {
opt(cfg)
}
ss := NewServerSnapshot(tb, cfg.ServerSnapshotOptions...)
if ss.ServerKey() != "" {
cfg.ServerOptions = append([]func(cfg *ServerConfig){
func(cfg *ServerConfig) {
cfg.Key = ss.ServerKey()
},
}, cfg.ServerOptions...)
}
var tribe domain.ServerMeta
if len(cfg.ServerOptions) > 0 {
tribe = NewServer(tb, cfg.ServerOptions...).Meta()
}
return ss.WithRelations(tribe)
}

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

@ -6,6 +6,10 @@ import (
"github.com/stretchr/testify/require"
)
func RandVersionCode() string {
return gofakeit.LetterN(2)
}
type VersionCursorConfig struct {
Code string
}
@ -52,7 +56,3 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
return v
}
func RandVersionCode() string {
return gofakeit.LetterN(2)
}

View File

@ -109,10 +109,11 @@ func (p ServerSyncedEventPayload) VersionCode() string {
}
type TribesSyncedEventPayload struct {
serverKey string
serverURL *url.URL
versionCode string
numTribes int
serverKey string
serverURL *url.URL
versionCode string
numTribes int
numActiveTribes int
}
const tribesSyncedEventPayloadModelName = "TribesSyncedEventPayload"
@ -122,6 +123,7 @@ func NewTribesSyncedEventPayload(
serverURL *url.URL,
versionCode string,
numTribes int,
numActiveTribes int,
) (TribesSyncedEventPayload, error) {
if serverKey == "" {
return TribesSyncedEventPayload{}, ValidationError{
@ -155,11 +157,20 @@ func NewTribesSyncedEventPayload(
}
}
if err := validateIntInRange(numActiveTribes, 0, math.MaxInt); err != nil {
return TribesSyncedEventPayload{}, ValidationError{
Model: tribesSyncedEventPayloadModelName,
Field: "numActiveTribes",
Err: err,
}
}
return TribesSyncedEventPayload{
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numTribes: numTribes,
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numTribes: numTribes,
numActiveTribes: numActiveTribes,
}, nil
}
@ -179,11 +190,20 @@ func (p TribesSyncedEventPayload) NumTribes() int {
return p.numTribes
}
func (p TribesSyncedEventPayload) NumActiveTribes() int {
return p.numActiveTribes
}
func (p TribesSyncedEventPayload) NumInactiveTribes() int {
return p.numTribes - p.numActiveTribes
}
type PlayersSyncedEventPayload struct {
serverKey string
serverURL *url.URL
versionCode string
numPlayers int
serverKey string
serverURL *url.URL
versionCode string
numPlayers int
numActivePlayers int
}
const playersSyncedEventPayloadModelName = "PlayersSyncedEventPayload"
@ -193,6 +213,7 @@ func NewPlayersSyncedEventPayload(
serverURL *url.URL,
versionCode string,
numPlayers int,
numActivePlayers int,
) (PlayersSyncedEventPayload, error) {
if serverKey == "" {
return PlayersSyncedEventPayload{}, ValidationError{
@ -226,11 +247,20 @@ func NewPlayersSyncedEventPayload(
}
}
if err := validateIntInRange(numActivePlayers, 0, math.MaxInt); err != nil {
return PlayersSyncedEventPayload{}, ValidationError{
Model: playersSyncedEventPayloadModelName,
Field: "numActivePlayers",
Err: err,
}
}
return PlayersSyncedEventPayload{
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numPlayers: numPlayers,
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
}, nil
}
@ -250,6 +280,14 @@ func (p PlayersSyncedEventPayload) NumPlayers() int {
return p.numPlayers
}
func (p PlayersSyncedEventPayload) NumActivePlayers() int {
return p.numActivePlayers
}
func (p PlayersSyncedEventPayload) NumInactivePlayers() int {
return p.numPlayers - p.numActivePlayers
}
type VillagesSyncedEventPayload struct {
serverKey string
serverURL *url.URL

View File

@ -59,18 +59,22 @@ func TestNewTribesSyncedEventPayload(t *testing.T) {
server := domaintest.NewServer(t)
numTribes := gofakeit.IntRange(0, math.MaxInt)
numActiveTribes := gofakeit.IntRange(0, math.MaxInt)
payload, err := domain.NewTribesSyncedEventPayload(
server.Key(),
server.URL(),
server.VersionCode(),
numTribes,
numActiveTribes,
)
require.NoError(t, err)
assert.Equal(t, server.Key(), payload.ServerKey())
assert.Equal(t, server.URL(), payload.ServerURL())
assert.Equal(t, server.VersionCode(), payload.VersionCode())
assert.Equal(t, numTribes, payload.NumTribes())
assert.Equal(t, numActiveTribes, payload.NumActiveTribes())
assert.Equal(t, numTribes-numActiveTribes, payload.NumInactiveTribes())
}
func TestNewPlayersSyncedEventPayload(t *testing.T) {
@ -78,18 +82,22 @@ func TestNewPlayersSyncedEventPayload(t *testing.T) {
server := domaintest.NewServer(t)
numPlayers := gofakeit.IntRange(0, math.MaxInt)
numActivePlayers := gofakeit.IntRange(0, math.MaxInt)
payload, err := domain.NewPlayersSyncedEventPayload(
server.Key(),
server.URL(),
server.VersionCode(),
numPlayers,
numActivePlayers,
)
require.NoError(t, err)
assert.Equal(t, server.Key(), payload.ServerKey())
assert.Equal(t, server.URL(), payload.ServerURL())
assert.Equal(t, server.VersionCode(), payload.VersionCode())
assert.Equal(t, numPlayers, payload.NumPlayers())
assert.Equal(t, numActivePlayers, payload.NumActivePlayers())
assert.Equal(t, numPlayers-numActivePlayers, payload.NumInactivePlayers())
}
func TestNewVillagesSyncedEventPayload(t *testing.T) {

View File

@ -1180,6 +1180,37 @@ func (res ListPlayersWithRelationsResult) Next() PlayerCursor {
return res.next
}
type CountPlayersParams struct {
serverKeys []string
}
const countPlayersParamsModelName = "CountPlayersParams"
func NewCountPlayersParams() CountPlayersParams {
return CountPlayersParams{}
}
func (params *CountPlayersParams) ServerKeys() []string {
return params.serverKeys
}
func (params *CountPlayersParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: countPlayersParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
type PlayerNotFoundError struct {
ID int
ServerKey string

View File

@ -1614,3 +1614,57 @@ func TestNewListPlayersWithRelationsResult(t *testing.T) {
assert.True(t, res.Next().IsZero())
})
}
func TestCountPlayersParams_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: "CountPlayersParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewCountPlayersParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}

View File

@ -15,7 +15,11 @@ type Server struct {
open bool
special bool
numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
@ -24,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
@ -45,7 +50,11 @@ func UnmarshalServerFromDatabase(
open bool,
special bool,
numPlayers int,
numActivePlayers int,
numInactivePlayers int,
numTribes int,
numActiveTribes int,
numInactiveTribes int,
numVillages int,
numPlayerVillages int,
numBarbarianVillages int,
@ -54,6 +63,7 @@ func UnmarshalServerFromDatabase(
buildingInfo BuildingInfo,
unitInfo UnitInfo,
createdAt time.Time,
snapshotCreatedAt time.Time,
playerDataSyncedAt time.Time,
playerSnapshotsCreatedAt time.Time,
tribeDataSyncedAt time.Time,
@ -88,11 +98,16 @@ func UnmarshalServerFromDatabase(
return Server{
key: key,
versionCode: versionCode,
url: u,
open: open,
special: special,
numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
numInactivePlayers: numInactivePlayers,
numTribes: numTribes,
numActiveTribes: numActiveTribes,
numInactiveTribes: numInactiveTribes,
numVillages: numVillages,
numPlayerVillages: numPlayerVillages,
numBarbarianVillages: numBarbarianVillages,
@ -101,13 +116,13 @@ func UnmarshalServerFromDatabase(
buildingInfo: buildingInfo,
unitInfo: unitInfo,
createdAt: createdAt,
snapshotCreatedAt: snapshotCreatedAt,
playerDataSyncedAt: playerDataSyncedAt,
playerSnapshotsCreatedAt: playerSnapshotsCreatedAt,
tribeDataSyncedAt: tribeDataSyncedAt,
tribeSnapshotsCreatedAt: tribeSnapshotsCreatedAt,
villageDataSyncedAt: villageDataSyncedAt,
ennoblementDataSyncedAt: ennoblementDataSyncedAt,
versionCode: versionCode,
}, nil
}
@ -135,10 +150,26 @@ func (s Server) NumPlayers() int {
return s.numPlayers
}
func (s Server) NumActivePlayers() int {
return s.numActivePlayers
}
func (s Server) NumInactivePlayers() int {
return s.numInactivePlayers
}
func (s Server) NumTribes() int {
return s.numTribes
}
func (s Server) NumActiveTribes() int {
return s.numActiveTribes
}
func (s Server) NumInactiveTribes() int {
return s.numInactiveTribes
}
func (s Server) NumVillages() int {
return s.numVillages
}
@ -171,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
}
@ -378,8 +413,12 @@ type UpdateServerParams struct {
buildingInfo NullBuildingInfo
unitInfo NullUnitInfo
numTribes NullInt
numActiveTribes NullInt
numInactiveTribes NullInt
tribeDataSyncedAt NullTime
numPlayers NullInt
numActivePlayers NullInt
numInactivePlayers NullInt
playerDataSyncedAt NullTime
numVillages NullInt
numPlayerVillages NullInt
@ -387,6 +426,7 @@ type UpdateServerParams struct {
numBonusVillages NullInt
villageDataSyncedAt NullTime
ennoblementDataSyncedAt NullTime
snapshotCreatedAt NullTime
tribeSnapshotsCreatedAt NullTime
playerSnapshotsCreatedAt NullTime
}
@ -424,9 +464,9 @@ func (params *UpdateServerParams) NumTribes() NullInt {
return params.numTribes
}
func (params *UpdateServerParams) SetNumTribes(numTribes NullInt) error {
if numTribes.Valid {
if err := validateIntInRange(numTribes.V, 0, math.MaxInt); err != nil {
func (params *UpdateServerParams) SetNumTribes(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numTribes",
@ -435,7 +475,47 @@ func (params *UpdateServerParams) SetNumTribes(numTribes NullInt) error {
}
}
params.numTribes = numTribes
params.numTribes = num
return nil
}
func (params *UpdateServerParams) NumActiveTribes() NullInt {
return params.numActiveTribes
}
func (params *UpdateServerParams) SetNumActiveTribes(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActiveTribes",
Err: err,
}
}
}
params.numActiveTribes = num
return nil
}
func (params *UpdateServerParams) NumInactiveTribes() NullInt {
return params.numInactiveTribes
}
func (params *UpdateServerParams) SetNumInactiveTribes(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactiveTribes",
Err: err,
}
}
}
params.numInactiveTribes = num
return nil
}
@ -453,9 +533,9 @@ func (params *UpdateServerParams) NumPlayers() NullInt {
return params.numPlayers
}
func (params *UpdateServerParams) SetNumPlayers(numPlayers NullInt) error {
if numPlayers.Valid {
if err := validateIntInRange(numPlayers.V, 0, math.MaxInt); err != nil {
func (params *UpdateServerParams) SetNumPlayers(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numPlayers",
@ -464,7 +544,47 @@ func (params *UpdateServerParams) SetNumPlayers(numPlayers NullInt) error {
}
}
params.numPlayers = numPlayers
params.numPlayers = num
return nil
}
func (params *UpdateServerParams) NumActivePlayers() NullInt {
return params.numActivePlayers
}
func (params *UpdateServerParams) SetNumActivePlayers(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActivePlayers",
Err: err,
}
}
}
params.numActivePlayers = num
return nil
}
func (params *UpdateServerParams) NumInactivePlayers() NullInt {
return params.numInactivePlayers
}
func (params *UpdateServerParams) SetNumInactivePlayers(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactivePlayers",
Err: err,
}
}
}
params.numInactivePlayers = num
return nil
}
@ -576,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
}
@ -599,9 +728,9 @@ func (params *UpdateServerParams) IsZero() bool {
return !params.config.Valid &&
!params.buildingInfo.Valid &&
!params.unitInfo.Valid &&
!params.numTribes.Valid &&
!params.numActiveTribes.Valid &&
!params.tribeDataSyncedAt.Valid &&
!params.numPlayers.Valid &&
!params.numActivePlayers.Valid &&
!params.playerDataSyncedAt.Valid &&
!params.numVillages.Valid &&
!params.numPlayerVillages.Valid &&
@ -609,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
}
@ -713,6 +843,7 @@ type ListServersParams struct {
versionCodes []string
open NullBool
special NullBool
snapshotCreatedAtLT NullTime
tribeSnapshotsCreatedAtLT NullTime
playerSnapshotsCreatedAtLT NullTime
sort []ServerSort
@ -796,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,658 @@
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) WithRelations(server ServerMeta) ServerSnapshotWithRelations {
return ServerSnapshotWithRelations{
snapshot: ss,
server: server,
}
}
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 ServerSnapshotWithRelations struct {
snapshot ServerSnapshot
server ServerMeta
}
func (ts ServerSnapshotWithRelations) ServerSnapshot() ServerSnapshot {
return ts.snapshot
}
func (ts ServerSnapshotWithRelations) Server() ServerMeta {
return ts.server
}
func (ts ServerSnapshotWithRelations) IsZero() bool {
return ts.snapshot.IsZero()
}
type ServerSnapshotsWithRelations []ServerSnapshotWithRelations
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.NumInactiveTribes(),
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
}
type ListServerSnapshotsWithRelationsResult struct {
snapshots ServerSnapshotsWithRelations
self ServerSnapshotCursor
next ServerSnapshotCursor
}
const listServerSnapshotsWithRelationsResultModelName = "ListServerSnapshotsWithRelationsResult"
func NewListServerSnapshotsWithRelationsResult(
snapshots ServerSnapshotsWithRelations,
next ServerSnapshotWithRelations,
) (ListServerSnapshotsWithRelationsResult, error) {
var err error
res := ListServerSnapshotsWithRelationsResult{
snapshots: snapshots,
}
if len(snapshots) > 0 {
res.self, err = snapshots[0].ServerSnapshot().ToCursor()
if err != nil {
return ListServerSnapshotsWithRelationsResult{}, ValidationError{
Model: listServerSnapshotsWithRelationsResultModelName,
Field: "self",
Err: err,
}
}
}
if !next.IsZero() {
res.next, err = next.ServerSnapshot().ToCursor()
if err != nil {
return ListServerSnapshotsWithRelationsResult{}, ValidationError{
Model: listServerSnapshotsWithRelationsResultModelName,
Field: "next",
Err: err,
}
}
}
return res, nil
}
func (res ListServerSnapshotsWithRelationsResult) ServerSnapshots() ServerSnapshotsWithRelations {
return res.snapshots
}
func (res ListServerSnapshotsWithRelationsResult) Self() ServerSnapshotCursor {
return res.self
}
func (res ListServerSnapshotsWithRelationsResult) Next() ServerSnapshotCursor {
return res.next
}

View File

@ -0,0 +1,901 @@
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())
})
}
func TestNewListServerSnapshotsWithRelationsResult(t *testing.T) {
t.Parallel()
snapshots := domain.ServerSnapshotsWithRelations{
domaintest.NewServerSnapshotWithRelations(t),
domaintest.NewServerSnapshotWithRelations(t),
domaintest.NewServerSnapshotWithRelations(t),
}
next := domaintest.NewServerSnapshotWithRelations(t)
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(snapshots, next)
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ServerSnapshot().ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerSnapshot().ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].ServerSnapshot().Date(), res.Self().Date())
assert.Equal(t, next.ServerSnapshot().ID(), res.Next().ID())
assert.Equal(t, next.ServerSnapshot().ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.ServerSnapshot().Date(), res.Next().Date())
})
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(snapshots, domain.ServerSnapshotWithRelations{})
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ServerSnapshot().ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerSnapshot().ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].ServerSnapshot().Date(), res.Self().Date())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 snapshots", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(nil, domain.ServerSnapshotWithRelations{})
require.NoError(t, err)
assert.Zero(t, res.ServerSnapshots())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -224,6 +224,132 @@ func TestUpdateServerParams_SetNumTribes(t *testing.T) {
}
}
func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) {
t.Parallel()
type args struct {
numActiveTribes domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numActiveTribes: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numActiveTribes: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numActiveTribes < 0",
args: args{
numActiveTribes: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActiveTribes",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumActiveTribes(tt.args.numActiveTribes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numActiveTribes, params.NumActiveTribes())
})
}
}
func TestUpdateServerParams_SetNumInactiveTribes(t *testing.T) {
t.Parallel()
type args struct {
numInactiveTribes domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numInactiveTribes: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numInactiveTribes: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numInactiveTribes < 0",
args: args{
numInactiveTribes: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numInactiveTribes",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumInactiveTribes(tt.args.numInactiveTribes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numInactiveTribes, params.NumInactiveTribes())
})
}
}
func TestUpdateServerParams_SetNumPlayers(t *testing.T) {
t.Parallel()
@ -287,6 +413,132 @@ func TestUpdateServerParams_SetNumPlayers(t *testing.T) {
}
}
func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) {
t.Parallel()
type args struct {
numActivePlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numActivePlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numActivePlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numActivePlayers < 0",
args: args{
numActivePlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActivePlayers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumActivePlayers(tt.args.numActivePlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numActivePlayers, params.NumActivePlayers())
})
}
}
func TestUpdateServerParams_SetNumInactivePlayers(t *testing.T) {
t.Parallel()
type args struct {
numInactivePlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numInactivePlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numInactivePlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numInactivePlayers < 0",
args: args{
numInactivePlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numInactivePlayers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumInactivePlayers(tt.args.numInactivePlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numInactivePlayers, params.NumInactivePlayers())
})
}
}
func TestUpdateServerParams_SetNumVillages(t *testing.T) {
t.Parallel()

View File

@ -969,6 +969,37 @@ func (res ListTribesResult) Next() TribeCursor {
return res.next
}
type CountTribesParams struct {
serverKeys []string
}
const countTribesParamsModelName = "CountTribesParams"
func NewCountTribesParams() CountTribesParams {
return CountTribesParams{}
}
func (params *CountTribesParams) ServerKeys() []string {
return params.serverKeys
}
func (params *CountTribesParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: countTribesParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
type TribeNotFoundError struct {
ID int
ServerKey string

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

@ -1205,3 +1205,57 @@ func TestNewListTribesResult(t *testing.T) {
assert.True(t, res.Next().IsZero())
})
}
func TestCountTribesParams_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: "CountTribesParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewCountTribesParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}

View File

@ -12,7 +12,7 @@ type Version struct {
timezone string
}
var versionModelName = "Version"
const versionModelName = "Version"
// UnmarshalVersionFromDatabase unmarshals Version from the database.
//

View File

@ -158,10 +158,12 @@ func TestDataSync(t *testing.T) {
ctx,
port.NewServerWatermillConsumer(
serverSvc,
nil,
serverSub,
nopLogger,
marshaler,
serverCmdSync,
"",
serverEventSynced,
tribeEventSynced,
playerEventSynced,
@ -249,7 +251,11 @@ func TestDataSync(t *testing.T) {
assert.Equal(collect, expected["Open"], actual.Open(), msg)
assert.Equal(collect, expected["VersionCode"], actual.VersionCode(), msg)
assert.EqualValues(collect, expected["NumPlayers"], actual.NumPlayers(), msg)
assert.EqualValues(collect, expected["NumActivePlayers"], actual.NumActivePlayers(), msg)
assert.EqualValues(collect, expected["NumInactivePlayers"], actual.NumInactivePlayers(), msg)
assert.EqualValues(collect, expected["NumTribes"], actual.NumTribes(), msg)
assert.EqualValues(collect, expected["NumActiveTribes"], actual.NumActiveTribes(), msg)
assert.EqualValues(collect, expected["NumInactiveTribes"], actual.NumInactiveTribes(), msg)
assert.EqualValues(collect, expected["NumVillages"], actual.NumVillages(), msg)
assert.EqualValues(collect, expected["NumPlayerVillages"], actual.NumPlayerVillages(), msg)
assert.EqualValues(collect, expected["NumBonusVillages"], actual.NumBonusVillages(), msg)
@ -257,8 +263,7 @@ func TestDataSync(t *testing.T) {
collect,
expected["NumBarbarianVillages"],
actual.NumBarbarianVillages(),
"Key=%s",
expected["Key"],
msg,
)
assert.WithinDuration(collect, time.Now(), actual.PlayerDataSyncedAt(), time.Minute, msg)
assert.WithinDuration(collect, time.Now(), actual.TribeDataSyncedAt(), time.Minute, msg)
@ -267,8 +272,7 @@ func TestDataSync(t *testing.T) {
collect,
string(marshalJSON(collect, expected["Config"])),
string(marshalJSON(collect, serverConfigToMap(actual.Config()))),
"Key=%s",
expected["Key"],
msg,
)
assert.JSONEq(
collect,

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
@ -156,6 +194,7 @@ func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error {
rawPayload.ServerURL,
rawPayload.VersionCode,
rawPayload.NumTribes,
rawPayload.NumActiveTribes,
)
if err != nil {
c.logger.Error("couldn't construct domain.TribesSyncedEventPayload", err, watermill.LogFields{
@ -182,6 +221,7 @@ func (c *ServerWatermillConsumer) updateNumPlayers(msg *message.Message) error {
rawPayload.ServerURL,
rawPayload.VersionCode,
rawPayload.NumPlayers,
rawPayload.NumActivePlayers,
)
if err != nil {
c.logger.Error("couldn't construct domain.PlayersSyncedEventPayload", err, watermill.LogFields{

View File

@ -20,6 +20,7 @@ type apiHTTPHandler struct {
villageSvc *app.VillageService
ennoblementSvc *app.EnnoblementService
tribeChangeSvc *app.TribeChangeService
serverSnapshotSvc *app.ServerSnapshotService
tribeSnapshotSvc *app.TribeSnapshotService
playerSnapshotSvc *app.PlayerSnapshotService
errorRenderer apiErrorRenderer
@ -40,6 +41,7 @@ func NewAPIHTTPHandler(
villageSvc *app.VillageService,
ennoblementSvc *app.EnnoblementService,
tribeChangeSvc *app.TribeChangeService,
serverSnapshotSvc *app.ServerSnapshotService,
tribeSnapshotSvc *app.TribeSnapshotService,
playerSnapshotSvc *app.PlayerSnapshotService,
opts ...APIHTTPHandlerOption,
@ -54,6 +56,7 @@ func NewAPIHTTPHandler(
villageSvc: villageSvc,
ennoblementSvc: ennoblementSvc,
tribeChangeSvc: tribeChangeSvc,
serverSnapshotSvc: serverSnapshotSvc,
tribeSnapshotSvc: tribeSnapshotSvc,
playerSnapshotSvc: playerSnapshotSvc,
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {

View File

@ -148,7 +148,6 @@ func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiErro
var err error = domainErr
for {
var withPath domain.ErrorWithPath
if !errors.As(err, &withPath) {
break
}

View File

@ -0,0 +1,96 @@
package port
import (
"net/http"
"strconv"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
)
const apiServerSnapshotSortMaxLength = 1
var apiServerSnapshotSortAllowedValues = []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
}
//nolint:gocyclo
func (h *apiHTTPHandler) ListServerServerSnapshots(
w http.ResponseWriter,
r *http.Request,
_ apimodel.VersionCodePathParam,
serverKey apimodel.ServerKeyPathParam,
params apimodel.ListServerServerSnapshotsParams,
) {
domainParams := domain.NewListServerSnapshotsParams()
if err := domainParams.SetSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(
*params.Sort,
apiServerSnapshotSortAllowedValues,
apiServerSnapshotSortMaxLength,
); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
} else {
if err := domainParams.PrependSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortDateASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
if params.Cursor != nil {
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
res, err := h.serverSnapshotSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
h.errorRenderer.render(w, r, err)
return
}
renderJSON(w, r, http.StatusOK, apimodel.NewListServerSnapshotsResponse(res))
}
func formatListServerSnapshotsErrorPath(segments []domain.ErrorPathSegment) []string {
if segments[0].Model != "ListServerSnapshotsParams" {
return nil
}
switch segments[0].Field {
case "cursor":
return []string{"$query", "cursor"}
case "limit":
return []string{"$query", "limit"}
case "sort":
path := []string{"$query", "sort"}
if segments[0].Index >= 0 {
path = append(path, strconv.Itoa(segments[0].Index))
}
return path
default:
return nil
}
}

View File

@ -0,0 +1,542 @@
package port_test
import (
"cmp"
"fmt"
"net/http"
"net/http/httptest"
"slices"
"strconv"
"strings"
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const endpointListServerServerSnapshots = "/v2/versions/%s/servers/%s/snapshots"
func TestListServerServerSnapshots(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
sss := getAllServerSnapshots(t, handler)
var server serverWithVersion
sssGroupedByServerKey := make(map[string][]serverSnapshotWithServer)
for _, ss := range sss {
key := ss.Server.Key
sssGroupedByServerKey[key] = append(sssGroupedByServerKey[key], ss)
}
currentMax := -1
for _, grouped := range sssGroupedByServerKey {
if l := len(grouped); l > currentMax && l > 0 {
currentMax = l
server = grouped[0].Server
}
}
tests := []struct {
name string
reqModifier func(t *testing.T, req *http.Request)
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
}{
{
name: "OK: without params",
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.ServerSnapshot) int {
return cmp.Or(
a.Date.Compare(b.Date.Time),
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: limit=1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: limit=1 cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
resp := doCustomRequest(handler, req.Clone(req.Context()))
defer resp.Body.Close()
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
require.NotEmpty(t, body.Cursor.Next)
q.Set("cursor", body.Cursor.Next)
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: sort=[date:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "date:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.ServerSnapshot) int {
return cmp.Or(
a.Date.Compare(b.Date.Time)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "ERR: limit is not an integer",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "asd")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
Message: fmt.Sprintf(
"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
req.URL.Query().Get("limit"),
),
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: limit < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "0")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
Min: 1,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.ServerSnapshotListMaxLimit),
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", strconv.Itoa(domain.ServerSnapshotListMaxLimit+1))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
Max: domain.ServerSnapshotListMaxLimit,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", "")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) > 1000",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(1001))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: invalid cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(sort) > 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "date:DESC")
q.Add("sort", "date:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: len(req.URL.Query()["sort"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid sort",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "date:")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.UnsupportedSortStringError{
Sort: req.URL.Query()["sort"][0],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"sort": domainErr.Sort,
},
Path: []string{"$query", "sort", "0"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(
endpointListServerServerSnapshots,
randInvalidVersionCode(t, handler),
server.Key,
)
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 7)
domainErr := domain.VersionNotFoundError{
VersionCode: pathSegments[3],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"code": domainErr.VersionCode,
},
},
},
}, body)
},
},
{
name: "ERR: server not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(
endpointListServerServerSnapshots,
server.Version.Code,
domaintest.RandServerKey(),
)
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 7)
domainErr := domain.ServerNotFoundError{
Key: pathSegments[5],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"key": domainErr.Key,
},
},
},
}, body)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf(endpointListServerServerSnapshots, server.Version.Code, server.Key),
nil,
)
if tt.reqModifier != nil {
tt.reqModifier(t, req)
}
resp := doCustomRequest(handler, req)
defer resp.Body.Close()
tt.assertResp(t, req, resp)
})
}
}
type serverSnapshotWithServer struct {
apimodel.ServerSnapshot
Server serverWithVersion
}
func getAllServerSnapshots(tb testing.TB, h http.Handler) []serverSnapshotWithServer {
tb.Helper()
servers := getAllServers(tb, h)
var sss []serverSnapshotWithServer
for _, s := range servers {
resp := doRequest(h, http.MethodGet, fmt.Sprintf(
endpointListServerServerSnapshots,
s.Version.Code,
s.Key,
), nil)
require.Equal(tb, http.StatusOK, resp.StatusCode)
for _, ts := range decodeJSON[apimodel.ListServerSnapshotsResponse](tb, resp.Body).Data {
sss = append(sss, serverSnapshotWithServer{
ServerSnapshot: ts,
Server: s,
})
}
_ = resp.Body.Close()
}
require.NotZero(tb, sss)
return sss
}

View File

@ -40,6 +40,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(bunDB)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
@ -51,6 +52,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
app.NewVillageService(villageRepo, nil, nil),
app.NewEnnoblementService(ennoblementRepo, nil, nil),
app.NewTribeChangeService(tribeChangeRepo),
app.NewServerSnapshotService(serverSnapshotRepo, nil, nil),
app.NewTribeSnapshotService(tribeSnapshotRepo, nil, nil),
app.NewPlayerSnapshotService(playerSnapshotRepo, nil, nil),
cfg.options...,

View File

@ -6,10 +6,14 @@ import (
func NewServer(s domain.Server) Server {
converted := Server{
Key: s.Key(),
CreatedAt: s.CreatedAt(),
Key: s.Key(),
NumActivePlayers: s.NumActivePlayers(),
NumActiveTribes: s.NumActiveTribes(),
NumBarbarianVillages: s.NumBarbarianVillages(),
NumBonusVillages: s.NumBonusVillages(),
NumInactivePlayers: s.NumInactivePlayers(),
NumInactiveTribes: s.NumInactiveTribes(),
NumPlayerVillages: s.NumPlayerVillages(),
NumPlayers: s.NumPlayers(),
NumTribes: s.NumTribes(),

View File

@ -0,0 +1,43 @@
package apimodel
import (
"gitea.dwysokinski.me/twhelp/core/internal/domain"
oapitypes "github.com/oapi-codegen/runtime/types"
)
func NewServerSnapshot(withRelations domain.ServerSnapshotWithRelations) ServerSnapshot {
ss := withRelations.ServerSnapshot()
return ServerSnapshot{
Date: oapitypes.Date{Time: ss.Date()},
Id: ss.ID(),
NumActivePlayers: ss.NumActivePlayers(),
NumActiveTribes: ss.NumActiveTribes(),
NumBarbarianVillages: ss.NumBarbarianVillages(),
NumBonusVillages: ss.NumBonusVillages(),
NumInactivePlayers: ss.NumInactivePlayers(),
NumInactiveTribes: ss.NumInactiveTribes(),
NumPlayerVillages: ss.NumPlayerVillages(),
NumPlayers: ss.NumPlayers(),
NumTribes: ss.NumTribes(),
NumVillages: ss.NumVillages(),
Server: NewServerMeta(withRelations.Server()),
}
}
func NewListServerSnapshotsResponse(res domain.ListServerSnapshotsWithRelationsResult) ListServerSnapshotsResponse {
sss := res.ServerSnapshots()
resp := ListServerSnapshotsResponse{
Data: make([]ServerSnapshot, 0, len(sss)),
Cursor: Cursor{
Next: res.Next().Encode(),
Self: res.Self().Encode(),
},
}
for _, ss := range sss {
resp.Data = append(resp.Data, NewServerSnapshot(ss))
}
return resp
}

View File

@ -5,95 +5,95 @@
url: https://de180.die-staemme.de
open: false
special: false
num_players: 313
num_tribes: 93
num_active_players: 313
num_active_tribes: 93
num_villages: 47802
num_player_villages: 47759
num_barbarian_villages: 44
num_bonus_villages: 0
created_at: 2022-01-19T12:00:54.000Z
player_data_updated_at: 2022-01-19T12:00:54.000Z
player_data_synced_at: 2022-01-19T12:00:54.000Z
player_snapshots_created_at: 2022-01-19T12:00:54.000Z
tribe_data_updated_at: 2022-01-19T12:00:54.000Z
tribe_data_synced_at: 2022-01-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-01-19T12:00:54.000Z
village_data_updated_at: 2022-01-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-01-19T12:00:54.000Z
village_data_synced_at: 2022-01-19T12:00:54.000Z
ennoblement_data_synced_at: 2022-01-19T12:00:54.000Z
version_code: de
- _id: de188
key: de188
url: https://de188.die-staemme.de
open: true
special: false
num_players: 180
num_tribes: 76
num_active_players: 180
num_active_tribes: 76
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z
player_data_updated_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_updated_at: 2022-03-19T12:00:54.000Z
tribe_data_synced_at: 2022-03-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z
village_data_updated_at: 2022-03-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:54.000Z
village_data_synced_at: 2022-03-19T12:00:54.000Z
ennoblement_data_synced_at: 2022-03-19T12:00:54.000Z
version_code: de
- _id: en113
key: en113
url: https://en113.tribalwars.net
open: false
special: false
num_players: 251
num_tribes: 57
num_active_players: 251
num_active_tribes: 57
num_villages: 41700
num_player_villages: 41000
num_barbarian_villages: 700
num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z
player_data_updated_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_updated_at: 2021-04-02T16:01:25.000Z
tribe_data_synced_at: 2021-04-02T16:01:25.000Z
tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z
village_data_updated_at: 2021-04-02T16:01:25.000Z
ennoblement_data_updated_at: 2021-04-02T16:01:25.000Z
village_data_synced_at: 2021-04-02T16:01:25.000Z
ennoblement_data_synced_at: 2021-04-02T16:01:25.000Z
version_code: en
- _id: it70
key: it70
url: https://it70.tribals.it
open: true
special: false
num_players: 1883
num_tribes: 101
num_active_players: 1883
num_active_tribes: 101
num_villages: 4882
num_player_villages: 3200
num_barbarian_villages: 1682
num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z
player_data_updated_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_updated_at: 2022-03-19T12:00:04.000Z
tribe_data_synced_at: 2022-03-19T12:00:04.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z
village_data_updated_at: 2022-03-19T12:00:04.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:04.000Z
village_data_synced_at: 2022-03-19T12:00:04.000Z
ennoblement_data_synced_at: 2022-03-19T12:00:04.000Z
version_code: it
- _id: pl169
key: pl169
url: https://pl169.plemiona.pl
open: true
special: false
num_players: 2001
num_tribes: 214
num_active_players: 2001
num_active_tribes: 214
num_villages: 49074
num_player_villages: 48500
num_barbarian_villages: 1574
num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z
player_data_updated_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_updated_at: 2022-03-19T12:01:39.000Z
tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z
village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl
- model: Tribe
rows:
@ -7393,6 +7393,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

@ -4,8 +4,8 @@
url: https://pl181.plemiona.pl/
open: false
special: false
num_players: 144
num_tribes: 26
num_active_players: 144
num_active_tribes: 26
num_villages: 59581
num_player_villages: 58558
num_barbarian_villages: 1023
@ -16,8 +16,8 @@
url: https://pl182.plemiona.pl/
open: true
special: false
num_players: 144
num_tribes: 26
num_active_players: 144
num_active_tribes: 26
num_villages: 39702
num_player_villages: 39697
num_barbarian_villages: 5
@ -28,20 +28,20 @@
url: https://pl185.plemiona.pl/
open: false
special: false
num_players: 458
num_tribes: 88
num_active_players: 458
num_active_tribes: 88
num_villages: 84069
num_player_villages: 83057
num_barbarian_villages: 1012
num_bonus_villages: 13379
created_at:
version_code: pl
player_data_updated_at: "{{ now }}"
player_data_synced_at: "{{ now }}"
player_snapshots_created_at: "{{ now }}"
tribe_data_updated_at: "{{ now }}"
tribe_data_synced_at: "{{ now }}"
tribe_snapshots_created_at: "{{ now }}"
village_data_updated_at: "{{ now }}"
ennoblement_data_updated_at: "{{ now }}"
village_data_synced_at: "{{ now }}"
ennoblement_data_synced_at: "{{ now }}"
- model: Ennoblement
rows:
# pl181

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
url: https://us63.tribalwars.us
open: true
special: false
num_players: 1438
num_tribes: 125
num_active_players: 1438
num_active_tribes: 125
num_villages: 10048
num_player_villages: 5386
num_barbarian_villages: 4662

View File

@ -5,19 +5,23 @@
url: https://pl169.plemiona.pl
open: true
special: false
num_players: 2001
num_tribes: 214
num_players: 10000
num_active_players: 2001
num_inactive_players: 7999
num_tribes: 500
num_active_tribes: 214
num_inactive_tribes: 286
num_villages: 49074
num_player_villages: 48500
num_barbarian_villages: 1574
num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z
player_data_updated_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_updated_at: 2022-03-19T12:01:39.000Z
tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z
village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl
- model: Tribe
rows:

View File

@ -3,8 +3,9 @@ package watermillmsg
import "net/url"
type PlayersSyncedEventPayload struct {
ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"`
NumPlayers int `json:"numPlayers"`
ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"`
NumPlayers int `json:"numPlayers"`
NumActivePlayers int `json:"numActivePlayers"`
}

View File

@ -3,8 +3,9 @@ package watermillmsg
import "net/url"
type TribesSyncedEventPayload struct {
ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"`
NumTribes int `json:"numTribes"`
ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"`
NumTribes int `json:"numTribes"`
NumActiveTribes int `json:"numActiveTribes"`
}

View File

@ -39,8 +39,6 @@ spec:
key: rabbitmq-connection-string
- name: AUTO_MAX_PROCS
value: "true"
- name: API_OPENAPI_SERVERS
value: https://twhelp.app,https://tribalwarshelp.com
resources:
requests:
cpu: 20m

View File

@ -29,3 +29,5 @@ spec:
key: rabbitmq-connection-string
- name: AUTO_MAX_PROCS
value: "true"
- name: API_OPENAPI_SERVERS
value: https://twhelp.app,https://tribalwarshelp.com