Skip to content

Commit

Permalink
Merge pull request #11 from daithihearn/calling
Browse files Browse the repository at this point in the history
Calling
  • Loading branch information
daithihearn authored Jan 17, 2024
2 parents d38ece3 + 321adfc commit 6f48943
Show file tree
Hide file tree
Showing 12 changed files with 684 additions and 136 deletions.
5 changes: 3 additions & 2 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@ func main() {
// Configure the routes
router.GET("/api/v1/profile", auth.EnsureValidTokenGin([]string{auth.ReadGame}), profileHandler.Get)
router.PUT("/api/v1/profile", auth.EnsureValidTokenGin([]string{auth.ReadGame}), profileHandler.Update)
router.GET("/api/v1/profile/all", auth.EnsureValidTokenGin([]string{auth.ReadAdmin}), profileHandler.GetAll)
router.GET("/api/v1/profile/all", auth.EnsureValidTokenGin([]string{auth.ReadGame}), profileHandler.GetAll)
router.GET("/api/v1/settings", auth.EnsureValidTokenGin([]string{auth.ReadGame}), settingsHandler.Get)
router.PUT("/api/v1/settings", auth.EnsureValidTokenGin([]string{auth.ReadGame}), settingsHandler.Update)
router.GET("/api/v1/game/:gameId", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.Get)
router.GET("/api/v1/game/:gameId/state", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.GetState)
router.PUT("/api/v1/game/:gameId/call", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.Call)
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.DELETE("/api/v1/game/:gameId", auth.EnsureValidTokenGin([]string{auth.WriteAdmin}), gameHandler.Delete)
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
8 changes: 8 additions & 0 deletions pkg/db/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type CollectionI[T any] interface {
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)
DeleteOne(ctx context.Context, id string) error
}

type Collection[T any] struct {
Expand Down Expand Up @@ -140,3 +141,10 @@ func (c *Collection[T]) Upsert(ctx context.Context, t T, id string) error {
func (c *Collection[T]) Aggregate(ctx context.Context, pipeline interface{}) (*mongo.Cursor, error) {
return c.Col.Aggregate(ctx, pipeline)
}

func (c *Collection[T]) DeleteOne(ctx context.Context, id string) error {
_, err := c.Col.DeleteOne(ctx, bson.M{
"_id": id,
})
return err
}
14 changes: 14 additions & 0 deletions pkg/db/collection_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type MockCollection[T any] struct {
MockFindErr *[]error
MockUpsertErr *[]error
MockUpdateOneErr *[]error
MockDeleteOneErr *[]error
}

func (m *MockCollection[T]) FindOne(ctx context.Context, filter bson.M) (T, bool, error) {
Expand Down Expand Up @@ -99,3 +100,16 @@ func (m *MockCollection[T]) FindOneAndReplace(ctx context.Context, filter bson.M
var result T
return result, nil
}

func (m *MockCollection[T]) DeleteOne(ctx context.Context, 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.MockDeleteOneErr) > 0 {
err = (*m.MockDeleteOneErr)[0]
*m.MockDeleteOneErr = (*m.MockDeleteOneErr)[1:]
} else {
err = nil
}

return err
}
67 changes: 60 additions & 7 deletions pkg/game/game-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ type Handler struct {
S ServiceI
}

type CreateGameRequest struct {
PlayerIDs []string `json:"players"`
Name string `json:"name"`
}

// Create @Summary Create a new game
// @Description Creates a new game with the given name and players
// @Tags Game
Expand Down Expand Up @@ -163,18 +168,17 @@ 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
// Delete @Summary Delete a game
// @Description Deletes a game with the given ID
// @Tags Game
// @ID cancel-game
// @Produce json
// @ID delete-game
// @Security Bearer
// @Param gameId path string true "Game ID"
// @Success 200 {object} Game
// @Success 200
// @Failure 400 {object} api.ErrorResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /game/{gameId} [delete]
func (h *Handler) Cancel(c *gin.Context) {
func (h *Handler) Delete(c *gin.Context) {
// Check the user is correctly authenticated
id, ok := auth.CheckValidated(c)
if !ok {
Expand All @@ -188,7 +192,56 @@ func (h *Handler) Cancel(c *gin.Context) {
gameId := c.Param("gameId")

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

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

c.Status(http.StatusOK)
}

// Call @Summary Make a call
// @Description Makes a call for the current user in the game with the given ID
// @Tags Game
// @ID call
// @Produce json
// @Security Bearer
// @Param gameId path string true "Game ID"
// @Param call query int true "Call"
// @Success 200 {object} Game
// @Failure 400 {object} api.ErrorResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /game/{gameId}/call [put]
func (h *Handler) Call(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")

// Get the call from the request
ca, exists := c.GetQuery("call")
if !exists {
c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: "Missing call"})
return
}
// Check if is a valid call
call, err := ParseCall(ca)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: err.Error()})
return
}

// Make the call
game, err := h.S.Call(ctx, gameId, id, call)

if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()})
Expand Down
40 changes: 32 additions & 8 deletions pkg/game/game-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ 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)
Delete(ctx context.Context, gameId string, adminId string) error
Call(ctx context.Context, gameId string, playerId string, call Call) (Game, error)
}

type Service struct {
Expand Down Expand Up @@ -57,24 +58,47 @@ 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) {
// Delete a game.
func (s *Service) Delete(ctx context.Context, gameId string, adminId string) error {
// Get the game from the database.
game, has, err := s.Get(ctx, gameId)
if err != nil {
return Game{}, err
return err
}
if !has {
return Game{}, errors.New("game not found")
return errors.New("game not found")
}

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

// Can only remove a game that is in an active state
if game.Status != Active {
return errors.New("can only delete games that are in an active state")
}

// Delete the game from the database.
return s.Col.DeleteOne(ctx, game.ID)
}

// Call make a call
func (s *Service) Call(ctx context.Context, gameId string, playerId string, call Call) (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")
}

// Cancel the game.
game.Cancel()
// Make the call.
err = game.Call(playerId, call)
if err != nil {
return Game{}, err
}

// Save the game to the database.
err = s.Col.UpdateOne(ctx, game, game.ID)
Expand Down
122 changes: 53 additions & 69 deletions pkg/game/game-service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,30 +227,28 @@ func TestGetAll(t *testing.T) {
}
}

func TestCancel(t *testing.T) {
func TestDelete(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 string
gameToCancel string
adminID string
mockGetResult *[]Game
mockGetExists *[]bool
mockGetError *[]error
mockDeleteOneError *[]error
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: "simple cancel",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{TwoPlayerGame()},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockDeleteOneError: &[]error{nil},
expectingError: false,
},
{
name: "error thrown",
Expand All @@ -259,21 +257,20 @@ func TestCancel(t *testing.T) {
mockGetResult: &[]Game{
{},
},
mockGetExists: &[]bool{false},
mockGetError: &[]error{errors.New("something went wrong")},
mockUpdateError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
mockGetExists: &[]bool{false},
mockGetError: &[]error{errors.New("something went wrong")},
mockDeleteOneError: &[]error{nil},
expectingError: true,
},
{
name: "not found",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{{}},
mockGetExists: &[]bool{false},
mockGetError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
name: "not found",
gameToCancel: TwoPlayerGame().ID,
adminID: "1",
mockGetResult: &[]Game{{}},
mockGetExists: &[]bool{false},
mockGetError: &[]error{nil},
mockDeleteOneError: &[]error{nil},
expectingError: true,
},
{
name: "update error",
Expand All @@ -282,11 +279,10 @@ func TestCancel(t *testing.T) {
mockGetResult: &[]Game{
TwoPlayerGame(),
},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockUpdateError: &[]error{errors.New("something went wrong")},
expectedResult: Game{},
expectingError: true,
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockDeleteOneError: &[]error{errors.New("something went wrong")},
expectingError: true,
},
{
name: "not admin",
Expand All @@ -295,11 +291,22 @@ func TestCancel(t *testing.T) {
mockGetResult: &[]Game{
TwoPlayerGame(),
},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockUpdateError: &[]error{nil},
expectedResult: Game{},
expectingError: true,
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockDeleteOneError: &[]error{nil},
expectingError: true,
},
{
name: "Game completed",
gameToCancel: CompletedGame().ID,
adminID: "1",
mockGetResult: &[]Game{
CompletedGame(),
},
mockGetExists: &[]bool{true},
mockGetError: &[]error{nil},
mockDeleteOneError: &[]error{nil},
expectingError: true,
},
}

Expand All @@ -309,40 +316,17 @@ func TestCancel(t *testing.T) {
MockFindOneResult: test.mockGetResult,
MockFindOneExists: test.mockGetExists,
MockFindOneErr: test.mockGetError,
MockUpdateOneErr: test.mockUpdateError,
MockDeleteOneErr: test.mockDeleteOneError,
}

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)
}
err := ds.Delete(ctx, test.gameToCancel, test.adminID)

// 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)
}
if test.expectingError && err == nil {
t.Errorf("expected error %v, got %v", test.expectingError, err)
}
})
}
Expand Down
Loading

0 comments on commit 6f48943

Please sign in to comment.