Skip to content

Commit

Permalink
Merge pull request #9 from daithihearn/cancel
Browse files Browse the repository at this point in the history
feat: adding cancel game
  • Loading branch information
daithihearn authored Jan 13, 2024
2 parents ee5fbdf + 286d78f commit efce715
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ docs: #@ Generate docs
.PHONY:docs
test: fmt vet #@ Run tests
go test -tags testutils -coverprofile=coverage-full.out ./...
grep -v "_mocks.go" coverage-full.out | grep -v "testdata.go" | grep -v "collection.go" > coverage.out
grep -v "_mocks.go" coverage-full.out | grep -v "handlers.go" | grep -v "testdata.go" | grep -v "collection.go" > coverage.out
go tool cover -html=coverage.out -o coverage.html
.PHONY:test
fmt: #@ Format the code
Expand Down
3 changes: 2 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func main() {

config := cors.Config{
AllowOrigins: strings.Split(origins, ","),
AllowMethods: []string{"GET", "POST", "PUT", "OPTIONS"},
AllowMethods: []string{"GET", "POST", "PUT", "OPTIONS", "DELETE"},
AllowHeaders: []string{"Authorization", "Origin", "Content-Length", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
}
Expand All @@ -131,6 +131,7 @@ func main() {
router.GET("/api/v1/game/:gameId/state", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.GetState)
router.GET("/api/v1/game/all", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.GetAll)
router.PUT("/api/v1/game", auth.EnsureValidTokenGin([]string{auth.WriteAdmin}), gameHandler.Create)
router.DELETE("/api/v1/game/:gameId", auth.EnsureValidTokenGin([]string{auth.WriteAdmin}), gameHandler.Cancel)
router.GET("/api/v1/stats", auth.EnsureValidTokenGin([]string{auth.ReadGame}), statsHandler.GetStats)
router.GET("/api/v1/stats/:playerId", auth.EnsureValidTokenGin([]string{auth.ReadAdmin}), statsHandler.GetStatsForPlayer)

Expand Down
9 changes: 9 additions & 0 deletions pkg/db/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type CollectionI[T any] interface {
Find(ctx context.Context, filter bson.M) ([]T, error)
FindOneAndUpdate(ctx context.Context, filter bson.M, update bson.M) (T, error)
FindOneAndReplace(ctx context.Context, filter bson.M, replacement T) (T, error)
UpdateOne(ctx context.Context, t T, id string) error
Upsert(ctx context.Context, t T, id string) error
Aggregate(ctx context.Context, pipeline interface{}) (*mongo.Cursor, error)
}
Expand Down Expand Up @@ -96,6 +97,14 @@ func (c *Collection[T]) FindOneAndReplace(ctx context.Context, filter bson.M, re
return t, nil
}

func (c *Collection[T]) UpdateOne(ctx context.Context, t T, id string) error {
_, err := c.Col.UpdateOne(ctx, bson.M{
"_id": id,
}, bson.M{"$set": t})

return err
}

func (c *Collection[T]) Upsert(ctx context.Context, t T, id string) error {

// Create filter
Expand Down
14 changes: 14 additions & 0 deletions pkg/db/collection_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type MockCollection[T any] struct {
MockFindResult *[][]T
MockFindErr *[]error
MockUpsertErr *[]error
MockUpdateOneErr *[]error
}

func (m *MockCollection[T]) FindOne(ctx context.Context, filter bson.M) (T, bool, error) {
Expand Down Expand Up @@ -76,6 +77,19 @@ func (m *MockCollection[T]) Upsert(ctx context.Context, t T, id string) error {
return err
}

func (m *MockCollection[T]) UpdateOne(ctx context.Context, t T, id string) error {
// Get the first element of the error array and remove it from the array, return nil if the array is empty
var err error
if len(*m.MockUpdateOneErr) > 0 {
err = (*m.MockUpdateOneErr)[0]
*m.MockUpdateOneErr = (*m.MockUpdateOneErr)[1:]
} else {
err = nil
}

return err
}

func (m *MockCollection[T]) FindOneAndUpdate(ctx context.Context, filter bson.M, update bson.M) (T, error) {
var result T
return result, nil
Expand Down
35 changes: 35 additions & 0 deletions pkg/game/game-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,38 @@ func (h *Handler) GetAll(c *gin.Context) {

c.IndentedJSON(http.StatusOK, games)
}

// Cancel @Summary Cancel a game
// @Description Cancels a game with the given ID
// @Tags Game
// @ID cancel-game
// @Produce json
// @Security Bearer
// @Param gameId path string true "Game ID"
// @Success 200 {object} Game
// @Failure 400 {object} api.ErrorResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /game/{gameId} [delete]
func (h *Handler) Cancel(c *gin.Context) {
// Check the user is correctly authenticated
id, ok := auth.CheckValidated(c)
if !ok {
return
}

// Get the context from the request
ctx := c.Request.Context()

// Get the game ID from the request
gameId := c.Param("gameId")

// Cancel the game
game, err := h.S.Cancel(ctx, gameId, id)

if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()})
return
}

c.IndentedJSON(http.StatusOK, game)
}
28 changes: 28 additions & 0 deletions pkg/game/game-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type ServiceI interface {
Create(ctx context.Context, playerIDs []string, name string, adminID string) (Game, error)
Get(ctx context.Context, gameId string) (Game, bool, error)
GetAll(ctx context.Context) ([]Game, error)
Cancel(ctx context.Context, gameId string, adminId string) (Game, error)
}

type Service struct {
Expand Down Expand Up @@ -55,3 +56,30 @@ func (s *Service) Get(ctx context.Context, gameId string) (Game, bool, error) {
func (s *Service) GetAll(ctx context.Context) ([]Game, error) {
return s.Col.Find(ctx, bson.M{})
}

// Cancel a game.
func (s *Service) Cancel(ctx context.Context, gameId string, adminId string) (Game, error) {
// Get the game from the database.
game, has, err := s.Get(ctx, gameId)
if err != nil {
return Game{}, err
}
if !has {
return Game{}, errors.New("game not found")
}

// Check correct admin
if game.AdminID != adminId {
return Game{}, errors.New("not admin")
}

// Cancel the game.
game.Cancel()

// Save the game to the database.
err = s.Col.UpdateOne(ctx, game, game.ID)
if err != nil {
return Game{}, err
}
return game, nil
}
121 changes: 121 additions & 0 deletions pkg/game/game-service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,124 @@ func TestGetAll(t *testing.T) {
})
}
}

func TestCancel(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
gameToCancel string
adminID string
mockGetResult *[]Game
mockGetExists *[]bool
mockGetError *[]error
mockUpdateError *[]error
expectedResult Game
expectingError bool
}{
{
name: "simple cancel",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{TwoPlayerGame()},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockUpdateError: &[]error{nil},
expectedResult: TwoPlayerGame(),
expectingError: false,
},
{
name: "error thrown",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{
{},
},
mockGetExists: &[]bool{false},
mockGetError: &[]error{errors.New("something went wrong")},
mockUpdateError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
},
{
name: "not found",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{{}},
mockGetExists: &[]bool{false},
mockGetError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
},
{
name: "update error",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{
TwoPlayerGame(),
},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockUpdateError: &[]error{errors.New("something went wrong")},
expectedResult: Game{},
expectingError: true,
},
{
name: "not admin",
gameToCancel: TwoPlayerGame().ID,
adminID: "2",
mockGetResult: &[]Game{
TwoPlayerGame(),
},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockUpdateError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mockCol := &db.MockCollection[Game]{
MockFindOneResult: test.mockGetResult,
MockFindOneExists: test.mockGetExists,
MockFindOneErr: test.mockGetError,
MockUpdateOneErr: test.mockUpdateError,
}

ds := &Service{
Col: mockCol,
}

result, err := ds.Cancel(ctx, test.gameToCancel, test.adminID)

if test.expectingError {
if err == nil {
t.Errorf("expected error %v, got %v", test.expectingError, err)
}
} else {
if result.Status != CANCELLED {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}

// Check all fields are the same except status
if result.ID != test.expectedResult.ID {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}
if !reflect.DeepEqual(result.Players, test.expectedResult.Players) {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}
if !reflect.DeepEqual(result.CurrentRound, test.expectedResult.CurrentRound) {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}
if result.AdminID != test.expectedResult.AdminID {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}
if result.Name != test.expectedResult.Name {
t.Errorf("expected result %v, got %v", test.expectedResult, result)
}
}
})
}
}
4 changes: 4 additions & 0 deletions pkg/game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ func (g *Game) GetState(playerID string) (GameState, error) {
return gameState, nil
}

func (g *Game) Cancel() {
g.Status = CANCELLED
}

type PlayerStats struct {
GameID string `bson:"gameId" json:"gameId"`
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
Expand Down

0 comments on commit efce715

Please sign in to comment.