Compare commits

...

15 Commits

Author SHA1 Message Date
36568adf46 feat: server map generator (#59)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/cron/govulncheck Pipeline was successful
Reviewed-on: #59
2024-06-21 05:03:08 +00:00
847f5d6b85 chore(deps): update module github.com/getkin/kin-openapi to v0.125.0 (#57)
Some checks failed
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/cron/govulncheck Pipeline failed
Reviewed-on: #57
Co-authored-by: Renovate <renovate@dwysokinski.me>
Co-committed-by: Renovate <renovate@dwysokinski.me>
2024-06-12 04:26:44 +00:00
86d78c0f0c feat: generic version of MinGreaterEqualError/MaxLessEqualError (#56)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Reviewed-on: #56
2024-06-10 13:52:59 +00:00
51a6270812 feat: tribe - sort by most points (#55)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/deployment Pipeline was successful
ci/woodpecker/cron/govulncheck Pipeline was successful
Reviewed-on: #55
2024-06-02 11:51:38 +00:00
4db3fee03f feat: player - sort by most points (#54)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Reviewed-on: #54
2024-06-02 11:33:50 +00:00
cf3eb9053d
chore: bump golangci-lint to v1.59 & alpine docker tag to v3.20
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2024-06-02 12:37:43 +02:00
9d5987f74c feat: new endpoint GET /api/v2/versions/{versionCode}/servers/{serverKey}/snapshots (#52)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/deployment Pipeline was successful
ci/woodpecker/cron/govulncheck Pipeline was successful
Reviewed-on: #52
2024-05-13 05:12:05 +00:00
4ca2ea9361
chore: update prod Dockerfile
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/deployment Pipeline was successful
2024-05-12 09:47:46 +02:00
6e2c54679e
feat: postgres - set application name
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/deployment Pipeline was successful
ci/woodpecker/cron/govulncheck Pipeline was successful
2024-05-11 17:30:06 +02:00
7de443c652
fix: server_snapshots - incorrect num_inactive_tribes
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2024-05-11 17:23:40 +02:00
c6d7ad8965
chore: update prod/Dockerfile
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/deployment Pipeline was successful
2024-05-10 07:04:36 +02:00
98361f3ae0 fix: ci/cd - test step timeouts (#51)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Reviewed-on: #51
2024-05-10 04:57:35 +00:00
08a7f0c504 feat: server snapshots (#49)
Some checks failed
ci/woodpecker/push/govulncheck Pipeline failed
ci/woodpecker/push/test Pipeline failed
Reviewed-on: #49
2024-05-09 07:49:04 +00:00
e3fa23d0c4 chore(deps): update module github.com/brianvoe/gofakeit/v7 to v7.0.3 (#48)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
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
f8c9bdb321 feat: add more stats to the Server model (#47)
All checks were successful
ci/woodpecker/push/govulncheck Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Reviewed-on: #47
2024-05-07 06:15:37 +00:00
116 changed files with 7887 additions and 1386 deletions

View File

@ -505,11 +505,14 @@ issues:
linters:
- gosec
- gocyclo
- path: server_map.go
linters:
- mnd
- path: _test\.go
text: add-constant
- path: bun/migrations
linters:
- init
- gochecknoinits
- linters:
- lll
source: "^//go:generate "

View File

@ -6,7 +6,7 @@ repos:
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
- repo: https://github.com/golangci/golangci-lint
rev: v1.58.0
rev: v1.59.0
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.58
image: golangci/golangci-lint:v1.59
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.58.0
@(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.59.0
.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
@ -583,8 +602,12 @@ components:
- key
- open
- url
- numPlayers
- numActivePlayers
- numInactivePlayers
- numTribes
- numActiveTribes
- numInactiveTribes
- numVillages
- numBarbarianVillages
- numBonusVillages
@ -599,13 +622,23 @@ components:
type: string
format: uri
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:
@ -1715,6 +1794,8 @@ components:
- odScoreTotal:DESC
- points:ASC
- points:DESC
- mostPoints:ASC
- mostPoints:DESC
- dominance:ASC
- dominance:DESC
- deletedAt:ASC
@ -1755,6 +1836,8 @@ components:
- odScoreTotal:DESC
- points:ASC
- points:DESC
- mostPoints:ASC
- mostPoints:DESC
- deletedAt:ASC
- deletedAt:DESC
maxItems: 2
@ -1826,6 +1909,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 +1988,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -1917,7 +2014,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -1976,7 +2073,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2002,7 +2099,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2017,7 +2114,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2043,7 +2140,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2069,7 +2166,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2084,7 +2181,7 @@ components:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
@ -2093,13 +2190,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 +2226,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.20
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

@ -1,33 +1,27 @@
package main
import (
"errors"
"fmt"
"go/ast"
"go/constant"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"log"
"os"
"slices"
"strings"
"golang.org/x/tools/go/packages"
"gopkg.in/yaml.v3"
)
func main() {
if _, err := os.Stat("./go.mod"); errors.Is(err, os.ErrNotExist) {
log.Fatalln("go.mod not found")
}
if err := generateYAML("./internal/domain"); err != nil {
if err := generateYAML("gitea.dwysokinski.me/twhelp/core/internal/domain"); err != nil {
log.Fatalln(err)
}
}
func generateYAML(root string) error {
m, err := getErrorCodesFromFolder(root)
func generateYAML(pkgPattern string) error {
m, err := getErrorCodesFromPackage(pkgPattern)
if err != nil {
return err
}
@ -48,6 +42,8 @@ func generateYAML(root string) error {
enum = append(enum, c)
}
slices.Sort(enum)
return yaml.NewEncoder(os.Stdout).Encode(struct {
Type string `yaml:"type"`
Description string `yaml:"description"`
@ -60,27 +56,25 @@ func generateYAML(root string) error {
}
//nolint:gocyclo
func getErrorCodesFromFolder(root string) (map[string]string, error) {
fset := token.NewFileSet()
astDomainPkg, err := parseDomainPackage(fset, root)
func getErrorCodesFromPackage(pkgPattern string) (map[string]string, error) {
pkg, err := packages.Load(&packages.Config{
Tests: false,
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedCompiledGoFiles |
packages.NeedTypes |
packages.NeedTypesInfo |
packages.NeedSyntax,
}, pkgPattern)
if err != nil {
return nil, err
}
conf := types.Config{Importer: importer.Default()}
files := make([]*ast.File, 0, len(astDomainPkg.Files))
for _, f := range astDomainPkg.Files {
files = append(files, f)
if len(pkg) == 0 {
return nil, fmt.Errorf("'%s': pkg not found", pkgPattern)
}
domainPkg, err := conf.Check(astDomainPkg.Name, fset, files, nil)
if err != nil {
return nil, err
}
scope := domainPkg.Scope()
domainPkg := pkg[0]
scope := domainPkg.Types.Scope()
m := make(map[string]string)
for _, n := range scope.Names() {
@ -95,14 +89,21 @@ func getErrorCodesFromFolder(root string) (map[string]string, error) {
continue
}
f := astDomainPkg.Files[fset.File(c.Pos()).Name()]
position := fset.Position(c.Pos())
commentMap := ast.NewCommentMap(fset, f, f.Comments)
filename := domainPkg.Fset.File(c.Pos()).Name()
fileIdx := slices.IndexFunc(domainPkg.GoFiles, func(f string) bool {
return f == filename
})
if fileIdx < 0 {
return nil, fmt.Errorf("'%s': file not found", filename)
}
f := domainPkg.Syntax[fileIdx]
position := domainPkg.Fset.Position(c.Pos())
commentMap := ast.NewCommentMap(domainPkg.Fset, f, f.Comments)
var desc string
for _, decl := range f.Decls {
declPosition := fset.Position(decl.Pos())
declPosition := domainPkg.Fset.Position(decl.Pos())
if declPosition.Filename != position.Filename || declPosition.Line != position.Line {
continue
}
@ -123,27 +124,3 @@ func getErrorCodesFromFolder(root string) (map[string]string, error) {
return m, nil
}
func parseDomainPackage(fset *token.FileSet, root string) (*ast.Package, error) {
pkgs, err := parser.ParseDir(fset, root, func(info fs.FileInfo) bool {
if info.IsDir() && info.Name() != root {
return false
}
if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") {
return false
}
return true
}, parser.AllErrors|parser.ParseComments)
if err != nil {
return nil, err
}
astDomainPkg, ok := pkgs["domain"]
if !ok {
return nil, errors.New("domain pkg not found")
}
return astDomainPkg, nil
}

View File

@ -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),
@ -332,7 +347,7 @@ 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
}

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

@ -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)
@ -160,6 +162,7 @@ var cmdServe = &cli.Command{
villageSvc,
ennoblementSvc,
tribeChangeSvc,
serverSnapshotSvc,
tribeSnapshotSvc,
playerSnapshotSvc,
port.WithOpenAPIConfig(oapiCfg),

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,

9
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.124.0
github.com/getkin/kin-openapi v0.125.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
@ -28,7 +28,9 @@ require (
github.com/uptrace/bun/extra/bundebug v1.2.1
github.com/urfave/cli/v2 v2.27.2
go.uber.org/automaxprocs v1.5.3
golang.org/x/image v0.17.0
golang.org/x/sync v0.7.0
golang.org/x/tools v0.19.0
gopkg.in/yaml.v3 v3.0.1
)
@ -43,7 +45,7 @@ require (
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
github.com/docker/docker v20.10.11+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@ -90,7 +92,6 @@ require (
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/tools v0.19.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
mellium.im/sasl v0.3.1 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect

14
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=
@ -40,8 +40,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ=
github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
@ -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.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/getkin/kin-openapi v0.125.0 h1:jyQCyf2qXS1qvs2U00xQzkGCqYPhEhZDmSmVt65fXno=
github.com/getkin/kin-openapi v0.125.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=
@ -233,6 +233,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=

View File

@ -38,6 +38,7 @@ func (pub *PlayerWatermillPublisher) EventSynced(
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumPlayers: p.NumPlayers(),
NumActivePlayers: p.NumActivePlayers(),
})
if err != nil {

View File

@ -38,6 +38,7 @@ func (pub *TribeWatermillPublisher) EventSynced(
ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(),
NumTribes: p.NumTribes(),
NumActiveTribes: p.NumActiveTribes(),
})
if err != nil {

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
@ -269,6 +280,9 @@ func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQue
case domain.PlayerSortPointsASC,
domain.PlayerSortPointsDESC:
el.value = cursor.Points()
case domain.PlayerSortMostPointsASC,
domain.PlayerSortMostPointsDESC:
el.value = cursor.MostPoints()
case domain.PlayerSortDeletedAtASC,
domain.PlayerSortDeletedAtDESC:
el.value = cursor.DeletedAt()
@ -315,6 +329,10 @@ func (a listPlayersParamsApplier) sortToColumnAndDirection(
return "player.points", sortDirectionASC, nil
case domain.PlayerSortPointsDESC:
return "player.points", sortDirectionDESC, nil
case domain.PlayerSortMostPointsASC:
return "player.most_points", sortDirectionASC, nil
case domain.PlayerSortMostPointsDESC:
return "player.most_points", sortDirectionDESC, nil
case domain.PlayerSortDeletedAtASC:
return "COALESCE(player.deleted_at, '0001-01-01 00:00:00+00:00')", sortDirectionASC, nil
case domain.PlayerSortDeletedAtDESC:
@ -323,3 +341,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

@ -126,18 +126,34 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("building_info = ?", bunmodel.NewBuildingInfo(buildingInfo.V))
}
if numTribes := a.params.NumTribes(); numTribes.Valid {
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 {
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 {
q = q.Set("player_data_synced_at = ?", playerDataSyncedAt.V)
}
@ -166,6 +182,10 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("ennoblement_data_synced_at = ?", ennoblementDataSyncedAt.V)
}
if snapshotCreatedAt := a.params.SnapshotCreatedAt(); snapshotCreatedAt.Valid {
q = q.Set("snapshot_created_at = ?", snapshotCreatedAt.V)
}
if tribeSnapshotsCreatedAt := a.params.TribeSnapshotsCreatedAt(); tribeSnapshotsCreatedAt.Valid {
q = q.Set("tribe_snapshots_created_at = ?", tribeSnapshotsCreatedAt.V)
}
@ -212,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
@ -230,6 +241,9 @@ func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuer
case domain.TribeSortPointsASC,
domain.TribeSortPointsDESC:
el.value = cursor.Points()
case domain.TribeSortMostPointsASC,
domain.TribeSortMostPointsDESC:
el.value = cursor.MostPoints()
case domain.TribeSortDominanceASC,
domain.TribeSortDominanceDESC:
el.value = cursor.Dominance()
@ -275,6 +289,10 @@ func (a listTribesParamsApplier) sortToColumnAndDirection(
return "tribe.points", sortDirectionASC, nil
case domain.TribeSortPointsDESC:
return "tribe.points", sortDirectionDESC, nil
case domain.TribeSortMostPointsASC:
return "tribe.most_points", sortDirectionASC, nil
case domain.TribeSortMostPointsDESC:
return "tribe.most_points", sortDirectionDESC, nil
case domain.TribeSortDominanceASC:
return "tribe.dominance", sortDirectionASC, nil
case domain.TribeSortDominanceDESC:
@ -287,3 +305,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

@ -276,6 +276,56 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
},
},
{
name: "OK: sort=[mostPoints ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortMostPointsASC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, players)
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.MostPoints(), b.MostPoints()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
},
{
name: "OK: sort=[mostPoints DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortMostPointsDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, players)
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.MostPoints(), b.MostPoints())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
},
{
name: "OK: sort=[deletedAt ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
@ -664,6 +714,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 {
@ -494,18 +514,34 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: domaintest.NewBuildingInfo(t),
Valid: true,
}))
require.NoError(t, updateParams.SetNumTribes(domain.NullInt{
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,
}))
require.NoError(t, updateParams.SetNumPlayers(domain.NullInt{
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,
@ -556,14 +596,18 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assert.Equal(t, updateParams.Config().V, serverAfterUpdate.Config())
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,
serverAfterUpdate.TribeDataSyncedAt(),
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

@ -339,6 +339,56 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
},
},
{
name: "OK: sort=[mostPoints ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortMostPointsASC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.MostPoints(), b.MostPoints()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
},
{
name: "OK: sort=[mostPoints DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortMostPointsDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.MostPoints(), b.MostPoints())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
},
{
name: "OK: sort=[deletedAt ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
@ -718,6 +768,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()

File diff suppressed because it is too large Load Diff

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

@ -183,12 +183,24 @@ func (svc *ServerService) UpdateNumTribes(ctx context.Context, payload domain.Tr
key := payload.ServerKey()
var updateParams domain.UpdateServerParams
if err := updateParams.SetNumTribes(domain.NullInt{
V: payload.NumTribes(),
Valid: true,
}); 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,
@ -203,12 +215,24 @@ func (svc *ServerService) UpdateNumPlayers(ctx context.Context, payload domain.P
key := payload.ServerKey()
var updateParams domain.UpdateServerParams
if err := updateParams.SetNumPlayers(domain.NullInt{
V: payload.NumPlayers(),
Valid: true,
}); 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

@ -16,8 +16,12 @@ type Server struct {
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"`
@ -26,6 +30,7 @@ type Server struct {
BuildingInfo BuildingInfo `bun:"building_info"`
UnitInfo UnitInfo `bun:"unit_info"`
CreatedAt time.Time `bun:"created_at,nullzero"`
SnapshotCreatedAt time.Time `bun:"snapshot_created_at,nullzero"`
PlayerDataUpdatedAt time.Time `bun:"player_data_synced_at,nullzero"`
PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"`
TribeDataUpdatedAt time.Time `bun:"tribe_data_synced_at,nullzero"`
@ -57,8 +62,12 @@ func (s Server) ToDomain() (domain.Server, error) {
s.URL,
s.Open,
s.Special,
s.NumPlayers,
s.NumActivePlayers,
s.NumInactivePlayers,
s.NumTribes,
s.NumActiveTribes,
s.NumInactiveTribes,
s.NumVillages,
s.NumPlayerVillages,
s.NumBarbarianVillages,
@ -67,6 +76,7 @@ func (s Server) ToDomain() (domain.Server, error) {
buildingInfo,
unitInfo,
s.CreatedAt,
s.SnapshotCreatedAt,
s.PlayerDataUpdatedAt,
s.PlayerSnapshotsCreatedAt,
s.TribeDataUpdatedAt,
@ -84,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 _, 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

@ -26,7 +26,7 @@ func NewBaseEnnoblement(
points int,
createdAt time.Time,
) (BaseEnnoblement, error) {
if err := validateIntInRange(villageID, 1, math.MaxInt); err != nil {
if err := validateInRange(villageID, 1, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "villageID",
@ -34,7 +34,7 @@ func NewBaseEnnoblement(
}
}
if err := validateIntInRange(newOwnerID, 0, math.MaxInt); err != nil {
if err := validateInRange(newOwnerID, 0, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "newOwnerID",
@ -42,7 +42,7 @@ func NewBaseEnnoblement(
}
}
if err := validateIntInRange(newTribeID, 0, math.MaxInt); err != nil {
if err := validateInRange(newTribeID, 0, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "newTribeID",
@ -50,7 +50,7 @@ func NewBaseEnnoblement(
}
}
if err := validateIntInRange(oldOwnerID, 0, math.MaxInt); err != nil {
if err := validateInRange(oldOwnerID, 0, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "oldOwnerID",
@ -58,7 +58,7 @@ func NewBaseEnnoblement(
}
}
if err := validateIntInRange(oldTribeID, 0, math.MaxInt); err != nil {
if err := validateInRange(oldTribeID, 0, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "oldTribeID",
@ -66,7 +66,7 @@ func NewBaseEnnoblement(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return BaseEnnoblement{}, ValidationError{
Model: baseEnnoblementModelName,
Field: "points",

View File

@ -56,7 +56,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "villageID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -76,7 +76,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "newOwnerID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -96,7 +96,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "newTribeID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -116,7 +116,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "oldOwnerID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -136,7 +136,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "oldTribeID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -156,7 +156,7 @@ func TestNewBaseEnnoblement(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseEnnoblement",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},

View File

@ -28,7 +28,7 @@ func NewBasePlayer(
od OpponentsDefeated,
profileURL *url.URL,
) (BasePlayer, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return BasePlayer{}, ValidationError{
Model: basePlayerModelName,
Field: "id",
@ -44,7 +44,7 @@ func NewBasePlayer(
}
}
if err := validateIntInRange(numVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numVillages, 0, math.MaxInt); err != nil {
return BasePlayer{}, ValidationError{
Model: basePlayerModelName,
Field: "numVillages",
@ -52,7 +52,7 @@ func NewBasePlayer(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return BasePlayer{}, ValidationError{
Model: basePlayerModelName,
Field: "points",
@ -60,7 +60,7 @@ func NewBasePlayer(
}
}
if err := validateIntInRange(rank, 0, math.MaxInt); err != nil {
if err := validateInRange(rank, 0, math.MaxInt); err != nil {
return BasePlayer{}, ValidationError{
Model: basePlayerModelName,
Field: "rank",
@ -68,7 +68,7 @@ func NewBasePlayer(
}
}
if err := validateIntInRange(tribeID, 0, math.MaxInt); err != nil {
if err := validateInRange(tribeID, 0, math.MaxInt); err != nil {
return BasePlayer{}, ValidationError{
Model: basePlayerModelName,
Field: "tribeID",

View File

@ -60,7 +60,7 @@ func TestNewBasePlayer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BasePlayer",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -125,7 +125,7 @@ func TestNewBasePlayer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BasePlayer",
Field: "numVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -146,7 +146,7 @@ func TestNewBasePlayer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BasePlayer",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -167,7 +167,7 @@ func TestNewBasePlayer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BasePlayer",
Field: "rank",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -188,7 +188,7 @@ func TestNewBasePlayer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BasePlayer",
Field: "tribeID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},

View File

@ -33,7 +33,7 @@ func NewBaseTribe(
od OpponentsDefeated,
profileURL *url.URL,
) (BaseTribe, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "id",
@ -60,7 +60,7 @@ func NewBaseTribe(
}
}
if err := validateIntInRange(numMembers, 0, math.MaxInt); err != nil {
if err := validateInRange(numMembers, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numMembers",
@ -68,7 +68,7 @@ func NewBaseTribe(
}
}
if err := validateIntInRange(numVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numVillages, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numVillages",
@ -76,7 +76,7 @@ func NewBaseTribe(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "points",
@ -84,7 +84,7 @@ func NewBaseTribe(
}
}
if err := validateIntInRange(allPoints, 0, math.MaxInt); err != nil {
if err := validateInRange(allPoints, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "allPoints",
@ -92,7 +92,7 @@ func NewBaseTribe(
}
}
if err := validateIntInRange(rank, 0, math.MaxInt); err != nil {
if err := validateInRange(rank, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "rank",

View File

@ -81,7 +81,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -200,7 +200,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "numMembers",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -223,7 +223,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "numVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -246,7 +246,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -269,7 +269,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "allPoints",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -292,7 +292,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "rank",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},

View File

@ -29,7 +29,7 @@ func NewBaseVillage(
playerID int,
profileURL *url.URL,
) (BaseVillage, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "id",
@ -45,7 +45,7 @@ func NewBaseVillage(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "points",
@ -53,7 +53,7 @@ func NewBaseVillage(
}
}
if err := validateIntInRange(x, 0, math.MaxInt); err != nil {
if err := validateInRange(x, 0, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "x",
@ -61,7 +61,7 @@ func NewBaseVillage(
}
}
if err := validateIntInRange(y, 0, math.MaxInt); err != nil {
if err := validateInRange(y, 0, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "y",
@ -77,7 +77,7 @@ func NewBaseVillage(
}
}
if err := validateIntInRange(bonus, 0, math.MaxInt); err != nil {
if err := validateInRange(bonus, 0, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "bonus",
@ -85,7 +85,7 @@ func NewBaseVillage(
}
}
if err := validateIntInRange(playerID, 0, math.MaxInt); err != nil {
if err := validateInRange(playerID, 0, math.MaxInt); err != nil {
return BaseVillage{}, ValidationError{
Model: baseVillageModelName,
Field: "playerID",

View File

@ -63,7 +63,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -131,7 +131,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -153,7 +153,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "x",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -175,7 +175,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "y",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -243,7 +243,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "bonus",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -265,7 +265,7 @@ func TestNewBaseVillage(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseVillage",
Field: "playerID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},

View File

@ -17,6 +17,7 @@ type PlayerCursorConfig struct {
ODScoreSup int
ODScoreTotal int
Points int
MostPoints int
DeletedAt time.Time
}
@ -31,6 +32,7 @@ func NewPlayerCursor(tb TestingTB, opts ...func(cfg *PlayerCursorConfig)) domain
ODScoreSup: gofakeit.IntRange(0, math.MaxInt),
ODScoreTotal: gofakeit.IntRange(0, math.MaxInt),
Points: gofakeit.IntRange(0, math.MaxInt),
MostPoints: gofakeit.IntRange(0, math.MaxInt),
DeletedAt: time.Time{},
}
@ -46,6 +48,7 @@ func NewPlayerCursor(tb TestingTB, opts ...func(cfg *PlayerCursorConfig)) domain
cfg.ODScoreSup,
cfg.ODScoreTotal,
cfg.Points,
cfg.MostPoints,
cfg.DeletedAt,
)
require.NoError(tb, err)

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

@ -20,7 +20,9 @@ func NewServerConfig(tb TestingTB) domain.ServerConfig {
domain.ServerConfigBuildings{},
domain.ServerConfigSnob{},
domain.ServerConfigAlly{Limit: gofakeit.IntRange(1, 20)},
domain.ServerConfigCoord{},
domain.ServerConfigCoord{
MapSize: gofakeit.IntRange(1000, 1000),
},
domain.ServerConfigSitter{},
domain.ServerConfigSleep{},
domain.ServerConfigNight{},

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

@ -20,6 +20,7 @@ type TribeCursorConfig struct {
ODScoreDef int
ODScoreTotal int
Points int
MostPoints int
Dominance float64
DeletedAt time.Time
}
@ -34,6 +35,7 @@ func NewTribeCursor(tb TestingTB, opts ...func(cfg *TribeCursorConfig)) domain.T
ODScoreDef: gofakeit.IntRange(0, math.MaxInt),
ODScoreTotal: gofakeit.IntRange(0, math.MaxInt),
Points: gofakeit.IntRange(0, math.MaxInt),
MostPoints: gofakeit.IntRange(0, math.MaxInt),
Dominance: gofakeit.Float64Range(0.1, 99.9),
DeletedAt: time.Time{},
}
@ -49,6 +51,7 @@ func NewTribeCursor(tb TestingTB, opts ...func(cfg *TribeCursorConfig)) domain.T
cfg.ODScoreDef,
cfg.ODScoreTotal,
cfg.Points,
cfg.MostPoints,
cfg.Dominance,
cfg.DeletedAt,
)

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

@ -37,6 +37,8 @@ func NewVillageCursor(tb TestingTB, opts ...func(cfg *VillageCursorConfig)) doma
type VillageConfig struct {
ID int
ServerKey string
X int
Y int
PlayerID int
}
@ -46,6 +48,8 @@ func NewVillage(tb TestingTB, opts ...func(cfg *VillageConfig)) domain.Village {
cfg := &VillageConfig{
ID: RandID(),
ServerKey: RandServerKey(),
X: gofakeit.IntRange(1, 999),
Y: gofakeit.IntRange(1, 999),
PlayerID: gofakeit.IntRange(0, 10000),
}
@ -58,8 +62,8 @@ func NewVillage(tb TestingTB, opts ...func(cfg *VillageConfig)) domain.Village {
cfg.ServerKey,
gofakeit.LetterN(50),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 999),
gofakeit.IntRange(1, 999),
cfg.X,
cfg.Y,
gofakeit.LetterN(3),
0,
cfg.PlayerID,

View File

@ -37,7 +37,7 @@ func UnmarshalEnnoblementFromDatabase(
points int,
createdAt time.Time,
) (Ennoblement, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return Ennoblement{}, ValidationError{
Model: ennoblementModelName,
Field: "id",
@ -265,7 +265,7 @@ type EnnoblementCursor struct {
const ennoblementCursorModelName = "EnnoblementCursor"
func NewEnnoblementCursor(id int, serverKey string, createdAt time.Time) (EnnoblementCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return EnnoblementCursor{}, ValidationError{
Model: ennoblementCursorModelName,
Field: "id",
@ -405,7 +405,7 @@ func (params *ListEnnoblementsParams) VillageIDs() []int {
func (params *ListEnnoblementsParams) SetVillageIDs(villageIDs []int) error {
for i, id := range villageIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "villageIDs",
@ -426,7 +426,7 @@ func (params *ListEnnoblementsParams) PlayerIDs() []int {
func (params *ListEnnoblementsParams) SetPlayerIDs(playerIDs []int) error {
for i, id := range playerIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "playerIDs",
@ -447,7 +447,7 @@ func (params *ListEnnoblementsParams) TribeIDs() []int {
func (params *ListEnnoblementsParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "tribeIDs",
@ -579,7 +579,7 @@ func (params *ListEnnoblementsParams) Limit() int {
}
func (params *ListEnnoblementsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, EnnoblementListMaxLimit); err != nil {
if err := validateInRange(limit, 1, EnnoblementListMaxLimit); err != nil {
return ValidationError{
Model: listEnnoblementsParamsModelName,
Field: "limit",

View File

@ -141,7 +141,7 @@ func TestNewEnnoblementCursor(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "EnnoblementCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -276,7 +276,7 @@ func TestListEnnoblementsParams_SetVillageIDs(t *testing.T) {
Model: "ListEnnoblementsParams",
Field: "villageIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -336,7 +336,7 @@ func TestListEnnoblementsParams_SetPlayerIDs(t *testing.T) {
Model: "ListEnnoblementsParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -396,7 +396,7 @@ func TestListEnnoblementsParams_SetTribeIDs(t *testing.T) {
Model: "ListEnnoblementsParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -957,7 +957,7 @@ func TestListEnnoblementsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -971,7 +971,7 @@ func TestListEnnoblementsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.EnnoblementListMaxLimit,
Current: domain.EnnoblementListMaxLimit + 1,
},

View File

@ -112,6 +112,7 @@ type TribesSyncedEventPayload struct {
serverKey string
serverURL *url.URL
versionCode string
numTribes int
numActiveTribes int
}
@ -121,6 +122,7 @@ func NewTribesSyncedEventPayload(
serverKey string,
serverURL *url.URL,
versionCode string,
numTribes int,
numActiveTribes int,
) (TribesSyncedEventPayload, error) {
if serverKey == "" {
@ -147,7 +149,15 @@ func NewTribesSyncedEventPayload(
}
}
if err := validateIntInRange(numActiveTribes, 0, math.MaxInt); err != nil {
if err := validateInRange(numTribes, 0, math.MaxInt); err != nil {
return TribesSyncedEventPayload{}, ValidationError{
Model: tribesSyncedEventPayloadModelName,
Field: "numTribes",
Err: err,
}
}
if err := validateInRange(numActiveTribes, 0, math.MaxInt); err != nil {
return TribesSyncedEventPayload{}, ValidationError{
Model: tribesSyncedEventPayloadModelName,
Field: "numActiveTribes",
@ -159,6 +169,7 @@ func NewTribesSyncedEventPayload(
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numTribes: numTribes,
numActiveTribes: numActiveTribes,
}, nil
}
@ -175,14 +186,23 @@ func (p TribesSyncedEventPayload) VersionCode() string {
return p.versionCode
}
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
numActivePlayers int
}
@ -192,6 +212,7 @@ func NewPlayersSyncedEventPayload(
serverKey string,
serverURL *url.URL,
versionCode string,
numPlayers int,
numActivePlayers int,
) (PlayersSyncedEventPayload, error) {
if serverKey == "" {
@ -218,7 +239,15 @@ func NewPlayersSyncedEventPayload(
}
}
if err := validateIntInRange(numActivePlayers, 0, math.MaxInt); err != nil {
if err := validateInRange(numPlayers, 0, math.MaxInt); err != nil {
return PlayersSyncedEventPayload{}, ValidationError{
Model: playersSyncedEventPayloadModelName,
Field: "numPlayers",
Err: err,
}
}
if err := validateInRange(numActivePlayers, 0, math.MaxInt); err != nil {
return PlayersSyncedEventPayload{}, ValidationError{
Model: playersSyncedEventPayloadModelName,
Field: "numActivePlayers",
@ -230,6 +259,7 @@ func NewPlayersSyncedEventPayload(
serverKey: serverKey,
serverURL: serverURL,
versionCode: versionCode,
numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
}, nil
}
@ -246,10 +276,18 @@ func (p PlayersSyncedEventPayload) VersionCode() string {
return p.versionCode
}
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
@ -295,7 +333,7 @@ func NewVillagesSyncedEventPayload(
}
}
if err := validateIntInRange(numVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numVillages, 0, math.MaxInt); err != nil {
return VillagesSyncedEventPayload{}, ValidationError{
Model: villagesSyncedEventPayloadModelName,
Field: "numVillages",
@ -303,7 +341,7 @@ func NewVillagesSyncedEventPayload(
}
}
if err := validateIntInRange(numPlayerVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numPlayerVillages, 0, math.MaxInt); err != nil {
return VillagesSyncedEventPayload{}, ValidationError{
Model: villagesSyncedEventPayloadModelName,
Field: "numPlayerVillages",
@ -311,7 +349,7 @@ func NewVillagesSyncedEventPayload(
}
}
if err := validateIntInRange(numBarbarianVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numBarbarianVillages, 0, math.MaxInt); err != nil {
return VillagesSyncedEventPayload{}, ValidationError{
Model: villagesSyncedEventPayloadModelName,
Field: "numBarbarianVillages",
@ -319,7 +357,7 @@ func NewVillagesSyncedEventPayload(
}
}
if err := validateIntInRange(numBonusVillages, 0, math.MaxInt); err != nil {
if err := validateInRange(numBonusVillages, 0, math.MaxInt); err != nil {
return VillagesSyncedEventPayload{}, ValidationError{
Model: villagesSyncedEventPayloadModelName,
Field: "numBonusVillages",

View File

@ -58,38 +58,46 @@ func TestNewTribesSyncedEventPayload(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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

@ -25,7 +25,7 @@ func NewOpponentsDefeated(
rankTotal int,
scoreTotal int,
) (OpponentsDefeated, error) {
if err := validateIntInRange(rankAtt, 0, math.MaxInt); err != nil {
if err := validateInRange(rankAtt, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankAtt",
@ -33,7 +33,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(scoreAtt, 0, math.MaxInt); err != nil {
if err := validateInRange(scoreAtt, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreAtt",
@ -41,7 +41,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(rankDef, 0, math.MaxInt); err != nil {
if err := validateInRange(rankDef, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankDef",
@ -49,7 +49,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(scoreDef, 0, math.MaxInt); err != nil {
if err := validateInRange(scoreDef, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreDef",
@ -57,7 +57,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(rankSup, 0, math.MaxInt); err != nil {
if err := validateInRange(rankSup, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankSup",
@ -65,7 +65,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(scoreSup, 0, math.MaxInt); err != nil {
if err := validateInRange(scoreSup, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreSup",
@ -73,7 +73,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(rankTotal, 0, math.MaxInt); err != nil {
if err := validateInRange(rankTotal, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankTotal",
@ -81,7 +81,7 @@ func NewOpponentsDefeated(
}
}
if err := validateIntInRange(scoreTotal, 0, math.MaxInt); err != nil {
if err := validateInRange(scoreTotal, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreTotal",

View File

@ -58,7 +58,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankAtt",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -79,7 +79,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreAtt",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -100,7 +100,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankDef",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -121,7 +121,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreDef",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -142,7 +142,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankSup",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -163,7 +163,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreSup",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -184,7 +184,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankTotal",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -205,7 +205,7 @@ func TestNewOpponentsDefeated(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreTotal",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},

View File

@ -61,7 +61,7 @@ func UnmarshalPlayerFromDatabase(
createdAt time.Time,
deletedAt time.Time,
) (Player, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return Player{}, ValidationError{
Model: playerModelName,
Field: "id",
@ -200,6 +200,7 @@ func (p Player) ToCursor() (PlayerCursor, error) {
p.od.scoreSup,
p.od.scoreTotal,
p.points,
p.mostPoints,
p.deletedAt,
)
}
@ -326,7 +327,7 @@ const playerMetaModelName = "PlayerMeta"
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalPlayerMetaFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalPlayerMetaFromDatabase(id int, name string, rawProfileURL string) (PlayerMeta, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return PlayerMeta{}, ValidationError{
Model: playerMetaModelName,
Field: "id",
@ -552,6 +553,8 @@ const (
PlayerSortODScoreTotalDESC
PlayerSortPointsASC
PlayerSortPointsDESC
PlayerSortMostPointsASC
PlayerSortMostPointsDESC
PlayerSortDeletedAtASC
PlayerSortDeletedAtDESC
)
@ -592,6 +595,10 @@ func (s PlayerSort) String() string {
return "points:ASC"
case PlayerSortPointsDESC:
return "points:DESC"
case PlayerSortMostPointsASC:
return "mostPoints:ASC"
case PlayerSortMostPointsDESC:
return "mostPoints:DESC"
case PlayerSortDeletedAtASC:
return "deletedAt:ASC"
case PlayerSortDeletedAtDESC:
@ -609,6 +616,7 @@ type PlayerCursor struct {
odScoreSup int
odScoreTotal int
points int
mostPoints int
deletedAt time.Time
}
@ -622,9 +630,10 @@ func NewPlayerCursor(
odScoreSup int,
odScoreTotal int,
points int,
mostPoints int,
deletedAt time.Time,
) (PlayerCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "id",
@ -640,7 +649,7 @@ func NewPlayerCursor(
}
}
if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreAtt, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreAtt",
@ -648,7 +657,7 @@ func NewPlayerCursor(
}
}
if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreDef, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreDef",
@ -656,7 +665,7 @@ func NewPlayerCursor(
}
}
if err := validateIntInRange(odScoreSup, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreSup, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreSup",
@ -664,7 +673,7 @@ func NewPlayerCursor(
}
}
if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreTotal, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreTotal",
@ -672,7 +681,7 @@ func NewPlayerCursor(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "points",
@ -680,6 +689,14 @@ func NewPlayerCursor(
}
}
if err := validateInRange(mostPoints, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "mostPoints",
Err: err,
}
}
return PlayerCursor{
id: id,
serverKey: serverKey,
@ -688,6 +705,7 @@ func NewPlayerCursor(
odScoreSup: odScoreSup,
odScoreTotal: odScoreTotal,
points: points,
mostPoints: mostPoints,
deletedAt: deletedAt,
}, nil
}
@ -734,6 +752,11 @@ func decodePlayerCursor(encoded string) (PlayerCursor, error) {
return PlayerCursor{}, ErrInvalidCursor
}
mostPoints, err := m.int("mostPoints")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
deletedAt, err := m.time("deletedAt")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
@ -747,6 +770,7 @@ func decodePlayerCursor(encoded string) (PlayerCursor, error) {
odScoreSup,
odScoreTotal,
points,
mostPoints,
deletedAt,
)
if err != nil {
@ -784,6 +808,10 @@ func (pc PlayerCursor) Points() int {
return pc.points
}
func (pc PlayerCursor) MostPoints() int {
return pc.mostPoints
}
func (pc PlayerCursor) DeletedAt() time.Time {
return pc.deletedAt
}
@ -805,6 +833,7 @@ func (pc PlayerCursor) Encode() string {
{"odScoreSup", pc.odScoreSup},
{"odScoreTotal", pc.odScoreTotal},
{"points", pc.points},
{"mostPoints", pc.mostPoints},
{"deletedAt", pc.deletedAt},
})
}
@ -842,7 +871,7 @@ func (params *ListPlayersParams) IDs() []int {
func (params *ListPlayersParams) SetIDs(ids []int) error {
for i, id := range ids {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
Field: "ids",
@ -939,7 +968,7 @@ func (params *ListPlayersParams) TribeIDs() []int {
func (params *ListPlayersParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
Field: "tribeIDs",
@ -1062,7 +1091,7 @@ func (params *ListPlayersParams) Limit() int {
}
func (params *ListPlayersParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, PlayerListMaxLimit); err != nil {
if err := validateInRange(limit, 1, PlayerListMaxLimit); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "limit",
@ -1180,6 +1209,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

@ -39,7 +39,7 @@ func UnmarshalPlayerSnapshotFromDatabase(
date time.Time,
createdAt time.Time,
) (PlayerSnapshot, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return PlayerSnapshot{}, ValidationError{
Model: playerSnapshotModelName,
Field: "id",
@ -47,7 +47,7 @@ func UnmarshalPlayerSnapshotFromDatabase(
}
}
if err := validateIntInRange(playerID, 1, math.MaxInt); err != nil {
if err := validateInRange(playerID, 1, math.MaxInt); err != nil {
return PlayerSnapshot{}, ValidationError{
Model: playerSnapshotModelName,
Field: "playerID",
@ -275,7 +275,7 @@ type PlayerSnapshotCursor struct {
const playerSnapshotCursorModelName = "PlayerSnapshotCursor"
func NewPlayerSnapshotCursor(id int, serverKey string, date time.Time) (PlayerSnapshotCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return PlayerSnapshotCursor{}, ValidationError{
Model: playerSnapshotCursorModelName,
Field: "id",
@ -411,7 +411,7 @@ func (params *ListPlayerSnapshotsParams) PlayerIDs() []int {
func (params *ListPlayerSnapshotsParams) SetPlayerIDs(playerIDs []int) error {
for i, id := range playerIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "playerIDs",
@ -529,7 +529,7 @@ func (params *ListPlayerSnapshotsParams) Limit() int {
}
func (params *ListPlayerSnapshotsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, PlayerSnapshotListMaxLimit); err != nil {
if err := validateInRange(limit, 1, PlayerSnapshotListMaxLimit); err != nil {
return ValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "limit",

View File

@ -147,7 +147,7 @@ func TestNewPlayerSnapshotCursor(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "PlayerSnapshotCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -282,7 +282,7 @@ func TestListPlayerSnapshotsParams_SetPlayerIDs(t *testing.T) {
Model: "ListPlayerSnapshotsParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -840,7 +840,7 @@ func TestListPlayerSnapshotsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -854,7 +854,7 @@ func TestListPlayerSnapshotsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.PlayerSnapshotListMaxLimit,
Current: domain.PlayerSnapshotListMaxLimit + 1,
},

View File

@ -303,6 +303,13 @@ func TestPlayerSort_IsInConflict(t *testing.T) {
},
expectedRes: true,
},
{
name: "OK: mostPoints:ASC mostPoints:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortMostPointsASC, domain.PlayerSortMostPointsDESC},
},
expectedRes: true,
},
{
name: "OK: deletedAt:ASC deletedAt:DESC",
args: args{
@ -334,6 +341,7 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup int
odScoreTotal int
points int
mostPoints int
deletedAt time.Time
}
@ -354,6 +362,7 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: nil,
@ -368,12 +377,13 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -389,12 +399,13 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreAtt",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -410,12 +421,13 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreDef",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -431,12 +443,13 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: -1,
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreSup",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -452,12 +465,13 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: -1,
points: validPlayerCursor.Points(),
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreTotal",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -473,12 +487,35 @@ func TestNewPlayerCursor(t *testing.T) {
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: -1,
mostPoints: validPlayerCursor.MostPoints(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: mostPoints < 0",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreSup: validPlayerCursor.ODScoreSup(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
mostPoints: -1,
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "mostPoints",
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -518,6 +555,7 @@ func TestNewPlayerCursor(t *testing.T) {
tt.args.odScoreSup,
tt.args.odScoreTotal,
tt.args.points,
tt.args.mostPoints,
tt.args.deletedAt,
)
require.ErrorIs(t, err, tt.expectedErr)
@ -531,6 +569,7 @@ func TestNewPlayerCursor(t *testing.T) {
assert.Equal(t, tt.args.odScoreSup, pc.ODScoreSup())
assert.Equal(t, tt.args.odScoreTotal, pc.ODScoreTotal())
assert.Equal(t, tt.args.points, pc.Points())
assert.Equal(t, tt.args.mostPoints, pc.MostPoints())
assert.Equal(t, tt.args.deletedAt, pc.DeletedAt())
assert.NotEmpty(t, pc.Encode())
})
@ -574,7 +613,7 @@ func TestListPlayersParams_SetIDs(t *testing.T) {
Model: "ListPlayersParams",
Field: "ids",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -845,7 +884,7 @@ func TestListPlayersParams_SetTribeIDs(t *testing.T) {
Model: "ListPlayersParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1490,7 +1529,7 @@ func TestListPlayersParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1504,7 +1543,7 @@ func TestListPlayersParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.PlayerListMaxLimit,
Current: domain.PlayerListMaxLimit + 1,
},
@ -1614,3 +1653,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

@ -14,8 +14,12 @@ type Server struct {
url *url.URL
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
@ -44,8 +49,12 @@ func UnmarshalServerFromDatabase(
rawURL string,
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
}
@ -131,14 +146,30 @@ func (s Server) Special() bool {
return s.special
}
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
}
@ -377,9 +412,13 @@ type UpdateServerParams struct {
config NullServerConfig
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
}
@ -420,13 +460,33 @@ func (params *UpdateServerParams) SetUnitInfo(unitInfo NullUnitInfo) error {
return nil
}
func (params *UpdateServerParams) NumTribes() NullInt {
return params.numTribes
}
func (params *UpdateServerParams) SetNumTribes(num NullInt) error {
if num.Valid {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numTribes",
Err: err,
}
}
}
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 {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActiveTribes",
@ -440,6 +500,26 @@ func (params *UpdateServerParams) SetNumActiveTribes(num NullInt) error {
return nil
}
func (params *UpdateServerParams) NumInactiveTribes() NullInt {
return params.numInactiveTribes
}
func (params *UpdateServerParams) SetNumInactiveTribes(num NullInt) error {
if num.Valid {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactiveTribes",
Err: err,
}
}
}
params.numInactiveTribes = num
return nil
}
func (params *UpdateServerParams) TribeDataSyncedAt() NullTime {
return params.tribeDataSyncedAt
}
@ -449,13 +529,33 @@ func (params *UpdateServerParams) SetTribeDataSyncedAt(tribeDataSyncedAt NullTim
return nil
}
func (params *UpdateServerParams) NumPlayers() NullInt {
return params.numPlayers
}
func (params *UpdateServerParams) SetNumPlayers(num NullInt) error {
if num.Valid {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numPlayers",
Err: err,
}
}
}
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 {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActivePlayers",
@ -469,6 +569,26 @@ func (params *UpdateServerParams) SetNumActivePlayers(num NullInt) error {
return nil
}
func (params *UpdateServerParams) NumInactivePlayers() NullInt {
return params.numInactivePlayers
}
func (params *UpdateServerParams) SetNumInactivePlayers(num NullInt) error {
if num.Valid {
if err := validateInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactivePlayers",
Err: err,
}
}
}
params.numInactivePlayers = num
return nil
}
func (params *UpdateServerParams) PlayerDataSyncedAt() NullTime {
return params.playerDataSyncedAt
}
@ -484,7 +604,7 @@ func (params *UpdateServerParams) NumVillages() NullInt {
func (params *UpdateServerParams) SetNumVillages(numVillages NullInt) error {
if numVillages.Valid {
if err := validateIntInRange(numVillages.V, 0, math.MaxInt); err != nil {
if err := validateInRange(numVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numVillages",
@ -504,7 +624,7 @@ func (params *UpdateServerParams) NumPlayerVillages() NullInt {
func (params *UpdateServerParams) SetNumPlayerVillages(numPlayerVillages NullInt) error {
if numPlayerVillages.Valid {
if err := validateIntInRange(numPlayerVillages.V, 0, math.MaxInt); err != nil {
if err := validateInRange(numPlayerVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numPlayerVillages",
@ -524,7 +644,7 @@ func (params *UpdateServerParams) NumBarbarianVillages() NullInt {
func (params *UpdateServerParams) SetNumBarbarianVillages(numBarbarianVillages NullInt) error {
if numBarbarianVillages.Valid {
if err := validateIntInRange(numBarbarianVillages.V, 0, math.MaxInt); err != nil {
if err := validateInRange(numBarbarianVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numBarbarianVillages",
@ -544,7 +664,7 @@ func (params *UpdateServerParams) NumBonusVillages() NullInt {
func (params *UpdateServerParams) SetNumBonusVillages(numBonusVillages NullInt) error {
if numBonusVillages.Valid {
if err := validateIntInRange(numBonusVillages.V, 0, math.MaxInt); err != nil {
if err := validateInRange(numBonusVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numBonusVillages",
@ -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
}
@ -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
}
@ -866,7 +1006,7 @@ func (params *ListServersParams) Limit() int {
}
func (params *ListServersParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, ServerListMaxLimit); err != nil {
if err := validateInRange(limit, 1, ServerListMaxLimit); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "limit",

View File

@ -0,0 +1,385 @@
package domain
import (
"fmt"
"image"
"image/color"
"image/png"
"io"
"strconv"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
)
type ServerMapMarker struct {
village VillageMeta
color color.RGBA
}
func NewServerMapMarker(village VillageMeta) ServerMapMarker {
return ServerMapMarker{
village: village,
color: color.RGBA{R: 255, G: 99, B: 71, A: 255},
}
}
func (m *ServerMapMarker) Village() VillageMeta {
return m.village
}
func (m *ServerMapMarker) SetColor(c color.RGBA) error {
m.color = c
return nil
}
func (m *ServerMapMarker) Color() color.RGBA {
return m.color
}
type ServerMapLegendEntry struct {
color color.Color
names []string
}
func NewServerMapLegendEntry(c color.Color, names ...string) ServerMapLegendEntry {
return ServerMapLegendEntry{color: c, names: names}
}
func (e ServerMapLegendEntry) Color() color.Color {
return e.color
}
func (e ServerMapLegendEntry) Names() []string {
return e.names
}
type ServerMap struct {
server Server
size int
markersCh <-chan ServerMapMarker
backgroundColor color.RGBA
scale float32
centerX int
centerY int
continentGrid bool
gridLineColor color.RGBA
continentNumbers bool
continentNumberColor color.RGBA
legendEntries []ServerMapLegendEntry
}
const serverMapModelName = "ServerMap"
func NewServerMap(server Server, markersCh <-chan ServerMapMarker) ServerMap {
size := server.Config().Coord().MapSize
return ServerMap{
server: server,
size: size,
markersCh: markersCh,
backgroundColor: color.RGBA{
A: 255,
},
scale: 1,
centerX: size / 2,
centerY: size / 2,
continentGrid: true,
gridLineColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
continentNumbers: true,
continentNumberColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
}
}
func (m *ServerMap) Scale() float32 {
return m.scale
}
const (
serverMapMinScale = 1
serverMapMaxScale = 5
)
func (m *ServerMap) SetScale(scale float32) error {
if err := validateInRange(scale, serverMapMinScale, serverMapMaxScale); err != nil {
return ValidationError{
Model: serverMapModelName,
Field: "scale",
Err: err,
}
}
m.scale = scale
return nil
}
func (m *ServerMap) BackgroundColor() color.RGBA {
return m.backgroundColor
}
func (m *ServerMap) SetBackgroundColor(backgroundColor color.RGBA) error {
m.backgroundColor = backgroundColor
return nil
}
func (m *ServerMap) CenterX() int {
return m.centerX
}
func (m *ServerMap) SetCenterX(centerX int) error {
m.centerX = centerX
return nil
}
func (m *ServerMap) CenterY() int {
return m.centerY
}
func (m *ServerMap) SetCenterY(centerY int) error {
m.centerY = centerY
return nil
}
func (m *ServerMap) ContinentGrid() bool {
return m.continentGrid
}
func (m *ServerMap) SetContinentGrid(continentGrid bool) error {
m.continentGrid = continentGrid
return nil
}
func (m *ServerMap) GridLineColor() color.RGBA {
return m.gridLineColor
}
func (m *ServerMap) SetGridLineColor(gridLineColor color.RGBA) error {
m.gridLineColor = gridLineColor
return nil
}
func (m *ServerMap) ContinentNumbers() bool {
return m.continentNumbers
}
func (m *ServerMap) SetContinentNumbers(continentNumbers bool) error {
m.continentNumbers = continentNumbers
return nil
}
func (m *ServerMap) ContinentNumberColor() color.RGBA {
return m.continentNumberColor
}
func (m *ServerMap) SetContinentNumberColor(continentNumberColor color.RGBA) error {
m.continentNumberColor = continentNumberColor
return nil
}
func (m *ServerMap) LegendEntries() []ServerMapLegendEntry {
return m.legendEntries
}
func (m *ServerMap) SetLegendEntries(legendEntries []ServerMapLegendEntry) error {
m.legendEntries = legendEntries
return nil
}
func (m *ServerMap) Generate(w io.Writer) error {
r := image.Rectangle{Max: image.Point{X: m.size, Y: m.size}}
imgHalfWidth := r.Dx() / 2
imgHalfHeight := r.Dy() / 2
img := image.NewRGBA(r)
draw.Draw(img, img.Bounds(), image.NewUniform(m.backgroundColor), image.Point{}, draw.Src)
m.drawContinentGrid(img)
m.drawMarkers(img)
m.drawContinentNumbers(img)
legend, err := m.generateLegend()
if err != nil {
return err
}
scaledImg := m.scaleImg(img)
final := image.NewRGBA(image.Rect(0, 0, r.Dx()+legend.Rect.Dx(), r.Dy()))
draw.Draw(final, scaledImg.Rect, scaledImg, image.Point{
X: int(float32(m.centerX)*m.scale) - imgHalfWidth,
Y: int(float32(m.centerY)*m.scale) - imgHalfHeight,
}, draw.Src)
draw.Draw(final, image.Rect(r.Dx(), 0, final.Rect.Dx(), final.Rect.Dy()), legend, image.Point{}, draw.Src)
if err = png.Encode(w, final); err != nil {
return fmt.Errorf("couldn't encode image: %w", err)
}
return nil
}
const serverMapContinentDx = 100
func (m *ServerMap) drawContinentGrid(img *image.RGBA) {
if !m.continentGrid {
return
}
for y := serverMapContinentDx; y < m.size; y += serverMapContinentDx {
for x := range m.size {
img.Set(x, y, m.gridLineColor)
img.Set(y, x, m.gridLineColor)
}
}
}
const serverMapMarkerRectSideLength = 1
func (m *ServerMap) drawMarkers(img *image.RGBA) {
for marker := range m.markersCh {
village := marker.Village()
x := village.X()
y := village.Y()
rect := image.Rect(x, y, x+serverMapMarkerRectSideLength, y+serverMapMarkerRectSideLength)
draw.Draw(img, rect, &image.Uniform{C: marker.color}, image.Point{}, draw.Over)
}
}
func (m *ServerMap) drawContinentNumbers(img *image.RGBA) {
if !m.continentNumbers {
return
}
continent := 0
d := &font.Drawer{
Dst: img,
Src: image.NewUniform(m.continentNumberColor),
Face: basicfont.Face7x13,
}
for y := serverMapContinentDx; y <= m.size; y += serverMapContinentDx {
for x := serverMapContinentDx; x <= m.size; x += serverMapContinentDx {
continentStr := strconv.Itoa(continent)
d.Dot = fixed.Point26_6{
X: fixed.I(x) - d.MeasureString(continentStr),
Y: fixed.I(y - 1),
}
d.DrawString(continentStr)
continent++
}
}
}
func (m *ServerMap) scaleImg(img *image.RGBA) *image.RGBA {
if m.scale == 1 {
return img
}
scaledSize := int(float32(m.size) * m.scale)
scaledImg := image.NewRGBA(image.Rect(0, 0, scaledSize, scaledSize))
draw.NearestNeighbor.Scale(scaledImg, scaledImg.Rect, img, img.Rect, draw.Over, nil)
return scaledImg
}
const (
serverMapLegendWidth = 400
serverMapLegendPadding = 10
serverMapColorWidth = 40
serverMapSpaceColorText = 10
serverMapLegendStringSeparator = " + "
serverMapLegendStringOverflow = "..."
serverMapLegendMaxStringLength = 45
)
var ErrTooManyLegendEntries error = simpleError{
msg: "too many legend entries",
typ: ErrorTypeIncorrectInput,
code: "too-many-legend-entries",
}
func (m *ServerMap) generateLegend() (*image.RGBA, error) {
if len(m.legendEntries) == 0 {
return image.NewRGBA(image.Rectangle{}), nil
}
img := image.NewRGBA(image.Rectangle{Max: image.Point{X: serverMapLegendWidth, Y: m.size}})
d := &font.Drawer{
Dst: img,
Src: image.NewUniform(m.continentNumberColor),
Face: basicfont.Face7x13,
}
lineHeight := basicfont.Face7x13.Metrics().Ascent.Ceil() + basicfont.Face7x13.Metrics().Descent.Ceil()
lineMaxWidth := img.Rect.Dx() - serverMapLegendPadding*2 - serverMapColorWidth - serverMapSpaceColorText
offset := serverMapLegendPadding
for _, e := range m.legendEntries {
namesLen := len(e.Names())
currentWidth := 0
line := ""
var lines []string
for i, name := range e.Names() {
if len(name) > serverMapLegendMaxStringLength {
name = name[:serverMapLegendMaxStringLength-len(serverMapLegendStringOverflow)] + "..."
}
if i != namesLen-1 {
name += serverMapLegendStringSeparator
}
strWidth := d.MeasureString(name).Ceil()
if currentWidth+strWidth > lineMaxWidth {
lines = append(lines, line)
currentWidth = 0
line = ""
}
line += name
currentWidth += strWidth
}
if line != "" {
lines = append(lines, line)
}
yMin := offset + (len(lines) * lineHeight / 2) - lineHeight/2
draw.Draw(
img,
image.Rect(
serverMapLegendPadding,
yMin,
serverMapColorWidth+serverMapLegendPadding,
yMin+lineHeight,
),
image.NewUniform(e.Color()),
image.Point{},
draw.Src,
)
for i, l := range lines {
d.Dot = fixed.Point26_6{
X: fixed.I(serverMapColorWidth + serverMapLegendPadding + serverMapSpaceColorText),
Y: fixed.I(offset + (i+1)*lineHeight),
}
d.DrawString(l)
}
offset += (len(lines) * lineHeight) + serverMapLegendPadding
if offset > img.Bounds().Dy()-serverMapLegendPadding {
return nil, ErrTooManyLegendEntries
}
}
return img, nil
}

View File

@ -0,0 +1,155 @@
package domain_test
import (
"image/color"
"io"
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServerMap_SetScale(t *testing.T) {
t.Parallel()
type args struct {
scale float32
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
scale: 1,
},
},
{
name: "ERR: scale < 1",
args: args{
scale: 0.99,
},
expectedErr: domain.ValidationError{
Model: "ServerMap",
Field: "scale",
Err: domain.MinGreaterEqualError[float32]{
Min: 1,
Current: 0.99,
},
},
},
{
name: "ERR: scale > 5",
args: args{
scale: 5.00001,
},
expectedErr: domain.ValidationError{
Model: "ServerMap",
Field: "scale",
Err: domain.MaxLessEqualError[float32]{
Max: 5,
Current: 5.00001,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewServerMap(domaintest.NewServer(t), make(chan domain.ServerMapMarker))
require.ErrorIs(t, params.SetScale(tt.args.scale), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.InDelta(t, tt.args.scale, params.Scale(), 0.001)
})
}
}
func TestServerMap_Generate(t *testing.T) {
t.Parallel()
t.Run("OK: default", func(t *testing.T) {
t.Parallel()
ch := make(chan domain.ServerMapMarker)
go func() {
defer close(ch)
for i := range 500 {
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
cfg.X = i
cfg.Y = i
}).Meta())
}
}()
m := domain.NewServerMap(domaintest.NewServer(t), ch)
require.NoError(t, m.Generate(io.Discard))
})
t.Run("OK: scale=2 legend", func(t *testing.T) {
t.Parallel()
ch := make(chan domain.ServerMapMarker)
go func() {
defer close(ch)
for i := range 500 {
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
cfg.X = i
cfg.Y = i
}).Meta())
}
}()
m := domain.NewServerMap(domaintest.NewServer(t), ch)
require.NoError(t, m.SetLegendEntries([]domain.ServerMapLegendEntry{
domain.NewServerMapLegendEntry(
color.RGBA{R: 255, G: 204, B: 229, A: 255},
"test22222^",
"test222",
),
domain.NewServerMapLegendEntry(
color.RGBA{R: 255, G: 255, B: 255, A: 255},
"test22222^",
"test2223",
"longstringlongstringlongstringlongstring",
"longstringlongstringlongstringlongstringlongstringlongstringlongstringlongstring",
"longstringlongstring2",
),
}))
require.NoError(t, m.SetScale(2))
require.NoError(t, m.Generate(io.Discard))
})
t.Run("OK: scale=2 centerX=250 centerY=250", func(t *testing.T) {
t.Parallel()
ch := make(chan domain.ServerMapMarker)
go func() {
defer close(ch)
for i := range 500 {
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
cfg.X = i
cfg.Y = i
}).Meta())
}
}()
m := domain.NewServerMap(domaintest.NewServer(t), ch)
require.NoError(t, m.SetCenterX(250))
require.NoError(t, m.SetCenterY(250))
require.NoError(t, m.SetScale(2))
require.NoError(t, m.Generate(io.Discard))
})
}

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 := validateInRange(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 := validateInRange(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 := validateInRange(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[int]{
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[int]{
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[int]{
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

@ -161,6 +161,69 @@ func TestNewCreateServerParams(t *testing.T) {
}
}
func TestUpdateServerParams_SetNumTribes(t *testing.T) {
t.Parallel()
type args struct {
numTribes domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numTribes: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numTribes: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numTribes < 0",
args: args{
numTribes: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numTribes",
Err: domain.MinGreaterEqualError[int]{
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.SetNumTribes(tt.args.numTribes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numTribes, params.NumTribes())
})
}
}
func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) {
t.Parallel()
@ -201,7 +264,7 @@ func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActiveTribes",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -224,6 +287,132 @@ func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) {
}
}
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[int]{
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()
type args struct {
numPlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numPlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numPlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numPlayers < 0",
args: args{
numPlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numPlayers",
Err: domain.MinGreaterEqualError[int]{
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.SetNumPlayers(tt.args.numPlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numPlayers, params.NumPlayers())
})
}
}
func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) {
t.Parallel()
@ -264,7 +453,7 @@ func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActivePlayers",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -287,6 +476,69 @@ func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) {
}
}
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[int]{
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()
@ -327,7 +579,7 @@ func TestUpdateServerParams_SetNumVillages(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -390,7 +642,7 @@ func TestUpdateServerParams_SetNumPlayerVillages(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numPlayerVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -453,7 +705,7 @@ func TestUpdateServerParams_SetNumBarbarianVillages(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numBarbarianVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -516,7 +768,7 @@ func TestUpdateServerParams_SetNumBonusVillages(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numBonusVillages",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -955,7 +1207,7 @@ func TestListServersParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -969,7 +1221,7 @@ func TestListServersParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.ServerListMaxLimit,
Current: domain.ServerListMaxLimit + 1,
},

View File

@ -67,7 +67,7 @@ func UnmarshalTribeFromDatabase(
createdAt time.Time,
deletedAt time.Time,
) (Tribe, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return Tribe{}, ValidationError{
Model: tribeModelName,
Field: "id",
@ -207,6 +207,7 @@ func (t Tribe) ToCursor() (TribeCursor, error) {
t.od.scoreDef,
t.od.scoreTotal,
t.points,
t.mostPoints,
t.dominance,
t.deletedAt,
)
@ -295,7 +296,7 @@ const tribeMetaModelName = "TribeMeta"
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalTribeMetaFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalTribeMetaFromDatabase(id int, name string, tag string, rawProfileURL string) (TribeMeta, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeMeta{}, ValidationError{
Model: tribeMetaModelName,
Field: "id",
@ -472,6 +473,8 @@ const (
TribeSortODScoreTotalDESC
TribeSortPointsASC
TribeSortPointsDESC
TribeSortMostPointsASC
TribeSortMostPointsDESC
TribeSortDominanceASC
TribeSortDominanceDESC
TribeSortDeletedAtASC
@ -510,6 +513,10 @@ func (s TribeSort) String() string {
return "points:ASC"
case TribeSortPointsDESC:
return "points:DESC"
case TribeSortMostPointsASC:
return "mostPoints:ASC"
case TribeSortMostPointsDESC:
return "mostPoints:DESC"
case TribeSortDominanceASC:
return "dominance:ASC"
case TribeSortDominanceDESC:
@ -530,6 +537,7 @@ type TribeCursor struct {
odScoreDef int
odScoreTotal int
points int
mostPoints int
dominance float64
deletedAt time.Time
}
@ -543,10 +551,11 @@ func NewTribeCursor(
odScoreDef int,
odScoreTotal int,
points int,
mostPoints int,
dominance float64,
deletedAt time.Time,
) (TribeCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "id",
@ -562,7 +571,7 @@ func NewTribeCursor(
}
}
if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreAtt, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreAtt",
@ -570,7 +579,7 @@ func NewTribeCursor(
}
}
if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreDef, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreDef",
@ -578,7 +587,7 @@ func NewTribeCursor(
}
}
if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
if err := validateInRange(odScoreTotal, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreTotal",
@ -586,7 +595,7 @@ func NewTribeCursor(
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
if err := validateInRange(points, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "points",
@ -594,6 +603,14 @@ func NewTribeCursor(
}
}
if err := validateInRange(mostPoints, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "mostPoints",
Err: err,
}
}
return TribeCursor{
id: id,
serverKey: serverKey,
@ -601,6 +618,7 @@ func NewTribeCursor(
odScoreDef: odScoreDef,
odScoreTotal: odScoreTotal,
points: points,
mostPoints: mostPoints,
dominance: dominance,
deletedAt: deletedAt,
}, nil
@ -643,6 +661,11 @@ func decodeTribeCursor(encoded string) (TribeCursor, error) {
return TribeCursor{}, ErrInvalidCursor
}
mostPoints, err := m.int("mostPoints")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
dominance, err := m.float64("dominance")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
@ -660,6 +683,7 @@ func decodeTribeCursor(encoded string) (TribeCursor, error) {
odScoreDef,
odScoreTotal,
points,
mostPoints,
dominance,
deletedAt,
)
@ -694,6 +718,10 @@ func (tc TribeCursor) Points() int {
return tc.points
}
func (tc TribeCursor) MostPoints() int {
return tc.mostPoints
}
func (tc TribeCursor) Dominance() float64 {
return tc.dominance
}
@ -718,6 +746,7 @@ func (tc TribeCursor) Encode() string {
{"odScoreDef", tc.odScoreDef},
{"odScoreTotal", tc.odScoreTotal},
{"points", tc.points},
{"mostPoints", tc.mostPoints},
{"dominance", tc.dominance},
{"deletedAt", tc.deletedAt},
})
@ -754,7 +783,7 @@ func (params *ListTribesParams) IDs() []int {
func (params *ListTribesParams) SetIDs(ids []int) error {
for i, id := range ids {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "ids",
@ -905,7 +934,7 @@ func (params *ListTribesParams) Limit() int {
}
func (params *ListTribesParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, TribeListMaxLimit); err != nil {
if err := validateInRange(limit, 1, TribeListMaxLimit); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "limit",
@ -969,6 +998,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

@ -31,7 +31,7 @@ func UnmarshalTribeChangeFromDatabase(
newTribeID int,
createdAt time.Time,
) (TribeChange, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeChange{}, ValidationError{
Model: tribeChangeModelName,
Field: "id",
@ -156,7 +156,7 @@ func NewCreateTribeChangeParams(
}
}
if err := validateIntInRange(playerID, 1, math.MaxInt); err != nil {
if err := validateInRange(playerID, 1, math.MaxInt); err != nil {
return CreateTribeChangeParams{}, ValidationError{
Model: createTribeChangeParamsModelName,
Field: "playerID",
@ -164,7 +164,7 @@ func NewCreateTribeChangeParams(
}
}
if err := validateIntInRange(oldTribeID, 0, math.MaxInt); err != nil {
if err := validateInRange(oldTribeID, 0, math.MaxInt); err != nil {
return CreateTribeChangeParams{}, ValidationError{
Model: createTribeChangeParamsModelName,
Field: "oldTribeID",
@ -172,7 +172,7 @@ func NewCreateTribeChangeParams(
}
}
if err := validateIntInRange(newTribeID, 0, math.MaxInt); err != nil {
if err := validateInRange(newTribeID, 0, math.MaxInt); err != nil {
return CreateTribeChangeParams{}, ValidationError{
Model: createTribeChangeParamsModelName,
Field: "newTribeID",
@ -297,7 +297,7 @@ type TribeChangeCursor struct {
const tribeChangeCursorModelName = "TribeChangeCursor"
func NewTribeChangeCursor(id int, serverKey string, createdAt time.Time) (TribeChangeCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeChangeCursor{}, ValidationError{
Model: tribeChangeCursorModelName,
Field: "id",
@ -436,7 +436,7 @@ func (params *ListTribeChangesParams) PlayerIDs() []int {
func (params *ListTribeChangesParams) SetPlayerIDs(playerIDs []int) error {
for i, id := range playerIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribeChangesParamsModelName,
Field: "playerIDs",
@ -457,7 +457,7 @@ func (params *ListTribeChangesParams) TribeIDs() []int {
func (params *ListTribeChangesParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribeChangesParamsModelName,
Field: "tribeIDs",
@ -542,7 +542,7 @@ func (params *ListTribeChangesParams) Limit() int {
}
func (params *ListTribeChangesParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, TribeChangeListMaxLimit); err != nil {
if err := validateInRange(limit, 1, TribeChangeListMaxLimit); err != nil {
return ValidationError{
Model: listTribeChangesParamsModelName,
Field: "limit",

View File

@ -53,7 +53,7 @@ func TestNewCreateTribeChangeParams(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "CreateTribeChangeParams",
Field: "playerID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -70,7 +70,7 @@ func TestNewCreateTribeChangeParams(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "CreateTribeChangeParams",
Field: "oldTribeID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -87,7 +87,7 @@ func TestNewCreateTribeChangeParams(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "CreateTribeChangeParams",
Field: "newTribeID",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -318,7 +318,7 @@ func TestNewTribeChangeCursor(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "TribeChangeCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -453,7 +453,7 @@ func TestListTribeChangesParams_SetPlayerIDs(t *testing.T) {
Model: "ListTribeChangesParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -513,7 +513,7 @@ func TestListTribeChangesParams_SetTribeIDs(t *testing.T) {
Model: "ListTribeChangesParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1071,7 +1071,7 @@ func TestListTribeChangesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1085,7 +1085,7 @@ func TestListTribeChangesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.TribeChangeListMaxLimit,
Current: domain.TribeChangeListMaxLimit + 1,
},

View File

@ -43,7 +43,7 @@ func UnmarshalTribeSnapshotFromDatabase(
date time.Time,
createdAt time.Time,
) (TribeSnapshot, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeSnapshot{}, ValidationError{
Model: tribeSnapshotModelName,
Field: "id",
@ -51,7 +51,7 @@ func UnmarshalTribeSnapshotFromDatabase(
}
}
if err := validateIntInRange(tribeID, 1, math.MaxInt); err != nil {
if err := validateInRange(tribeID, 1, math.MaxInt); err != nil {
return TribeSnapshot{}, ValidationError{
Model: tribeSnapshotModelName,
Field: "tribeID",
@ -295,7 +295,7 @@ type TribeSnapshotCursor struct {
const tribeSnapshotCursorModelName = "TribeSnapshotCursor"
func NewTribeSnapshotCursor(id int, serverKey string, date time.Time) (TribeSnapshotCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return TribeSnapshotCursor{}, ValidationError{
Model: tribeSnapshotCursorModelName,
Field: "id",
@ -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 {
@ -431,7 +430,7 @@ func (params *ListTribeSnapshotsParams) TribeIDs() []int {
func (params *ListTribeSnapshotsParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "tribeIDs",
@ -549,7 +548,7 @@ func (params *ListTribeSnapshotsParams) Limit() int {
}
func (params *ListTribeSnapshotsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, TribeSnapshotListMaxLimit); err != nil {
if err := validateInRange(limit, 1, TribeSnapshotListMaxLimit); err != nil {
return ValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "limit",

View File

@ -149,7 +149,7 @@ func TestNewTribeSnapshotCursor(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "TribeSnapshotCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -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())
})
}
}
@ -284,7 +284,7 @@ func TestListTribeSnapshotsParams_SetTribeIDs(t *testing.T) {
Model: "ListTribeSnapshotsParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -842,7 +842,7 @@ func TestListTribeSnapshotsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -856,7 +856,7 @@ func TestListTribeSnapshotsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.TribeSnapshotListMaxLimit,
Current: domain.TribeSnapshotListMaxLimit + 1,
},

View File

@ -255,6 +255,13 @@ func TestTribeSort_IsInConflict(t *testing.T) {
},
expectedRes: true,
},
{
name: "OK: mostPoints:ASC mostPoints:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortMostPointsASC, domain.TribeSortMostPointsDESC},
},
expectedRes: true,
},
{
name: "OK: dominance:ASC dominance:DESC",
args: args{
@ -292,6 +299,7 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef int
odScoreTotal int
points int
mostPoints int
dominance float64
deletedAt time.Time
}
@ -312,6 +320,7 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
@ -326,13 +335,14 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -347,13 +357,14 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreAtt",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -368,13 +379,14 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: -1,
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreDef",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -389,13 +401,14 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: -1,
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreTotal",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -410,13 +423,36 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: -1,
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "points",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: points < 0",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: -1,
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "mostPoints",
Err: domain.MinGreaterEqualError[int]{
Min: 0,
Current: -1,
},
@ -434,6 +470,7 @@ func TestNewTribeCursor(t *testing.T) {
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
mostPoints: validTribeCursor.MostPoints(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
@ -456,6 +493,7 @@ func TestNewTribeCursor(t *testing.T) {
tt.args.odScoreDef,
tt.args.odScoreTotal,
tt.args.points,
tt.args.mostPoints,
tt.args.dominance,
tt.args.deletedAt,
)
@ -469,6 +507,7 @@ func TestNewTribeCursor(t *testing.T) {
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
assert.Equal(t, tt.args.points, tc.Points())
assert.Equal(t, tt.args.mostPoints, tc.MostPoints())
assert.InDelta(t, tt.args.dominance, tc.Dominance(), 0.001)
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
assert.NotEmpty(t, tc.Encode())
@ -513,7 +552,7 @@ func TestListTribesParams_SetIDs(t *testing.T) {
Model: "ListTribesParams",
Field: "ids",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1125,7 +1164,7 @@ func TestListTribesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -1139,7 +1178,7 @@ func TestListTribesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.TribeListMaxLimit,
Current: domain.TribeListMaxLimit + 1,
},
@ -1205,3 +1244,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

@ -122,56 +122,56 @@ func (e SliceElementValidationError) Unwrap() error {
return e.Err
}
type MinGreaterEqualError struct {
Min int
Current int
type MinGreaterEqualError[T float32 | float64 | int] struct {
Min T
Current T
}
var _ ErrorWithParams = MinGreaterEqualError{}
var _ ErrorWithParams = MinGreaterEqualError[int]{}
func (e MinGreaterEqualError) Error() string {
return fmt.Sprintf("must be no less than %d (current: %d)", e.Min, e.Current)
func (e MinGreaterEqualError[T]) Error() string {
return fmt.Sprintf("must be no less than %v (current: %v)", e.Min, e.Current)
}
func (e MinGreaterEqualError) Type() ErrorType {
func (e MinGreaterEqualError[T]) Type() ErrorType {
return ErrorTypeIncorrectInput
}
const errorCodeMinGreaterEqual = "min-greater-equal"
func (e MinGreaterEqualError) Code() string {
func (e MinGreaterEqualError[T]) Code() string {
return errorCodeMinGreaterEqual
}
func (e MinGreaterEqualError) Params() map[string]any {
func (e MinGreaterEqualError[T]) Params() map[string]any {
return map[string]any{
"Min": e.Min,
"Current": e.Current,
}
}
type MaxLessEqualError struct {
Max int
Current int
type MaxLessEqualError[T float32 | float64 | int] struct {
Max T
Current T
}
var _ ErrorWithParams = MaxLessEqualError{}
var _ ErrorWithParams = MaxLessEqualError[int]{}
func (e MaxLessEqualError) Error() string {
return fmt.Sprintf("must be no greater than %d (current: %d)", e.Max, e.Current)
func (e MaxLessEqualError[T]) Error() string {
return fmt.Sprintf("must be no greater than %v (current: %v)", e.Max, e.Current)
}
func (e MaxLessEqualError) Type() ErrorType {
func (e MaxLessEqualError[T]) Type() ErrorType {
return ErrorTypeIncorrectInput
}
const errorCodeMaxLessEqual = "max-less-equal"
func (e MaxLessEqualError) Code() string {
func (e MaxLessEqualError[T]) Code() string {
return errorCodeMaxLessEqual
}
func (e MaxLessEqualError) Params() map[string]any {
func (e MaxLessEqualError[T]) Params() map[string]any {
return map[string]any{
"Max": e.Max,
"Current": e.Current,
@ -356,16 +356,16 @@ func validateServerKey(key string) error {
return validateStringLen(key, serverKeyMinLength, serverKeyMaxLength)
}
func validateIntInRange(current, min, max int) error {
func validateInRange[T float32 | float64 | int](current, min, max T) error {
if current < min {
return MinGreaterEqualError{
return MinGreaterEqualError[T]{
Min: min,
Current: current,
}
}
if current > max {
return MaxLessEqualError{
return MaxLessEqualError[T]{
Max: max,
Current: current,
}

View File

@ -269,7 +269,7 @@ func (params *ListVersionsParams) Limit() int {
}
func (params *ListVersionsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, VersionListMaxLimit); err != nil {
if err := validateInRange(limit, 1, VersionListMaxLimit); err != nil {
return ValidationError{
Model: listVersionsParamsModelName,
Field: "limit",

View File

@ -324,7 +324,7 @@ func TestListVersionsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -338,7 +338,7 @@ func TestListVersionsParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.VersionListMaxLimit,
Current: domain.VersionListMaxLimit + 1,
},

View File

@ -49,7 +49,7 @@ func UnmarshalVillageFromDatabase(
rawProfileURL string,
createdAt time.Time,
) (Village, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return Village{}, ValidationError{
Model: villageModelName,
Field: "id",
@ -263,7 +263,7 @@ func UnmarshalVillageMetaFromDatabase(
continent string,
rawProfileURL string,
) (VillageMeta, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return VillageMeta{}, ValidationError{
Model: villageMetaModelName,
Field: "id",
@ -423,7 +423,7 @@ type VillageCursor struct {
const villageCursorModelName = "VillageCursor"
func NewVillageCursor(id int, serverKey string) (VillageCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return VillageCursor{}, ValidationError{
Model: villageCursorModelName,
Field: "id",
@ -528,7 +528,7 @@ func (params *ListVillagesParams) IDs() []int {
func (params *ListVillagesParams) SetIDs(ids []int) error {
for i, id := range ids {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listVillagesParamsModelName,
Field: "ids",
@ -623,7 +623,7 @@ func (params *ListVillagesParams) PlayerIDs() []int {
func (params *ListVillagesParams) SetPlayerIDs(playerIDs []int) error {
for i, id := range playerIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listVillagesParamsModelName,
Field: "playerIDs",
@ -644,7 +644,7 @@ func (params *ListVillagesParams) TribeIDs() []int {
func (params *ListVillagesParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
if err := validateInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listVillagesParamsModelName,
Field: "tribeIDs",
@ -711,7 +711,7 @@ func (params *ListVillagesParams) Limit() int {
}
func (params *ListVillagesParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, VillageListMaxLimit); err != nil {
if err := validateInRange(limit, 1, VillageListMaxLimit); err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "limit",

View File

@ -181,7 +181,7 @@ func TestNewVillageCursor(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "VillageCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -260,7 +260,7 @@ func TestListVillagesParams_SetIDs(t *testing.T) {
Model: "ListVillagesParams",
Field: "ids",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -545,7 +545,7 @@ func TestListVillagesParams_SetPlayerIDs(t *testing.T) {
Model: "ListVillagesParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -605,7 +605,7 @@ func TestListVillagesParams_SetTribeIDs(t *testing.T) {
Model: "ListVillagesParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -814,7 +814,7 @@ func TestListVillagesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Err: domain.MinGreaterEqualError[int]{
Min: 1,
Current: 0,
},
@ -828,7 +828,7 @@ func TestListVillagesParams_SetLimit(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Err: domain.MaxLessEqualError[int]{
Max: domain.VillageListMaxLimit,
Current: domain.VillageListMaxLimit + 1,
},

View File

@ -158,10 +158,12 @@ func TestDataSync(t *testing.T) {
ctx,
port.NewServerWatermillConsumer(
serverSvc,
nil,
serverSub,
nopLogger,
marshaler,
serverCmdSync,
"",
serverEventSynced,
tribeEventSynced,
playerEventSynced,
@ -248,8 +250,12 @@ func TestDataSync(t *testing.T) {
assert.Equal(collect, expected["URL"], actual.URL().String(), msg)
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
@ -155,6 +193,7 @@ func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error {
rawPayload.ServerKey,
rawPayload.ServerURL,
rawPayload.VersionCode,
rawPayload.NumTribes,
rawPayload.NumActiveTribes,
)
if err != nil {
@ -181,6 +220,7 @@ func (c *ServerWatermillConsumer) updateNumPlayers(msg *message.Message) error {
rawPayload.ServerKey,
rawPayload.ServerURL,
rawPayload.VersionCode,
rawPayload.NumPlayers,
rawPayload.NumActivePlayers,
)
if err != nil {

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

@ -258,7 +258,7 @@ func TestListEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -294,7 +294,7 @@ func TestListEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.EnnoblementListMaxLimit,
Current: limit,
}
@ -866,7 +866,7 @@ func TestListPlayerEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -902,7 +902,7 @@ func TestListPlayerEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.EnnoblementListMaxLimit,
Current: limit,
}
@ -1532,7 +1532,7 @@ func TestListTribeEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -1568,7 +1568,7 @@ func TestListTribeEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.EnnoblementListMaxLimit,
Current: limit,
}
@ -2185,7 +2185,7 @@ func TestListVillageEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -2221,7 +2221,7 @@ func TestListVillageEnnoblements(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.EnnoblementListMaxLimit,
Current: limit,
}

View File

@ -24,6 +24,8 @@ var apiPlayerSortAllowedValues = []domain.PlayerSort{
domain.PlayerSortODScoreTotalDESC,
domain.PlayerSortPointsASC,
domain.PlayerSortPointsDESC,
domain.PlayerSortMostPointsASC,
domain.PlayerSortMostPointsDESC,
domain.PlayerSortDeletedAtASC,
domain.PlayerSortDeletedAtDESC,
}

View File

@ -192,7 +192,7 @@ func TestListPlayerPlayerSnapshots(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -228,7 +228,7 @@ func TestListPlayerPlayerSnapshots(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.PlayerSnapshotListMaxLimit,
Current: limit,
}

View File

@ -181,6 +181,33 @@ func TestListVersionPlayers(t *testing.T) {
}))
},
},
{
name: "OK: sort=[mostPoints:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "mostPoints: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.ListPlayersWithServersResponse](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.PlayerWithServer) int {
return cmp.Or(
cmp.Compare(a.Server.Key, b.Server.Key),
cmp.Compare(a.MostPoints, b.MostPoints)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: name",
reqModifier: func(t *testing.T, req *http.Request) {
@ -343,7 +370,7 @@ func TestListVersionPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -379,7 +406,7 @@ func TestListVersionPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.PlayerListMaxLimit,
Current: limit,
}
@ -653,7 +680,7 @@ func TestListVersionPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
id, err := strconv.Atoi(req.URL.Query().Get("id"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: id,
}
@ -1023,6 +1050,32 @@ func TestListServerPlayers(t *testing.T) {
}))
},
},
{
name: "OK: sort=[mostPoints:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "mostPoints: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.ListPlayersWithServersResponse](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.PlayerWithServer) int {
return cmp.Or(
cmp.Compare(a.MostPoints, b.MostPoints)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: name",
reqModifier: func(t *testing.T, req *http.Request) {
@ -1185,7 +1238,7 @@ func TestListServerPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -1221,7 +1274,7 @@ func TestListServerPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.PlayerListMaxLimit,
Current: limit,
}
@ -1495,7 +1548,7 @@ func TestListServerPlayers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
id, err := strconv.Atoi(req.URL.Query().Get("id"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: id,
}
@ -1863,6 +1916,32 @@ func TestListTribeMembers(t *testing.T) {
}))
},
},
{
name: "OK: sort=[mostPoints:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "mostPoints: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.ListPlayersWithServersResponse](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.PlayerWithServer) int {
return cmp.Or(
cmp.Compare(a.MostPoints, b.MostPoints)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "ERR: limit is not an integer",
reqModifier: func(t *testing.T, req *http.Request) {
@ -1909,7 +1988,7 @@ func TestListTribeMembers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -1945,7 +2024,7 @@ func TestListTribeMembers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.PlayerListMaxLimit,
Current: limit,
}
@ -2339,7 +2418,7 @@ func TestGetPlayer(t *testing.T) {
require.Len(t, pathSegments, 8)
id, err := strconv.Atoi(pathSegments[7])
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: id,
}

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[int]{
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[int]{
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

@ -198,7 +198,7 @@ func TestListServers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
domainErr := domain.MinGreaterEqualError[int]{
Min: 1,
Current: limit,
}
@ -234,7 +234,7 @@ func TestListServers(t *testing.T) {
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
domainErr := domain.MaxLessEqualError[int]{
Max: domain.ServerListMaxLimit,
Current: limit,
}

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...,

Some files were not shown because too many files have changed in this diff Show More