示例#1
0
class APNSRouterTestCase(unittest.TestCase):
    def _waitfor(self, func):
        times = 0
        while not func():  # pragma: nocover
            time.sleep(1)
            times += 1
            if times > 9:
                break

    @patch('autopush.router.apns2.HTTP20Connection',
           spec=hyper.HTTP20Connection)
    @patch('hyper.tls', spec=hyper.tls)
    def setUp(self, mt, mc):
        from twisted.logger import Logger
        conf = AutopushConfig(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {
            'firefox': {
                'cert': 'fake.cert',
                'key': 'fake.key',
                'topic': 'com.example.SomeApp',
                'max_connections': 2,
            }
        }
        self.mock_connection = mc
        mc.return_value = mc
        self.metrics = metrics = Mock(spec=SinkMetrics)
        self.router = APNSRouter(conf, apns_config, metrics)
        self.mock_response = Mock()
        self.mock_response.status = 200
        mc.get_response.return_value = self.mock_response
        # toss the existing connection
        try:
            self.router.apns['firefox'].connections.pop()
        except IndexError:  # pragma nocover
            pass
        self.router.apns['firefox'].connections.append(self.mock_connection)
        self.router.apns['firefox'].log = Mock(spec=Logger)
        self.headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "encryption-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=self.headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        self.router_data = dict(
            router_data=dict(token="connect_data", rel_channel="firefox"))

    def test_register(self):
        router_data = {"token": "connect_data"}
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        assert router_data == {
            "rel_channel": "firefox",
            "token": "connect_data"
        }

    def test_extended_register(self):
        router_data = {
            "token": "connect_data",
            "aps": {
                "foo": "bar",
                "gorp": "baz"
            }
        }
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        assert router_data == {
            "rel_channel": "firefox",
            "token": "connect_data",
            "aps": {
                "foo": "bar",
                "gorp": "baz"
            }
        }

    def test_register_bad(self):
        with pytest.raises(RouterException):
            self.router.register("uaid", router_data={}, app_id="firefox")

    def test_register_bad_channel(self):
        with pytest.raises(RouterException):
            self.router.register("uaid",
                                 router_data={"token": "connect_data"},
                                 app_id="unknown")

    @inlineCallbacks
    def test_connection_error(self):
        from hyper.http20.exceptions import ConnectionError

        def raiser(*args, **kwargs):
            raise ConnectionError("oops")

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)

        assert ex.value.response_body == ('APNS returned an error '
                                          'processing request')
        assert ex.value.status_code == 502
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_connection_fail_error(self):
        def raiser(*args, **kwargs):
            error = socket.error()
            error.errno = socket.errno.EPIPE
            raise error

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)

        assert ex.value.response_body == "APNS returned an error processing " \
                                         "request"
        assert ex.value.status_code == 502
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_route_notification(self):
        result = yield self.router.route_notification(self.notif,
                                                      self.router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)

        assert isinstance(result, RouterResponse)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        assert 'chid' in body_json
        # The ChannelID is a UUID4, and unpredictable.
        del (body_json['chid'])
        assert body_json == {
            "body": "q60d6g",
            "enc": "test",
            "ver": 10,
            "aps": {
                "mutable-content": 1,
                "alert": {
                    "loc-key": "SentTab.NoTabArrivingNotification.body",
                    "title-loc-key": "SentTab.NoTabArrivingNotification.title",
                },
            },
            "enckey": "test",
            "con": "aesgcm",
        }

    @inlineCallbacks
    def test_route_notification_complex(self):
        router_data = dict(
            router_data=dict(token="connect_data",
                             rel_channel="firefox",
                             aps=dict(string="String",
                                      array=['a', 'b', 'c'],
                                      number=decimal.Decimal(4))))
        result = yield self.router.route_notification(self.notif, router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        assert isinstance(result, RouterResponse)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        assert body_json['aps']['number'] == 4
        assert body_json['aps']['string'] == 'String'

    @inlineCallbacks
    def test_route_low_priority_notification(self):
        """low priority and empty apns_ids are not yet used, but may feature
        when priorty work is done."""
        apns2 = self.router.apns['firefox']
        exp = int(time.time() + 300)
        yield apns2.send("abcd0123", {}, 'apnsid', priority=False, exp=exp)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        assert self.mock_connection.request.called
        body = self.mock_connection.request.call_args[1]
        headers = body['headers']
        assert headers == {
            'apns-expiration': str(exp),
            'apns-topic': 'com.example.SomeApp',
            'apns-priority': '5',
            'apns-id': 'apnsid'
        }

    @inlineCallbacks
    def test_bad_send(self):
        self.mock_response.status = 400
        self.mock_response.read.return_value = json.dumps({'reason': 'boo'})
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == 'APNS Transmit Error 400:boo'
        assert ex.value.response_body == (
            'APNS could not process your message boo')

    @inlineCallbacks
    def test_fail_send(self):
        def throw(*args, **kwargs):
            raise HTTP20Error("oops")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == "Server error"
        assert ex.value.response_body == 'APNS returned an error ' \
                                         'processing request'
        assert self.metrics.increment.called
        assert self.metrics.increment.call_args[0][0] == \
            'notification.bridge.connection.error'
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_fail_send_bad_write_retry(self):
        def throw(*args, **kwargs):
            raise ssl.SSLError(ssl.SSL_ERROR_SSL,
                               "[SSL: BAD_WRITE_RETRY] bad write retry")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with pytest.raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 502
        assert ex.value.message == "Server error"
        assert ex.value.response_body == 'APNS returned an error ' \
                                         'processing request'
        assert self.metrics.increment.called
        assert self.metrics.increment.call_args[0][0] == \
            'notification.bridge.connection.error'
        self.flushLoggedErrors()

    def test_too_many_connections(self):
        rr = self.router.apns['firefox']
        with pytest.raises(RouterException) as ex:
            while True:
                rr._get_connection()

        assert isinstance(ex.value, RouterException)
        assert ex.value.status_code == 503
        assert ex.value.message == "Too many APNS requests, " \
                                   "increase pool from 2"
        assert ex.value.response_body == "APNS busy, please retry"

    def test_amend(self):
        resp = {"key": "value"}
        expected = resp.copy()
        self.router.amend_endpoint_response(resp, {})
        assert resp == expected

    def test_route_crypto_key(self):
        headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "crypto-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            assert isinstance(result, RouterResponse)
            assert result.status_code == 201
            assert result.logged_status == 200
            assert "TTL" in result.headers
            assert self.mock_connection.called

        d.addCallback(check_results)
        return d
示例#2
0
class APNSRouterTestCase(unittest.TestCase):
    def _waitfor(self, func):
        times = 0
        while not func():  # pragma: nocover
            time.sleep(1)
            times += 1
            if times > 9:
                break

    @patch('autopush.router.apns2.HTTP20Connection',
           spec=hyper.HTTP20Connection)
    @patch('hyper.tls', spec=hyper.tls)
    def setUp(self, mt, mc):
        from twisted.logger import Logger
        settings = AutopushSettings(
            hostname="localhost",
            statsd_host=None,
        )
        apns_config = {
            'firefox': {
                'cert': 'fake.cert',
                'key': 'fake.key',
                'topic': 'com.example.SomeApp',
                'max_connections': 2,
            }
        }
        self.mock_connection = mc
        mc.return_value = mc
        self.router = APNSRouter(settings, apns_config)
        self.mock_response = Mock()
        self.mock_response.status = 200
        mc.get_response.return_value = self.mock_response
        # toss the existing connection
        try:
            self.router.apns['firefox'].connections.pop()
        except IndexError:  # pragma nocover
            pass
        self.router.apns['firefox'].connections.append(self.mock_connection)
        self.router.apns['firefox'].log = Mock(spec=Logger)
        self.headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "encryption-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=self.headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        self.router_data = dict(
            router_data=dict(token="connect_data", rel_channel="firefox"))

    def test_register(self):
        router_data = {"token": "connect_data"}
        self.router.register("uaid", router_data=router_data, app_id="firefox")
        eq_(router_data, {"rel_channel": "firefox", "token": "connect_data"})

    def test_register_bad(self):
        with assert_raises(RouterException):
            self.router.register("uaid", router_data={}, app_id="firefox")

    def test_register_bad_channel(self):
        with assert_raises(RouterException):
            self.router.register("uaid",
                                 router_data={"token": "connect_data"},
                                 app_id="unknown")

    @inlineCallbacks
    def test_connection_error(self):
        from hyper.http20.exceptions import ConnectionError

        def raiser(*args, **kwargs):
            raise ConnectionError("oops")

        self.router.apns['firefox'].connections[1].request = Mock(
            side_effect=raiser)

        with assert_raises(RouterException) as e:
            yield self.router.route_notification(self.notif, self.router_data)

        eq_(e.exception.response_body, 'APNS returned an error '
            'processing request')
        eq_(e.exception.status_code, 502)
        self.flushLoggedErrors()

    @inlineCallbacks
    def test_route_notification(self):
        result = yield self.router.route_notification(self.notif,
                                                      self.router_data)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)

        ok_(isinstance(result, RouterResponse))
        ok_(self.mock_connection.request.called)
        body = self.mock_connection.request.call_args[1]
        body_json = json.loads(body['body'])
        ok_('chid' in body_json)
        # The ChannelID is a UUID4, and unpredictable.
        del (body_json['chid'])
        eq_(
            body_json, {
                "body": "q60d6g",
                "enc": "test",
                "ver": 10,
                "aps": {
                    "content-available": 1,
                },
                "enckey": "test",
                "con": "aesgcm",
            })

    @inlineCallbacks
    def test_route_low_priority_notification(self):
        """low priority and empty apns_ids are not yet used, but may feature
        when priorty work is done."""
        apns2 = self.router.apns['firefox']
        exp = int(time.time() + 300)
        yield apns2.send("abcd0123", {}, 'apnsid', priority=False, exp=exp)
        yield self._waitfor(
            lambda: self.mock_connection.request.called is True)
        ok_(self.mock_connection.request.called)
        body = self.mock_connection.request.call_args[1]
        headers = body['headers']
        eq_(
            headers, {
                'apns-expiration': str(exp),
                'apns-topic': 'com.example.SomeApp',
                'apns-priority': '5',
                'apns-id': 'apnsid'
            })

    @inlineCallbacks
    def test_bad_send(self):
        self.mock_response.status = 400
        self.mock_response.read.return_value = json.dumps({'reason': 'boo'})
        with assert_raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 502)
        eq_(ex.exception.message, 'APNS Transmit Error 400:boo')
        eq_(ex.exception.response_body, 'APNS could not process your '
            'message boo')

    @inlineCallbacks
    def test_fail_send(self):
        def throw(*args, **kwargs):
            raise HTTP20Error("oops")

        self.router.apns['firefox'].connections[0].request.side_effect = throw
        with assert_raises(RouterException) as ex:
            yield self.router.route_notification(self.notif, self.router_data)
        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 502)
        eq_(ex.exception.message, "Server error")
        eq_(ex.exception.response_body, 'APNS returned an error processing '
            'request')
        self.flushLoggedErrors()

    def test_too_many_connections(self):
        rr = self.router.apns['firefox']
        with assert_raises(RouterException) as ex:
            while True:
                rr._get_connection()

        ok_(isinstance(ex.exception, RouterException))
        eq_(ex.exception.status_code, 503)
        eq_(ex.exception.message, "Too many APNS requests, "
            "increase pool from 2")
        eq_(ex.exception.response_body, "APNS busy, please retry")

    def test_amend(self):
        resp = {"key": "value"}
        expected = resp.copy()
        self.router.amend_endpoint_response(resp, {})
        eq_(resp, expected)

    def test_route_crypto_key(self):
        headers = {
            "content-encoding": "aesgcm",
            "encryption": "test",
            "crypto-key": "test"
        }
        self.notif = WebPushNotification(
            uaid=uuid.UUID(dummy_uaid),
            channel_id=uuid.UUID(dummy_chid),
            data="q60d6g",
            headers=headers,
            ttl=200,
            message_id=10,
        )
        self.notif.cleanup_headers()
        d = self.router.route_notification(self.notif, self.router_data)

        def check_results(result):
            ok_(isinstance(result, RouterResponse))
            eq_(result.status_code, 201)
            eq_(result.logged_status, 200)
            ok_("TTL" in result.headers)
            ok_(self.mock_connection.called)

        d.addCallback(check_results)
        return d