diff --git a/autopush/http.py b/autopush/http.py index 86a9f8c8..0bce5f2b 100644 --- a/autopush/http.py +++ b/autopush/http.py @@ -83,6 +83,20 @@ def add_health_handlers(self): def _hostname(self): return self.ap_settings.hostname + @classmethod + def for_handler(cls, handler_cls, *args, **kwargs): + # type: (Type[BaseHandler], *Any, **Any) -> BaseHTTPFactory + """Create a cyclone app around a specific handler_cls. + + handler_cls must be included in ap_handlers or a ValueError is + thrown. + + """ + for pattern, handler in cls.ap_handlers: + if handler is handler_cls: + return cls(handlers=[(pattern, handler)], *args, **kwargs) + raise ValueError("{!r} not found".format(handler_cls)) + class EndpointHTTPFactory(BaseHTTPFactory): diff --git a/autopush/tests/client.py b/autopush/tests/client.py new file mode 100644 index 00000000..97a6c496 --- /dev/null +++ b/autopush/tests/client.py @@ -0,0 +1,133 @@ +# +# Copyright 2014 David Novakovic +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cyclone.httpserver import HTTPRequest, HTTPConnection +from cyclone.web import decode_signed_value +from cyclone.httputil import HTTPHeaders +import urllib +from twisted.test import proto_helpers +from twisted.internet.defer import inlineCallbacks, returnValue +from Cookie import SimpleCookie + + +class DecodingSimpleCookie(SimpleCookie): + def __init__(self, app, *args, **kwargs): + self.app = app + + def get_secure_cookie(self, name, value=None, max_age_days=31): + + if value is None and name in self: + value = self[name].value + return decode_signed_value( + self.app.settings["cookie_secret"], + name, value, max_age_days=max_age_days) + + +class Client(object): + def __init__(self, app): + self.app = app + self.cookies = DecodingSimpleCookie(self.app) + + def get(self, uri, params=None, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + return self.request( + "GET", uri, params=params, version=version, headers=headers, + body=body, remote_ip=remote_ip, protocol=protocol, host=host, + files=files, connection=connection + ) + + def put(self, uri, params=None, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + return self.request( + "PUT", uri, params=params, version=version, headers=headers, + body=body, remote_ip=remote_ip, protocol=protocol, host=host, + files=files, connection=connection + ) + + def post(self, uri, params=None, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + return self.request( + "POST", uri, params=params, version=version, headers=headers, + body=body, remote_ip=remote_ip, protocol=protocol, host=host, + files=files, connection=connection + ) + + def delete(self, uri, params=None, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + return self.request( + "DELETE", uri, params=params, version=version, headers=headers, + body=body, remote_ip=remote_ip, protocol=protocol, host=host, + files=files, connection=connection + ) + + def head(self, uri, params=None, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + return self.request( + "HEAD", uri, params=params, version=version, headers=headers, + body=body, remote_ip=remote_ip, protocol=protocol, host=host, + files=files, connection=connection + ) + + @inlineCallbacks + def request(self, method, uri, *args, **kwargs): + params = kwargs.pop("params", {}) or {} + if method in ["GET", "HEAD", "OPTIONS"] and params: + uri = uri + "?" + urllib.urlencode(params) + elif method in ["POST", "PATCH", "PUT"]\ + and params and not kwargs['body']: + kwargs['body'] = urllib.urlencode(params) + connection = kwargs.pop('connection') + if not connection: + connection = HTTPConnection() + connection.xheaders = False + kwargs['connection'] = connection + connection.factory = self.app + cookie_value = self.cookies.output(header="") + if cookie_value.strip(): + if kwargs['headers'] is None: + kwargs['headers'] = {} + kwargs['headers']['Cookie'] = cookie_value.strip() + request = HTTPRequest(method, uri, *args, **kwargs) + for k, p in params.items(): + request.arguments.setdefault(k, []).append(p) + connection.connectionMade() + connection._request = request + connection.transport = proto_helpers.StringTransport() + request.remote_ip = connection.transport.getHost().host + handler = self.app(request) + + def setup_response(): + headers = HTTPHeaders() + for line in handler._generate_headers().split("\r\n"): + if line.startswith("HTTP") or not line.strip(): + continue + headers.parse_line(line) + for cookie in headers.get_list("Set-Cookie"): + self.cookies.load(cookie) + response_body = connection.transport.io.getvalue() + handler.content = response_body.split("\r\n\r\n", 1)[1] + handler.headers = headers + + if handler._finished: + setup_response() + returnValue(handler) + yield connection.notifyFinish() + setup_response() + returnValue(handler) diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index c56573b2..ca934eed 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -4,12 +4,11 @@ import twisted.internet.base from cryptography.fernet import Fernet, InvalidToken from cyclone.web import Application -from mock import Mock +from mock import Mock, patch from moto import mock_dynamodb2 from nose.tools import eq_, ok_ -from twisted.internet.defer import Deferred +from twisted.internet.defer import inlineCallbacks from twisted.trial import unittest -from txstatsd.metrics.metrics import Metrics import autopush.utils as utils @@ -23,8 +22,10 @@ has_connected_this_month, ) from autopush.exceptions import RouterException +from autopush.http import EndpointHTTPFactory from autopush.settings import AutopushSettings from autopush.router.interface import IRouter +from autopush.tests.client import Client from autopush.tests.test_db import make_webpush_notification from autopush.utils import ( generate_hash, @@ -64,120 +65,81 @@ def setUp(self): crypto_key='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', ) self.fernet_mock = settings.fernet = Mock(spec=Fernet) - self.metrics_mock = settings.metrics = Mock(spec=Metrics) self.router_mock = settings.router = Mock(spec=Router) self.storage_mock = settings.storage = Mock(spec=Storage) self.message_mock = settings.message = Mock(spec=Message) - self.request_mock = Mock(body=b'', arguments={}, headers={}) - self.message = MessageHandler(Application(), - self.request_mock, - ap_settings=settings) - - self.status_mock = self.message.set_status = Mock() - self.write_mock = self.message.write = Mock() + app = EndpointHTTPFactory.for_handler(MessageHandler, settings) + self.client = Client(app) - d = self.finish_deferred = Deferred() - self.message.finish = lambda: d.callback(True) + def url(self, **kwargs): + return '/m/{message_id}'.format(**kwargs) + @inlineCallbacks def test_delete_token_invalid(self): self.fernet_mock.configure_mock(**{ "decrypt.side_effect": InvalidToken}) + resp = yield self.client.delete(self.url(message_id='%20')) + eq_(resp.get_status(), 400) - def handle_finish(result): - self.status_mock.assert_called_with(400, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id='') - return self.finish_deferred - + @inlineCallbacks def test_delete_token_wrong_components(self): self.fernet_mock.decrypt.return_value = "123:456" + resp = yield self.client.delete(self.url(message_id="ignored")) + eq_(resp.get_status(), 400) - def handle_finish(result): - self.status_mock.assert_called_with(400, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id="ignored") - return self.finish_deferred - + @inlineCallbacks def test_delete_token_wrong_kind(self): tok = ":".join(["r", dummy_uaid.hex, str(dummy_chid)]) self.fernet_mock.decrypt.return_value = tok + resp = yield self.client.delete(self.url(message_id='ignored')) + eq_(resp.get_status(), 400) - def handle_finish(result): - self.status_mock.assert_called_with(400, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id='ignored') - return self.finish_deferred - + @inlineCallbacks def test_delete_invalid_timestamp_token(self): tok = ":".join(["02", str(dummy_chid)]) self.fernet_mock.decrypt.return_value = tok + resp = yield self.client.delete(self.url(message_id='ignored')) + eq_(resp.get_status(), 400) - def handle_finish(result): - self.status_mock.assert_called_with(400, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id='ignored') - return self.finish_deferred - + @inlineCallbacks def test_delete_success(self): tok = ":".join(["m", dummy_uaid.hex, str(dummy_chid)]) self.fernet_mock.decrypt.return_value = tok self.message_mock.configure_mock(**{ "delete_message.return_value": True}) + resp = yield self.client.delete(self.url(message_id="123-456")) + self.message_mock.delete_message.assert_called() + eq_(resp.get_status(), 204) - def handle_finish(result): - self.message_mock.delete_message.assert_called() - self.status_mock.assert_called_with(204) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id="123-456") - return self.finish_deferred - + @inlineCallbacks def test_delete_topic_success(self): tok = ":".join(["01", dummy_uaid.hex, str(dummy_chid), "Inbox"]) self.fernet_mock.decrypt.return_value = tok self.message_mock.configure_mock(**{ "delete_message.return_value": True}) + resp = yield self.client.delete(self.url(message_id="123-456")) + self.message_mock.delete_message.assert_called() + eq_(resp.get_status(), 204) - def handle_finish(result): - self.message_mock.delete_message.assert_called() - self.status_mock.assert_called_with(204) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id="123-456") - return self.finish_deferred - + @inlineCallbacks def test_delete_topic_error_parts(self): tok = ":".join(["01", dummy_uaid.hex, str(dummy_chid)]) self.fernet_mock.decrypt.return_value = tok self.message_mock.configure_mock(**{ "delete_message.return_value": True}) + resp = yield self.client.delete(self.url(message_id="123-456")) + eq_(resp.get_status(), 400) - def handle_finish(result): - self.status_mock.assert_called_with(400, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id="123-456") - return self.finish_deferred - + @inlineCallbacks def test_delete_db_error(self): tok = ":".join(["m", dummy_uaid.hex, str(dummy_chid)]) self.fernet_mock.decrypt.return_value = tok self.message_mock.configure_mock(**{ "delete_message.side_effect": ProvisionedThroughputExceededException(None, None)}) - - def handle_finish(result): - ok_(result) - self.status_mock.assert_called_with(503, reason=None) - self.finish_deferred.addCallback(handle_finish) - - self.message.delete(message_id="ignored") - return self.finish_deferred + resp = yield self.client.delete(self.url(message_id="ignored")) + eq_(resp.get_status(), 503) CORS_HEAD = "GET,POST,PUT,DELETE" @@ -193,7 +155,6 @@ def setUp(self): bear_hash_key='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=', ) self.fernet_mock = settings.fernet = Mock(spec=Fernet) - self.metrics_mock = settings.metrics = Mock(spec=Metrics) self.router_mock = settings.router = Mock(spec=Router) self.storage_mock = settings.storage = Mock(spec=Storage) self.router_mock.register_user = Mock() @@ -203,41 +164,32 @@ def setUp(self): "router_type": "test", "router_data": dict() } + app = EndpointHTTPFactory.for_handler(RegistrationHandler, settings) + self.client = Client(app) self.request_mock = Mock(body=b'', arguments={}, headers={}) self.reg = RegistrationHandler(Application(), self.request_mock, ap_settings=settings) - self.reg.request.uri = '/v1/xxx/yyy/register' - self.status_mock = self.reg.set_status = Mock() - self.write_mock = self.reg.write = Mock() self.auth = ("WebPush %s" % - generate_hash(self.reg.ap_settings.bear_hash_key[0], - dummy_uaid.hex)) + generate_hash(settings.bear_hash_key[0], dummy_uaid.hex)) - d = self.finish_deferred = Deferred() - self.reg.finish = lambda: d.callback(True) self.settings = settings - def _req(self, meth, router_type="", router_token="", uaid=None, - chid=None): - return meth( - router_type=router_type, - router_token=router_token, - uaid=uaid, - chid=chid) - - def _post(self, **kwargs): - return self._req(self.reg.post, **kwargs) - - def _put(self, **kwargs): - return self._req(self.reg.put, **kwargs) - - def _delete(self, **kwargs): - return self._req(self.reg.delete, **kwargs) - - def _get(self, **kwargs): - return self._req(self.reg.get, **kwargs) + def url(self, router_token='test', **kwargs): + urlfmt = '/v1/{router_type}/{router_token}/registration' + result = urlfmt.format(router_token=router_token, **kwargs) + if kwargs.get('uaid'): + result += '/' + kwargs.get('uaid') + if kwargs.get('chid'): + result += '/subscription/' + kwargs.get('chid') + return result + + def patch(self, *args, **kwargs): + """Patch an object only for the duration of a test""" + patch_obj = patch(*args, **kwargs) + patch_obj.__enter__() + self.addCleanup(patch_obj.__exit__) def test_base_tags(self): self.reg._base_tags = [] @@ -249,8 +201,8 @@ def test_base_tags(self): tags = self.reg.base_tags() eq_(tags, ['user_agent:test', 'host:example.com:8080']) - def _check_error(self, code, errno, error, message=None): - d = json.loads(self.write_mock.call_args[0][0]) + def _check_error(self, resp, code, errno, error, message=None): + d = json.loads(resp.content) eq_(d.get("code"), code) eq_(d.get("errno"), errno) eq_(d.get("error"), error) @@ -311,301 +263,235 @@ def test_cors_options(self): eq_(reg._headers[ch1], "*") eq_(reg._headers[ch2], CORS_HEAD) - def test_post(self, *args): - self.reg.request.body = json.dumps(dict( - type="simplepush", - channelID=str(dummy_chid), - data={}, - )) - self.reg.request.uri = "/v1/xxx/yyy/register" + @inlineCallbacks + def test_post(self): + self.patch('uuid.uuid4', return_value=dummy_uaid) + self.fernet_mock.configure_mock(**{ 'encrypt.return_value': 'abcd123', }) - self.reg.request.headers["Authorization"] = self.auth - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - - def handle_finish(value): - uuid.uuid4 = old_func - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["uaid"], dummy_uaid.hex.replace('-', '')) - eq_(call_arg["channelID"], dummy_uaid.hex) - eq_(call_arg["endpoint"], "http://localhost/wpush/v1/abcd123") - ok_("secret" in call_arg) - - self.finish_deferred.addBoth(handle_finish) - self._post(router_type="simplepush") - return self.finish_deferred - - def test_post_gcm(self, *args): + + resp = yield self.client.post( + self.url(router_type='simplepush', router_token='yyy'), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + channelID=str(dummy_chid), + data={}, + )) + ) + eq_(resp.get_status(), 200) + + payload = json.loads(resp.content) + eq_(payload["uaid"], dummy_uaid.hex.replace('-', '')) + eq_(payload["channelID"], dummy_uaid.hex) + eq_(payload["endpoint"], "http://localhost/wpush/v1/abcd123") + ok_("secret" in payload) + + @inlineCallbacks + def test_post_gcm(self): + self.patch('uuid.uuid4', + side_effect=(uuid.uuid4(), dummy_chid, dummy_uaid)) + from autopush.router.gcm import GCMRouter sids = {"182931248179192": {"auth": "aailsjfilajdflijdsilfjsliaj"}} gcm = GCMRouter(self.settings, {"dryrun": True, "senderIDs": sids}) - self.reg.ap_settings.routers["gcm"] = gcm - self.reg.request.body = json.dumps(dict( - channelID=str(dummy_chid), - token="182931248179192", - )) + self.settings.routers["gcm"] = gcm self.fernet_mock.configure_mock(**{ 'encrypt.return_value': 'abcd123', }) - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["uaid"], dummy_uaid.hex) - eq_(call_arg["channelID"], dummy_chid.hex) - eq_(call_arg["endpoint"], "http://localhost/wpush/v1/abcd123") - calls = self.reg.ap_settings.router.register_user.call_args - call_args = calls[0][0] - eq_(True, has_connected_this_month(call_args)) - ok_("secret" in call_arg) - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - self.finish_deferred.addCallback(handle_finish) - self.finish_deferred.addBoth(restore) - old_func = uuid.uuid4 - ids = [dummy_uaid, dummy_chid] - uuid.uuid4 = lambda: ids.pop() - self._post(router_type="gcm", router_token="182931248179192") - return self.finish_deferred + resp = yield self.client.post( + self.url(router_type="gcm", router_token="182931248179192"), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + channelID=str(dummy_chid), + token="182931248179192", + )) + ) + eq_(resp.get_status(), 200) + + payload = json.loads(resp.content) + eq_(payload["uaid"], dummy_uaid.hex) + eq_(payload["channelID"], dummy_chid.hex) + eq_(payload["endpoint"], "http://localhost/wpush/v1/abcd123") + calls = self.settings.router.register_user.call_args + call_args = calls[0][0] + eq_(True, has_connected_this_month(call_args)) + ok_("secret" in payload) + + @inlineCallbacks def test_post_invalid_args(self, *args): - self.reg.request.body = json.dumps(dict( - type="invalid", - data={}, - )) - - def handle_finish(value): - self._check_error(400, 108, "Bad Request") - - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post() - return self.finish_deferred - - def test_post_bad_router_type(self, *args): - self.reg.request.body = json.dumps(dict( - type="invalid", - channelID=str(dummy_chid), - data={}, - )) - - def handle_finish(value): - self._check_error(400, 108, "Bad Request") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post() - return self.finish_deferred + resp = yield self.client.post( + self.url(router_type="foo"), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="invalid", + data={}, + )) + ) + self._check_error(resp, 400, 108, "Bad Request") + + @inlineCallbacks + def test_post_bad_router_type(self): + self.patch('uuid.uuid4', return_value=dummy_uaid) + + resp = yield self.client.post( + self.url(router_type="foo"), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="invalid", + channelID=str(dummy_chid), + data={}, + )) + ) + self._check_error(resp, 400, 108, "Bad Request") + @inlineCallbacks def test_post_bad_router_register(self, *args): frouter = Mock(spec=IRouter) - self.reg.ap_settings.routers["simplepush"] = frouter + self.settings.routers["simplepush"] = frouter rexc = RouterException("invalid", status_code=402, errno=107) frouter.register = Mock(side_effect=rexc) - self.reg.request.body = json.dumps(dict( - type="simplepush", - channelID=str(dummy_chid), - data={}, - )) - self.reg.request.uri = "/v1/xxx/yyy/register" - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - self._check_error(rexc.status_code, rexc.errno, "") - - self.finish_deferred.addBoth(handle_finish) - self._post(router_type="simplepush") - return self.finish_deferred - - def test_post_existing_uaid(self, *args): - self.reg.request.body = json.dumps(dict( - channelID=str(dummy_chid), - )) + resp = yield self.client.post( + self.url(router_type="simplepush"), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + channelID=str(dummy_chid), + data={}, + )) + ) + self._check_error(resp, rexc.status_code, rexc.errno, "") + + @inlineCallbacks + def test_post_existing_uaid(self): + self.patch('uuid.uuid4', return_value=dummy_chid) + self.fernet_mock.configure_mock(**{ 'encrypt.return_value': 'abcd123', }) - def handle_finish(value): - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["channelID"], dummy_chid.hex) - eq_(call_arg["endpoint"], "http://localhost/wpush/v1/abcd123") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_chid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post(router_type="test", uaid=dummy_uaid.hex) - return self.finish_deferred - - def test_post_bad_uaid(self, *args): - self.reg.request.body = json.dumps(dict( - type="simplepush", - channelID=str(dummy_chid), - data={}, - )) - - def handle_finish(value): - self._check_error(401, 109, "Unauthorized") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post(router_type="simplepush", uaid='invalid') - return self.finish_deferred + resp = yield self.client.post( + self.url(router_type="test", uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + channelID=str(dummy_chid), + )) + ) + payload = json.loads(resp.content) + eq_(payload["channelID"], dummy_chid.hex) + eq_(payload["endpoint"], "http://localhost/wpush/v1/abcd123") + + @inlineCallbacks + def test_post_bad_uaid(self): + self.patch('uuid.uuid4', return_value=dummy_uaid) + + resp = yield self.client.post( + self.url(router_type="simplepush", uaid='invalid'), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + channelID=str(dummy_chid), + data={}, + )) + ) + self._check_error(resp, 401, 109, "Unauthorized") + @inlineCallbacks def test_no_uaid(self): - def handle_finish(value): - self._check_error(410, 103, "") - - self.finish_deferred.addCallback(handle_finish) self.settings.router.get_uaid = Mock() self.settings.router.get_uaid.side_effect = ItemNotFound - self._post(router_type="webpush", - uaid=dummy_uaid.hex, - chid=str(dummy_chid)) - return self.finish_deferred + resp = yield self.client.post( + self.url(router_type="webpush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid)) + ) + self._check_error(resp, 410, 103, "") + @inlineCallbacks def test_no_auth(self): - def handle_finish(value): - self._check_error(401, 109, "Unauthorized") + resp = yield self.client.post( + self.url(router_type="webpush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid)), + ) + self._check_error(resp, 401, 109, "Unauthorized") - self.finish_deferred.addCallback(handle_finish) - self._post(router_type="webpush", - uaid=dummy_uaid.hex, - chid=str(dummy_chid)) + @inlineCallbacks + def test_bad_body(self): + resp = yield self.client.post( + self.url(router_type="webpush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid)), + body="{invalid" + ) + self._check_error(resp, 401, 108, "Unauthorized") - return self.finish_deferred + @inlineCallbacks + def test_post_bad_params(self): + self.patch('uuid.uuid4', return_value=dummy_uaid) - def test_bad_body(self): - self.reg.request.body = "{invalid" - - def handle_finish(value): - self._check_error(401, 108, "Unauthorized") - - self.finish_deferred.addCallback(handle_finish) - self._post(router_type="webpush", - uaid=dummy_uaid.hex, - chid=str(dummy_chid)) - return self.finish_deferred - - def test_post_bad_params(self, *args): - self.reg.request.body = json.dumps(dict( - channelID=str(dummy_chid), - )) - - def handle_finish(value): - self._check_error(401, 109, 'Unauthorized') - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = "WebPush Invalid" - self._post(router_type="simplepush", - uaid=dummy_uaid.hex, - chid=str(dummy_chid)) - return self.finish_deferred + resp = yield self.client.post( + self.url(router_type="simplepush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid)), + headers={"Authorization": "WebPush Invalid"}, + body=json.dumps(dict( + channelID=str(dummy_chid), + )) + ) + self._check_error(resp, 401, 109, 'Unauthorized') + @inlineCallbacks def test_post_uaid_chid(self, *args): - self.reg.request.body = json.dumps(dict( - type="simplepush", - channelID=str(dummy_chid), - data={}, - )) + self.patch('uuid.uuid4', return_value=dummy_uaid) + self.fernet_mock.configure_mock(**{ 'encrypt.return_value': 'abcd123', }) - def handle_finish(value): - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["channelID"], str(dummy_chid)) - eq_(call_arg["endpoint"], "http://localhost/wpush/v1/abcd123") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post(router_type="simplepush", - uaid=dummy_uaid.hex, - chid=str(dummy_chid)) - return self.finish_deferred - - def test_post_nochid(self): - self.reg.request.body = json.dumps(dict( - type="simplepush", - data={}, - )) + resp = yield self.client.post( + self.url(router_type="simplepush", + uaid=dummy_uaid.hex, + chid=str(dummy_chid)), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + channelID=str(dummy_chid), + data={}, + )) + ) + payload = json.loads(resp.content) + eq_(payload["channelID"], str(dummy_chid)) + eq_(payload["endpoint"], "http://localhost/wpush/v1/abcd123") + + @inlineCallbacks + def test_post_nochid(self, *args): + self.patch('uuid.uuid4', return_value=dummy_chid) + self.fernet_mock.configure_mock(**{ 'encrypt.return_value': 'abcd123', }) - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["channelID"], dummy_chid.hex) - eq_(call_arg["endpoint"], "http://localhost/wpush/v1/abcd123") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_chid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post(router_type="simplepush", uaid=dummy_uaid.hex) - return self.finish_deferred - - def test_post_with_app_server_key(self): + resp = yield self.client.post( + self.url(router_type="simplepush", uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + data={}, + )) + ) + payload = json.loads(resp.content) + eq_(payload["channelID"], dummy_chid.hex) + eq_(payload["endpoint"], "http://localhost/wpush/v1/abcd123") + + @inlineCallbacks + def test_post_with_app_server_key(self, *args): + self.patch('uuid.uuid4', return_value=dummy_chid) + dummy_key = "RandomKeyString" - self.reg.request.body = json.dumps(dict( - type="simplepush", - key=utils.base64url_encode(dummy_key), - data={}, - )) def mock_encrypt(cleartext): eq_(len(cleartext), 64) @@ -623,255 +509,204 @@ def mock_encrypt(cleartext): self.fernet_mock.configure_mock(**{ 'encrypt.side_effect': mock_encrypt, }) - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - call_args = self.reg.write.call_args - ok_(call_args is not None) - args = call_args[0] - call_arg = json.loads(args[0]) - eq_(call_arg["channelID"], dummy_chid.hex) - eq_(call_arg["endpoint"], "http://localhost/wpush/v2/abcd123") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_chid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._post(router_type="simplepush", uaid=dummy_uaid.hex) - return self.finish_deferred - - def test_put(self): + + resp = yield self.client.post( + self.url(router_type="simplepush", uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="simplepush", + key=utils.base64url_encode(dummy_key), + data={}, + )) + ) + payload = json.loads(resp.content) + eq_(payload["channelID"], dummy_chid.hex) + eq_(payload["endpoint"], "http://localhost/wpush/v2/abcd123") + + @inlineCallbacks + def test_put(self, *args): + self.patch('uuid.uuid4', return_value=dummy_uaid) + data = dict(token="some_token") - frouter = self.reg.ap_settings.routers["test"] + frouter = self.settings.routers["test"] frouter.register = Mock() frouter.register.return_value = data - self.reg.request.body = json.dumps(data) - - def handle_finish(value): - self.reg.write.assert_called_with({}) - frouter.register.assert_called_with( - dummy_uaid.hex, - router_data=data, - app_id='', - uri=self.reg.request.uri - ) - user_data = self.router_mock.register_user.call_args[0][0] - eq_(user_data['uaid'], dummy_uaid.hex) - eq_(user_data['router_type'], 'test') - eq_(user_data['router_data']['token'], 'some_token') - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._put(router_type='test', uaid=dummy_uaid.hex) - return self.finish_deferred - - def test_put_bad_auth(self): - self.reg.request.headers["Authorization"] = "Fred Smith" - - def handle_finish(value): - self._check_error(401, 109, "Unauthorized") - - def restore(*args, **kwargs): - uuid.uuid4 = old_func - - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_uaid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self._put(router_type="test", uaid=dummy_uaid.hex) - return self.finish_deferred - - def test_put_bad_arguments(self, *args): - self.reg.request.headers["Authorization"] = self.auth - data = dict(token="some_token") - self.reg.request.body = json.dumps(dict( - type="test", - data=data, - )) - - def handle_finish(value): - self._check_error(400, 108, "Bad Request") - def restore(*args, **kwargs): - uuid.uuid4 = old_func + uri = self.url(router_type='test', uaid=dummy_uaid.hex) + resp = yield self.client.put( + uri, + headers={"Authorization": self.auth}, + body=json.dumps(data), + ) + payload = json.loads(resp.content) + eq_(payload, {}) + frouter.register.assert_called_with( + dummy_uaid.hex, + router_data=data, + app_id='test', + uri=uri + ) + user_data = self.router_mock.register_user.call_args[0][0] + eq_(user_data['uaid'], dummy_uaid.hex) + eq_(user_data['router_type'], 'test') + eq_(user_data['router_data']['token'], 'some_token') + + @inlineCallbacks + def test_put_bad_auth(self, *args): + self.patch('uuid.uuid4', return_value=dummy_uaid) + + resp = yield self.client.put( + self.url(router_type="test", uaid=dummy_uaid.hex), + headers={"Authorization": "Fred Smith"} + ) + self._check_error(resp, 401, 109, "Unauthorized") - old_func = uuid.uuid4 - uuid.uuid4 = lambda: dummy_chid - self.finish_deferred.addBoth(restore) - self.finish_deferred.addCallback(handle_finish) - self._put(uaid=dummy_uaid.hex) - return self.finish_deferred + @inlineCallbacks + def test_put_bad_arguments(self, *args): + self.patch('uuid.uuid4', return_value=dummy_chid) + + resp = yield self.client.put( + self.url(router_type='foo', uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + body=json.dumps(dict( + type="test", + data=dict(token="some_token"), + )) + ) + self._check_error(resp, 400, 108, "Bad Request") + @inlineCallbacks def test_put_bad_router_register(self): - frouter = self.reg.ap_settings.routers["test"] + frouter = self.settings.routers["test"] rexc = RouterException("invalid", status_code=402, errno=107) frouter.register = Mock(side_effect=rexc) - def handle_finish(value): - self._check_error(rexc.status_code, rexc.errno, "") - - self.finish_deferred.addCallback(handle_finish) - self.reg.request.headers["Authorization"] = self.auth - self._put(router_type='test', uaid=dummy_uaid.hex) - return self.finish_deferred + resp = yield self.client.put( + self.url(router_type='test', uaid=dummy_uaid.hex), + headers={"Authorization": self.auth} + ) + self._check_error(resp, rexc.status_code, rexc.errno, "") + @inlineCallbacks def test_delete_bad_chid_value(self): notif = make_webpush_notification(dummy_uaid.hex, str(dummy_chid)) - messages = self.reg.ap_settings.message + messages = self.settings.message messages.register_channel(dummy_uaid.hex, str(dummy_chid)) messages.store_message(notif) - self.reg.request.headers["Authorization"] = self.auth - def handle_finish(value): - self._check_error(410, 106, "") - - self.finish_deferred.addCallback(handle_finish) - self._delete(router_type="test", + resp = yield self.client.delete( + self.url(router_type="test", router_token="test", uaid=dummy_uaid.hex, - chid="invalid") - return self.finish_deferred + chid="invalid"), + headers={"Authorization": self.auth}, + ) + self._check_error(resp, 410, 106, "") + @inlineCallbacks def test_delete_no_such_chid(self): notif = make_webpush_notification(dummy_uaid.hex, str(dummy_chid)) - messages = self.reg.ap_settings.message + messages = self.settings.message messages.register_channel(dummy_uaid.hex, str(dummy_chid)) messages.store_message(notif) # Moto can't handle set operations of this nature so we have # to mock the reply - unreg = messages.unregister_channel - messages.unregister_channel = Mock(return_value=False) - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - self._check_error(410, 106, "") + self.patch('autopush.db.Message.unregister_channel', + return_value=False) - def fixup_messages(result): - messages.unregister_channel = unreg - - self.finish_deferred.addCallback(handle_finish) - self.finish_deferred.addBoth(fixup_messages) - self.reg.delete(router_type="test", - router_token="test", - uaid=dummy_uaid.hex, - chid=str(uuid.uuid4())) - return self.finish_deferred + resp = yield self.client.delete( + self.url(router_type="test", + router_token="test", + uaid=dummy_uaid.hex, + chid=str(uuid.uuid4())), + headers={"Authorization": self.auth} + ) + self._check_error(resp, 410, 106, "") + @inlineCallbacks def test_delete_uaid(self): notif = make_webpush_notification(dummy_uaid.hex, str(dummy_chid)) notif2 = make_webpush_notification(dummy_uaid.hex, str(dummy_chid)) - messages = self.reg.ap_settings.message - chid2 = str(uuid.uuid4()) + messages = self.settings.message messages.store_message(notif) messages.store_message(notif2) - self.reg.ap_settings.router.drop_user = Mock() - self.reg.ap_settings.router.drop_user.return_value = True - - def handle_finish(value, chid2): - # Note: Router is mocked, so the UAID is never actually - # dropped. - ok_(self.reg.ap_settings.router.drop_user.called) - eq_(self.reg.ap_settings.router.drop_user.call_args_list[0][0], - (dummy_uaid.hex,)) - - self.finish_deferred.addCallback(handle_finish, chid2) - self.reg.request.headers["Authorization"] = self.auth - self._delete(router_type="simplepush", + self.settings.router.drop_user = Mock() + self.settings.router.drop_user.return_value = True + + yield self.client.delete( + self.url(router_type="simplepush", router_token="test", - uaid=dummy_uaid.hex) - return self.finish_deferred + uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + ) + # Note: Router is mocked, so the UAID is never actually + # dropped. + ok_(self.settings.router.drop_user.called) + eq_(self.settings.router.drop_user.call_args_list[0][0], + (dummy_uaid.hex,)) + @inlineCallbacks def test_delete_bad_uaid(self): - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - self.status_mock.assert_called_with(401, reason=None) - - self.finish_deferred.addCallback(handle_finish) - self._delete(router_type="test", - router_token="test", - uaid="invalid") - return self.finish_deferred + resp = yield self.client.delete( + self.url(router_type="test", router_token="test", uaid="invalid"), + headers={"Authorization": self.auth}, + ) + eq_(resp.get_status(), 401) + @inlineCallbacks def test_delete_orphans(self): - self.reg.request.headers["Authorization"] = self.auth - - def handle_finish(value): - self.status_mock.assert_called_with(410, reason=None) - self.router_mock.drop_user = Mock() self.router_mock.drop_user.return_value = False - self.finish_deferred.addCallback(handle_finish) - self._delete(router_type="test", + resp = yield self.client.delete( + self.url(router_type="test", router_token="test", - uaid=dummy_uaid.hex) - return self.finish_deferred + uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + ) + eq_(resp.get_status(), 410) + @inlineCallbacks def test_delete_bad_auth(self, *args): - self.reg.request.headers["Authorization"] = "Invalid" - - def handle_finish(value): - self.status_mock.assert_called_with(401, reason=None) - - self.finish_deferred.addCallback(handle_finish) - self._delete(router_type="test", + resp = yield self.client.delete( + self.url(router_type="test", router_token="test", - uaid=dummy_uaid.hex) - return self.finish_deferred + uaid=dummy_uaid.hex), + headers={"Authorization": "Invalid"}, + ) + eq_(resp.get_status(), 401) + @inlineCallbacks def test_delete_bad_router(self): - self.reg.request.headers['Authorization'] = self.auth - - def handle_finish(value): - self.status_mock.assert_called_with(400, reason=None) - - self.finish_deferred.addCallback(handle_finish) - self._delete(router_type="invalid", + resp = yield self.client.delete( + self.url(router_type="invalid", router_token="test", - uaid=dummy_uaid.hex) - return self.finish_deferred + uaid=dummy_uaid.hex), + headers={"Authorization": self.auth}, + ) + eq_(resp.get_status(), 400) + @inlineCallbacks def test_get(self): - self.reg.request.headers['Authorization'] = self.auth chids = [str(dummy_chid), str(dummy_uaid)] - def handle_finish(value): - self.settings.message.all_channels.assert_called_with( - str(dummy_uaid)) - call_args = json.loads( - self.reg.write.call_args[0][0] - ) - eq_(chids, call_args['channelIDs']) - eq_(dummy_uaid.hex, call_args['uaid']) - - self.finish_deferred.addCallback(handle_finish) self.settings.message.all_channels = Mock() self.settings.message.all_channels.return_value = (True, chids) - self._get( - router_type="test", - router_token="test", - uaid=dummy_uaid.hex) - return self.finish_deferred + resp = yield self.client.get( + self.url(router_type="test", + router_token="test", + uaid=dummy_uaid.hex), + headers={"Authorization": self.auth} + ) + self.settings.message.all_channels.assert_called_with(str(dummy_uaid)) + payload = json.loads(resp.content) + eq_(chids, payload['channelIDs']) + eq_(dummy_uaid.hex, payload['uaid']) + @inlineCallbacks def test_get_no_uaid(self): - self.reg.request.headers['Authorization'] = self.auth - - def handle_finish(value): - self.status_mock.assert_called_with(410, reason=None) - - self.finish_deferred.addCallback(handle_finish) - self._get( - router_type="test", - router_token="test") - return self.finish_deferred + resp = yield self.client.get( + self.url(router_type="test", router_token="test"), + headers={"Authorization": self.auth} + ) + eq_(resp.get_status(), 410)