Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling #11

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading