diff --git a/cmd/twhelp/internal/serve/serve.go b/cmd/twhelp/internal/serve/serve.go index 960b4fb..bedefef 100644 --- a/cmd/twhelp/internal/serve/serve.go +++ b/cmd/twhelp/internal/serve/serve.go @@ -97,6 +97,7 @@ func newServer(cfg serverConfig) (*http.Server, error) { tribeRepo := bundb.NewTribe(cfg.db) playerRepo := bundb.NewPlayer(cfg.db) villageRepo := bundb.NewVillage(cfg.db) + ennoblementRepo := bundb.NewEnnoblement(cfg.db) // services versionSvc := service.NewVersion(versionRepo) @@ -104,17 +105,19 @@ func newServer(cfg serverConfig) (*http.Server, error) { tribeSvc := service.NewTribe(tribeRepo, client) playerSvc := service.NewPlayer(playerRepo, client) villageSvc := service.NewVillage(villageRepo, client) + ennoblementSvc := service.NewEnnoblement(ennoblementRepo, client) // router r := chi.NewRouter() r.Use(getChiMiddlewares(cfg.logger)...) r.Mount("/api", rest.NewRouter(rest.RouterConfig{ - VersionService: versionSvc, - ServerService: serverSvc, - TribeService: tribeSvc, - PlayerService: playerSvc, - VillageService: villageSvc, + VersionService: versionSvc, + ServerService: serverSvc, + TribeService: tribeSvc, + PlayerService: playerSvc, + VillageService: villageSvc, + EnnoblementService: ennoblementSvc, CORS: rest.CORSConfig{ Enabled: apiCfg.CORSEnabled, AllowedOrigins: nil, diff --git a/internal/bundb/ennoblement.go b/internal/bundb/ennoblement.go index 15d3480..e6112c5 100644 --- a/internal/bundb/ennoblement.go +++ b/internal/bundb/ennoblement.go @@ -76,7 +76,35 @@ func (l listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) (*bun.SelectQue var err error if l.params.ServerKeys != nil { - q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys)) + q = q.Where("ennoblement.server_key IN (?)", bun.In(l.params.ServerKeys)) + } + + if l.params.CreatedAtGTE.Valid { + q = q.Where("ennoblement.created_at >= ?", l.params.CreatedAtGTE.Time) + } + + if l.params.CreatedAtLTE.Valid { + q = q.Where("ennoblement.created_at <= ?", l.params.CreatedAtLTE.Time) + } + + if l.params.IncludeVillage { + q = q.Relation("Village") + } + + if l.params.IncludeNewOwner { + q = q.Relation("NewOwner") + } + + if l.params.IncludeNewTribe { + q = q.Relation("NewTribe") + } + + if l.params.IncludeOldOwner { + q = q.Relation("OldOwner") + } + + if l.params.IncludeOldTribe { + q = q.Relation("OldTribe") } q, err = l.applySort(q) diff --git a/internal/bundb/ennoblement_test.go b/internal/bundb/ennoblement_test.go index eab7608..95f04eb 100644 --- a/internal/bundb/ennoblement_test.go +++ b/internal/bundb/ennoblement_test.go @@ -123,38 +123,21 @@ func TestEnnoblement_List(t *testing.T) { ennoblements := getAllEnnoblementsFromFixture(t, fixture) type expectedEnnoblement struct { - id int64 - serverKey string - } - - newExpectedEnnoblement := func(e model.Ennoblement) expectedEnnoblement { - return expectedEnnoblement{ - id: e.ID, - serverKey: e.ServerKey, - } + id int64 + serverKey string + villageID int64 + newOwnerID int64 + newTribeID int64 + oldOwnerID int64 + oldTribeID int64 } allEnnoblements := make([]expectedEnnoblement, 0, len(ennoblements)) for _, ennoblement := range ennoblements { - allEnnoblements = append(allEnnoblements, newExpectedEnnoblement(ennoblement)) - } - - //nolint:prealloc - var ennoblementsPL169 []expectedEnnoblement - for _, ennoblement := range ennoblements { - if ennoblement.ServerKey != "pl169" { - continue - } - ennoblementsPL169 = append(ennoblementsPL169, newExpectedEnnoblement(ennoblement)) - } - - //nolint:prealloc - var ennoblementsIT70 []expectedEnnoblement - for _, ennoblement := range ennoblements { - if ennoblement.ServerKey != "it70" { - continue - } - ennoblementsIT70 = append(ennoblementsIT70, newExpectedEnnoblement(ennoblement)) + allEnnoblements = append(allEnnoblements, expectedEnnoblement{ + id: ennoblement.ID, + serverKey: ennoblement.ServerKey, + }) } tests := []struct { @@ -182,8 +165,17 @@ func TestEnnoblement_List(t *testing.T) { Count: true, ServerKeys: []string{"pl169"}, }, - expectedEnnoblements: ennoblementsPL169, - expectedCount: int64(len(ennoblementsPL169)), + expectedEnnoblements: []expectedEnnoblement{ + { + id: 1, + serverKey: "pl169", + }, + { + id: 2, + serverKey: "pl169", + }, + }, + expectedCount: 2, }, { name: "ServerKeys=[pl169],Sort=[{By=CreatedAt,Direction=DESC}],Limit=1,Count=true", @@ -198,9 +190,12 @@ func TestEnnoblement_List(t *testing.T) { }, }, expectedEnnoblements: []expectedEnnoblement{ - newExpectedEnnoblement(getEnnoblementFromFixture(t, fixture, "pl169-village-2-2")), + { + id: 2, + serverKey: "pl169", + }, }, - expectedCount: int64(len(ennoblementsPL169)), + expectedCount: 2, }, { name: "ServerKeys=[it70],Sort=[{By=ID,Direction=DESC}],Limit=1,Offset=1,Count=true", @@ -216,9 +211,70 @@ func TestEnnoblement_List(t *testing.T) { }, }, expectedEnnoblements: []expectedEnnoblement{ - newExpectedEnnoblement(getEnnoblementFromFixture(t, fixture, "it70-village-2-1")), + { + id: 3, + serverKey: "it70", + }, }, - expectedCount: int64(len(ennoblementsIT70)), + expectedCount: 2, + }, + { + name: "ServerKeys=[it70],IncludeVillage=true,IncludeNewOwner=true,IncludeNewTribe=true,IncludeOldOwner=true,IncludeOldTribe=true,Count=true", + params: domain.ListEnnoblementsParams{ + Count: true, + ServerKeys: []string{"it70"}, + IncludeVillage: true, + IncludeNewOwner: true, + IncludeNewTribe: true, + IncludeOldOwner: true, + IncludeOldTribe: true, + }, + expectedEnnoblements: []expectedEnnoblement{ + { + id: 3, + serverKey: "it70", + villageID: 10023, + newOwnerID: 848881282, + newTribeID: 31, + oldOwnerID: 0, + oldTribeID: 0, + }, + { + id: 4, + serverKey: "it70", + villageID: 10023, + newOwnerID: 578014, + newTribeID: 1, + oldOwnerID: 848881282, + oldTribeID: 31, + }, + }, + expectedCount: 2, + }, + { + name: "CreatedAtLTE=2021-12-31T23:59:59Z,CreatedAtGTE=2021-01-01T00:00:00Z,Count=true", + params: domain.ListEnnoblementsParams{ + Count: true, + CreatedAtLTE: domain.NullTime{ + Time: time.Date(2021, time.December, 31, 23, 59, 59, 0, time.UTC), + Valid: true, + }, + CreatedAtGTE: domain.NullTime{ + Time: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC), + Valid: true, + }, + }, + expectedEnnoblements: []expectedEnnoblement{ + { + id: 1, + serverKey: "pl169", + }, + { + id: 2, + serverKey: "pl169", + }, + }, + expectedCount: 2, }, { name: "ERR: unsupported sort by", @@ -256,17 +312,50 @@ func TestEnnoblement_List(t *testing.T) { assert.Len(t, res, len(tt.expectedEnnoblements)) for _, expEnnoblement := range tt.expectedEnnoblements { found := false + for _, ennoblement := range res { - if ennoblement.ID == expEnnoblement.id && ennoblement.ServerKey == expEnnoblement.serverKey { - found = true - break + if ennoblement.ID != expEnnoblement.id { + continue } + + if ennoblement.ServerKey != expEnnoblement.serverKey { + continue + } + + if ennoblement.Village.Village.ID != expEnnoblement.villageID { + continue + } + + if ennoblement.NewOwner.Player.ID != expEnnoblement.newOwnerID { + continue + } + + if ennoblement.NewTribe.Tribe.ID != expEnnoblement.newTribeID { + continue + } + + if ennoblement.OldOwner.Player.ID != expEnnoblement.oldOwnerID { + continue + } + + if ennoblement.OldTribe.Tribe.ID != expEnnoblement.oldTribeID { + continue + } + + found = true + break } + assert.True( t, found, - "ennoblement (id=%d,serverkey=%s) not found", + "ennoblement (id=%d,villageID=%d,newOwnerID=%d,newTribeID=%d,oldOwnerID=%d,oldTribeID=%d,serverkey=%s) not found", expEnnoblement.id, + expEnnoblement.villageID, + expEnnoblement.newOwnerID, + expEnnoblement.newTribeID, + expEnnoblement.oldOwnerID, + expEnnoblement.oldTribeID, expEnnoblement.serverKey, ) } diff --git a/internal/bundb/internal/model/building_info_test.go b/internal/bundb/internal/model/building_info_test.go index 7d64792..60adb0c 100644 --- a/internal/bundb/internal/model/building_info_test.go +++ b/internal/bundb/internal/model/building_info_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewBuildingInfo(t *testing.T) { +func TestBuildingInfo(t *testing.T) { t.Parallel() info := domain.BuildingInfo{ diff --git a/internal/bundb/internal/model/ennoblement.go b/internal/bundb/internal/model/ennoblement.go index 9b0233d..1c24fc5 100644 --- a/internal/bundb/internal/model/ennoblement.go +++ b/internal/bundb/internal/model/ennoblement.go @@ -14,10 +14,15 @@ type Ennoblement struct { ID int64 `bun:"id,pk,autoincrement,identity"` ServerKey string `bun:"server_key,nullzero,pk,type:varchar(100)"` VillageID int64 `bun:"village_id,nullzero,notnull"` + Village Village `bun:"village,rel:belongs-to,join:village_id=id,join:server_key=server_key"` NewOwnerID int64 `bun:"new_owner_id,nullzero"` + NewOwner Player `bun:"new_owner,rel:belongs-to,join:new_owner_id=id,join:server_key=server_key"` NewTribeID int64 `bun:"new_tribe_id,nullzero"` + NewTribe Tribe `bun:"new_tribe,rel:belongs-to,join:new_tribe_id=id,join:server_key=server_key"` OldOwnerID int64 `bun:"old_owner_id,nullzero"` + OldOwner Player `bun:"old_owner,rel:belongs-to,join:old_owner_id=id,join:server_key=server_key"` OldTribeID int64 `bun:"old_tribe_id,nullzero"` + OldTribe Tribe `bun:"old_tribe,rel:belongs-to,join:old_tribe_id=id,join:server_key=server_key"` Points int64 `bun:"points,default:0"` CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` } @@ -36,6 +41,46 @@ func NewEnnoblement(p domain.CreateEnnoblementParams) Ennoblement { } func (e Ennoblement) ToDomain() domain.Ennoblement { + var newOwner domain.NullPlayer + if e.NewOwner.ID > 0 { + newOwner = domain.NullPlayer{ + Valid: true, + Player: e.NewOwner.ToDomain(), + } + } + + var newTribe domain.NullTribe + if e.NewTribe.ID > 0 { + newTribe = domain.NullTribe{ + Valid: true, + Tribe: e.NewTribe.ToDomain(), + } + } + + var oldOwner domain.NullPlayer + if e.OldOwner.ID > 0 { + oldOwner = domain.NullPlayer{ + Valid: true, + Player: e.OldOwner.ToDomain(), + } + } + + var oldTribe domain.NullTribe + if e.OldTribe.ID > 0 { + oldTribe = domain.NullTribe{ + Valid: true, + Tribe: e.OldTribe.ToDomain(), + } + } + + var village domain.NullVillage + if e.Village.ID > 0 { + village = domain.NullVillage{ + Valid: true, + Village: e.Village.ToDomain(), + } + } + return domain.Ennoblement{ BaseEnnoblement: domain.BaseEnnoblement{ VillageID: e.VillageID, @@ -47,6 +92,11 @@ func (e Ennoblement) ToDomain() domain.Ennoblement { CreatedAt: e.CreatedAt, }, ID: e.ID, + Village: village, + NewOwner: newOwner, + NewTribe: newTribe, + OldOwner: oldOwner, + OldTribe: oldTribe, ServerKey: e.ServerKey, } } diff --git a/internal/bundb/internal/model/ennoblement_test.go b/internal/bundb/internal/model/ennoblement_test.go index 196ba40..999603b 100644 --- a/internal/bundb/internal/model/ennoblement_test.go +++ b/internal/bundb/internal/model/ennoblement_test.go @@ -10,7 +10,7 @@ import ( "gitea.dwysokinski.me/twhelp/core/internal/domain" ) -func TestNewEnnoblement(t *testing.T) { +func TestEnnoblement(t *testing.T) { t.Parallel() var id int64 = 1234 @@ -29,8 +29,39 @@ func TestNewEnnoblement(t *testing.T) { result := model.NewEnnoblement(params) result.ID = id - ennoblement := result.ToDomain() - assert.Equal(t, params.BaseEnnoblement, ennoblement.BaseEnnoblement) - assert.Equal(t, params.ServerKey, ennoblement.ServerKey) - assert.Equal(t, id, ennoblement.ID) + + ennoblementWithoutRelations := result.ToDomain() + assert.Equal(t, params.BaseEnnoblement, ennoblementWithoutRelations.BaseEnnoblement) + assert.Equal(t, params.ServerKey, ennoblementWithoutRelations.ServerKey) + assert.Equal(t, id, ennoblementWithoutRelations.ID) + + result.Village = model.Village{ + ID: 12345, + } + result.NewOwner = model.Player{ + ID: 11231, + } + result.NewTribe = model.Tribe{ + ID: 11232, + } + result.OldOwner = model.Player{ + ID: 11233, + } + result.OldTribe = model.Tribe{ + ID: 11234, + } + ennoblementWithRelations := result.ToDomain() + assert.Equal(t, params.BaseEnnoblement, ennoblementWithRelations.BaseEnnoblement) + assert.Equal(t, params.ServerKey, ennoblementWithRelations.ServerKey) + assert.Equal(t, id, ennoblementWithRelations.ID) + assert.True(t, ennoblementWithRelations.Village.Valid) + assert.Equal(t, result.Village.ID, ennoblementWithRelations.Village.Village.ID) + assert.True(t, ennoblementWithRelations.NewOwner.Valid) + assert.Equal(t, result.NewOwner.ID, ennoblementWithRelations.NewOwner.Player.ID) + assert.True(t, ennoblementWithRelations.NewTribe.Valid) + assert.Equal(t, result.NewTribe.ID, ennoblementWithRelations.NewTribe.Tribe.ID) + assert.True(t, ennoblementWithRelations.OldOwner.Valid) + assert.Equal(t, result.OldOwner.ID, ennoblementWithRelations.OldOwner.Player.ID) + assert.True(t, ennoblementWithRelations.OldTribe.Valid) + assert.Equal(t, result.OldTribe.ID, ennoblementWithRelations.OldTribe.Tribe.ID) } diff --git a/internal/bundb/internal/model/player_test.go b/internal/bundb/internal/model/player_test.go index 7a2e32d..04caf12 100644 --- a/internal/bundb/internal/model/player_test.go +++ b/internal/bundb/internal/model/player_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewPlayer(t *testing.T) { +func TestPlayer(t *testing.T) { t.Parallel() deletedAt := time.Now().Add(-100 * time.Hour) @@ -44,20 +44,20 @@ func TestNewPlayer(t *testing.T) { result := model.NewPlayer(playerParams) result.DeletedAt = deletedAt - player := result.ToDomain() - assert.Equal(t, playerParams.BasePlayer, player.BasePlayer) - assert.Equal(t, playerParams.ServerKey, player.ServerKey) - assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond) - assert.Zero(t, player.Tribe) - assert.Equal(t, deletedAt, player.DeletedAt) + playerWithoutTribe := result.ToDomain() + assert.Equal(t, playerParams.BasePlayer, playerWithoutTribe.BasePlayer) + assert.Equal(t, playerParams.ServerKey, playerWithoutTribe.ServerKey) + assert.WithinDuration(t, time.Now(), playerWithoutTribe.CreatedAt, 10*time.Millisecond) + assert.Zero(t, playerWithoutTribe.Tribe) + assert.Equal(t, deletedAt, playerWithoutTribe.DeletedAt) result.Tribe = model.NewTribe(tribeParams) result.DeletedAt = time.Time{} - player = result.ToDomain() - assert.Equal(t, playerParams.BasePlayer, player.BasePlayer) - assert.Equal(t, playerParams.ServerKey, player.ServerKey) - assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond) - assert.Equal(t, tribeParams.BaseTribe, player.Tribe.Tribe.BaseTribe) - assert.True(t, player.Tribe.Valid) - assert.Zero(t, player.DeletedAt) + playerWithTribe := result.ToDomain() + assert.Equal(t, playerParams.BasePlayer, playerWithTribe.BasePlayer) + assert.Equal(t, playerParams.ServerKey, playerWithTribe.ServerKey) + assert.WithinDuration(t, time.Now(), playerWithTribe.CreatedAt, 10*time.Millisecond) + assert.Zero(t, playerWithTribe.DeletedAt) + assert.True(t, playerWithTribe.Tribe.Valid) + assert.Equal(t, tribeParams.ID, playerWithTribe.Tribe.Tribe.ID) } diff --git a/internal/bundb/internal/model/server_config_test.go b/internal/bundb/internal/model/server_config_test.go index 491cf64..c1438ae 100644 --- a/internal/bundb/internal/model/server_config_test.go +++ b/internal/bundb/internal/model/server_config_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewServerConfig(t *testing.T) { +func TestServerConfig(t *testing.T) { t.Parallel() cfg := domain.ServerConfig{ diff --git a/internal/bundb/internal/model/tribe_test.go b/internal/bundb/internal/model/tribe_test.go index 2cbc914..d60c0f6 100644 --- a/internal/bundb/internal/model/tribe_test.go +++ b/internal/bundb/internal/model/tribe_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewTribe(t *testing.T) { +func TestTribe(t *testing.T) { t.Parallel() deletedAt := time.Now().Add(-100 * time.Hour) diff --git a/internal/bundb/internal/model/unit_info_test.go b/internal/bundb/internal/model/unit_info_test.go index 8140061..95459dd 100644 --- a/internal/bundb/internal/model/unit_info_test.go +++ b/internal/bundb/internal/model/unit_info_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewUnitInfo(t *testing.T) { +func TestUnitInfo(t *testing.T) { t.Parallel() info := domain.UnitInfo{ diff --git a/internal/bundb/internal/model/village_test.go b/internal/bundb/internal/model/village_test.go index d913c1c..2e35572 100644 --- a/internal/bundb/internal/model/village_test.go +++ b/internal/bundb/internal/model/village_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewVillage(t *testing.T) { +func TestVillage(t *testing.T) { t.Parallel() params := domain.CreateVillageParams{ @@ -27,8 +27,24 @@ func TestNewVillage(t *testing.T) { } result := model.NewVillage(params) - village := result.ToDomain() - assert.Equal(t, params.BaseVillage, village.BaseVillage) - assert.Equal(t, params.ServerKey, village.ServerKey) - assert.WithinDuration(t, time.Now(), village.CreatedAt, 10*time.Millisecond) + + villageWithoutPlayer := result.ToDomain() + assert.Equal(t, params.BaseVillage, villageWithoutPlayer.BaseVillage) + assert.Equal(t, params.ServerKey, villageWithoutPlayer.ServerKey) + assert.WithinDuration(t, time.Now(), villageWithoutPlayer.CreatedAt, 10*time.Millisecond) + + result.Player = model.Player{ + ID: 12321, + Tribe: model.Tribe{ + ID: 1234112, + }, + } + villageWithPlayer := result.ToDomain() + assert.Equal(t, params.BaseVillage, villageWithPlayer.BaseVillage) + assert.Equal(t, params.ServerKey, villageWithPlayer.ServerKey) + assert.WithinDuration(t, time.Now(), villageWithPlayer.CreatedAt, 10*time.Millisecond) + assert.True(t, villageWithPlayer.Player.Valid) + assert.Equal(t, result.Player.ID, villageWithPlayer.Player.Player.ID) + assert.True(t, villageWithPlayer.Player.Player.Tribe.Valid) + assert.Equal(t, result.Player.Tribe.ID, villageWithPlayer.Player.Player.Tribe.Tribe.ID) } diff --git a/internal/bundb/testdata/fixture.yml b/internal/bundb/testdata/fixture.yml index 9cceac2..e53adbc 100644 --- a/internal/bundb/testdata/fixture.yml +++ b/internal/bundb/testdata/fixture.yml @@ -7239,7 +7239,7 @@ created_at: 2022-02-25T15:00:10.000Z - model: Ennoblement rows: - - _id: pl169-village-2-1 + - _id: pl169-village-2-1 # id: 1 server_key: pl169 village_id: 1112 new_owner_id: 699513260 @@ -7248,7 +7248,7 @@ old_tribe_id: 0 points: 2500 created_at: 2021-09-30T09:01:00.000Z - - _id: pl169-village-2-2 + - _id: pl169-village-2-2 # id: 2 server_key: pl169 village_id: 1112 new_owner_id: 699783765 @@ -7257,7 +7257,7 @@ old_tribe_id: 27 points: 5000 created_at: 2021-10-30T09:01:00.000Z - - _id: it70-village-2-1 + - _id: it70-village-2-1 # id: 3 server_key: it70 village_id: 10023 new_owner_id: 848881282 @@ -7266,7 +7266,7 @@ old_tribe_id: 0 points: 2500 created_at: 2022-03-15T15:00:10.000Z - - _id: it70-village-2-2 + - _id: it70-village-2-2 # id: 4 server_key: it70 village_id: 10023 new_owner_id: 578014 diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 2f2a417..1b4bf7c 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -1,5 +1,7 @@ package domain +import "time" + type NullBool struct { Bool bool Valid bool // Valid is true if Bool is not NULL @@ -15,6 +17,11 @@ type NullInt32 struct { Valid bool // Valid is true if Int32 is not NULL } +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + type Pagination struct { Offset int32 Limit int32 diff --git a/internal/domain/ennoblement.go b/internal/domain/ennoblement.go index 871f9e0..66f4708 100644 --- a/internal/domain/ennoblement.go +++ b/internal/domain/ennoblement.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "fmt" + "time" +) type BaseEnnoblement struct { VillageID int64 @@ -16,6 +19,11 @@ type Ennoblement struct { BaseEnnoblement ID int64 + Village NullVillage + NewOwner NullPlayer + NewTribe NullTribe + OldOwner NullPlayer + OldTribe NullTribe ServerKey string } @@ -32,16 +40,33 @@ const ( EnnoblementSortByCreatedAt ) +func NewEnnoblementSortBy(s string) (EnnoblementSortBy, error) { + switch s { + case "id": + return EnnoblementSortByID, nil + case "createdAt": + return EnnoblementSortByCreatedAt, nil + } + return 0, fmt.Errorf("%w: \"%s\"", ErrUnsupportedSortBy, s) +} + type EnnoblementSort struct { By EnnoblementSortBy Direction SortDirection } type ListEnnoblementsParams struct { - ServerKeys []string - Pagination Pagination - Sort []EnnoblementSort - Count bool + ServerKeys []string + CreatedAtGTE NullTime + CreatedAtLTE NullTime + Pagination Pagination + Sort []EnnoblementSort + IncludeVillage bool + IncludeNewOwner bool + IncludeNewTribe bool + IncludeOldOwner bool + IncludeOldTribe bool + Count bool } type RefreshEnnoblementsCmdPayload struct { diff --git a/internal/domain/ennoblement_test.go b/internal/domain/ennoblement_test.go new file mode 100644 index 0000000..bd4ee5a --- /dev/null +++ b/internal/domain/ennoblement_test.go @@ -0,0 +1,50 @@ +package domain_test + +import ( + "testing" + + "gitea.dwysokinski.me/twhelp/core/internal/domain" + "github.com/stretchr/testify/assert" +) + +func TestNewEnnoblementSortBy(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + tests := []struct { + s string + output domain.EnnoblementSortBy + }{ + { + s: "id", + output: domain.EnnoblementSortByID, + }, + { + s: "createdAt", + output: domain.EnnoblementSortByCreatedAt, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.s, func(t *testing.T) { + t.Parallel() + + res, err := domain.NewEnnoblementSortBy(tt.s) + assert.NoError(t, err) + assert.Equal(t, tt.output, res) + }) + } + }) + + t.Run("ERR: unsupported sort by", func(t *testing.T) { + t.Parallel() + + res, err := domain.NewEnnoblementSortBy("unsupported") + assert.ErrorIs(t, err, domain.ErrUnsupportedSortBy) + assert.Zero(t, res) + }) +} diff --git a/internal/domain/village.go b/internal/domain/village.go index eab6af5..291a2cc 100644 --- a/internal/domain/village.go +++ b/internal/domain/village.go @@ -24,6 +24,11 @@ type Village struct { CreatedAt time.Time } +type NullVillage struct { + Village Village + Valid bool +} + type CreateVillageParams struct { BaseVillage diff --git a/internal/rest/.swaggo b/internal/rest/.swaggo index b132a64..8cbdad1 100644 --- a/internal/rest/.swaggo +++ b/internal/rest/.swaggo @@ -1,2 +1,3 @@ replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullTribeMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.TribeMeta replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullPlayerMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.PlayerMeta +replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullVillageMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.VillageMeta diff --git a/internal/rest/ennoblement.go b/internal/rest/ennoblement.go new file mode 100644 index 0000000..be9ed18 --- /dev/null +++ b/internal/rest/ennoblement.go @@ -0,0 +1,91 @@ +package rest + +import ( + "context" + "net/http" + + "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model" + "github.com/go-chi/chi/v5" + + "gitea.dwysokinski.me/twhelp/core/internal/domain" +) + +const ( + ennoblementDefaultOffset = 0 + ennoblementDefaultLimit = 3000 +) + +//counterfeiter:generate -o internal/mock/ennoblement_service.gen.go . EnnoblementService +type EnnoblementService interface { + List(ctx context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) +} + +type ennoblement struct { + svc EnnoblementService +} + +// @ID listEnnoblements +// @Summary List ennoblements +// @Description List all ennoblements +// @Tags versions,servers,ennoblements +// @Produce json +// @Success 200 {object} model.ListEnnoblementsResp +// @Success 400 {object} model.ErrorResp +// @Success 404 {object} model.ErrorResp +// @Failure 500 {object} model.ErrorResp +// @Header 200 {integer} X-Total-Count "Total number of records" +// @Param versionCode path string true "Version code" +// @Param serverKey path string true "Server key" +// @Param since query string false "only show items created after the given time, this is a timestamp in RFC 3339 format" format(date-time) +// @Param before query string false "only show items created before the given time, this is a timestamp in RFC 3339 format" format(date-time) +// @Param offset query int false "specifies where to start a page" minimum(0) default(0) +// @Param limit query int false "page size" minimum(1) maximum(3000) default(3000) +// @Param sort query []string false "format: field:direction, default: [id:asc]" Enums(id:asc,id:desc,createdAt:asc,createdAt:desc) +// @Router /versions/{versionCode}/servers/{serverKey}/ennoblements [get] +func (e *ennoblement) list(w http.ResponseWriter, r *http.Request) { + var err error + ctx := r.Context() + params := domain.ListEnnoblementsParams{ + ServerKeys: []string{chi.URLParamFromCtx(ctx, "serverKey")}, + Count: true, + IncludeVillage: true, + IncludeNewOwner: true, + IncludeNewTribe: true, + IncludeOldOwner: true, + IncludeOldTribe: true, + } + query := queryParams{r.URL.Query()} + + params.CreatedAtGTE, err = query.nullTime("since") + if err != nil { + renderErr(w, err) + return + } + + params.CreatedAtLTE, err = query.nullTime("before") + if err != nil { + renderErr(w, err) + return + } + + params.Pagination, err = query.pagination(ennoblementDefaultOffset, ennoblementDefaultLimit) + if err != nil { + renderErr(w, err) + return + } + + params.Sort, err = query.ennoblementSort() + if err != nil { + renderErr(w, err) + return + } + + ennoblements, count, err := e.svc.List(ctx, params) + if err != nil { + renderErr(w, err) + return + } + + setTotalCountHeader(w.Header(), count) + renderJSON(w, http.StatusOK, model.NewListEnnoblementsResp(ennoblements)) +} diff --git a/internal/rest/ennoblement_test.go b/internal/rest/ennoblement_test.go new file mode 100644 index 0000000..1173038 --- /dev/null +++ b/internal/rest/ennoblement_test.go @@ -0,0 +1,591 @@ +package rest_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/google/go-cmp/cmp" + + "gitea.dwysokinski.me/twhelp/core/internal/domain" + "gitea.dwysokinski.me/twhelp/core/internal/rest" + "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock" + "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model" +) + +func TestEnnoblement_list(t *testing.T) { + t.Parallel() + + now := time.Now() + version := domain.Version{ + Code: "pl", + Name: "Poland", + Host: "plemiona.pl", + Timezone: "Europe/Warsaw", + } + server := domain.Server{ + Key: "pl151", + URL: "https://pl151.plemiona.pl", + Open: true, + Special: false, + VersionCode: "pl", + } + + tests := []struct { + name string + setup func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) + versionCode string + key string + queryParams url.Values + expectedStatus int + target any + expectedResponse any + expectedTotalCount string + }{ + { + name: "OK: without params", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + ennoblementSvc.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) { + expectedParams := domain.ListEnnoblementsParams{ + ServerKeys: []string{server.Key}, + IncludeVillage: true, + IncludeNewOwner: true, + IncludeNewTribe: true, + IncludeOldOwner: true, + IncludeOldTribe: true, + Pagination: domain.Pagination{ + Limit: 3000, + Offset: 0, + }, + Count: true, + } + + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) + } + + ennoblements := []domain.Ennoblement{ + { + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 123, + NewOwnerID: 124, + NewTribeID: 125, + OldOwnerID: 126, + OldTribeID: 127, + Points: 128, + CreatedAt: now, + }, + ID: 1234, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 123, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 124, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 125, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + OldOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 126, + Name: "old owner name", + }, + }, + }, + OldTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 127, + Name: "old tribe name", + Tag: "old tribe tag", + }, + }, + }, + ServerKey: server.Key, + }, + { + + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 311, + NewOwnerID: 312, + NewTribeID: 313, + OldOwnerID: 0, + OldTribeID: 0, + Points: 129, + CreatedAt: now, + }, + ID: 12345, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 311, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 312, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 313, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + ServerKey: server.Key, + }, + } + return ennoblements, int64(len(ennoblements)), nil + }) + }, + versionCode: version.Code, + key: server.Key, + expectedStatus: 200, + target: &model.ListEnnoblementsResp{}, + expectedResponse: &model.ListEnnoblementsResp{ + Data: []model.Ennoblement{ + { + ID: 1234, + Points: 128, + Village: model.VillageMeta{ + ID: 123, + Name: "village name", + }, + NewOwner: model.NullPlayerMeta{ + Valid: true, + Player: model.PlayerMeta{ + ID: 124, + Name: "new owner name", + Tribe: model.NullTribeMeta{ + Valid: true, + Tribe: model.TribeMeta{ + ID: 125, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + }, + OldOwner: model.NullPlayerMeta{ + Valid: true, + Player: model.PlayerMeta{ + ID: 126, + Name: "old owner name", + Tribe: model.NullTribeMeta{ + Valid: true, + Tribe: model.TribeMeta{ + ID: 127, + Name: "old tribe name", + Tag: "old tribe tag", + }, + }, + }, + }, + CreatedAt: now, + }, + { + ID: 12345, + Points: 129, + Village: model.VillageMeta{ + ID: 311, + Name: "village name", + }, + NewOwner: model.NullPlayerMeta{ + Valid: true, + Player: model.PlayerMeta{ + ID: 312, + Name: "new owner name", + Tribe: model.NullTribeMeta{ + Valid: true, + Tribe: model.TribeMeta{ + ID: 313, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + }, + CreatedAt: now, + }, + }, + }, + expectedTotalCount: "2", + }, + { + name: "OK: limit=1,offset=100,since=now-30h,before=now,sort=[createdAt:desc]", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + ennoblementSvc.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) { + expectedParams := domain.ListEnnoblementsParams{ + ServerKeys: []string{server.Key}, + IncludeVillage: true, + IncludeNewOwner: true, + IncludeNewTribe: true, + IncludeOldOwner: true, + IncludeOldTribe: true, + CreatedAtGTE: domain.NullTime{ + Valid: true, + Time: now.Add(-30 * time.Hour).Truncate(time.Second), + }, + CreatedAtLTE: domain.NullTime{ + Valid: true, + Time: now.Truncate(time.Second), + }, + Sort: []domain.EnnoblementSort{ + {By: domain.EnnoblementSortByCreatedAt, Direction: domain.SortDirectionDESC}, + }, + Pagination: domain.Pagination{ + Limit: 1, + Offset: 100, + }, + Count: true, + } + + if diff := cmp.Diff(params, expectedParams, cmpopts.IgnoreUnexported(time.Time{})); diff != "" { + return nil, 0, errors.New("invalid params: " + diff) + } + + ennoblements := []domain.Ennoblement{ + { + + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 311, + NewOwnerID: 312, + NewTribeID: 313, + OldOwnerID: 0, + OldTribeID: 0, + Points: 129, + CreatedAt: now, + }, + ID: 12345, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 311, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 312, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 313, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + ServerKey: server.Key, + }, + } + return ennoblements, int64(len(ennoblements)), nil + }) + }, + versionCode: version.Code, + key: server.Key, + expectedStatus: 200, + queryParams: url.Values{ + "limit": []string{"1"}, + "offset": []string{"100"}, + "sort": []string{"createdAt:DESC"}, + "since": []string{now.Add(-30 * time.Hour).Format(time.RFC3339)}, + "before": []string{now.Format(time.RFC3339)}, + }, + target: &model.ListEnnoblementsResp{}, + expectedResponse: &model.ListEnnoblementsResp{ + Data: []model.Ennoblement{ + { + ID: 12345, + Points: 129, + Village: model.VillageMeta{ + ID: 311, + Name: "village name", + }, + NewOwner: model.NullPlayerMeta{ + Valid: true, + Player: model.PlayerMeta{ + ID: 312, + Name: "new owner name", + Tribe: model.NullTribeMeta{ + Valid: true, + Tribe: model.TribeMeta{ + ID: 313, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + }, + CreatedAt: now, + }, + }, + }, + expectedTotalCount: "1", + }, + { + name: "ERR: since is not a valid timestamp", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "since": []string{"???"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "since: parsing time \"???\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"???\" as \"2006\"", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: before is not a valid timestamp", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "before": []string{"???"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "before: parsing time \"???\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"???\" as \"2006\"", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: limit is not a valid int32", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "limit": []string{"asd"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "limit: strconv.ParseInt: parsing \"asd\": invalid syntax", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: offset is not a valid int32", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "offset": []string{"asd"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "offset: strconv.ParseInt: parsing \"asd\": invalid syntax", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: sort - invalid format", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "sort": []string{"id:asc", "test"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "sort[1]: parsing \"test\": invalid syntax, expected field:direction", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: sort - unsupported field", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "sort": []string{"id:asc", "test:asc"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "sort[1]: unsupported sort by: \"test\"", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: sort - unsupported direction", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) + }, + versionCode: version.Code, + key: server.Key, + queryParams: url.Values{ + "sort": []string{"id:asc2"}, + }, + expectedStatus: 400, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeValidationError.String(), + Message: "sort[0]: unsupported sort direction: \"asc2\"", + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: version not found", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(domain.Version{}, domain.VersionNotFoundError{VerCode: version.Code + "2"}) + }, + versionCode: version.Code + "2", + key: server.Key, + expectedStatus: 404, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeEntityNotFound.String(), + Message: fmt.Sprintf("version (code=%s) not found", version.Code+"2"), + }, + }, + expectedTotalCount: "", + }, + { + name: "ERR: server not found", + setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) { + versionSvc.GetByCodeReturns(version, nil) + serverSvc.GetNormalByVersionCodeAndKeyReturns(domain.Server{}, domain.ServerNotFoundError{Key: server.Key + "2"}) + }, + versionCode: version.Code, + key: server.Key + "2", + expectedStatus: 404, + target: &model.ErrorResp{}, + expectedResponse: &model.ErrorResp{ + Error: model.APIError{ + Code: domain.ErrorCodeEntityNotFound.String(), + Message: fmt.Sprintf("server (key=%s) not found", server.Key+"2"), + }, + }, + expectedTotalCount: "", + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + versionSvc := &mock.FakeVersionService{} + serverSvc := &mock.FakeServerService{} + ennoblementSvc := &mock.FakeEnnoblementService{} + tt.setup(versionSvc, serverSvc, ennoblementSvc) + + router := rest.NewRouter(rest.RouterConfig{ + VersionService: versionSvc, + ServerService: serverSvc, + EnnoblementService: ennoblementSvc, + }) + + target := fmt.Sprintf( + "/v1/versions/%s/servers/%s/ennoblements?%s", + tt.versionCode, + tt.key, + tt.queryParams.Encode(), + ) + resp := doRequest(router, http.MethodGet, target, nil) + defer resp.Body.Close() + assertTotalCount(t, resp.Header, tt.expectedTotalCount) + assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target) + }) + } +} diff --git a/internal/rest/internal/model/ennoblement.go b/internal/rest/internal/model/ennoblement.go new file mode 100644 index 0000000..ab135b1 --- /dev/null +++ b/internal/rest/internal/model/ennoblement.go @@ -0,0 +1,59 @@ +package model + +import ( + "time" + + "gitea.dwysokinski.me/twhelp/core/internal/domain" +) + +type Ennoblement struct { + ID int64 `json:"id"` + Village VillageMeta `json:"village"` + NewOwner NullPlayerMeta `json:"newOwner" extensions:"x-nullable"` + OldOwner NullPlayerMeta `json:"oldOwner" extensions:"x-nullable"` + Points int64 `json:"points"` + CreatedAt time.Time `json:"createdAt" format:"date-time"` +} // @name Ennoblement + +func NewEnnoblement(e domain.Ennoblement) Ennoblement { + var newOwner NullPlayerMeta + if e.NewOwner.Valid { + e.NewOwner.Player.Tribe = e.NewTribe + newOwner = NullPlayerMeta{ + Valid: true, + Player: NewPlayerMeta(e.NewOwner.Player), + } + } + + var oldOwner NullPlayerMeta + if e.OldOwner.Valid { + e.OldOwner.Player.Tribe = e.OldTribe + oldOwner = NullPlayerMeta{ + Valid: true, + Player: NewPlayerMeta(e.OldOwner.Player), + } + } + + return Ennoblement{ + ID: e.ID, + Village: NewVillageMeta(e.Village.Village), + NewOwner: newOwner, + OldOwner: oldOwner, + Points: e.Points, + CreatedAt: e.CreatedAt, + } +} + +type ListEnnoblementsResp struct { + Data []Ennoblement `json:"data"` +} // @name ListEnnoblementsResp + +func NewListEnnoblementsResp(ennoblements []domain.Ennoblement) ListEnnoblementsResp { + resp := ListEnnoblementsResp{ + Data: make([]Ennoblement, 0, len(ennoblements)), + } + for _, e := range ennoblements { + resp.Data = append(resp.Data, NewEnnoblement(e)) + } + return resp +} diff --git a/internal/rest/internal/model/ennoblement_test.go b/internal/rest/internal/model/ennoblement_test.go new file mode 100644 index 0000000..f4b6087 --- /dev/null +++ b/internal/rest/internal/model/ennoblement_test.go @@ -0,0 +1,205 @@ +package model_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model" + + "gitea.dwysokinski.me/twhelp/core/internal/domain" +) + +func TestNewEnnoblement(t *testing.T) { + t.Parallel() + + ennoblement := domain.Ennoblement{ + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 123, + NewOwnerID: 124, + NewTribeID: 125, + OldOwnerID: 126, + OldTribeID: 127, + Points: 128, + CreatedAt: time.Now(), + }, + ID: 1234, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 123, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 124, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 125, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + OldOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 126, + Name: "old owner name", + }, + }, + }, + OldTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 125, + Name: "old tribe name", + Tag: "old tribe tag", + }, + }, + }, + ServerKey: "pl151", + } + assertEnnoblement(t, ennoblement, model.NewEnnoblement(ennoblement)) +} + +func TestNewListEnnoblementsResp(t *testing.T) { + t.Parallel() + + ennoblements := []domain.Ennoblement{ + { + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 123, + NewOwnerID: 124, + NewTribeID: 125, + OldOwnerID: 126, + OldTribeID: 127, + Points: 128, + CreatedAt: time.Now(), + }, + ID: 1234, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 123, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 124, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 125, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + OldOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 126, + Name: "old owner name", + }, + }, + }, + OldTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 125, + Name: "old tribe name", + Tag: "old tribe tag", + }, + }, + }, + ServerKey: "pl151", + }, + { + + BaseEnnoblement: domain.BaseEnnoblement{ + VillageID: 311, + NewOwnerID: 312, + NewTribeID: 313, + OldOwnerID: 0, + OldTribeID: 0, + Points: 129, + CreatedAt: time.Now(), + }, + ID: 1234, + Village: domain.NullVillage{ + Valid: true, + Village: domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 311, + Name: "village name", + }, + }, + }, + NewOwner: domain.NullPlayer{ + Valid: true, + Player: domain.Player{ + BasePlayer: domain.BasePlayer{ + ID: 312, + Name: "new owner name", + }, + }, + }, + NewTribe: domain.NullTribe{ + Valid: true, + Tribe: domain.Tribe{ + BaseTribe: domain.BaseTribe{ + ID: 313, + Name: "new tribe name", + Tag: "new tribe tag", + }, + }, + }, + ServerKey: "pl151", + }, + } + resp := model.NewListEnnoblementsResp(ennoblements) + assert.Len(t, resp.Data, len(ennoblements)) + for i, e := range resp.Data { + assertEnnoblement(t, ennoblements[i], e) + } +} + +func assertEnnoblement(tb testing.TB, de domain.Ennoblement, re model.Ennoblement) { + tb.Helper() + + assert.Equal(tb, de.ID, re.ID) + assert.Equal(tb, de.Points, re.Points) + assertVillageMeta(tb, de.Village.Village, re.Village) + de.NewOwner.Player.Tribe = de.NewTribe + assertNullPlayerMeta(tb, de.NewOwner, re.NewOwner) + de.OldOwner.Player.Tribe = de.OldTribe + assertNullPlayerMeta(tb, de.OldOwner, re.OldOwner) + assert.Equal(tb, de.CreatedAt, re.CreatedAt) +} diff --git a/internal/rest/internal/model/village.go b/internal/rest/internal/model/village.go index 8989d2e..f15db88 100644 --- a/internal/rest/internal/model/village.go +++ b/internal/rest/internal/model/village.go @@ -1,6 +1,7 @@ package model import ( + "encoding/json" "time" "gitea.dwysokinski.me/twhelp/core/internal/domain" @@ -39,6 +40,48 @@ func NewVillage(v domain.Village) Village { } } +// VillageMeta represents basic village information +// @Description Basic village information +type VillageMeta struct { + ID int64 `json:"id"` + Name string `json:"name"` +} // @name VillageMeta + +func NewVillageMeta(v domain.Village) VillageMeta { + return VillageMeta{ + ID: v.ID, + Name: v.Name, + } +} + +type NullVillageMeta struct { + Village VillageMeta + Valid bool +} + +func (v NullVillageMeta) MarshalJSON() ([]byte, error) { + if !v.Valid { + return []byte("null"), nil + } + + return json.Marshal(v.Village) +} + +func (v *NullVillageMeta) UnmarshalJSON(data []byte) error { + // Ignore null, like in the main JSON package. + if string(data) == "null" { + return nil + } + + if err := json.Unmarshal(data, &v.Village); err != nil { + return err + } + + v.Valid = true + + return nil +} + type ListVillagesResp struct { Data []Village `json:"data"` } // @name ListVillagesResp diff --git a/internal/rest/internal/model/village_test.go b/internal/rest/internal/model/village_test.go index 6dfb7c0..9987611 100644 --- a/internal/rest/internal/model/village_test.go +++ b/internal/rest/internal/model/village_test.go @@ -1,6 +1,7 @@ package model_test import ( + "encoding/json" "testing" "time" @@ -99,6 +100,139 @@ func TestNewVillage(t *testing.T) { assertVillage(t, playerVillage, model.NewVillage(playerVillage)) } +func TestNewVillageMeta(t *testing.T) { + t.Parallel() + + village := domain.Village{ + BaseVillage: domain.BaseVillage{ + ID: 1234, + Name: "name", + Points: 1, + X: 2, + Y: 3, + Continent: "K11", + Bonus: 1, + PlayerID: 0, + }, + ServerKey: "pl151", + Player: domain.NullPlayer{}, + CreatedAt: time.Time{}, + } + assertVillageMeta(t, village, model.NewVillageMeta(village)) +} + +func TestNullVillageMeta_MarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + village model.NullVillageMeta + expectedJSON string + }{ + { + name: "OK: null 1", + village: model.NullVillageMeta{ + Village: model.VillageMeta{}, + Valid: false, + }, + expectedJSON: "null", + }, + { + name: "OK: null 2", + village: model.NullVillageMeta{ + Village: model.VillageMeta{ID: 1234}, + Valid: false, + }, + expectedJSON: "null", + }, + { + name: "OK: valid struct", + village: model.NullVillageMeta{ + Village: model.VillageMeta{ + ID: 997, + Name: "name 997", + }, + Valid: true, + }, + expectedJSON: `{"id":997,"name":"name 997"}`, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + b, err := json.Marshal(tt.village) + assert.NoError(t, err) + assert.JSONEq(t, tt.expectedJSON, string(b)) + }) + } +} + +func TestNullVillageMeta_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + expectedVillage model.NullVillageMeta + expectedJSONSyntaxError bool + }{ + { + name: "OK: null", + json: "null", + expectedVillage: model.NullVillageMeta{ + Village: model.VillageMeta{}, + Valid: false, + }, + }, + { + name: "OK: valid struct", + //nolint:lll + json: `{"id":997,"name":"name 997"}`, + expectedVillage: model.NullVillageMeta{ + Village: model.VillageMeta{ + ID: 997, + Name: "name 997", + }, + Valid: true, + }, + }, + { + name: "ERR: invalid tribe 1", + json: "2022-07-30T14:13:12.0000005Z", + expectedVillage: model.NullVillageMeta{}, + expectedJSONSyntaxError: true, + }, + { + name: "ERR: invalid tribe 2", + json: "hello world", + expectedVillage: model.NullVillageMeta{}, + expectedJSONSyntaxError: true, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var target model.NullVillageMeta + err := json.Unmarshal([]byte(tt.json), &target) + if tt.expectedJSONSyntaxError { + var syntaxError *json.SyntaxError + assert.ErrorAs(t, err, &syntaxError) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectedVillage, target) + }) + } +} + func TestNewListVillagesResp(t *testing.T) { t.Parallel() @@ -227,3 +361,10 @@ func assertVillage(tb testing.TB, dv domain.Village, rv model.Village) { assert.Equal(tb, dv.CreatedAt, rv.CreatedAt) assertNullPlayerMeta(tb, dv.Player, rv.Player) } + +func assertVillageMeta(tb testing.TB, dv domain.Village, rv model.VillageMeta) { + tb.Helper() + + assert.Equal(tb, dv.ID, rv.ID) + assert.Equal(tb, dv.Name, rv.Name) +} diff --git a/internal/rest/player_test.go b/internal/rest/player_test.go index 8907492..06db261 100644 --- a/internal/rest/player_test.go +++ b/internal/rest/player_test.go @@ -2,7 +2,6 @@ package rest_test import ( "context" - "errors" "fmt" "net/http" "net/url" @@ -53,10 +52,6 @@ func TestPlayer_list(t *testing.T) { serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil) playerSvc.ListCalls(func(_ context.Context, params domain.ListPlayersParams) ([]domain.Player, int64, error) { expectedParams := domain.ListPlayersParams{ - Deleted: domain.NullBool{ - Valid: false, - Bool: false, - }, ServerKeys: []string{server.Key}, IncludeTribe: true, Pagination: domain.Pagination{ @@ -66,8 +61,8 @@ func TestPlayer_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } players := []domain.Player{ @@ -226,8 +221,8 @@ func TestPlayer_list(t *testing.T) { }, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } players := []domain.Player{ @@ -325,8 +320,8 @@ func TestPlayer_list(t *testing.T) { }, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } players := []domain.Player{ diff --git a/internal/rest/query_params.go b/internal/rest/query_params.go index 9a4cb6d..c4ef77e 100644 --- a/internal/rest/query_params.go +++ b/internal/rest/query_params.go @@ -5,6 +5,7 @@ import ( "net/url" "strconv" "strings" + "time" "gitea.dwysokinski.me/twhelp/core/internal/domain" ) @@ -140,6 +141,42 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) { return res, nil } +func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) { + ss, err := q.sort() + if err != nil { + return nil, err + } + if ss == nil { + return nil, nil + } + + res := make([]domain.EnnoblementSort, 0, len(ss)) + for i, s := range ss { + by, err := domain.NewEnnoblementSortBy(s[0]) + if err != nil { + return nil, domain.ValidationError{ + Field: fmt.Sprintf("sort[%d]", i), + Err: err, + } + } + + direction, err := domain.NewSortDirection(s[1]) + if err != nil { + return nil, domain.ValidationError{ + Field: fmt.Sprintf("sort[%d]", i), + Err: err, + } + } + + res = append(res, domain.EnnoblementSort{ + By: by, + Direction: direction, + }) + } + + return res, nil +} + func (q queryParams) sort() ([][2]string, error) { ss, ok := q.values["sort"] if !ok || len(ss) == 0 { @@ -161,3 +198,22 @@ func (q queryParams) sort() ([][2]string, error) { return res, nil } + +func (q queryParams) nullTime(key string) (domain.NullTime, error) { + if !q.values.Has(key) { + return domain.NullTime{}, nil + } + + t, err := time.Parse(time.RFC3339, q.values.Get(key)) + if err != nil { + return domain.NullTime{}, domain.ValidationError{ + Field: key, + Err: err, + } + } + + return domain.NullTime{ + Valid: true, + Time: t, + }, nil +} diff --git a/internal/rest/rest.go b/internal/rest/rest.go index 073632a..8df7867 100644 --- a/internal/rest/rest.go +++ b/internal/rest/rest.go @@ -29,13 +29,14 @@ type CORSConfig struct { } type RouterConfig struct { - VersionService VersionService - ServerService ServerService - TribeService TribeService - PlayerService PlayerService - VillageService VillageService - CORS CORSConfig - Swagger bool + VersionService VersionService + ServerService ServerService + TribeService TribeService + PlayerService PlayerService + VillageService VillageService + EnnoblementService EnnoblementService + CORS CORSConfig + Swagger bool } func NewRouter(cfg RouterConfig) *chi.Mux { @@ -55,6 +56,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux { villageHandler := &village{ svc: cfg.VillageService, } + ennoblementHandler := &ennoblement{ + svc: cfg.EnnoblementService, + } router := chi.NewRouter() @@ -104,6 +108,8 @@ func NewRouter(cfg RouterConfig) *chi.Mux { r.Get("/", villageHandler.list) r.Get("/{villageId}", villageHandler.getByID) }) + r.With(verifyServerKeyMiddleware). + Get("/ennoblements", ennoblementHandler.list) }) }) }) diff --git a/internal/rest/server_test.go b/internal/rest/server_test.go index f0c0339..f3b9b42 100644 --- a/internal/rest/server_test.go +++ b/internal/rest/server_test.go @@ -2,7 +2,6 @@ package rest_test import ( "context" - "errors" "fmt" "net/http" "net/url" @@ -61,8 +60,8 @@ func TestServer_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } servers := []domain.Server{ @@ -177,8 +176,8 @@ func TestServer_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } servers := []domain.Server{ @@ -298,8 +297,8 @@ func TestServer_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } servers := []domain.Server{ diff --git a/internal/rest/tribe_test.go b/internal/rest/tribe_test.go index 73a0032..9e396dd 100644 --- a/internal/rest/tribe_test.go +++ b/internal/rest/tribe_test.go @@ -2,7 +2,6 @@ package rest_test import ( "context" - "errors" "fmt" "net/http" "net/url" @@ -77,8 +76,8 @@ func TestTribe_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } tribes := []domain.Tribe{ @@ -220,8 +219,8 @@ func TestTribe_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } tribes := []domain.Tribe{ @@ -352,8 +351,8 @@ func TestTribe_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } tribes := []domain.Tribe{ diff --git a/internal/rest/village_test.go b/internal/rest/village_test.go index dadac50..789b235 100644 --- a/internal/rest/village_test.go +++ b/internal/rest/village_test.go @@ -2,7 +2,6 @@ package rest_test import ( "context" - "errors" "fmt" "net/http" "net/url" @@ -63,8 +62,8 @@ func TestVillage_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } villages := []domain.Village{ @@ -220,8 +219,8 @@ func TestVillage_list(t *testing.T) { Count: true, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } villages := []domain.Village{ diff --git a/internal/service/ennoblement.go b/internal/service/ennoblement.go index fc5645d..b1c6730 100644 --- a/internal/service/ennoblement.go +++ b/internal/service/ennoblement.go @@ -9,6 +9,11 @@ import ( "gitea.dwysokinski.me/twhelp/core/internal/domain" ) +const ( + ennoblementMaxLimit = 3000 + ennoblementSortMaxLen = 2 +) + var ( errEnnoblementNotFound = errors.New("ennoblement not found") ) @@ -64,6 +69,41 @@ func (e *Ennoblement) Refresh(ctx context.Context, key, url string) error { return nil } +func (e *Ennoblement) List(ctx context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) { + if len(params.Sort) == 0 { + params.Sort = []domain.EnnoblementSort{ + { + By: domain.EnnoblementSortByID, + Direction: domain.SortDirectionASC, + }, + } + } + + if len(params.Sort) > ennoblementSortMaxLen { + return nil, 0, domain.ValidationError{ + Field: "sort", + Err: domain.MaxLengthError{ + Max: ennoblementSortMaxLen, + }, + } + } + + if params.Pagination.Limit == 0 { + params.Pagination.Limit = ennoblementMaxLimit + } + + if err := validatePagination(params.Pagination, ennoblementMaxLimit); err != nil { + return nil, 0, fmt.Errorf("validatePagination: %w", err) + } + + ennoblements, count, err := e.repo.List(ctx, params) + if err != nil { + return nil, 0, fmt.Errorf("EnnoblementRepository.List: %w", err) + } + + return ennoblements, count, nil +} + func (e *Ennoblement) getLatestEnnoblement(ctx context.Context, key string) (domain.Ennoblement, error) { ennoblements, _, err := e.repo.List(ctx, domain.ListEnnoblementsParams{ ServerKeys: []string{key}, diff --git a/internal/service/ennoblement_test.go b/internal/service/ennoblement_test.go index 73fd833..077905a 100644 --- a/internal/service/ennoblement_test.go +++ b/internal/service/ennoblement_test.go @@ -2,9 +2,12 @@ package service_test import ( "context" + "fmt" "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" "gitea.dwysokinski.me/twhelp/core/internal/service" @@ -123,3 +126,181 @@ func TestEnnoblement_Refresh(t *testing.T) { }) } } + +func TestEnnoblement_List(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + limit int32 + sort []domain.EnnoblementSort + }{ + { + name: "default limit, default sort", + limit: 0, + sort: nil, + }, + { + name: "custom limit", + limit: 2999, + }, + { + name: "custom sort", + limit: 0, + sort: []domain.EnnoblementSort{ + {By: domain.EnnoblementSortByCreatedAt, Direction: domain.SortDirectionDESC}, + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + limit := tt.limit + if limit == 0 { + limit = 3000 + } + + repo := &mock.FakeEnnoblementRepository{} + repo.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) { + expectedParams := domain.ListEnnoblementsParams{ + Pagination: domain.Pagination{ + Limit: limit, + }, + Sort: func(sort []domain.EnnoblementSort) []domain.EnnoblementSort { + if len(sort) == 0 { + return []domain.EnnoblementSort{ + { + By: domain.EnnoblementSortByID, + Direction: domain.SortDirectionASC, + }, + } + } + return sort + }(tt.sort), + } + + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) + } + + return make([]domain.Ennoblement, params.Pagination.Limit), int64(params.Pagination.Limit), nil + }) + client := &mock.FakeEnnoblementsGetter{} + + players, count, err := service.NewEnnoblement(repo, client). + List(context.Background(), domain.ListEnnoblementsParams{ + Pagination: domain.Pagination{ + Limit: tt.limit, + }, + Sort: tt.sort, + }) + assert.NoError(t, err) + assert.EqualValues(t, limit, count) + assert.Len(t, players, int(limit)) + }) + } + }) + + t.Run("ERR: validation failed", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params domain.ListEnnoblementsParams + expectedErr error + }{ + { + name: "params.Pagination.Limit < 0", + params: domain.ListEnnoblementsParams{ + Pagination: domain.Pagination{ + Limit: -1, + }, + }, + expectedErr: domain.ValidationError{ + Field: "limit", + Err: domain.MinError{ + Min: 1, + }, + }, + }, + { + name: "params.Pagination.Limit > 3000", + params: domain.ListEnnoblementsParams{ + Pagination: domain.Pagination{ + Limit: 3001, + }, + }, + expectedErr: domain.ValidationError{ + Field: "limit", + Err: domain.MaxError{ + Max: 3000, + }, + }, + }, + { + name: "params.Pagination.Offset < 0", + params: domain.ListEnnoblementsParams{ + Pagination: domain.Pagination{ + Offset: -1, + }, + }, + expectedErr: domain.ValidationError{ + Field: "offset", + Err: domain.MinError{ + Min: 0, + }, + }, + }, + { + name: "len(params.Sort) > 2", + params: domain.ListEnnoblementsParams{ + Sort: []domain.EnnoblementSort{ + { + By: domain.EnnoblementSortByID, + Direction: domain.SortDirectionASC, + }, + { + By: domain.EnnoblementSortByCreatedAt, + Direction: domain.SortDirectionASC, + }, + { + By: domain.EnnoblementSortByCreatedAt, + Direction: domain.SortDirectionASC, + }, + }, + }, + expectedErr: domain.ValidationError{ + Field: "sort", + Err: domain.MaxLengthError{ + Max: 2, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeEnnoblementRepository{} + client := &mock.FakeEnnoblementsGetter{} + + players, count, err := service.NewEnnoblement(repo, client). + List(context.Background(), tt.params) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Zero(t, players) + assert.Zero(t, count) + assert.Equal(t, 0, repo.ListCallCount()) + }) + } + }) +} diff --git a/internal/service/player_test.go b/internal/service/player_test.go index b0d0ac7..3215708 100644 --- a/internal/service/player_test.go +++ b/internal/service/player_test.go @@ -176,8 +176,8 @@ func TestPlayer_List(t *testing.T) { }(tt.sort), } - if !cmp.Equal(params, expectedParams) { - return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } return make([]domain.Player, params.Pagination.Limit), int64(params.Pagination.Limit), nil diff --git a/internal/service/tribe_test.go b/internal/service/tribe_test.go index 027c965..765ba43 100644 --- a/internal/service/tribe_test.go +++ b/internal/service/tribe_test.go @@ -189,8 +189,8 @@ func TestTribe_List(t *testing.T) { }(tt.sort), } - if !cmp.Equal(params, expectedParams) { - return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } return make([]domain.Tribe, params.Pagination.Limit), int64(params.Pagination.Limit), nil diff --git a/internal/service/village_test.go b/internal/service/village_test.go index 7df4899..d6fa23c 100644 --- a/internal/service/village_test.go +++ b/internal/service/village_test.go @@ -131,8 +131,8 @@ func TestVillage_List(t *testing.T) { }, } - if !cmp.Equal(params, expectedParams) { - return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams)) + if diff := cmp.Diff(params, expectedParams); diff != "" { + return nil, 0, fmt.Errorf("validation failed: %s", diff) } return make([]domain.Village, params.Pagination.Limit), int64(params.Pagination.Limit), nil