From 3a282db05c20bdee839d967fda974d1ee2a9d768 Mon Sep 17 00:00:00 2001 From: Bernardo Heynemann Date: Mon, 1 Aug 2016 11:58:20 -0300 Subject: [PATCH] Reapply and Reinvite with cooldowns. Added cooldownBeforeApply, cooldownBeforeInvite for game. Allow application and invitations to be created multiple times. --- api/app.go | 4 +- api/game.go | 153 +++++--- api/game_test.go | 26 +- api/healthcheck_test.go | 2 +- config/default.yaml | 2 + config/test.yaml | 11 + db/migrations.go | 53 ++- ...9184159_CreateCooldownAfterInviteField.sql | 10 + docs/API.md | 10 +- docs/game.md | 200 +++++++++++ docs/index.rst | 1 + models/fixtures.go | 13 +- models/game.go | 68 ++-- models/game_test.go | 14 +- models/membership.go | 26 +- models/membership_test.go | 331 +++++++++++++++++- 16 files changed, 814 insertions(+), 110 deletions(-) create mode 100644 db/migrations/20160729184159_CreateCooldownAfterInviteField.sql create mode 100644 docs/game.md diff --git a/api/app.go b/api/app.go index 92a74ad0..c05a1bb4 100644 --- a/api/app.go +++ b/api/app.go @@ -86,7 +86,9 @@ func (app *App) setConfigurationDefaults() { app.Config.SetDefault("postgres.port", 5432) app.Config.SetDefault("postgres.sslMode", "disable") app.Config.SetDefault("webhooks.timeout", 2) - app.Config.SetDefault("khan.MaxPendingInvites", -1) + app.Config.SetDefault("khan.maxPendingInvites", -1) + app.Config.SetDefault("khan.defaultCooldownBeforeInvite", -1) + app.Config.SetDefault("khan.defaultCooldownBeforeApply", -1) l.Debug("Configuration defaults set.") } diff --git a/api/game.go b/api/game.go index b320afa3..b57656d6 100644 --- a/api/game.go +++ b/api/game.go @@ -9,7 +9,6 @@ package api import ( "encoding/json" - "reflect" "strings" "time" @@ -19,6 +18,10 @@ import ( "github.com/uber-go/zap" ) +type validatable interface { + Validate() []string +} + type gamePayload struct { Name string MembershipLevels map[string]interface{} @@ -35,6 +38,23 @@ type gamePayload struct { CooldownAfterDelete int } +func (p *gamePayload) Validate() []string { + sortedLevels := util.SortLevels(p.MembershipLevels) + minMembershipLevel := sortedLevels[0].Value + + var errors []string + if p.MinLevelToAcceptApplication < minMembershipLevel { + errors = append(errors, "minLevelToAcceptApplication should be greater or equal to minMembershipLevel") + } + if p.MinLevelToCreateInvitation < minMembershipLevel { + errors = append(errors, "minLevelToCreateInvitation should be greater or equal to minMembershipLevel") + } + if p.MinLevelToRemoveMember < minMembershipLevel { + errors = append(errors, "minLevelToRemoveMember should be greater or equal to minMembershipLevel") + } + return errors +} + type createGamePayload struct { PublicID string Name string @@ -52,35 +72,27 @@ type createGamePayload struct { CooldownAfterDelete int } -func getAsInt(field string, payload interface{}) int { - v := reflect.ValueOf(payload) - fieldValue := v.FieldByName(field).Interface() - return fieldValue.(int) -} - -func getAsJSON(field string, payload interface{}) map[string]interface{} { - v := reflect.ValueOf(payload) - fieldValue := v.FieldByName(field).Interface() - return fieldValue.(map[string]interface{}) -} - -func validateGamePayload(payload interface{}) []string { - sortedLevels := util.SortLevels(getAsJSON("MembershipLevels", payload)) +func (p *createGamePayload) Validate() []string { + sortedLevels := util.SortLevels(p.MembershipLevels) minMembershipLevel := sortedLevels[0].Value var errors []string - if getAsInt("MinLevelToAcceptApplication", payload) < minMembershipLevel { + if p.MinLevelToAcceptApplication < minMembershipLevel { errors = append(errors, "minLevelToAcceptApplication should be greater or equal to minMembershipLevel") } - if getAsInt("MinLevelToCreateInvitation", payload) < minMembershipLevel { + if p.MinLevelToCreateInvitation < minMembershipLevel { errors = append(errors, "minLevelToCreateInvitation should be greater or equal to minMembershipLevel") } - if getAsInt("MinLevelToRemoveMember", payload) < minMembershipLevel { + if p.MinLevelToRemoveMember < minMembershipLevel { errors = append(errors, "minLevelToRemoveMember should be greater or equal to minMembershipLevel") } return errors } +func validateGamePayload(payload validatable) []string { + return payload.Validate() +} + func logPayloadErrors(l zap.Logger, errors []string) { var fields []zap.Field for _, err := range errors { @@ -92,6 +104,62 @@ func logPayloadErrors(l zap.Logger, errors []string) { ) } +type optionalParams struct { + maxPendingInvites int + cooldownBeforeApply int + cooldownBeforeInvite int +} + +func getOptionalParameters(app *App, c *iris.Context) (*optionalParams, error) { + data := c.RequestCtx.Request.Body() + var jsonPayload map[string]interface{} + err := json.Unmarshal(data, &jsonPayload) + if err != nil { + return nil, err + } + + var maxPendingInvites int + if val, ok := jsonPayload["maxPendingInvites"]; ok { + maxPendingInvites = int(val.(float64)) + } else { + maxPendingInvites = app.Config.GetInt("khan.maxPendingInvites") + } + + var cooldownBeforeInvite int + if val, ok := jsonPayload["cooldownBeforeInvite"]; ok { + cooldownBeforeInvite = int(val.(float64)) + } else { + cooldownBeforeInvite = app.Config.GetInt("khan.defaultCooldownBeforeInvite") + } + + var cooldownBeforeApply int + if val, ok := jsonPayload["cooldownBeforeApply"]; ok { + cooldownBeforeApply = int(val.(float64)) + } else { + cooldownBeforeApply = app.Config.GetInt("khan.defaultCooldownBeforeApply") + } + + return &optionalParams{ + maxPendingInvites: maxPendingInvites, + cooldownBeforeInvite: cooldownBeforeInvite, + cooldownBeforeApply: cooldownBeforeApply, + }, nil +} + +func getCreateGamePayload(app *App, c *iris.Context) (*createGamePayload, *optionalParams, error) { + var payload createGamePayload + if err := LoadJSONPayload(&payload, c); err != nil { + return nil, nil, err + } + + optional, err := getOptionalParameters(app, c) + if err != nil { + return nil, nil, err + } + + return &payload, optional, nil +} + // CreateGameHandler is the handler responsible for creating new games func CreateGameHandler(app *App) func(c *iris.Context) { return func(c *iris.Context) { @@ -101,26 +169,19 @@ func CreateGameHandler(app *App) func(c *iris.Context) { zap.String("operation", "createGame"), ) - var payload createGamePayload - if err := LoadJSONPayload(&payload, c); err != nil { - FailWith(400, err.Error(), c) - return - } - - data := c.RequestCtx.Request.Body() - var jsonPayload map[string]interface{} - err := json.Unmarshal(data, &jsonPayload) + l.Debug("Retrieving parameters...") + payload, optional, err := getCreateGamePayload(app, c) if err != nil { + l.Error("Failed to retrieve parameters.", zap.Error(err)) FailWith(400, err.Error(), c) return } - - var maxPendingInvites int - if val, ok := jsonPayload["maxPendingInvites"]; ok { - maxPendingInvites = int(val.(float64)) - } else { - maxPendingInvites = app.Config.GetInt("khan.MaxPendingInvites") - } + l.Debug( + "Parameters retrieved successfully.", + zap.Int("maxPendingInvites", optional.maxPendingInvites), + zap.Int("cooldownBeforeInvite", optional.cooldownBeforeInvite), + zap.Int("cooldownBeforeApply", optional.cooldownBeforeApply), + ) if payloadErrors := validateGamePayload(payload); len(payloadErrors) != 0 { logPayloadErrors(l, payloadErrors) @@ -155,7 +216,9 @@ func CreateGameHandler(app *App) func(c *iris.Context) { payload.MaxClansPerPlayer, payload.CooldownAfterDeny, payload.CooldownAfterDelete, - maxPendingInvites, + optional.cooldownBeforeApply, + optional.cooldownBeforeInvite, + optional.maxPendingInvites, false, ) @@ -195,22 +258,13 @@ func UpdateGameHandler(app *App) func(c *iris.Context) { return } - data := c.RequestCtx.Request.Body() - var jsonPayload map[string]interface{} - err := json.Unmarshal(data, &jsonPayload) + optional, err := getOptionalParameters(app, c) if err != nil { FailWith(400, err.Error(), c) return } - var maxPendingInvites int - if val, ok := jsonPayload["maxPendingInvites"]; ok { - maxPendingInvites = int(val.(float64)) - } else { - maxPendingInvites = app.Config.GetInt("khan.MaxPendingInvites") - } - - if payloadErrors := validateGamePayload(payload); len(payloadErrors) != 0 { + if payloadErrors := validateGamePayload(&payload); len(payloadErrors) != 0 { logPayloadErrors(l, payloadErrors) errorString := strings.Join(payloadErrors[:], ", ") FailWith(422, errorString, c) @@ -243,7 +297,9 @@ func UpdateGameHandler(app *App) func(c *iris.Context) { payload.MaxClansPerPlayer, payload.CooldownAfterDeny, payload.CooldownAfterDelete, - maxPendingInvites, + optional.cooldownBeforeApply, + optional.cooldownBeforeInvite, + optional.maxPendingInvites, ) if err != nil { @@ -272,6 +328,9 @@ func UpdateGameHandler(app *App) func(c *iris.Context) { "maxClansPerPlayer": payload.MaxClansPerPlayer, "cooldownAfterDeny": payload.CooldownAfterDeny, "cooldownAfterDelete": payload.CooldownAfterDelete, + "cooldownBeforeApply": optional.cooldownBeforeApply, + "cooldownBeforeInvite": optional.cooldownBeforeInvite, + "maxPendingInvites": optional.maxPendingInvites, } app.DispatchHooks(gameID, models.GameUpdatedHook, successPayload) diff --git a/api/game_test.go b/api/game_test.go index 9adf1698..3487fb43 100644 --- a/api/game_test.go +++ b/api/game_test.go @@ -43,7 +43,6 @@ func getGamePayload(publicID, name string) map[string]interface{} { "maxClansPerPlayer": 1, "cooldownAfterDeny": 30, "cooldownAfterDelete": 30, - "maxPendingInvites": 30, } } @@ -88,6 +87,31 @@ var _ = Describe("Player API Handler", func() { Expect(dbGame.MaxClansPerPlayer).To(Equal(payload["maxClansPerPlayer"])) Expect(dbGame.CooldownAfterDeny).To(Equal(payload["cooldownAfterDeny"])) Expect(dbGame.CooldownAfterDelete).To(Equal(payload["cooldownAfterDelete"])) + Expect(dbGame.CooldownBeforeInvite).To(Equal(0)) + Expect(dbGame.CooldownBeforeApply).To(Equal(3600)) + Expect(dbGame.MaxPendingInvites).To(Equal(-1)) + }) + + It("Should create game with custom optional params", func() { + a := GetDefaultTestApp() + + payload := getGamePayload("", "") + payload["maxPendingInvites"] = 27 + payload["cooldownBeforeApply"] = 2874 + payload["cooldownBeforeInvite"] = 2384 + res := PostJSON(a, "/games", payload) + + Expect(res.Raw().StatusCode).To(Equal(http.StatusOK)) + var result map[string]interface{} + json.Unmarshal([]byte(res.Body().Raw()), &result) + Expect(result["success"]).To(BeTrue()) + Expect(result["publicID"]).To(Equal(payload["publicID"].(string))) + + dbGame, err := models.GetGameByPublicID(a.Db, payload["publicID"].(string)) + Expect(err).NotTo(HaveOccurred()) + Expect(dbGame.CooldownBeforeInvite).To(Equal(2384)) + Expect(dbGame.CooldownBeforeApply).To(Equal(2874)) + Expect(dbGame.MaxPendingInvites).To(Equal(27)) }) It("Should not create game if missing parameters", func() { diff --git a/api/healthcheck_test.go b/api/healthcheck_test.go index fcece4f6..4d40eb22 100644 --- a/api/healthcheck_test.go +++ b/api/healthcheck_test.go @@ -26,7 +26,7 @@ var _ = Describe("Healthcheck API Handler", func() { It("Should respond with customized WORKING string", func() { a := GetDefaultTestApp() - a.Config.SetDefault("healthcheck.workingText", "OTHERWORKING") + a.Config.Set("healthcheck.workingText", "OTHERWORKING") res := Get(a, "/healthcheck") Expect(res.Raw().StatusCode).To(Equal(http.StatusOK)) diff --git a/config/default.yaml b/config/default.yaml index 2676869c..0be1d89d 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -7,6 +7,8 @@ postgres: khan: maxPendingInvites: -1 + defaultCooldownBeforeInvite: 0 + defaultCooldownBeforeApply: 3600 healthcheck: workingText: "WORKING" diff --git a/config/test.yaml b/config/test.yaml index d9f70d09..e8e8aec0 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -4,3 +4,14 @@ postgres: host: "localhost" port: 5432 sslMode: "disable" + +khan: + maxPendingInvites: -1 + defaultCooldownBeforeInvite: 0 + defaultCooldownBeforeApply: 3600 + +healthcheck: + workingText: "WORKING" + +webhooks: + timeout: 2 diff --git a/db/migrations.go b/db/migrations.go index 00e14f87..8a766428 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -15,6 +15,7 @@ // migrations/20160713191703_CreateMembershipDenierField.sql // migrations/20160728180524_CreateMaxPendingInvitesField.sql // migrations/20160728195902_CreateMembershipMessageField.sql +// migrations/20160729184159_CreateCooldownAfterInviteField.sql // DO NOT EDIT! package db @@ -97,7 +98,7 @@ func migrations20160608133902_creategametableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160608133902_CreateGameTable.sql", size: 1193, mode: os.FileMode(420), modTime: time.Unix(1466797802, 0)} + info := bindataFileInfo{name: "migrations/20160608133902_CreateGameTable.sql", size: 1193, mode: os.FileMode(420), modTime: time.Unix(1466798697, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -117,7 +118,7 @@ func migrations20160608150958_createplayertableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160608150958_CreatePlayerTable.sql", size: 730, mode: os.FileMode(420), modTime: time.Unix(1466707165, 0)} + info := bindataFileInfo{name: "migrations/20160608150958_CreatePlayerTable.sql", size: 730, mode: os.FileMode(420), modTime: time.Unix(1466707272, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -137,7 +138,7 @@ func migrations20160608174439_createclantableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160608174439_CreateClanTable.sql", size: 906, mode: os.FileMode(420), modTime: time.Unix(1466707165, 0)} + info := bindataFileInfo{name: "migrations/20160608174439_CreateClanTable.sql", size: 906, mode: os.FileMode(420), modTime: time.Unix(1466707272, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -157,7 +158,7 @@ func migrations20160608182307_createmembershiptableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160608182307_CreateMembershipTable.sql", size: 1018, mode: os.FileMode(420), modTime: time.Unix(1466787920, 0)} + info := bindataFileInfo{name: "migrations/20160608182307_CreateMembershipTable.sql", size: 1018, mode: os.FileMode(420), modTime: time.Unix(1466792887, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -177,7 +178,7 @@ func migrations20160621161411_createhookstableSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160621161411_CreateHooksTable.sql", size: 700, mode: os.FileMode(420), modTime: time.Unix(1466601889, 0)} + info := bindataFileInfo{name: "migrations/20160621161411_CreateHooksTable.sql", size: 700, mode: os.FileMode(420), modTime: time.Unix(1466538319, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -197,7 +198,7 @@ func migrations20160627110742_loaduuidmoduleSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160627110742_LoadUUIDModule.sql", size: 75, mode: os.FileMode(420), modTime: time.Unix(1467060502, 0)} + info := bindataFileInfo{name: "migrations/20160627110742_LoadUUIDModule.sql", size: 75, mode: os.FileMode(420), modTime: time.Unix(1467036481, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -217,7 +218,7 @@ func migrations20160627153918_createretrieveclanindexesSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160627153918_CreateRetrieveClanIndexes.sql", size: 674, mode: os.FileMode(420), modTime: time.Unix(1467060534, 0)} + info := bindataFileInfo{name: "migrations/20160627153918_CreateRetrieveClanIndexes.sql", size: 674, mode: os.FileMode(420), modTime: time.Unix(1467054354, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -237,7 +238,7 @@ func migrations20160627155249_createplayermembershipandownershipcountSql() (*ass return nil, err } - info := bindataFileInfo{name: "migrations/20160627155249_CreatePlayerMembershipAndOwnershipCount.sql", size: 415, mode: os.FileMode(420), modTime: time.Unix(1467060707, 0)} + info := bindataFileInfo{name: "migrations/20160627155249_CreatePlayerMembershipAndOwnershipCount.sql", size: 415, mode: os.FileMode(420), modTime: time.Unix(1467061932, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -257,7 +258,7 @@ func migrations20160628181530_createclanmembershipcountSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160628181530_CreateClanMembershipCount.sql", size: 287, mode: os.FileMode(420), modTime: time.Unix(1467154167, 0)} + info := bindataFileInfo{name: "migrations/20160628181530_CreateClanMembershipCount.sql", size: 287, mode: os.FileMode(420), modTime: time.Unix(1467237210, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -277,7 +278,7 @@ func migrations20160708161944_createmembershipapproverfieldSql() (*asset, error) return nil, err } - info := bindataFileInfo{name: "migrations/20160708161944_CreateMembershipApproverField.sql", size: 409, mode: os.FileMode(420), modTime: time.Unix(1468017766, 0)} + info := bindataFileInfo{name: "migrations/20160708161944_CreateMembershipApproverField.sql", size: 409, mode: os.FileMode(420), modTime: time.Unix(1468011708, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -297,7 +298,7 @@ func migrations20160708192007_createownerindexSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160708192007_CreateOwnerIndex.sql", size: 250, mode: os.FileMode(420), modTime: time.Unix(1468017766, 0)} + info := bindataFileInfo{name: "migrations/20160708192007_CreateOwnerIndex.sql", size: 250, mode: os.FileMode(420), modTime: time.Unix(1468016466, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -317,7 +318,7 @@ func migrations20160713185332_creategamecooldownafterdenyanddeleteSql() (*asset, return nil, err } - info := bindataFileInfo{name: "migrations/20160713185332_CreateGameCooldownAfterDenyAndDelete.sql", size: 425, mode: os.FileMode(420), modTime: time.Unix(1468451926, 0)} + info := bindataFileInfo{name: "migrations/20160713185332_CreateGameCooldownAfterDenyAndDelete.sql", size: 425, mode: os.FileMode(420), modTime: time.Unix(1468506203, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -337,7 +338,7 @@ func migrations20160713191703_createmembershipdenierfieldSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "migrations/20160713191703_CreateMembershipDenierField.sql", size: 401, mode: os.FileMode(420), modTime: time.Unix(1468451926, 0)} + info := bindataFileInfo{name: "migrations/20160713191703_CreateMembershipDenierField.sql", size: 401, mode: os.FileMode(420), modTime: time.Unix(1468506203, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -357,7 +358,7 @@ func migrations20160728180524_createmaxpendinginvitesfieldSql() (*asset, error) return nil, err } - info := bindataFileInfo{name: "migrations/20160728180524_CreateMaxPendingInvitesField.sql", size: 336, mode: os.FileMode(420), modTime: time.Unix(1469813639, 0)} + info := bindataFileInfo{name: "migrations/20160728180524_CreateMaxPendingInvitesField.sql", size: 336, mode: os.FileMode(420), modTime: time.Unix(1469810580, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -377,7 +378,27 @@ func migrations20160728195902_createmembershipmessagefieldSql() (*asset, error) return nil, err } - info := bindataFileInfo{name: "migrations/20160728195902_CreateMembershipMessageField.sql", size: 273, mode: os.FileMode(420), modTime: time.Unix(1469821643, 0)} + info := bindataFileInfo{name: "migrations/20160728195902_CreateMembershipMessageField.sql", size: 273, mode: os.FileMode(420), modTime: time.Unix(1469824937, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _migrations20160729184159_createcooldownafterinvitefieldSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x8f\x31\x4f\xc3\x30\x14\x84\xf7\xfc\x8a\xdb\x3a\xa0\x4a\x95\x90\x58\x3a\x05\x1c\x26\x93\x40\x89\xe7\x2a\x75\x1e\xae\x85\xeb\x67\xd9\x86\xc2\xbf\xc7\xa9\x04\x62\x28\x11\x1d\xef\xec\xbb\x77\x5f\xb5\x5c\xe2\xca\x30\x27\x82\x0a\x93\x78\x7e\x92\xb0\x1e\x89\x74\xb6\xec\xb1\x50\x61\x01\x9b\x40\x1f\xa4\xdf\x32\x8d\x38\xee\xc9\x23\xef\x8b\x75\xb0\x26\x0e\xa7\x4f\x45\x0c\x21\x38\x4b\x63\x55\xcb\xbe\xd9\xa0\xaf\x6f\x65\x03\x33\x1c\x28\xa1\x16\x02\x77\x9d\x54\x0f\x2d\x34\xb3\x1b\xf9\xe8\xb7\x3b\x7a\xe1\x48\x5b\xeb\xdf\x6d\xa6\x72\x2e\x93\xa1\x88\xb6\xeb\xd1\x2a\x29\x21\x9a\xfb\x5a\xc9\x1e\xab\xf5\x65\x7d\xd3\x88\xcf\xbf\xeb\xae\x6f\x56\xa5\xf1\x17\xb1\x28\xd9\x6f\xe6\x1f\xe0\xc9\xfc\x17\x72\x64\xe7\xca\xeb\x6e\xd0\xaf\x67\x66\x8a\x4d\xf7\x38\xcf\x7d\x0e\x6e\x2e\x75\xa2\x5b\x57\x5f\x01\x00\x00\xff\xff\x05\xf9\x9d\xb8\xb2\x01\x00\x00") + +func migrations20160729184159_createcooldownafterinvitefieldSqlBytes() ([]byte, error) { + return bindataRead( + _migrations20160729184159_createcooldownafterinvitefieldSql, + "migrations/20160729184159_CreateCooldownAfterInviteField.sql", + ) +} + +func migrations20160729184159_createcooldownafterinvitefieldSql() (*asset, error) { + bytes, err := migrations20160729184159_createcooldownafterinvitefieldSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/20160729184159_CreateCooldownAfterInviteField.sql", size: 434, mode: os.FileMode(420), modTime: time.Unix(1470070949, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -449,6 +470,7 @@ var _bindata = map[string]func() (*asset, error){ "migrations/20160713191703_CreateMembershipDenierField.sql": migrations20160713191703_createmembershipdenierfieldSql, "migrations/20160728180524_CreateMaxPendingInvitesField.sql": migrations20160728180524_createmaxpendinginvitesfieldSql, "migrations/20160728195902_CreateMembershipMessageField.sql": migrations20160728195902_createmembershipmessagefieldSql, + "migrations/20160729184159_CreateCooldownAfterInviteField.sql": migrations20160729184159_createcooldownafterinvitefieldSql, } // AssetDir returns the file names below a certain @@ -507,6 +529,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "20160713191703_CreateMembershipDenierField.sql": &bintree{migrations20160713191703_createmembershipdenierfieldSql, map[string]*bintree{}}, "20160728180524_CreateMaxPendingInvitesField.sql": &bintree{migrations20160728180524_createmaxpendinginvitesfieldSql, map[string]*bintree{}}, "20160728195902_CreateMembershipMessageField.sql": &bintree{migrations20160728195902_createmembershipmessagefieldSql, map[string]*bintree{}}, + "20160729184159_CreateCooldownAfterInviteField.sql": &bintree{migrations20160729184159_createcooldownafterinvitefieldSql, map[string]*bintree{}}, }}, }} diff --git a/db/migrations/20160729184159_CreateCooldownAfterInviteField.sql b/db/migrations/20160729184159_CreateCooldownAfterInviteField.sql new file mode 100644 index 00000000..6dd77a33 --- /dev/null +++ b/db/migrations/20160729184159_CreateCooldownAfterInviteField.sql @@ -0,0 +1,10 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +ALTER TABLE games ADD COLUMN cooldown_before_invite integer NOT NULL DEFAULT 0; +ALTER TABLE games ADD COLUMN cooldown_before_apply integer NOT NULL DEFAULT 3600; + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +ALTER TABLE games DROP COLUMN cooldown_before_invite; +ALTER TABLE games DROP COLUMN cooldown_before_apply; diff --git a/docs/API.md b/docs/API.md index 6b12be2f..d8191277 100644 --- a/docs/API.md +++ b/docs/API.md @@ -80,6 +80,8 @@ Khan API "maxClansPerPlayer": [int], "cooldownAfterDeny": [int], "cooldownAfterDelete": [int], + "cooldownBeforeInvite": [int], + "cooldownBeforeApply": [int], "maxPendingInvites": [int] } ``` @@ -102,7 +104,7 @@ Khan API **minLevelToCreateInvitation**: A member cannot invite a player to join the clan unless their level is greater or equal to this parameter. - **MinLevelOffsetToRemoveMember**: A member cannot remove another member unless their level is at least `MinLevelOffsetToRemoveMember` levels greater than the level of the member they wish to promote. + **minLevelOffsetToRemoveMember**: A member cannot remove another member unless their level is at least `MinLevelOffsetToRemoveMember` levels greater than the level of the member they wish to promote. **minLevelOffsetToPromoteMember**: A member cannot promote another member unless their level is at least `minLevelOffsetToPromoteMember` levels greater than the level of the member they wish to promote. @@ -116,6 +118,10 @@ Khan API **cooldownAfterDelete**: Time (in seconds) the player must wait before applying/being invited to a new membership after the last membership application/invite was deleted. + **cooldownBeforeInvite**: Time (in seconds) a clan member must wait before inviting a member to a new membership after the last membership application/invite was created. + + **cooldownBeforeApply**: Time (in seconds) a player must wait before applying for a clan after the last membership application/invite was created. + **maxPendingInvites**: Maximum number of pending invites each player can have withstanding. Set this value to -1 if your game has no limits on maximum pending invites. * Success Response @@ -182,6 +188,8 @@ Khan API "maxClansPerPlayer": [int], "cooldownAfterDeny": [int], "cooldownAfterDelete": [int], + "cooldownBeforeInvite": [int], + "cooldownBeforeApply": [int], "maxPendingInvites": [int] } ``` diff --git a/docs/game.md b/docs/game.md new file mode 100644 index 00000000..1f73308a --- /dev/null +++ b/docs/game.md @@ -0,0 +1,200 @@ +Game Configuration +================== + +Being a multi-tenant clan server, Khan allows for many different configurations per tenant. Each tenant is a different game and is identified by it's game ID. + +Before any clan operation can be performed, you must create a game in Khan. The good news here is that creating/updating games are idempotent operations. You can keep executing it any time your game changes. That's ideal to be executed in a deploy script, for instance. + +## Creating/Updating a Game + +We recommend that the `Update` operation of the Game resource be used in detriment of the `Create` one. The reasoning here is that the Ùpdate` operation is idempotent (you can run it as many times as you want with the same result). If your game does not exist yet, it will create it, otherwise just updated it with the new configurations. + +To Create/Update your game, just do a `PUT` request to `http://my-khan-server/games/my-game-public-id`, where `my-game-public-id` is the ID you'll be using for all your game's operations in the future. The payload for the request is a JSON object in the body and should be as follows: + +``` + { + "name": [string], + "metadata": [JSON], + "membershipLevels": [JSON], + "minLevelToAcceptApplication": [int], + "minLevelToCreateInvitation": [int], + "minLevelToRemoveMember": [int], + "minLevelOffsetToPromoteMember": [int], + "minLevelOffsetToDemoteMember": [int], + "maxMembers": [int], + "maxClansPerPlayer": [int], + "cooldownAfterDeny": [int], + "cooldownAfterDelete": [int], + "cooldownBeforeInvite": [int], + "cooldownBeforeApply": [int], + "maxPendingInvites": [int] + } +``` + +If the operation is successful, you'll receive a JSON object saying it succeeded: + +``` + { + "success": true + } + +``` + +## Game Configuration Settings + +As can be seen from the previous section, there are a lot of different configurations you can do per game. These will be thoroughly explained in this section. + +### name + +The name of your game. This is used mainly for easier reasoning of what this game is when debugging. + +**Type**: `string`
+**Sample Value**: `My Sample Game` + +### metadata + +Metadata related to your clan. This is a JSON object and can store anything you need to. Each game will probably have a different usage for this attribute: clan nationality, clan flag image URL, number of victories for the clan to date, etc. + +This value is a black box as far as Khan is concerned. It's not used to decide any rules for clan management. + +**Type**: `JSON`
+**Sample Value**: `{ "country": "BR", "language": "pt-BR" }` + +### membershipLevels + +The available membership levels the specified game supports. This is a way to specify the hierarchy between members in your game's clans. These levels are used in other configuration settings like `minLevelToRemoveMember` or `minLevelOffsetToPromoteMember`, among others. + +The membership values (integer) should grow in importance, with the highest number being the highest member level. + +**Type**: `JSON`
+**Sample Value**: `{ "member": 1, "leader": 2, "owner": 3 }` + +### minLevelToAcceptApplication + +The minimum member level (as specified in the membershipLevels configuration) required to accept a pending application to the clan. + +**Type**: `integer`
+**Sample Value**: `2` + +### minLevelToCreateInvitation + +The minimum member level (as specified in the membershipLevels configuration) required to invite someone into a clan. + +**Type**: `integer`
+**Sample Value**: `2` + +### minLevelToRemoveMember + +The minimum member level (as specified in the membershipLevels configuration) required to remove someone from the clan. + +**Type**: `integer`
+**Sample Value**: `2` + +### minLevelOffsetToRemoveMember + +This configuration specifies the required difference in level between a player and the player being removed from the clan. + +Let's look at an example to make things easier: + +``` +John has a membership level of 3, Paul has a membership level of 2 and +Ted has a membership level of 1. + +If the clan has a minLevelOffsetToRemoveMember of 2, that means that only +John can remove Ted, but if that configuration is 1, then both John and Paul +can remove Ted. +``` + +**Type**: `integer`
+**Sample Value**: `2` + +### minLevelOffsetToPromoteMember + +This configuration specifies the required difference in level between a player and the player being promoted (calculated BEFORE the promotion). + +What this means is that a player can only promote another player if that player is `minLevelOffsetToPromoteMember` levels below their own level before the promotion takes place. + +If the `minLevelOffsetToPromoteMember` is greater than 1, then only the clan owner can promote someone to the highest available level(s). + +Let's look at an example to make things easier: + +``` +John has a membership level of 5, Paul has a membership level of 3 and +Ted has a membership level of 1. + +If the clan has a minLevelOffsetToPromoteMember of 2, that means that only +John can promote Ted up to level 4, but if that configuration is 1, +then both John and Paul can promote Ted (Paul can promote Ted to level 3). +``` + +**Type**: `integer`
+**Sample Value**: `2` + +### minLevelOffsetToDemoteMember + +This configuration specifies the required difference in level between a player and the player being demoted (calculated BEFORE the demotion). + +If the `minLevelOffsetToDemoteMember` is greater than 1, then only the clan owner can demote someone from the highest available level(s). + +Let's look at an example to make things easier: + +``` +John has a membership level of 5, Paul has a membership level of 4 and +Ted has a membership level of 3. + +If the clan has a minLevelOffsetToDemoteMember of 2, that means that only +John can demote Ted, but if that configuration is 1, then both John and +Paul can demote Ted. +``` + +**Type**: `integer`
+**Sample Value**: `2` + +### maxMembers + +This configuration specifies the maximum number of members a clan can have. + +**Type**: `integer`
+**Sample Value**: `50` + +### maxClansPerPlayer + +This configuration specifies the maximum number of clans a player can be a member of. + +**Type**: `integer`
+**Sample Value**: `1` + +### cooldownAfterDeny + +Time (in seconds) the player must wait before applying/being invited to a new membership after the last membership application/invite was denied. + +**Type**: `integer`
+**Sample Value**: `360` + +### cooldownAfterDelete + +Time (in seconds) the player must wait before applying/being invited to a new membership after the last membership application/invite was deleted. + +**Type**: `integer`
+**Sample Value**: `720` + +### cooldownBeforeInvite + +Time (in seconds) a clan member must wait before inviting a member to a new membership after the last membership application/invite was created. + +**Type**: `integer`
+**Sample Value**: `720` + +### cooldownBeforeApply + +Time (in seconds) a player must wait before applying for a clan after the last membership application/invite was created. + +**Type**: `integer`
+**Sample Value**: `480` + +### maxPendingInvites + +Maximum number of pending invites each player can have withstanding. Set this value to `-1` if your game has no limits on maximum pending invites. + +**Type**: `integer`
+**Sample Value**: `20` diff --git a/docs/index.rst b/docs/index.rst index 323e61f2..56717a22 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Contents: overview installing hosting + game using_webhooks API benchmark diff --git a/models/fixtures.go b/models/fixtures.go index 8e65770e..82bfad59 100644 --- a/models/fixtures.go +++ b/models/fixtures.go @@ -30,6 +30,8 @@ var GameFactory = factory.NewFactory( MaxMembers: 100, CooldownAfterDeny: 0, CooldownAfterDelete: 0, + CooldownBeforeApply: 3600, + CooldownBeforeInvite: 0, MaxPendingInvites: 20, }, ).Attr("PublicID", func(args factory.Args) (interface{}, error) { @@ -224,11 +226,12 @@ func GetClanWithMemberships( } clan := ClanFactory.MustCreateWithOption(map[string]interface{}{ - "GameID": owner.GameID, - "PublicID": clanPublicID, - "OwnerID": owner.ID, - "Metadata": map[string]interface{}{"x": "a"}, - "MembershipCount": approvedMemberships + 1, + "GameID": owner.GameID, + "PublicID": clanPublicID, + "OwnerID": owner.ID, + "Metadata": map[string]interface{}{"x": "a"}, + "MembershipCount": approvedMemberships + 1, + "AllowApplication": true, }).(*Clan) err = db.Insert(clan) if err != nil { diff --git a/models/game.go b/models/game.go index 637dbb7c..27e3f264 100644 --- a/models/game.go +++ b/models/game.go @@ -37,6 +37,8 @@ type Game struct { UpdatedAt int64 `db:"updated_at"` CooldownAfterDeny int `db:"cooldown_after_deny"` CooldownAfterDelete int `db:"cooldown_after_delete"` + CooldownBeforeApply int `db:"cooldown_before_apply"` + CooldownBeforeInvite int `db:"cooldown_before_invite"` MaxPendingInvites int `db:"max_pending_invites"` } @@ -104,8 +106,8 @@ func CreateGame( levels, metadata map[string]interface{}, minLevelAccept, minLevelCreate, minLevelRemove, minOffsetRemove, minOffsetPromote, minOffsetDemote, maxMembers, - maxClans, cooldownAfterDeny, cooldownAfterDelete, maxPendingInvites int, - upsert bool, + maxClans, cooldownAfterDeny, cooldownAfterDelete, cooldownBeforeApply, + cooldownBeforeInvite, maxPendingInvites int, upsert bool, ) (*Game, error) { levelsJSON, err := json.Marshal(levels) if err != nil { @@ -137,13 +139,15 @@ func CreateGame( metadata, cooldown_after_delete, cooldown_after_deny, + cooldown_before_apply, + cooldown_before_invite, min_membership_level, max_membership_level, max_pending_invites, created_at, updated_at ) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $18)%s` + VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $20)%s` onConflict := ` ON CONFLICT (public_id) DO UPDATE set name=$2, @@ -159,10 +163,12 @@ func CreateGame( metadata=$12, cooldown_after_delete=$13, cooldown_after_deny=$14, - min_membership_level=$15, - max_membership_level=$16, - max_pending_invites=$17, - updated_at=$18 + cooldown_before_apply=$15, + cooldown_before_invite=$16, + min_membership_level=$17, + max_membership_level=$18, + max_pending_invites=$19, + updated_at=$20 WHERE games.public_id=$1` if upsert { @@ -172,24 +178,26 @@ func CreateGame( } _, err = db.Exec(query, - publicID, // $1 - name, // $2 - minLevelAccept, // $3 - minLevelCreate, // $4 - minLevelRemove, // $5 - minOffsetRemove, // $6 - minOffsetPromote, // $7 - minOffsetDemote, // $8 - maxMembers, // $9 - maxClans, // $10 - levelsJSON, // $11 - metadataJSON, // $12 - cooldownAfterDelete, // $13 - cooldownAfterDeny, // $14 - minMembershipLevel, // $15 - maxMembershipLevel, // $16 - maxPendingInvites, // $17 - util.NowMilli(), // $18 + publicID, // $1 + name, // $2 + minLevelAccept, // $3 + minLevelCreate, // $4 + minLevelRemove, // $5 + minOffsetRemove, // $6 + minOffsetPromote, // $7 + minOffsetDemote, // $8 + maxMembers, // $9 + maxClans, // $10 + levelsJSON, // $11 + metadataJSON, // $12 + cooldownAfterDelete, // $13 + cooldownAfterDeny, // $14 + cooldownBeforeApply, // $15 + cooldownBeforeInvite, // $16 + minMembershipLevel, // $17 + maxMembershipLevel, // $18 + maxPendingInvites, // $19 + util.NowMilli(), // $20 ) if err != nil { return nil, err @@ -198,12 +206,16 @@ func CreateGame( } // UpdateGame updates an existing game -func UpdateGame(db DB, publicID, name string, levels, metadata map[string]interface{}, - minLevelAccept, minLevelCreate, minLevelRemove, minOffsetRemove, minOffsetPromote, minOffsetDemote, maxMembers, maxClans, cooldownAfterDeny, cooldownAfterDelete, maxPendingInvites int, +func UpdateGame( + db DB, publicID, name string, levels, metadata map[string]interface{}, + minLevelAccept, minLevelCreate, minLevelRemove, minOffsetRemove, minOffsetPromote, + minOffsetDemote, maxMembers, maxClans, cooldownAfterDeny, cooldownAfterDelete, + cooldownBeforeApply, cooldownBeforeInvite, maxPendingInvites int, ) (*Game, error) { return CreateGame( db, publicID, name, levels, metadata, minLevelAccept, minLevelCreate, minLevelRemove, minOffsetRemove, minOffsetPromote, minOffsetDemote, - maxMembers, maxClans, cooldownAfterDeny, cooldownAfterDelete, maxPendingInvites, true, + maxMembers, maxClans, cooldownAfterDeny, cooldownAfterDelete, cooldownBeforeApply, + cooldownBeforeInvite, maxPendingInvites, true, ) } diff --git a/models/game_test.go b/models/game_test.go index 39c48cec..789272a7 100644 --- a/models/game_test.go +++ b/models/game_test.go @@ -146,6 +146,8 @@ var _ = Describe("Game Model", func() { maxClansPerPlayer := 1 cooldownAfterDeny := 5 cooldownAfterDelete := 10 + cooldownBeforeInvite := 8 + cooldownBeforeApply := 25 maxPendingInvites := 20 game, err := CreateGame( @@ -164,6 +166,8 @@ var _ = Describe("Game Model", func() { maxClansPerPlayer, cooldownAfterDeny, cooldownAfterDelete, + cooldownBeforeInvite, + cooldownBeforeApply, maxPendingInvites, false, ) @@ -208,7 +212,7 @@ var _ = Describe("Game Model", func() { "game-new-name", map[string]interface{}{"Member": 1, "Elder": 2, "CoLeader": 3}, map[string]interface{}{"x": 1}, - 5, 4, 7, 1, 1, 1, 100, 1, 5, 15, 20, + 5, 4, 7, 1, 1, 1, 100, 1, 5, 15, 8, 25, 20, ) Expect(err).NotTo(HaveOccurred()) @@ -230,6 +234,8 @@ var _ = Describe("Game Model", func() { Expect(dbGame.MaxClansPerPlayer).To(Equal(updGame.MaxClansPerPlayer)) Expect(dbGame.CooldownAfterDelete).To(Equal(updGame.CooldownAfterDelete)) Expect(dbGame.CooldownAfterDeny).To(Equal(updGame.CooldownAfterDeny)) + Expect(dbGame.CooldownBeforeInvite).To(Equal(updGame.CooldownBeforeInvite)) + Expect(dbGame.CooldownBeforeApply).To(Equal(updGame.CooldownBeforeApply)) Expect(dbGame.MaxPendingInvites).To(Equal(updGame.MaxPendingInvites)) for k, v := range dbGame.MembershipLevels { Expect(v.(float64)).To(BeEquivalentTo(updGame.MembershipLevels[k])) @@ -245,7 +251,7 @@ var _ = Describe("Game Model", func() { gameID, map[string]interface{}{"Member": 1, "Elder": 2, "CoLeader": 3}, map[string]interface{}{"x": 1}, - 5, 4, 7, 1, 1, 1, 100, 1, 10, 30, 20, + 5, 4, 7, 1, 1, 1, 100, 1, 10, 30, 8, 25, 20, ) Expect(err).NotTo(HaveOccurred()) @@ -265,6 +271,8 @@ var _ = Describe("Game Model", func() { Expect(dbGame.MaxMembers).To(Equal(updGame.MaxMembers)) Expect(dbGame.CooldownAfterDelete).To(Equal(updGame.CooldownAfterDelete)) Expect(dbGame.CooldownAfterDeny).To(Equal(updGame.CooldownAfterDeny)) + Expect(dbGame.CooldownBeforeInvite).To(Equal(updGame.CooldownBeforeInvite)) + Expect(dbGame.CooldownBeforeApply).To(Equal(updGame.CooldownBeforeApply)) Expect(dbGame.MaxPendingInvites).To(Equal(updGame.MaxPendingInvites)) for k, v := range dbGame.MembershipLevels { Expect(v.(float64)).To(Equal(updGame.MembershipLevels[k].(float64))) @@ -283,7 +291,7 @@ var _ = Describe("Game Model", func() { strings.Repeat("a", 256), map[string]interface{}{"Member": 1, "Elder": 2, "CoLeader": 3}, map[string]interface{}{"x": 1}, - 5, 4, 7, 1, 1, 0, 100, 1, 0, 0, 20, + 5, 4, 7, 1, 1, 0, 100, 1, 0, 0, 8, 25, 20, ) Expect(err).To(HaveOccurred()) diff --git a/models/membership.go b/models/membership.go index 4d7d96d3..e4041f09 100644 --- a/models/membership.go +++ b/models/membership.go @@ -294,7 +294,7 @@ func CreateMembership(db DB, game *Game, gameID, level, playerPublicID, clanPubl } membership, _ := GetMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID) - playerID, previousMembership, err := validateMembership(db, game, membership, clanPublicID, playerPublicID) + playerID, previousMembership, err := validateMembership(db, game, membership, clanPublicID, playerPublicID, requestorPublicID) if err != nil { return nil, err } @@ -306,24 +306,36 @@ func CreateMembership(db DB, game *Game, gameID, level, playerPublicID, clanPubl return inviteMember(db, game, membership, level, clanPublicID, playerID, requestorPublicID, message, previousMembership) } -func validateMembership(db DB, game *Game, membership *Membership, clanPublicID, playerPublicID string) (int, bool, error) { +func validateMembership(db DB, game *Game, membership *Membership, clanPublicID, playerPublicID, requestorPublicID string) (int, bool, error) { playerID := -1 previousMembership := false if membership != nil { previousMembership = true nowInMilliseconds := util.NowMilli() - if membership.DeletedAt > 0 { - timeToBeReady := game.CooldownAfterDelete - int(nowInMilliseconds-membership.DeletedAt)/1000 + if membership.Approved { + return -1, false, &AlreadyHasValidMembershipError{playerPublicID, clanPublicID} + } else if membership.Denied { + timeToBeReady := game.CooldownAfterDeny - int(nowInMilliseconds-membership.DeniedAt)/1000 if timeToBeReady > 0 { return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clanPublicID} } - } else if membership.Denied { - timeToBeReady := game.CooldownAfterDeny - int(nowInMilliseconds-membership.DeniedAt)/1000 + } else if membership.DeletedAt > 0 { + timeToBeReady := game.CooldownAfterDelete - int(nowInMilliseconds-membership.DeletedAt)/1000 if timeToBeReady > 0 { return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clanPublicID} } } else { - return -1, false, &AlreadyHasValidMembershipError{playerPublicID, clanPublicID} + cd := game.CooldownBeforeInvite + if requestorPublicID == playerPublicID { + cd = game.CooldownBeforeApply + } + + if cd != 0 { + timeToBeReady := cd - int(nowInMilliseconds-membership.CreatedAt)/1000 + if timeToBeReady > 0 { + return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clanPublicID} + } + } } playerID = membership.PlayerID diff --git a/models/membership_test.go b/models/membership_test.go index 8843c7a6..c743fb6d 100644 --- a/models/membership_test.go +++ b/models/membership_test.go @@ -124,6 +124,331 @@ var _ = Describe("Hook Model", func() { Expect(membership.PlayerID).To(Equal(player.ID)) Expect(membership.ClanID).To(Equal(clan.ID)) }) + + It("Should allow users to recreate an invitation if no cooldown", func() { + game, clan, owner, players, memberships, err := GetClanWithMemberships(testDb, 0, 0, 0, 1, "", "") + Expect(err).NotTo(HaveOccurred()) + membership := memberships[0] + Expect(membership.ID).NotTo(BeEquivalentTo(0)) + + updMembership, err := CreateMembership( + testDb, + game, game.PublicID, + "Member", + players[0].PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + dbMembership, err := GetMembershipByID(testDb, updMembership.ID) + Expect(err).NotTo(HaveOccurred()) + + Expect(dbMembership.GameID).To(Equal(updMembership.GameID)) + Expect(dbMembership.PlayerID).To(Equal(updMembership.PlayerID)) + Expect(dbMembership.ClanID).To(Equal(updMembership.ClanID)) + }) + + It("Should allow users to recreate an application if no cooldown", func() { + game, clan, _, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeApply = 0 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + updMembership, err := CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + dbMembership, err := GetMembershipByID(testDb, updMembership.ID) + Expect(err).NotTo(HaveOccurred()) + + Expect(dbMembership.GameID).To(Equal(updMembership.GameID)) + Expect(dbMembership.PlayerID).To(Equal(updMembership.PlayerID)) + Expect(dbMembership.ClanID).To(Equal(updMembership.ClanID)) + }) + + It("Should fail if user re-applies before cooldown", func() { + game, clan, _, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must wait 3600 seconds before creating a membership in clan")) + }) + + It("Should allow users to recreate an invitation if no cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + updMembership, err := CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + dbMembership, err := GetMembershipByID(testDb, updMembership.ID) + Expect(err).NotTo(HaveOccurred()) + + Expect(dbMembership.GameID).To(Equal(updMembership.GameID)) + Expect(dbMembership.PlayerID).To(Equal(updMembership.PlayerID)) + Expect(dbMembership.ClanID).To(Equal(updMembership.ClanID)) + }) + + It("Should fail if user to be re-invited before cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeInvite = 1000 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must wait 1000 seconds before creating a membership in clan")) + }) + + It("Should allow users to create an application after an invitation if no cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeInvite = 1000 + game.CooldownBeforeApply = 0 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + updMembership, err := CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + dbMembership, err := GetMembershipByID(testDb, updMembership.ID) + Expect(err).NotTo(HaveOccurred()) + + Expect(dbMembership.GameID).To(Equal(updMembership.GameID)) + Expect(dbMembership.PlayerID).To(Equal(updMembership.PlayerID)) + Expect(dbMembership.ClanID).To(Equal(updMembership.ClanID)) + }) + + It("Should allow users to create an invitation after an application if no cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeInvite = 0 + game.CooldownBeforeApply = 1000 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + updMembership, err := CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + dbMembership, err := GetMembershipByID(testDb, updMembership.ID) + Expect(err).NotTo(HaveOccurred()) + + Expect(dbMembership.GameID).To(Equal(updMembership.GameID)) + Expect(dbMembership.PlayerID).To(Equal(updMembership.PlayerID)) + Expect(dbMembership.ClanID).To(Equal(updMembership.ClanID)) + }) + + It("Should fail if an application after an invitation has cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeInvite = 2000 + game.CooldownBeforeApply = 1000 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must wait 1000 seconds before creating a membership in clan")) + }) + + It("Should fail if an invitation after an application has cooldown", func() { + game, clan, owner, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 0, "", "") + Expect(err).NotTo(HaveOccurred()) + + game.CooldownBeforeInvite = 2000 + game.CooldownBeforeApply = 1000 + _, err = testDb.Update(game) + Expect(err).NotTo(HaveOccurred()) + + _, player, err := CreatePlayerFactory(testDb, game.PublicID, true) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + player.PublicID, + "", + ) + Expect(err).NotTo(HaveOccurred()) + + _, err = CreateMembership( + testDb, + game, game.PublicID, + "Member", + player.PublicID, + clan.PublicID, + owner.PublicID, + "", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must wait 2000 seconds before creating a membership in clan")) + }) }) It("Should update a Membership", func() { @@ -790,6 +1115,10 @@ var _ = Describe("Hook Model", func() { game, clan, _, _, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 1, "", "") Expect(err).NotTo(HaveOccurred()) + clan.AllowApplication = false + _, err = testDb.Update(clan) + Expect(err).NotTo(HaveOccurred()) + player := PlayerFactory.MustCreateWithOption(map[string]interface{}{ "GameID": clan.GameID, }).(*Player) @@ -906,7 +1235,7 @@ var _ = Describe("Hook Model", func() { }) It("Membership already exists", func() { - game, clan, owner, players, _, err := GetClanWithMemberships(testDb, 0, 0, 0, 1, "", "") + game, clan, owner, players, _, err := GetClanWithMemberships(testDb, 1, 0, 0, 0, "", "") Expect(err).NotTo(HaveOccurred()) membership, err := CreateMembership(