bensaufley/code-words-api

View on GitHub
test/requests/games-resource-test.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const helper = require('../test-helper'),
      expect = helper.expect,
      sinon = helper.sinon,
      { stubRes } = require('../support/request-helper'),
      { completeGame, prepareGame } = require('../support/game-helpers'),
      User = require('../../models/user'),
      Game = require('../../models/game'),
      Player = require('../../models/player'),
      GameSerializer = require('../../lib/game-serializer'),
      { SocketNotifier, GAME_UPDATED } = require('../../lib/sockets/socket-notifier'),
      gamesResource = require('../../resources/games');

describe('Games Resource', () => {
  describe('index', () => {
    let user, game1, game3, player1, player2, player3, player4, player5;

    beforeEach(() => {
      return User.create({ username: 'my-user', password: 'my-password' })
        .then((u) => {
          user = u;
          return Promise.all([Game.create(), Game.create(), Game.create()]);
        })
        .then(([g1, , g3]) => {
          [game1, game3] = [g1, g3];
          return Promise.all([
            Player.create({ gameId: game1.id, userId: user.id, role: 'transmitter', team: 'b' }),
            Player.create({ gameId: game3.id, userId: user.id, role: 'decoder' }),
            User.create({ username: 'my-user-2', password: 'password2' }).then((u) => Player.create({ gameId: game1.id, userId: u.id, role: 'decoder', team: 'b' })),
            User.create({ username: 'my-user-3', password: 'password3' }).then((u) => Player.create({ gameId: game1.id, userId: u.id, role: 'transmitter', team: 'a' })),
            User.create({ username: 'my-user-4', password: 'password4' }).then((u) => Player.create({ gameId: game1.id, userId: u.id, role: 'decoder', team: 'a' }))
          ]);
        })
        .then((players) => {
          [player1, player2, player3, player4, player5] = players;
        });
    });

    afterEach(helper.cleanDatabase);

    it('returns only the user\'s games', () => {
      let res = stubRes();

      return gamesResource.index({ user }, res)
        .then(() => {
          let json = res.json.getCall(0).args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(json.games.map((g) => g.id).sort()).to.eql([game1.id, game3.id].sort());
        });
    });

    it('returns the associated players and their associated users', () => {
      let res = stubRes();

      return gamesResource.index({ user }, res)
        .then(() => {
          let json = res.json.getCall(0).args[0],
              game1json = json.games.find((g) => g.id === game1.id),
              game3json = json.games.find((g) => g.id === game3.id);

          expect(game1json.players.map((p) => p.id).sort()).to.eql([player1.id, player3.id, player4.id, player5.id].sort());
          expect(game3json.players.map((p) => p.id)).to.eql([player2.id]);
          expect(game1json.players.map((p) => p.username).sort()).to.eql([player1.username, player3.username, player4.username, player5.username].sort());
          expect(game3json.players.map((p) => p.username)).to.eql([player2.username]);
        });
    });

    it('redacts games appropriately', () => {
      let res = stubRes();

      return game1.start()
        .then(() => gamesResource.index({ user }, res))
        .then(() => {
          let json = res.json.getCall(0).args[0],
              decoderGame = json.games.find((g) => g.id === game3.id),
              decoderTileTypes = decoderGame.board.map((t) => t.type),
              transmitterGame = json.games.find((g) => g.id === game1.id),
              transmitterBoard = transmitterGame.board;

          expect(decoderTileTypes).to.eql(new Array(25).fill('redacted'));
          expect(transmitterBoard).to.eql(game1.getDataValue('board'));
        });
    });
  });

  describe('create', () => {
    let user;

    beforeEach(() => {
      return User.create({ username: 'my-user', password: 'my-password' })
        .then((u) => { user = u; });
    });

    afterEach(() => helper.cleanDatabase());

    it('rejects if no user', () => {
      let res = stubRes();

      return gamesResource.create({}, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No user defined' }));
        });
    });

    it('creates a new game', () => {
      let res = stubRes();

      return gamesResource.create({ user }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(200);
          expect(res.json).to.have.been.calledWith(sinon.match({
            id: sinon.match.string,
            board: sinon.match.array,
            turns: sinon.match.array,
            updatedAt: sinon.match.date,
            activePlayerId: null,
            completed: false,
            started: false,
            players: [sinon.match.has('user', sinon.match(user.serialize()))]
          }));
        });
    });
  });

  describe('show', () => {
    let user, anotherUser, userPlayer, anotherUserPlayer, game, game2;

    beforeEach(() => {
      return Promise.all([
        User.create({ username: 'my-user', password: 'my-password' }),
        User.create({ username: 'another-user', password: 'another-pass' })
      ])
        .then((users) => {
          [user, anotherUser] = users;
          return Promise.all([Game.create(), Game.create()]);
        })
        .then((games) => {
          [game, game2] = games;
          return Promise.all([
            Player.create({ userId: user.id, gameId: game.id, role: 'transmitter', team: 'a' }),
            Player.create({ userId: anotherUser.id, gameId: game.id, role: 'decoder', team: 'b' }),
            Player.create({ userId: user.id, gameId: game2.id, role: 'transmitter', team: 'b' })
          ]);
        }).then((players) => {
          [userPlayer, anotherUserPlayer] = players;
        });
    });

    afterEach(() => helper.cleanDatabase());

    it('rejects if no game ID', () => {
      let res = stubRes();

      return gamesResource.show({ user }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No game id specified' }));
        });
    });

    it('rejects if no user', () => {
      let res = stubRes();

      return gamesResource.show({ params: { gameId: game.id } }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No user defined' }));
        });
    });

    it('rejects if no player', () => {
      let res = stubRes();

      return Player.destroy({ where: { userId: user.id, gameId: game.id } })
        .then(() => {
          return gamesResource.show({ user, params: { gameId: game.id } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No player found' }));
            });
        });
    });

    it('returns all players with relevant users', () => {
      let res = stubRes();

      return gamesResource.show({ user, params: { gameId: game.id } }, res)
        .then(() => {
          let resJson = res.json.getCall(0).args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(resJson.players.map((p) => p.id).sort()).to.eql([userPlayer.id, anotherUserPlayer.id].sort());
          expect(resJson.players.map((p) => p.user).map((u) => u.id).sort()).to.eql([user.id, anotherUser.id].sort());
        });
    });

    it('returns a redacted game for a decoder', () => {
      let res = stubRes();

      return gamesResource.show({ user: anotherUser, params: { gameId: game.id } }, res)
        .then(() => {
          let resJson = res.json.getCall(0).args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(resJson.board.map((tile) => tile.type)).to.eql(new Array(25).fill('redacted'));
        });
    });

    it('returns a redacted game for a transmitter before the game has started', () => {
      let res = stubRes();

      return gamesResource.show({ user: user, params: { gameId: game.id } }, res)
        .then(() => {
          let resJson = res.json.getCall(0).args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(resJson.board.map((tile) => tile.type)).to.eql(new Array(25).fill('redacted'));
        });
    });

    it('returns an unredacted game for a transmitter', () => {
      let res = stubRes();

      return Promise.all([
        User.create({ username: 'my-user-7', password: 'password7' }).then((u) => Player.create({ gameId: game.id, userId: u.id, role: 'decoder', team: 'a' })),
        User.create({ username: 'my-user-8', password: 'password8' }).then((u) => Player.create({ gameId: game.id, userId: u.id, role: 'transmitter', team: 'b' }))
      ]).then(() => game.start())
        .then(() => gamesResource.show({ user: user, params: { gameId: game.id } }, res))
        .then(() => {
          let resJson = res.json.getCall(0).args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(resJson.board.map((tile) => tile.type)).to.have.members(['a', 'b', null, 'x']);
          expect(resJson.board.map((tile) => tile.type)).not.to.have.members(['redacted']);
        });
    });
  });

  describe('start', () => {
    let sandbox, aTransmitterUser, aTransmitterPlayer, bDecoderPlayer, game;

    beforeEach(() => {
      sandbox = sinon.sandbox.create();
      return prepareGame()
        .then((response) => {
          ({ aTransmitterUser, aTransmitterPlayer, bDecoderPlayer, game } = response);
        });
    });

    afterEach(() => {
      sandbox.restore();
      return helper.cleanDatabase();
    });

    it('rejects a game without enough players', () => {
      let res = stubRes();
      return bDecoderPlayer.destroy()
        .then(() => gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res))
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith({ error: 'Error: Not enough players' });
        });
    });

    it('rejects a game without proper teams and roles', () => {
      let res = stubRes();
      return bDecoderPlayer.update({ role: null })
        .then(() => gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res))
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith({ error: 'Error: All players must have roles and teams' });
        });
    });

    it('rejects a game that is already started', () => {
      let res = stubRes();

      return game.start()
        .then(() => gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res))
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith({ error: 'Error: Game has already been started' });
        });
    });

    it('assigns activePlayerId', () => {
      let res = stubRes();
      return gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res)
        .then(() => game.reload())
        .then(() => {
          expect(res.status).to.have.been.calledWith(200);
          expect(game.activePlayerId).not.to.be.null;
        });
    });

    it('returns the serialized game', () => {
      let res = stubRes();
      sandbox.spy(GameSerializer, 'serializeGameForPlayer');

      return gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res)
        .then(() => {
          expect(GameSerializer.serializeGameForPlayer).to.have.been.calledWith(sinon.match.instanceOf(Player).and(sinon.match.has('id', aTransmitterPlayer.id)));
          expect(res.status).to.have.been.calledWith(200);
          expect(res.json).to.have.been.calledWith(sinon.match.has('id', game.id));
        });
    });

    it('attempts to notify other players via WebSockets', () => {
      let res = stubRes(),
          eventStub = sandbox.stub(SocketNotifier.prototype, 'event');

      return gamesResource.start({ user: aTransmitterUser, params: { gameId: game.id } }, res)
        .then(() => {
          expect(eventStub).to.have.callCount(3);
          expect(eventStub).to.have.been.always.calledWith(GAME_UPDATED);
        });
    });
  });

  describe('turns', () => {
    let sandbox, game, aTransmitterUser, aTransmitterPlayer, aDecoderUser, aDecoderPlayer, bTransmitterUser, bDecoderUser, bDecoderPlayer;

    beforeEach(() => {
      sandbox = sinon.sandbox.create();
      return Promise.all([
        Game.create(),
        User.create({ username: 'my-user-1', password: 'password-1' }),
        User.create({ username: 'my-user-2', password: 'password-2' }),
        User.create({ username: 'my-user-3', password: 'password-3' }),
        User.create({ username: 'my-user-4', password: 'password-4' })
      ])
        .then((results) => {
          [game, aTransmitterUser, aDecoderUser, bTransmitterUser, bDecoderUser] = results;
          return Promise.all([
            Player.create({ userId: aTransmitterUser.id, gameId: game.id, team: 'a', role: 'transmitter' }),
            Player.create({ userId: aDecoderUser.id, gameId: game.id, team: 'a', role: 'decoder' }),
            Player.create({ userId: bTransmitterUser.id, gameId: game.id, team: 'b', role: 'transmitter' }),
            Player.create({ userId: bDecoderUser.id, gameId: game.id, team: 'b', role: 'decoder' })
          ]);
        })
        .then((ps) => {
          [aTransmitterPlayer, aDecoderPlayer, , bDecoderPlayer] = ps;
        });
    });

    afterEach(() => {
      sandbox.restore();
      return helper.cleanDatabase();
    });

    describe('transmit', () => {
      context('invalid', () => {
        it('rejects an empty body', () => {
          let res = stubRes();

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Missing params: word, number' }));
            });
        });

        it('rejects a missing param', () => {
          let res = stubRes();

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { number: 2 } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Missing param: word' }));
            });
        });

        it('rejects an un-started Game', () => {
          let res = stubRes();

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'transmission', number: 3 } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Game has not begun' }));
            });
        });

        it('rejects an invalid body', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aTransmitterPlayer.id })
            .then(() => gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'two words', number: 4 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Transmission must be one single word' }));
            });
        });

        it('rejects a decoder', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aDecoderPlayer.id })
            .then(() => gamesResource.transmit({ user: aDecoderUser, params: { gameId: game.id }, body: { word: 'two words', number: 4 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: User is not transmitter in this game' }));
            });
        });

        it('rejects when not User\'s turn', () => {
          let res = stubRes();

          return game.update({ activePlayerId: bDecoderPlayer.id })
            .then(() => gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'transmission', number: 5 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: `Error: It is not ${aTransmitterUser.username}'s turn` }));
            });
        });
      });

      context('valid', () => {
        beforeEach(() => {
          return game.update({ activePlayerId: aTransmitterPlayer.id });
        });

        it('adds the turn to the game', () => {
          let res = stubRes();

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'erudition', number: 5 } }, res)
            .then(() => game.reload())
            .then(() => {
              const lastTurn = game.turns[game.turns.length - 1];

              expect(lastTurn).to.have.all.keys(['event', 'number', 'playerId', 'word', 'timestamp']);
              expect(lastTurn.event).to.eq('transmission');
              expect(lastTurn.number).to.eq(5);
              expect(lastTurn.playerId).to.eq(aTransmitterPlayer.id);
              expect(lastTurn.word).to.eq('erudition');
              expect(lastTurn.timestamp).to.be.closeTo(new Date().getTime(), 500);
            });
        });

        it('advances activePlayerId', () => {
          let res = stubRes();

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'transmission', number: 5 } }, res)
            .then(() => game.reload())
            .then(() => {
              expect(game.activePlayerId).to.eq(aDecoderPlayer.id);
            });
        });

        it('returns the serialized game', () => {
          let res = stubRes();
          sandbox.spy(GameSerializer, 'serializeGameForPlayer');

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'transmission', number: 5 } }, res)
            .then(() => {
              expect(GameSerializer.serializeGameForPlayer).to.have.been.calledWith(sinon.match.instanceOf(Player).and(sinon.match.has('id', aTransmitterPlayer.id)));
              expect(res.status).to.have.been.calledWith(200);
              expect(res.json).to.have.been.calledWith(sinon.match.has('id', game.id));
            });
        });

        it('attempts to notify other players via WebSockets', () => {
          let res = stubRes(),
              eventStub = sandbox.stub(SocketNotifier.prototype, 'event');

          return gamesResource.transmit({ user: aTransmitterUser, params: { gameId: game.id }, body: { word: 'transmission', number: 5 } }, res)
            .then(() => {
              expect(eventStub).to.have.callCount(3);
              expect(eventStub).to.have.been.always.calledWith(GAME_UPDATED);
            });
        });
      });
    });

    describe('decode', () => {
      context('invalid', () => {
        it('rejects an empty body', () => {
          let res = stubRes();

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Missing param: tile' }));
            });
        });

        it('rejects a missing param', () => {
          let res = stubRes();

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Missing param: tile' }));
            });
        });

        it('rejects an un-started Game', () => {
          let res = stubRes();

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: 12 } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Game has not begun' }));
            });
        });

        it('rejects an invalid body', () => {
          let res = stubRes();

          return game.update({ activePlayerId: bDecoderPlayer.id })
            .then(() => gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: 28 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No such tile' }));
            });
        });

        it('rejects when not User\'s turn', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aTransmitterPlayer.id })
            .then(() => gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: 13 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: `Error: It is not ${bDecoderUser.username}'s turn` }));
            });
        });

        it('rejects a transmitter', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aTransmitterPlayer.id })
            .then(() => gamesResource.decode({ user: aTransmitterUser, params: { gameId: game.id }, body: { tile: 13 } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: User is not decoder in this game' }));
            });
        });
      });

      context('valid', () => {
        let safeTile;

        beforeEach(() => {
          safeTile = game.getDataValue('board').findIndex((t) => t.type === 'b');
          return game.update({ activePlayerId: bDecoderPlayer.id });
        });

        it('adds the turn to the game', () => {
          let res = stubRes();

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: safeTile } }, res)
            .then(() => game.reload())
            .then(() => {
              const lastTurn = game.turns[game.turns.length - 1];

              expect(lastTurn).to.have.all.keys(['correct', 'event', 'playerId', 'tile', 'timestamp']);
              expect(lastTurn.correct).to.eq(true);
              expect(lastTurn.event).to.eq('decoding');
              expect(lastTurn.playerId).to.eq(bDecoderPlayer.id);
              expect(lastTurn.tile).to.eq(safeTile);
              expect(lastTurn.timestamp).to.be.closeTo(new Date().getTime(), 500);
            });
        });

        it('reveals tile', () => {
          let res = stubRes();

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: safeTile } }, res)
            .then(() => game.reload())
            .then(() => {
              expect(game.getDataValue('board')[safeTile].revealed).to.be.true;
            });
        });

        it('returns the serialized game', () => {
          let res = stubRes();
          sandbox.spy(GameSerializer, 'serializeGameForPlayer');

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: safeTile } }, res)
            .then(() => {
              expect(GameSerializer.serializeGameForPlayer).to.have.been.calledWith(sinon.match.instanceOf(Player).and(sinon.match.has('id', bDecoderPlayer.id)));
              expect(res.status).to.have.been.calledWith(200);
              expect(res.json).to.have.been.calledWith(sinon.match.has('id', game.id));
            });
        });

        it('advances turn when guess is wrong', () => {
          let res = stubRes(),
              wrongTile = game.getDataValue('board').findIndex((t) => t.type === 'a');
          sandbox.spy(GameSerializer, 'serializeGameForPlayer');

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: wrongTile } }, res)
            .then(() => game.reload())
            .then(() => {
              expect(game.activePlayerId).to.eq(aTransmitterPlayer.id);
            });
        });

        it('attempts to notify other players via WebSockets', () => {
          let res = stubRes(),
              eventStub = sandbox.stub(SocketNotifier.prototype, 'event');

          return gamesResource.decode({ user: bDecoderUser, params: { gameId: game.id }, body: { tile: safeTile } }, res)
            .then(() => {
              expect(eventStub).to.have.callCount(3);
              expect(eventStub).to.have.been.always.calledWith(GAME_UPDATED);
            });
        });
      });
    });

    describe('endTurn', () => {
      context('invalid', () => {
        it('rejects an unstarted game', () => {
          let res = stubRes();

          return gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res)
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith({ error: 'Error: Game is not started' });
            });
        });

        it('rejects a transmitter', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aTransmitterPlayer.id })
            .then(() => gamesResource.endTurn({ user: aTransmitterUser, params: { gameId: game.id } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith({ error: 'Error: User is not decoder in this game' });
            });
        });
        it('rejects a decoder whose turn it is not', () => {
          let res = stubRes();

          return game.update({ activePlayerId: aDecoderPlayer.id })
            .then(() => gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith({ error: 'Error: It is not my-user-4\'s turn' });
            });
        });

        it('rejects a finished game', () => {
          let res = stubRes();

          return game.update({ activePlayerId: bDecoderPlayer.id, turns: [{ event: 'end' }] })
            .then(() => gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res))
            .then(() => {
              expect(res.status).to.have.been.calledWith(500);
              expect(res.json).to.have.been.calledWith({ error: 'Error: Game is over' });
            });
        });
      });

      context('valid', () => {
        beforeEach(() => {
          return game.update({ activePlayerId: bDecoderPlayer.id });
        });

        it('changes the activePlayerId', () => {
          let res = stubRes();

          return gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res)
            .then(() => game.reload())
            .then(() => {
              expect(game.activePlayerId).to.eq(aTransmitterPlayer.id);
            });
        });

        it('returns the serialized game', () => {
          let res = stubRes();
          sandbox.spy(GameSerializer, 'serializeGameForPlayer');

          return gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res)
            .then(() => {
              expect(GameSerializer.serializeGameForPlayer).to.have.been.calledWith(sinon.match.instanceOf(Player).and(sinon.match.has('id', bDecoderPlayer.id)));
              expect(res.status).to.have.been.calledWith(200);
              expect(res.json).to.have.been.calledWith(sinon.match.has('id', game.id));
            });
        });

        it('attempts to notify other players via WebSockets', () => {
          let res = stubRes(),
              eventStub = sandbox.stub(SocketNotifier.prototype, 'event');

          return gamesResource.endTurn({ user: bDecoderUser, params: { gameId: game.id } }, res)
            .then(() => {
              expect(eventStub).to.have.callCount(3);
              expect(eventStub).to.have.been.always.calledWith(GAME_UPDATED);
            });
        });
      });
    });
  });

  describe('rematch', () => {
    let user, game;

    beforeEach(() => {
      return prepareGame()
        .then((response) => {
          ({ aTransmitterUser: user, game } = response);
          return completeGame(game);
        });
    });

    afterEach(() => helper.cleanDatabase());

    it('rejects if no user', () => {
      const res = stubRes();

      return gamesResource.rematch({ params: { gameId: game.id } }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No user defined' }));
        });
    });

    it('rejects incomplete games', () => {
      const res = stubRes(),
            board = game.getDataValue('board').map((tile) => ({ ...tile, revealed: false })),
            [turns, , ,] = game.turns;

      return game.update({ board, turns })
        .then(() => gamesResource.rematch({ user, params: { gameId: game.id } }, res))
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: Cannot rematch incomplete game' }));
        });
    });

    it('rejects deleted games', () => {
      const res = stubRes();

      return game.update({ deletedAt: new Date() })
        .then(() => gamesResource.rematch({ user, params: { gameId: game.id } }, res))
        .then(() => {
          expect(res.status).to.have.been.calledWith(404);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'NotFoundError: No game found with that id' }));
        });
    });

    it('creates a new game based on existing game', () => {
      const res = stubRes();

      return gamesResource.rematch({ user, params: { gameId: game.id } }, res)
        .then(() => {
          const gameResponse = res.json.getCalls()[0].args[0];

          expect(res.status).to.have.been.calledWith(200);
          expect(gameResponse.id).not.to.eq(game.id);
          expect(gameResponse).to.include.keys({ activePlayerId: null, turns: [] });
          expect(gameResponse.players.find((p) => p.user.username === 'transmitter-a')).to.include.keys({ team: 'b', role: 'decoder' });
          expect(gameResponse.players.find((p) => p.user.username === 'decoder-a')).to.include.keys({ team: 'b', role: 'transmitter' });
          expect(gameResponse.players.find((p) => p.user.username === 'transmitter-b')).to.include.keys({ team: 'a', role: 'decoder' });
          expect(gameResponse.players.find((p) => p.user.username === 'decoder-b')).to.include.keys({ team: 'a', role: 'transmitter' });
        });
    });
  });

  describe('destroy', () => {
    let user, player, game;

    beforeEach(() => {
      return Promise.all([
        User.create({ username: 'my-user', password: 'my-password' }),
        Game.create()
      ])
        .then((response) => {
          [user, game] = response;

          return Player.create({ gameId: game.id, userId: user.id });
        })
        .then((p) => { player = p; });
    });

    afterEach(() => helper.cleanDatabase());

    it('rejects if no user', () => {
      let res = stubRes();

      return gamesResource.destroy({ params: { gameId: game.id } }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No user defined' }));
        });
    });

    it('rejects if no game ID', () => {
      let res = stubRes();

      return gamesResource.destroy({ user }, res)
        .then(() => {
          expect(res.status).to.have.been.calledWith(500);
          expect(res.json).to.have.been.calledWith(sinon.match({ error: 'Error: No game id specified' }));
        });
    });

    it('marks the game as deleted', () => {
      let res = stubRes();

      return gamesResource.destroy({ user, params: { gameId: game.id } }, res)
        .then(() => {
          return Game.unscoped().findOne({ where: { id: game.id } });
        })
        .then((g) => {
          expect(res.status).to.have.been.calledWith(200);
          expect(g.deletedAt).not.to.be.null;
        });
    });

    it('marks the players as deleted', () => {
      let res = stubRes();

      return gamesResource.destroy({ user, params: { gameId: game.id } }, res)
        .then(() => {
          return Player.unscoped().findOne({ where: { id: player.id } });
        })
        .then((p) => {
          expect(res.status).to.have.been.calledWith(200);
          expect(p.deletedAt).not.to.be.null;
        });
    });
  });
});