Example #1
0
    def test_non_ascii(self):
        # meta-data size. ensure 'alert' is included.
        empty_msg_size = len(
            Message(tokens=[VALID_TOKEN_ONE],
                    alert="a").get_json_payload()) - 1

        max_utf8_size = 3  # size of maximum utf8 encoded character in bytes

        chinese_str = (
            u'\u5187\u869a\u5487\u6b8f\u5cca\u9f46\u9248\u6935\u4ef1\u752a'
            u'\u67cc\u521e\u62b0\u530a\u6748\u9692\u5c6e\u653d\u588f\u6678')
        chinese_msg_size = len(
            Message(tokens=[VALID_TOKEN_ONE],
                    alert=chinese_str).get_json_payload())

        self.assertLessEqual(chinese_msg_size,
                             empty_msg_size + len(chinese_str) * max_utf8_size)

        max_emoji_size = 4  # size of maximum utf8 encoded character in bytes

        # emoji
        emoji_str = u'\U0001f601\U0001f603\U0001f638\U00002744'
        emoji_msg_size = len(
            Message(tokens=VALID_TOKEN_ONE,
                    alert=emoji_str).get_json_payload())

        self.assertLessEqual(emoji_msg_size,
                             empty_msg_size + len(emoji_str) * max_emoji_size)
Example #2
0
    def test_send(self):
        # success, retry + include-failed, don't-retry + include-failed
        session = self.get_session(push=(None, 1, 3))

        msg = Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                      alert="my alert",
                      badge=10,
                      content_available=1,
                      my_extra=15)

        srv = APNs(self.get_connection(session))
        res = srv.send(msg)

        self.assertEqual(len(res.failed), 0)
        self.assertEqual(len(res.errors), 0)
        self.assertFalse(res.needs_retry())

        srv = APNs(self.get_connection(session))
        self.assertEqual(session.pool.push_result_pos, 0)

        session.pool.push_result_pos += 1
        res = srv.send(msg)

        self.assertEqual(len(res.failed), 0)
        self.assertEqual(len(res.errors), 1)
        self.assertTrue(res.needs_retry())

        # indeed, we have used the cache
        self.assertEqual(session.pool.new_connections, 1)

        srv = APNs(self.get_connection(session))
        res = srv.send(msg)

        self.assertEqual(len(res.failed), 0)
        self.assertEqual(len(res.errors), 1)
        self.assertFalse(res.needs_retry())

        # indeed, new connection, we haven't used the cache
        self.assertEqual(session.pool.new_connections, 2)

        messages = [
            Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                    alert="bar alert",
                    badge=4,
                    my_extra=15),
            Message(tokens=[VALID_TOKEN_THREE],
                    alert="foo alert",
                    badge=0,
                    content_available=1,
                    more_extra=15)
        ]

        srv = APNs(self.get_connection(session))
        res = srv.send(messages)
        self.assertEqual(len(res.failed), 0)
        self.assertEqual(len(res.errors), 0)
        self.assertFalse(res.needs_retry())
Example #3
0
    def test_validate_tokens(self):
        with self.assertRaises(ValueError):
            Message(tokens="")

        with self.assertRaises(ValueError):
            Message(tokens=[""])

        with self.assertRaises(ValueError):
            Message(tokens=[VALID_TOKEN_ONE, INVALID_TOKEN_ONE])

        with self.assertRaises(TypeError):
            Message(tokens=[INVALID_TOKEN_TWO])
Example #4
0
    def setUp(self):
        self.now = datetime.datetime.now()

        # Typical message
        self.message = Message(
            tokens=VALID_TOKEN_ONE,
            alert=u"Russian: \u0421\u0430\u0440\u0434\u0430\u0440",
            badge=10,
            sound="test.mp3",
            content_available=1,
            expiry=self.now + datetime.timedelta(days=3),
            priority=30,
            extra={'key': 'value'})

        # Message with a custom payload
        self.raw_message = Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                                   payload=self.message.payload,
                                   priority=5,
                                   expiry=datetime.timedelta(days=5))
Example #5
0
 def setUp(self):
     self.now = datetime.datetime.now()
     # Typical message
     self.message = Message(
         "0123456789ABCDEF",
         alert=u"Russian: \u0421\u0430\u0440\u0434\u0430\u0440",
         badge=10,
         sound="test.mp3",
         content_available=1,
         expiry=self.now + datetime.timedelta(days=3),
         priority=30,
         extra={'key': 'value'}
     )
     # Message with a custom payload
     self.raw_message = Message(
         ["0123456789ABCDEF", "FEDCBA9876543210"],
         payload=self.message.payload,
         priority=5,
         expiry=datetime.timedelta(days=5)
     )
Example #6
0
    def test_send(self):
        if not self._certificate_available():
            # Skip, no certificate available
            return

        session = self.get_session()

        # Test with single message
        msg = Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                      alert="my alert",
                      badge=10,
                      content_available=1,
                      my_extra=15)

        srv = APNs(self.get_connection(session))
        res = srv.send(msg)

        self.assertEqual(len(res.failed), 1)
        self.assertEqual(len(res.errors), 0)
        self.assertTrue(res.needs_retry())

        # Test with multiple messages
        messages = [
            Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                    alert="bar alert",
                    badge=4,
                    my_extra=15),
            Message(tokens=[VALID_TOKEN_THREE],
                    alert="foo alert",
                    badge=0,
                    content_available=1,
                    more_extra=15)
        ]

        srv = APNs(self.get_connection(session))
        res = srv.send(messages)

        self.assertEqual(len(res.failed), 1)
        self.assertEqual(len(res.errors), 0)
        self.assertTrue(res.needs_retry())
Example #7
0
    def test_serialization(self):
        # standard pickle
        s_message = pickle.dumps(self.message)
        s_raw_message = pickle.dumps(self.raw_message)
        c_message = pickle.loads(s_message)
        c_raw_message = pickle.loads(s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key),
                             getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(c_raw_message, key))

        # custom
        s_message = self.message.__getstate__()
        s_raw_message = self.raw_message.__getstate__()

        # JSON/XML/etc and store/send
        s_message = json.dumps(s_message)
        s_raw_message = json.dumps(s_raw_message)

        # unserialize
        s_message = json.loads(s_message)
        s_raw_message = json.loads(s_raw_message)

        # reconstruct
        c_message = Message(**s_message)
        c_raw_message = Message(**s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key),
                             getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(c_raw_message, key))
Example #8
0
    def _apns_push(self, packet):
        envelope = packet.get('envelope')
        to_jid = envelope.get('to')
        to = to_jid.split('@')[0]
        user_info = UserInfo.objects.get(id=to)
        if user_info.device_token:
            logger.info('APNS : %s' % user_info.device_token)
            device_token = user_info.device_token
            payload = packet.get('payload')
            attrs = payload.get('attrs')
            messageType = attrs.get('messageType')
            if 'text' == messageType:
                content = payload.get('content')
            elif 'image' == messageType:
                content = '收到一张图片'
            elif 'geo' == messageType:
                content = '收到一个坐标分享'
            elif 'audio' == messageType:
                content = '收到一条语音'
            elif 'contact' == messageType:  # 联系人接口通知
                action = attrs.get('action')
                if 'add' == action:
                    content = '添加联系人通知'
                elif 'accept' == action:
                    content = '接受好友申请通知'
                elif 'reject' == action:
                    content = '拒绝好友申请通知'
            total = packet.get('total')
            if content and device_token:
                message = Message(tokens=[device_token],
                                  alert=content,
                                  badge=int(total),
                                  content_available=1,
                                  my_extra=15)
                srv = APNs(con)
                res = srv.send(message)

                # Check failures. Check codes in APNs reference docs.
                for token, reason in res.failed.items():
                    code, errmsg = reason
                    logger.info("Device faled: {0}, reason: {1}".format(
                        token, errmsg))

            # Check failures not related to devices.
            for code, errmsg in res.errors:
                logger.error("Error: %s" % errmsg)
Example #9
0
class APNsClerkMessageTest(Python26Mixin, unittest.TestCase):
    """ Test Message API. """
    def setUp(self):
        self.now = datetime.datetime.now()

        # Typical message
        self.message = Message(
            tokens=VALID_TOKEN_ONE,
            alert=u"Russian: \u0421\u0430\u0440\u0434\u0430\u0440",
            badge=10,
            sound="test.mp3",
            content_available=1,
            expiry=self.now + datetime.timedelta(days=3),
            priority=30,
            extra={'key': 'value'})

        # Message with a custom payload
        self.raw_message = Message(tokens=[VALID_TOKEN_ONE, VALID_TOKEN_TWO],
                                   payload=self.message.payload,
                                   priority=5,
                                   expiry=datetime.timedelta(days=5))

    def test_payload(self):
        payload = self.message.get_json_payload()

        self.assertIsInstance(payload, six.binary_type)

        unicode_src = payload.decode('utf-8')
        payload = json.loads(unicode_src)

        self.assertEqual(
            payload["aps"], {
                "alert": self.message.alert,
                "badge": self.message.badge,
                "sound": self.message.sound,
                "content-available": self.message.content_available
            })

        for k, v in self.message.extra.items():
            self.assertEqual(payload[k], v)

    def test_serialization(self):
        # standard pickle
        s_message = pickle.dumps(self.message)
        s_raw_message = pickle.dumps(self.raw_message)
        c_message = pickle.loads(s_message)
        c_raw_message = pickle.loads(s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key),
                             getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(c_raw_message, key))

        # custom
        s_message = self.message.__getstate__()
        s_raw_message = self.raw_message.__getstate__()

        # JSON/XML/etc and store/send
        s_message = json.dumps(s_message)
        s_raw_message = json.dumps(s_raw_message)

        # unserialize
        s_message = json.loads(s_message)
        s_raw_message = json.loads(s_raw_message)

        # reconstruct
        c_message = Message(**s_message)
        c_raw_message = Message(**s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key),
                             getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(c_raw_message, key))

    def test_non_ascii(self):
        # meta-data size. ensure 'alert' is included.
        empty_msg_size = len(
            Message(tokens=[VALID_TOKEN_ONE],
                    alert="a").get_json_payload()) - 1

        max_utf8_size = 3  # size of maximum utf8 encoded character in bytes

        chinese_str = (
            u'\u5187\u869a\u5487\u6b8f\u5cca\u9f46\u9248\u6935\u4ef1\u752a'
            u'\u67cc\u521e\u62b0\u530a\u6748\u9692\u5c6e\u653d\u588f\u6678')
        chinese_msg_size = len(
            Message(tokens=[VALID_TOKEN_ONE],
                    alert=chinese_str).get_json_payload())

        self.assertLessEqual(chinese_msg_size,
                             empty_msg_size + len(chinese_str) * max_utf8_size)

        max_emoji_size = 4  # size of maximum utf8 encoded character in bytes

        # emoji
        emoji_str = u'\U0001f601\U0001f603\U0001f638\U00002744'
        emoji_msg_size = len(
            Message(tokens=VALID_TOKEN_ONE,
                    alert=emoji_str).get_json_payload())

        self.assertLessEqual(emoji_msg_size,
                             empty_msg_size + len(emoji_str) * max_emoji_size)

    def test_batch(self):
        # binary serialization in ridiculously small buffer =)
        b_message = list(self.message.batch(10))
        b_raw_message = list(self.raw_message.batch(10))

        # number of batches
        self.assertEqual(len(b_message), 1)
        self.assertEqual(len(b_raw_message), 2)

        # lets read stuff back. number of sent before ID's is of course 0.
        self.check_message(b_message[0], 0, self.message)
        self.check_message(b_raw_message[0], 0, self.raw_message)
        self.check_message(b_raw_message[1], 1, self.raw_message)

    def check_message(self, batch, itr, msg):
        sent, data = batch

        # we send batches of 1 token size
        self.assertEqual(sent, itr)

        # |COMMAND|FRAME-LEN|{token}|{payload}|{id:4}|{expiry:4}|{priority:1}
        command, frame_len = struct.unpack(">BI", data[0:5])

        self.assertEqual(command, 2)
        self.assertEqual(frame_len, len(data) - 5)

        off = 5
        restored = {}

        for itm in range(1, 6):
            hdr, length = struct.unpack(">BH", data[off:(off + 3)])
            off += 3
            value = data[off:(off + length)]
            off += length

            if hdr == 1:
                restored['token'] = binascii.hexlify(value).decode('ascii')
            elif hdr == 2:
                restored['payload'] = json.loads(value.decode('utf-8'))
            elif hdr == 3:
                restored['index'] = struct.unpack(">I", value)[0]
            elif hdr == 4:
                restored['expiry'] = struct.unpack(">I", value)[0]
            elif hdr == 5:
                restored['priority'] = struct.unpack(">B", value)[0]

        for key in ('token', 'payload', 'index', 'expiry', 'priority'):
            if key not in restored:
                self.fail("Binary message is missing: %s" % key)

        # check message
        self.assertEqual(msg.tokens[itr].lower(), restored['token'].lower())
        self.assertEqual(msg.payload['aps'], restored['payload']['aps'])
        restored['payload'].pop('aps')
        self.assertEqual(msg.extra, restored['payload'])
        self.assertEqual(restored['index'], itr)
        self.assertEqual(msg.expiry, restored['expiry'])
        self.assertEqual(msg.priority, restored['priority'])

    def test_retry(self):
        # include failed
        r_message = self.message.retry(0, True)
        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'priority', 'extra'):
            self.assertEqual(getattr(self.message, key),
                             getattr(r_message, key))

        # nothing to retry, we skip the token
        self.assertEqual(self.message.retry(0, False), None)

        # include failed
        r_raw_message = self.raw_message.retry(0, True)
        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available',
                    'expiry', 'priority', 'extra'):
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(r_raw_message, key))

        # skip failed
        r_raw_message = self.raw_message.retry(0, False)
        self.assertEqual(self.raw_message.tokens[1:], r_raw_message.tokens)
        for key in ('alert', 'badge', 'sound', 'content_available', 'expiry',
                    'priority', 'extra'):
            self.assertEqual(getattr(self.raw_message, key),
                             getattr(r_raw_message, key))
Example #10
0
 def setUp(self):
     self.msg = Message([VALID_TOKEN_ONE, VALID_TOKEN_TWO], alert="message")
Example #11
0
 def _callback(self, ch, method, properties, body):
     """
     send apns notifications
     :param ch:
     :param method:
     :param properties:
     :param body:
     :return:
     """
     self.session.outdate(timedelta(minutes=5))
     if conf.getboolean('application', 'debug'):
         self.apns_con = self.session.get_connection("push_sandbox", cert_file=conf.get('apns', 'cert_sandbox'))
     else:
         self.apns_con = self.session.get_connection("push_production", cert_file=conf.get('apns', 'cert_production'))
     message = loads(body)
     logger.debug('payload: %s' % message)
     badge = None
     sound = 'default'
     content_available = None
     # time to live
     ttl = timedelta(days=5)
     if 'apns' in message['payload']:
         if 'badge' in message['payload']['apns']:
             badge = message['payload']['apns']['badge']
         if 'sound' in message['payload']['apns']:
             sound = message['payload']['apns']['sound']
         if 'content_available' in message['payload']['apns']:
             content_available = message['payload']['apns']['content_available']
     if 'ttl' in message['payload']:
         ttl = timedelta(seconds=message['payload']['ttl'])
     message = Message(message['devices'],
                       alert=message['payload']['alert'],
                       badge=badge,
                       sound=sound,
                       content_available=content_available,
                       expiry=ttl,
                       extra=message['payload']['data'] if 'data' in message['payload'] else None)
     try:
         srv = APNs(self.apns_con)
         response = srv.send(message)
         logger.debug('apns response: %s' % response)
     except Exception as ex:
         ch.basic_ack(delivery_tag=method.delivery_tag)
         logger.exception(ex)
         return
     # Check failures. Check codes in APNs reference docs.
     for token, reason in response.failed.items():
         code, errmsg = reason
         # according to APNs protocol the token reported here
         # is garbage (invalid or empty), stop using and remove it.
         logger.info('delivery failure apns_token: %s, reason: %s' % (token, errmsg))
         device_obj = Device.query.filter_by(platform_id=token).first()
         if device_obj:
             db.session.delete(device_obj)
     try:
         db.session.commit()
     except Exception as ex:
         db.session.rollback()
         logger.exception(ex)
     # Check failures not related to devices.
     for code, errmsg in response.errors:
         logger.error(errmsg)
     # Check if there are tokens that can be retried
     if response.needs_retry():
         # repeat with retry_message or reschedule your task
         srv.send(response.retry())
     ch.basic_ack(delivery_tag=method.delivery_tag)
Example #12
0
def send_apns_message(device, app, message_type, data=None):
    """
    Send an Apple Push Notification message.
    """
    token_list = [device.token]
    unique_key = device.token

    if message_type == TYPE_CALL:
        unique_key = data['unique_key']
        message = Message(token_list,
                          payload=get_call_push_payload(
                              unique_key, data['phonenumber'],
                              data['caller_id']))
    elif message_type == TYPE_MESSAGE:
        message = Message(token_list,
                          payload=get_message_push_payload(data['message']))
    else:
        logger.warning('{0} | TRYING TO SENT MESSAGE OF UNKNOWN TYPE: {1}',
                       unique_key, message_type)

    session = Session()

    push_mode = settings.APNS_PRODUCTION
    if device.sandbox:
        # Sandbox push mode.
        push_mode = settings.APNS_SANDBOX

    full_cert_path = os.path.join(settings.CERT_DIR, app.push_key)

    con = session.get_connection(push_mode, cert_file=full_cert_path)
    srv = APNs(con)

    try:
        logger.info(
            '{0} | Sending APNS \'{1}\' message at time:{2} to {3} Data:{4}'.
            format(
                unique_key, message_type,
                datetime.datetime.fromtimestamp(
                    time()).strftime('%H:%M:%S.%f'), device.token, data))
        res = srv.send(message)

    except Exception:
        logger.exception('{0} | Error sending APNS message'.format(
            unique_key, ))

    else:
        # Check failures. Check codes in APNs reference docs.
        for token, reason in res.failed.items():
            code, errmsg = reason
            # According to APNs protocol the token reported here
            # is garbage (invalid or empty), stop using and remove it.
            logger.warning(
                '{0} | Sending APNS message failed for device: {1}, reason: {2}'
                .format(unique_key, token, errmsg))

        # Check failures not related to devices.
        for code, errmsg in res.errors:
            logger.warning('{0} | Error sending APNS message. \'{1}\''.format(
                unique_key, errmsg))

        # Check if there are tokens that can be retried.
        if res.needs_retry():
            logger.info('{0} | Could not sent APNS message, retrying...')
            # Repeat with retry_message or reschedule your task.
            res.retry()
Example #13
0
class APNsclerkMessageTest(Python26Mixin, unittest.TestCase):
    """ Test Message API. """

    def setUp(self):
        self.now = datetime.datetime.now()
        # Typical message
        self.message = Message(
            "0123456789ABCDEF",
            alert=u"Russian: \u0421\u0430\u0440\u0434\u0430\u0440",
            badge=10,
            sound="test.mp3",
            content_available=1,
            expiry=self.now + datetime.timedelta(days=3),
            priority=30,
            extra={'key': 'value'}
        )
        # Message with a custom payload
        self.raw_message = Message(
            ["0123456789ABCDEF", "FEDCBA9876543210"],
            payload=self.message.payload,
            priority=5,
            expiry=datetime.timedelta(days=5)
        )

    def test_payload(self):
        payload = self.message.get_json_payload()
        self.assertIsInstance(payload, six.binary_type)
        unicode_src = payload.decode('utf-8')
        payload = json.loads(unicode_src)
        self.assertEqual(payload["aps"], {
            "alert": self.message.alert,
            "badge": self.message.badge,
            "sound": self.message.sound,
            "content-available": self.message.content_available
        })
        for k, v in self.message.extra.items():
            self.assertEqual(payload[k], v)

    def test_serialization(self):
        # standard pickle
        s_message = pickle.dumps(self.message)
        s_raw_message = pickle.dumps(self.raw_message)
        c_message = pickle.loads(s_message)
        c_raw_message = pickle.loads(s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available', 'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key), getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key), getattr(c_raw_message, key))

        # custom
        s_message = self.message.__getstate__()
        s_raw_message = self.raw_message.__getstate__()
        # JSON/XML/etc and store/send
        s_message = json.dumps(s_message)
        s_raw_message = json.dumps(s_raw_message)
        # unserialize
        s_message = json.loads(s_message)
        s_raw_message = json.loads(s_raw_message)
        # reconstruct
        c_message = Message(**s_message)
        c_raw_message = Message(**s_raw_message)

        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available', 'expiry', 'extra', 'priority', '_payload'):
            self.assertEqual(getattr(self.message, key), getattr(c_message, key))
            self.assertEqual(getattr(self.raw_message, key), getattr(c_raw_message, key))

    def test_non_ascii(self):
        # meta-data size. ensure 'alert' is included.
        empty_msg_size = len(Message(tokens=[], alert="a").get_json_payload()) - 1

        MAX_UTF8_SIZE = 3  # size of maximum utf8 encoded character in bytes
        chinese_str = (
            u'\u5187\u869a\u5487\u6b8f\u5cca\u9f46\u9248\u6935\u4ef1\u752a'
            u'\u67cc\u521e\u62b0\u530a\u6748\u9692\u5c6e\u653d\u588f\u6678')
        chinese_msg_size = len(Message(tokens=[], alert=chinese_str).get_json_payload())
        self.assertLessEqual(
            chinese_msg_size,
            empty_msg_size + len(chinese_str) * MAX_UTF8_SIZE)

        MAX_EMOJI_SIZE = 4  # size of maximum utf8 encoded character in bytes
        # emoji
        emoji_str = (u'\U0001f601\U0001f603\U0001f638\U00002744')
        emoji_msg_size = len(Message(tokens="", alert=emoji_str).get_json_payload())
        self.assertLessEqual(
            emoji_msg_size,
            empty_msg_size + len(emoji_str) * MAX_EMOJI_SIZE)

    def test_batch(self):
        # binary serialization in ridiculously small buffer =)
        b_message = list(self.message.batch(10))
        b_raw_message = list(self.raw_message.batch(10))

        # number of batches
        self.assertEqual(len(b_message), 1)
        self.assertEqual(len(b_raw_message), 2)

        # lets read stuff back. number of sent before ID's is of course 0.
        self.check_message(b_message[0], 0, self.message)
        self.check_message(b_raw_message[0], 0, self.raw_message)
        self.check_message(b_raw_message[1], 1, self.raw_message)

    def check_message(self, batch, itr, msg):
        sent, data = batch
        # we send batches of 1 token size
        self.assertEqual(sent, itr)
        # |COMMAND|FRAME-LEN|{token}|{payload}|{id:4}|{expiry:4}|{priority:1}
        command, frame_len = struct.unpack(">BI", data[0:5])
        self.assertEqual(command, 2)
        self.assertEqual(frame_len, len(data) - 5)
        
        off = 5
        restored = {}
        for itm in range(1, 6):
            hdr, length = struct.unpack(">BH", data[off:(off+3)])
            off += 3
            value = data[off:(off+length)]
            off += length
            if hdr == 1:
                restored['token'] = binascii.hexlify(value).decode('ascii')
            elif hdr == 2:
                restored['payload'] = json.loads(value.decode('utf-8'))
            elif hdr == 3:
                restored['index'] = struct.unpack(">I", value)[0]
            elif hdr == 4:
                restored['expiry'] = struct.unpack(">I", value)[0]
            elif hdr == 5:
                restored['priority'] = struct.unpack(">B", value)[0]

        for key in ('token', 'payload', 'index', 'expiry', 'priority'):
            if key not in restored:
                self.fail("Binary message is missing: %s" % key)

        # check message
        self.assertEqual(msg.tokens[itr].lower(), restored['token'].lower())
        self.assertEqual(msg.payload['aps'], restored['payload']['aps'])
        restored['payload'].pop('aps')
        self.assertEqual(msg.extra, restored['payload'])
        self.assertEqual(restored['index'], itr)
        self.assertEqual(msg.expiry, restored['expiry'])
        self.assertEqual(msg.priority, restored['priority'])

    def test_retry(self):
        # include failed
        r_message = self.message.retry(0, True)
        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available', 'expiry', 'priority', 'extra'):
            self.assertEqual(getattr(self.message, key), getattr(r_message, key))

        # nothing to retry, we skip the token
        self.assertEqual(self.message.retry(0, False), None)

        # include failed
        r_raw_message = self.raw_message.retry(0, True)
        for key in ('tokens', 'alert', 'badge', 'sound', 'content_available', 'expiry', 'priority', 'extra'):
            self.assertEqual(getattr(self.raw_message, key), getattr(r_raw_message, key))

        # skip failed
        r_raw_message = self.raw_message.retry(0, False)
        self.assertEqual(self.raw_message.tokens[1:], r_raw_message.tokens)
        for key in ('alert', 'badge', 'sound', 'content_available', 'expiry', 'priority', 'extra'):
            self.assertEqual(getattr(self.raw_message, key), getattr(r_raw_message, key))