Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
feat: avoid triggering validation errors from bridge API
Browse files Browse the repository at this point in the history
The existing bridge API was overly permissive on parameters and
difficult to follow on necessary values. As a result this became
a bit larger than originally anticiated and is a refactor of the
bridge API into distinct handlers for each distinct URL path with
re-usable and specific validation schema's.

Closes #893
  • Loading branch information
bbangert committed May 26, 2017
1 parent 09e5eb2 commit 7af2b5e
Show file tree
Hide file tree
Showing 4 changed files with 404 additions and 345 deletions.
21 changes: 17 additions & 4 deletions autopush/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
from autopush.web.limitedhttpconnection import LimitedHTTPConnection
from autopush.web.log_check import LogCheckHandler
from autopush.web.message import MessageHandler
from autopush.web.registration import RegistrationHandler
from autopush.web.registration import (
ChannelRegistrationHandler,
NewRegistrationHandler,
SubRegistrationHandler,
UaidRegistrationHandler,
)
from autopush.web.simplepush import SimplePushHandler
from autopush.web.webpush import WebPushHandler
from autopush.websocket import (
Expand Down Expand Up @@ -109,9 +114,17 @@ class EndpointHTTPFactory(BaseHTTPFactory):
(r"/wpush/(?:(?P<api_ver>v\d+)\/)?(?P<token>[^\/]+)",
WebPushHandler),
(r"/m/(?P<message_id>[^\/]+)", MessageHandler),
(r"/v1/(?P<router_type>[^\/]+)/(?P<router_token>[^\/]+)/"
r"registration(?:/(?P<uaid>[^\/]+))?(?:/subscription)?"
r"(?:/(?P<chid>[^\/]+))?", RegistrationHandler),
(r"/v1/(?P<type>[^\/]+)/(?P<app_id>[^\/]+)/registration",
NewRegistrationHandler),
(r"/v1/(?P<type>[^\/]+)/(?P<app_id>[^\/]+)/registration/"
r"(?P<uaid>[^\/]+)",
UaidRegistrationHandler),
(r"/v1/(?P<type>[^\/]+)/(?P<app_id>[^\/]+)/registration/"
r"(?P<uaid>[^\/]+)/subscription",
SubRegistrationHandler),
(r"/v1/(?P<type>[^\/]+)/(?P<app_id>[^\/]+)/registration/"
r"(?P<uaid>[^\/]+)/subscription/(?P<chid>[^\/]+)",
ChannelRegistrationHandler),
(r"/v1/err(?:/(?P<err_type>[^\/]+))?", LogCheckHandler),
)

Expand Down
135 changes: 52 additions & 83 deletions autopush/tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@
generate_hash,
)
from autopush.web.message import MessageHandler
from autopush.web.registration import RegistrationHandler
from autopush.web.registration import (
ChannelRegistrationHandler,
NewRegistrationHandler,
SubRegistrationHandler,
UaidRegistrationHandler,
)

mock_dynamodb2 = mock_dynamodb2()
dummy_uaid = uuid.UUID("abad1dea00000000aabbccdd00000000")
dummy_chid = uuid.UUID("deadbeef00000000decafbad00000000")
dummy_token = dummy_uaid.hex + ":" + str(dummy_chid)


def setUp():
mock_dynamodb2.start()
from .test_integration import setUp
setUp()
create_rotating_message_table()


def tearDown():
mock_dynamodb2.stop()
from .test_integration import tearDown
tearDown()


class FileConsumer(object): # pragma: no cover
Expand Down Expand Up @@ -142,10 +147,8 @@ def test_delete_db_error(self):
eq_(resp.get_status(), 503)


CORS_HEAD = "GET,POST,PUT,DELETE"


class RegistrationTestCase(unittest.TestCase):
CORS_HEAD = "POST"

def setUp(self):
twisted.internet.base.DelayedCall.debug = True
Expand All @@ -164,13 +167,13 @@ def setUp(self):
"router_type": "test",
"router_data": dict()
}
app = EndpointHTTPFactory.for_handler(RegistrationHandler, settings)
app = EndpointHTTPFactory(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 = NewRegistrationHandler(Application(),
self.request_mock,
ap_settings=settings)
self.auth = ("WebPush %s" %
generate_hash(settings.bear_hash_key[0], dummy_uaid.hex))

Expand Down Expand Up @@ -234,14 +237,14 @@ def test_cors(self):
reg = self.reg
reg.ap_settings.cors = False
ok_(reg._headers.get(ch1) != "*")
ok_(reg._headers.get(ch2) != CORS_HEAD)
ok_(reg._headers.get(ch2) != self.CORS_HEAD)

reg.clear_header(ch1)
reg.clear_header(ch2)
reg.ap_settings.cors = True
reg.prepare()
eq_(reg._headers[ch1], "*")
eq_(reg._headers[ch2], CORS_HEAD)
eq_(reg._headers[ch2], self.CORS_HEAD)

def test_cors_head(self):
ch1 = "Access-Control-Allow-Origin"
Expand All @@ -251,7 +254,7 @@ def test_cors_head(self):
reg.prepare()
reg.head(None)
eq_(reg._headers[ch1], "*")
eq_(reg._headers[ch2], CORS_HEAD)
eq_(reg._headers[ch2], self.CORS_HEAD)

def test_cors_options(self):
ch1 = "Access-Control-Allow-Origin"
Expand All @@ -261,7 +264,7 @@ def test_cors_options(self):
reg.prepare()
reg.options(None)
eq_(reg._headers[ch1], "*")
eq_(reg._headers[ch2], CORS_HEAD)
eq_(reg._headers[ch2], self.CORS_HEAD)

@inlineCallbacks
def test_post(self):
Expand All @@ -284,14 +287,14 @@ def test_post(self):

payload = json.loads(resp.content)
eq_(payload["uaid"], dummy_uaid.hex.replace('-', ''))
eq_(payload["channelID"], dummy_uaid.hex)
eq_(payload["channelID"], dummy_chid.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))
side_effect=(uuid.uuid4(), dummy_uaid, dummy_chid))

from autopush.router.gcm import GCMRouter
sids = {"182931248179192": {"auth": "aailsjfilajdflijdsilfjsliaj"}}
Expand Down Expand Up @@ -375,7 +378,7 @@ def test_post_existing_uaid(self):
})

resp = yield self.client.post(
self.url(router_type="test", uaid=dummy_uaid.hex),
self.url(router_type="test", uaid=dummy_uaid.hex) + "/subscription",
headers={"Authorization": self.auth},
body=json.dumps(dict(
channelID=str(dummy_chid),
Expand All @@ -385,26 +388,11 @@ def test_post_existing_uaid(self):
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):
self.settings.router.get_uaid = Mock()
self.settings.router.get_uaid.side_effect = ItemNotFound
resp = yield self.client.post(
resp = yield self.client.delete(
self.url(router_type="webpush",
uaid=dummy_uaid.hex,
chid=str(dummy_chid))
Expand All @@ -413,7 +401,7 @@ def test_no_uaid(self):

@inlineCallbacks
def test_no_auth(self):
resp = yield self.client.post(
resp = yield self.client.delete(
self.url(router_type="webpush",
uaid=dummy_uaid.hex,
chid=str(dummy_chid)),
Expand All @@ -422,19 +410,16 @@ def test_no_auth(self):

@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")
url = self.url(router_type="webpush",
uaid=dummy_uaid.hex)
resp = yield self.client.put(url, body="{invalid")
self._check_error(resp, 400, 108, "Bad Request")

@inlineCallbacks
def test_post_bad_params(self):
self.patch('uuid.uuid4', return_value=dummy_uaid)

resp = yield self.client.post(
resp = yield self.client.delete(
self.url(router_type="simplepush",
uaid=dummy_uaid.hex,
chid=str(dummy_chid)),
Expand All @@ -445,29 +430,6 @@ def test_post_bad_params(self):
)
self._check_error(resp, 401, 109, 'Unauthorized')

@inlineCallbacks
def test_post_uaid_chid(self, *args):
self.patch('uuid.uuid4', return_value=dummy_uaid)

self.fernet_mock.configure_mock(**{
'encrypt.return_value': 'abcd123',
})

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)
Expand All @@ -476,7 +438,8 @@ def test_post_nochid(self, *args):
'encrypt.return_value': 'abcd123',
})
resp = yield self.client.post(
self.url(router_type="simplepush", uaid=dummy_uaid.hex),
self.url(router_type="simplepush", uaid=dummy_uaid.hex) +
"/subscription",
headers={"Authorization": self.auth},
body=json.dumps(dict(
type="simplepush",
Expand Down Expand Up @@ -511,7 +474,8 @@ def mock_encrypt(cleartext):
})

resp = yield self.client.post(
self.url(router_type="simplepush", uaid=dummy_uaid.hex),
self.url(router_type="simplepush", uaid=dummy_uaid.hex) +
"/subscription",
headers={"Authorization": self.auth},
body=json.dumps(dict(
type="simplepush",
Expand Down Expand Up @@ -541,10 +505,9 @@ def test_put(self, *args):
payload = json.loads(resp.content)
eq_(payload, {})
frouter.register.assert_called_with(
dummy_uaid.hex,
uaid="",
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)
Expand All @@ -557,10 +520,22 @@ def test_put_bad_auth(self, *args):

resp = yield self.client.put(
self.url(router_type="test", uaid=dummy_uaid.hex),
headers={"Authorization": "Fred Smith"}
headers={"Authorization": "Fred Smith"},
body=json.dumps(dict(token="blah"))
)
self._check_error(resp, 401, 109, "Unauthorized")

@inlineCallbacks
def test_put_bad_uaid_path(self, *args):
self.patch('uuid.uuid4', return_value=dummy_uaid)

resp = yield self.client.put(
self.url(router_type="test", uaid="invalid"),
headers={"Authorization": "Fred Smith"},
body=json.dumps(dict(token="blah"))
)
eq_(resp.get_status(), 404)

@inlineCallbacks
def test_put_bad_arguments(self, *args):
self.patch('uuid.uuid4', return_value=dummy_chid)
Expand All @@ -583,7 +558,8 @@ def test_put_bad_router_register(self):

resp = yield self.client.put(
self.url(router_type='test', uaid=dummy_uaid.hex),
headers={"Authorization": self.auth}
headers={"Authorization": self.auth},
body=json.dumps(dict(token="blah"))
)
self._check_error(resp, rexc.status_code, rexc.errno, "")

Expand All @@ -598,7 +574,7 @@ def test_delete_bad_chid_value(self):
self.url(router_type="test",
router_token="test",
uaid=dummy_uaid.hex,
chid="invalid"),
chid=uuid.uuid4().hex),
headers={"Authorization": self.auth},
)
self._check_error(resp, 410, 106, "")
Expand Down Expand Up @@ -649,7 +625,8 @@ def test_delete_uaid(self):
@inlineCallbacks
def test_delete_bad_uaid(self):
resp = yield self.client.delete(
self.url(router_type="test", router_token="test", uaid="invalid"),
self.url(router_type="test", router_token="test",
uaid=uuid.uuid4().hex),
headers={"Authorization": self.auth},
)
eq_(resp.get_status(), 401)
Expand Down Expand Up @@ -702,11 +679,3 @@ def test_get(self):
payload = json.loads(resp.content)
eq_(chids, payload['channelIDs'])
eq_(dummy_uaid.hex, payload['uaid'])

@inlineCallbacks
def test_get_no_uaid(self):
resp = yield self.client.get(
self.url(router_type="test", router_token="test"),
headers={"Authorization": self.auth}
)
eq_(resp.get_status(), 410)
Loading

0 comments on commit 7af2b5e

Please sign in to comment.