100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
"""Every JSON API endpoint works as intended."""
|
|
|
|
import json
|
|
|
|
|
|
def _json_endpoints(app):
|
|
for rule in app.url_map.iter_rules():
|
|
if rule.endpoint == 'static':
|
|
continue
|
|
view = app.view_functions.get(rule.endpoint)
|
|
if getattr(view, 'returns_json', False):
|
|
yield rule.endpoint, rule.rule, sorted(rule.methods - {'HEAD', 'OPTIONS'})
|
|
|
|
|
|
class TestJsonEndpoints:
|
|
"""All API endpoints are properly protected and return JSON."""
|
|
|
|
def test_all_json_endpoints_require_login(self, app, client):
|
|
errors = []
|
|
for endpoint, url, methods in _json_endpoints(app):
|
|
if '<' in url:
|
|
continue
|
|
for method in methods:
|
|
func = getattr(client, method.lower())
|
|
resp = func(url, follow_redirects=False)
|
|
if endpoint == 'data_revision':
|
|
continue
|
|
if resp.status_code in (200, 201):
|
|
errors.append(
|
|
f'{method} {url} ({endpoint}): returned {resp.status_code} without auth'
|
|
)
|
|
elif resp.status_code not in (301, 302, 401, 403):
|
|
errors.append(f'{method} {url} ({endpoint}): unexpected {resp.status_code}')
|
|
assert not errors, '\n'.join(errors[:20])
|
|
|
|
def test_json_endpoints_return_json_content_type(self, app, client, as_role):
|
|
errors = []
|
|
as_role('admin')
|
|
for endpoint, url, methods in _json_endpoints(app):
|
|
if '<' in url:
|
|
continue
|
|
for method in methods:
|
|
func = getattr(client, method.lower())
|
|
resp = func(url, follow_redirects=False)
|
|
if resp.status_code == 301:
|
|
continue
|
|
ct = resp.content_type or ''
|
|
if resp.status_code < 500 and 'application/json' not in ct:
|
|
errors.append(f'{method} {url} ({endpoint}): content_type={ct}')
|
|
assert not errors, '\n'.join(errors[:20])
|
|
|
|
def test_json_endpoints_return_reasonable_payload(self, app, client, as_role):
|
|
errors = []
|
|
as_role('admin')
|
|
for endpoint, url, methods in _json_endpoints(app):
|
|
if '<' in url:
|
|
continue
|
|
for method in methods:
|
|
func = getattr(client, method.lower())
|
|
resp = func(url, follow_redirects=False)
|
|
if resp.status_code in (301, 302):
|
|
continue
|
|
if 'application/json' not in (resp.content_type or ''):
|
|
continue
|
|
try:
|
|
data = json.loads(resp.data)
|
|
except (ValueError, TypeError) as exc:
|
|
errors.append(f'{method} {url} ({endpoint}): unparseable JSON: {exc}')
|
|
continue
|
|
if not isinstance(data, (dict, list)):
|
|
errors.append(f'{method} {url} ({endpoint}): JSON is not a dict: {type(data)}')
|
|
assert not errors, '\n'.join(errors[:20])
|
|
|
|
def test_api_consistent_error_format(self, app, client):
|
|
errors = []
|
|
for _endpoint, url, methods in _json_endpoints(app):
|
|
if '<' not in url:
|
|
for method in methods:
|
|
func = getattr(client, method.lower())
|
|
resp = func(url, follow_redirects=False)
|
|
if resp.status_code >= 400 and 'application/json' in (resp.content_type or ''):
|
|
try:
|
|
data = json.loads(resp.data)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
if 'error' not in data:
|
|
errors.append(f'{method} {url}: 4xx JSON missing "error" key: {data}')
|
|
assert not errors, '\n'.join(errors[:20])
|
|
|
|
|
|
class TestAuthorizedApiAccess:
|
|
"""Each JSON endpoint only exposes data the user is allowed to see."""
|
|
|
|
def test_player_cannot_access_admin_json(self, app, client, as_role):
|
|
as_role('player')
|
|
admin_only = ['/matches/api/manageable-tryouts', '/team-matches/api/manageable-teams']
|
|
for url in admin_only:
|
|
resp = client.get(url, follow_redirects=False)
|
|
assert resp.status_code not in (500,), f'{url} returned {resp.status_code}'
|