Esempio n. 1
0
class TestUserManager(unittest.TestCase):
    def setUp(self):
        self.fake_logger = MagicMock()
        self.fake_redis = fakeredis.FakeStrictRedis()
        self.user_manager = UserManager(
            self.fake_redis,
            self.fake_logger,
        )

    def test_register_success(self, send_simple_message_patch):
        fake_registration_token = 'ABCDEF'
        with patch('uuid.uuid4', return_value=fake_registration_token), \
             patch.dict('os.environ', {'DOMAIN_URL': 'http://MY_DOMAIN_URL'}):
            self.user_manager.register('gabriel', '12345678',
                                       '*****@*****.**')
        self.assertEqual(send_simple_message_patch.call_count, 1)
        self.assertEqual(
            send_simple_message_patch.call_args[0],
            ('*****@*****.**', 'Welcome to Megachess!!',
             ('<p>Please confirm your email account</p>'
              '<a href="http://MY_DOMAIN_URL/confirm_registration?token=ABCDEF">CONFIRM YOUR REGISTRATION</a>'
              )),
        )
        self.assertTrue(
            self.fake_redis.exists(
                self.user_manager._registration_id(fake_registration_token)))

    def test_register_invalid_username(self, send_simple_message_patch):
        with self.assertRaises(InvalidRegistrationUsername):
            self.user_manager.register('g aby', 'pass', '*****@*****.**')

        with self.assertRaises(InvalidRegistrationUsername):
            self.user_manager.register('gaby1', 'pass', '*****@*****.**')

    def test_register_invalid_email(self, send_simple_message_patch):
        with self.assertRaises(InvalidRegistrationEmail):
            self.user_manager.register('gaby', 'pass', 'gab@')

        with self.assertRaises(InvalidRegistrationEmail):
            self.user_manager.register('gaby', 'pass', 'gabexample.com')

    def test_register_already_exists(self, send_simple_message_patch):
        fake_registration_token = 'ABCDEFH'
        self.user_manager._save_user('gabrieltwo', '<fake+pass>',
                                     '*****@*****.**')
        with patch('uuid.uuid4', return_value=fake_registration_token), \
             self.assertRaises(UserAlreadyExistsException):
            self.user_manager.register('gabrieltwo', '12345678',
                                       '*****@*****.**')
        self.assertEqual(send_simple_message_patch.call_count, 0)
        self.assertFalse(
            self.fake_redis.exists(
                self.user_manager._registration_id(fake_registration_token)))

    def test_confirm_registration_success(self, send_simple_message_patch):
        fake_registration_token = 'ABCDEFGI'
        with patch('uuid.uuid4', return_value=fake_registration_token):
            self.user_manager.register('gabrielthree', '12345678',
                                       '*****@*****.**')
        fake_auth_token = 'wqerqwerqwer'
        with patch('uuid.uuid4', return_value=fake_auth_token):
            self.user_manager.confirm_registration(fake_registration_token)
        self.assertIsNotNone(
            self.user_manager.get_user_by_username('gabrielthree'))
        self.assertFalse(
            self.fake_redis.exists(
                self.user_manager._registration_id(fake_registration_token)))
        self.assertEqual(send_simple_message_patch.call_count, 2)
        self.assertEqual(
            send_simple_message_patch.call_args[0],
            ('*****@*****.**',
             'Your account in Megachess is confirmed!!!',
             ('<p>This is your personal auth_token to play</p>'
              '<p><strong>{}</strong></p>').format(fake_auth_token)),
        )

    def test_confirm_registration_error(self, send_simple_message_patch):
        with self.assertRaises(InvalidRegistrationToken):
            self.user_manager.confirm_registration('<fake_registration_token>')
Esempio n. 2
0
class Controller:
    def __init__(self, redis_pool, app, connected_websockets):
        self.chess_manager = ChessManager(redis_pool)
        self.user_manager = UserManager(redis_pool, app)
        self.tournament_manager = TournamentManager(redis_pool,
                                                    self.chess_manager)
        self.board_subscribers = {}
        self.redis_pool = redis_pool
        self.app = app
        self.connected_websockets = connected_websockets

    async def execute_message(self, client, message):
        self.app.logger.info('process_message: message: {}'.format(message))
        await self.process_message(client, message)

    async def get_current_username(self, client):
        auth_token = client.args.get('authtoken')
        if not auth_token:
            raise NoTokenException()
        return await self.user_manager.get_username_by_auth_token(auth_token)

    async def process_message(self, client, message):
        method_name, data = await self.parse_message(message)
        current_username = await self.get_current_username(client)
        self.app.logger.info('process_message from {}: {} {}'.format(
            current_username, method_name, data))
        method = getattr(self, method_name)
        try:
            await method(current_username, client, data)
            # await self.send(client, 'response_ok', data)
        except Exception as e:
            tb = traceback.format_exc()
            self.app.logger.error('exception {} {}'.format(e, tb))

            data = {'exception': str(type(e))}
            # gevent.spawn(
            await self.send(client, 'response_error', data)
            raise e

    async def parse_message(self, message):
        try:
            job = ujson.loads(message)
        except ValueError:
            raise InvalidActionFormatException()

        if 'action' not in job:
            raise InvalidNoActionException()
        action_name = job['action']
        method_name = 'action_' + str(action_name)
        if not hasattr(self, method_name):
            raise InvalidActionNameException()

        if 'data' not in job:
            raise InvalidNoDataException()

        data = job['data']
        return method_name, data

    def valid_auth(self, data):
        return 'username' in data and 'password' in data

    async def action_register(self, current_username, client, data):
        if not self.valid_auth(data):
            raise InvalidRegisterException()
        return self.user_manager.register(data['username'], data['password'])

    async def action_get_connected_users(self, current_username, client, data):
        self.app.logger.info('action_get_connected {} {}'.format(
            client, current_username))
        data = {'users_list': await self.get_active_users()}
        await self.send(client, 'update_user_list', data)

    async def action_login(self, current_username, client, data=None):
        self.app.logger.info('action_login {} {}'.format(
            client, current_username))
        client.username = current_username
        client.queue.username = current_username
        data = {'users_list': await self.get_active_users()}
        self.app.logger.info('connected users: {}'.format(data))
        await self.broadcast('update_user_list', data)

        return True

    async def get_active_users(self):
        return {
            queue.username
            for queue in self.connected_websockets
            if hasattr(queue, 'username')
        }

    def get_username_by_client(self, client):
        for queue in self.connected_websockets:
            if (hasattr(queue, 'webservice') and queue.webservice == client
                    and hasattr(queue, 'username')):
                return queue.username

    async def action_challenge(self, current_username, client, data):
        challenged_username = data['username']
        challenger_username = current_username
        await self._challenge(challenger_username, challenged_username)

    async def challenge_with_auth_token(self, auth_token, username, message):
        challenger_username = await self.user_manager.get_username_by_auth_token(
            auth_token)
        return await self._challenge(challenger_username, username)

    async def _challenge(self, challenger_username, challenged_username):
        self.app.logger.info('action_challenge {} from {}'.format(
            challenged_username, challenger_username))
        if random.choice([True, False]):
            white_username = challenger_username
            black_username = challenged_username
        else:
            white_username = challenged_username
            black_username = challenger_username
        move_left = 200
        board_id = self.chess_manager.challenge(
            white_username=white_username,
            black_username=black_username,
            move_left=move_left,
        )
        data = {
            'username': challenger_username,
            'board_id': board_id,
        }
        await self.broadcast('ask_challenge', data, challenged_username)
        return True

    async def broadcast(self, event, data, username=None):
        for queue in self.connected_websockets:
            if (not username or
                (hasattr(queue, 'username') and username == queue.username)):
                message = {
                    'event': event,
                    'data': data,
                }
                await queue.put(ujson.dumps(message))

    async def action_accept_challenge(self, current_username, client, data):
        board_id = data['board_id']
        await self._start_board(board_id)
        return True

    async def _start_board(self, board_id):
        turn_token, username, actual_turn, board, move_left, opponent_username = self.chess_manager.challenge_accepted(
            board_id)
        next_turn_data = {
            'board_id': board_id,
            'turn_token': turn_token,
            'username': username,
            'actual_turn': actual_turn,
            'board': board,
            'move_left': move_left,
            'opponent_username': opponent_username,
        }
        self.app.logger.info('action_accept_challenge ok'.format(
            board_id, next_turn_data))
        await self.set_next_turn(board_id, next_turn_data)

    async def action_abort(self, current_username, client, data):
        board_id = data['board_id']
        self.chess_manager.abort(board_id, current_username)
        await self.send_gameover(board_id)

    async def action_move(self, current_username, client, data):
        board_id = data['board_id']
        turn_token = data['turn_token']
        key = self.get_next_turn_key(board_id, turn_token)
        self.app.logger.info('action_move control timeout {}'.format(key))
        if self.redis_pool.exists(key):
            self.app.logger.info(
                'action_move control timeout OK {}'.format(key))
            self.redis_pool.delete(key)
        else:
            # timeout...
            self.app.logger.info(
                'action_move control timeout ERROR {}'.format(key))
            raise TimeoutException()
        processed = False
        try:
            turn_token, username, actual_turn, board, move_left, opponent_username = self.chess_manager.move_with_turn_token(
                turn_token=data['turn_token'],
                from_row=data['from_row'],
                from_col=data['from_col'],
                to_row=data['to_row'],
                to_col=data['to_col'],
            )
            processed = True
        except GameOverException:
            await self.send_gameover(board_id)
            return
        except Exception as e:
            tb = traceback.format_exc()
            try:
                self.app.logger.error('action_move {} exception  {} {}'.format(
                    board_id, e, tb))
                await self.force_change_turn(data['board_id'],
                                             data['turn_token'])
                return
                # turn_token, username, actual_turn, board, move_left = self.chess_manager._next_turn_token(board_id)
            except GameOverException:
                await self.send_gameover(board_id)
                return
        next_turn_data = {
            'board_id': board_id,
            'turn_token': turn_token,
            'username': username,
            'actual_turn': actual_turn,
            'board': board,
            'move_left': move_left,
            'opponent_username': opponent_username,
        }
        await self.set_next_turn(board_id, next_turn_data)

    def get_next_turn_key(self, board_id, turn_token):
        return "next_turn:{}:{}".format(board_id, turn_token)

    async def set_next_turn(self, board_id, next_turn_data):
        self.app.logger.info('set_next_turn {} {}'.format(
            board_id, next_turn_data))
        key = self.get_next_turn_key(board_id, next_turn_data['turn_token'])
        self.redis_pool.set(key, ujson.dumps(next_turn_data))
        await self.enqueue_next_turn(key)
        # if not self._save_turn(next_turn_data):
        #     raise InvalidSaveTurnException()

    async def enqueue_next_turn(self, key):
        self.app.logger.info('enqueue_next_turn {}'.format(key))
        # self.redis_pool.rpush("next_turn_queue", key)
        # self.pool.wait_available()
        # self.pool.spawn(self.process_next_turn, key)
        await self.process_next_turn(key)

    def _save_turn(self, data):
        try:
            data_json = ujson.dumps(data)
            self.redis_pool.set("{0}:{1}".format('turn', data['turn_token']),
                                data_json)
            return True
        except Exception:
            return False

    async def send_gameover(self, board_id):
        board = self.chess_manager.get_board_by_id(board_id)
        if self.tournament_manager.get_tournament_key('') in board_id:
            self.tournament_manager.board_finish(board_id)
        data = {
            'board': board.board.get_simple(),
            'white_username': str(board.white_username),
            'black_username': str(board.black_username),
            'white_score': str(board.white_score),
            'black_score': str(board.black_score),
            'board_id': board_id,
        }
        await self.broadcast('gameover', data, board.white_username)
        await self.broadcast('gameover', data, board.black_username)

    async def force_change_turn(self, board_id, turn_token):
        self.app.logger.info('force_change_turn {} {}'.format(
            board_id, turn_token))
        try:
            turn_token, username, actual_turn, board, move_left, opponent_username = self.chess_manager.force_change_turn(
                board_id, turn_token)
        except GameOverException:
            await self.send_gameover(board_id)
            return
        next_turn_data = {
            'board_id': board_id,
            'turn_token': turn_token,
            'username': username,
            'actual_turn': actual_turn,
            'board': board,
            'move_left': move_left,
            'opponent_username': opponent_username,
        }
        self.app.logger.info('force_change_turn set_next_turn {} {}'.format(
            board_id, turn_token))
        await self.set_next_turn(board_id, next_turn_data)

    async def process_next_turn(self, key):
        self.app.logger.info('process_next_turn {}'.format(key))
        try:
            # key = self.redis_pool.blpop('next_turn_queue')
            if not key:
                self.app.logger.info('Nothing pending to process')
                return
            data = ujson.loads(self.redis_pool.get(key))
            self.app.logger.info('next_turn key: {} data: {}'.format(
                key, data))
            await self.broadcast('your_turn', data, data['username'])
            # self.notify_to_board_subscribers(data['board_id'])
            # control timeout
            await asyncio.sleep(30)
            self.app.logger.info('Checking timeout {} {}'.format(
                data['board_id'], data['turn_token']))
            if self.redis_pool.exists(key):
                self.app.logger.info('Forcing timeout {} {}'.format(
                    data['board_id'], data['turn_token']))
                self.redis_pool.delete(key)
                await self.force_change_turn(data['board_id'],
                                             data['turn_token'])
        except Exception as e:
            tb = traceback.format_exc()
            self.app.logger.error(
                'process_next_turn {} exception  {} {}'.format(key, e, tb))
        self.app.logger.info('end process_next_turn {}'.format(key))

    def notify_to_board_subscribers(self, board_id):
        board = self.chess_manager.get_board_by_id(board_id)
        for board_subscriber_client in self.board_subscribers.get(
                board_id, []):
            self.notify_board_update(board_subscriber_client, board)

    async def notify_board_update(self, board_subscriber_client, board):
        data = {
            'board': board.board.get_simple(),
            'white_username': board.white_username,
            'black_username': board.black_username,
            'white_score': board.white_score,
            'black_score': board.black_score,
        }
        await self.send(board_subscriber_client, 'update_board', data)

    async def action_subscribe(self, current_username, client, data):
        board_id = data['board_id']
        board = self.chess_manager.get_board_by_id(board_id)
        if board_id not in self.board_subscribers:
            self.board_subscribers[board_id] = []
        self.board_subscribers[board_id].append(client)
        self.notify_board_update(client, board)
        return True

    async def send(self, client, event, data):
        """
        Send given data to the registered client.
        Automatically discards invalid connections.
        """
        try:
            self.app.logger.info(
                u'send to client: {}, event: {}, data: {}'.format(
                    client, event, data))
            message = {
                'event': event,
                'data': data,
            }
            # print 'sent to {0}: {1}'.format(client, message)
            await client.send(ujson.dumps(message))
        except Exception:
            pass
            #  app.logger.info(u'Exception on sending to client: {}'.format(client))
            #  self.clients.remove(client)

    async def action_create_tournament(self, current_username, client, data):
        tournament = self.tournament_manager.create_tournament()
        await self.send(client, 'tournament_created', tournament)
        return True

    async def action_add_user_to_tournament(self, current_username, client,
                                            data):
        tournament_id = data['tournament_id']
        username = data['username']
        if username == '*':
            active_usernames = await self.get_active_users()
            for username in active_usernames:
                self.tournament_manager.add_user(tournament_id, username)
        else:
            self.tournament_manager.add_user(tournament_id, username)
        users = self.tournament_manager.get_users(tournament_id)
        await self.send(client, 'user_added_to_tournament', users)
        return True

    async def action_start_tournament(self, current_username, client, data):
        tournament_id = data['tournament_id']
        tournament = self.tournament_manager.get_tournament(tournament_id)
        # TODO: control and change state...
        boards = self.tournament_manager.start(tournament_id)
        for board_id in boards:
            asyncio.create_task(self._start_board(board_id))
        users = self.tournament_manager.get_users(tournament_id)

        await self.send(client, 'tournament_started', users)
        return True