"""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}'