API - update OAuth2 scopes

This commit is contained in:
Sam
2022-06-15 19:16:14 +02:00
parent 969a92b8d4
commit 8b2543eb61
25 changed files with 1111 additions and 293 deletions

View File

@ -324,3 +324,39 @@ class TestUpdateConfig(ApiTestCaseMixin):
data = json.loads(response.data.decode())
assert 'success' in data['status']
assert data['data']['admin_contact'] is None
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', True),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.patch(
'/api/config',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -268,15 +268,14 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
@staticmethod
def assert_not_insufficient_scope_error(response: TestResponse) -> None:
assert response.status_code != 403
if response.status_code != 204:
data = json.loads(response.data.decode())
if 'error' in data:
assert 'insufficient_scope' not in data['error']
if 'error_description' in data:
assert (
'The request requires higher privileges than provided by '
'the access token.'
) != data['error_description']
def assert_response_scope(
self, response: TestResponse, can_access: bool
) -> None:
if can_access:
self.assert_not_insufficient_scope_error(response)
else:
self.assert_insufficient_scope(response)
class CallArgsMixin:

View File

@ -6,6 +6,7 @@ import pytest
from flask import Flask
from fittrackee.oauth2.client import check_scope, create_oauth_client
from fittrackee.oauth2.exceptions import InvalidOAuth2Scopes
from fittrackee.oauth2.models import OAuth2Client
from fittrackee.users.models import User
@ -15,7 +16,7 @@ TEST_METADATA = {
'client_name': random_string(),
'client_uri': random_string(),
'redirect_uris': [random_domain()],
'scope': 'read write',
'scope': 'profile:read',
}
@ -127,13 +128,21 @@ class TestCreateOAuth2Client:
def test_oauth_client_has_expected_scope(
self, app: Flask, user_1: User
) -> None:
scope = 'write'
scope = 'workouts:write'
client_metadata: Dict = {**TEST_METADATA, 'scope': scope}
oauth_client = create_oauth_client(client_metadata, user_1)
assert oauth_client.scope == scope
def test_it_raises_error_when_scope_is_invalid(
self, app: Flask, user_1: User
) -> None:
client_metadata: Dict = {**TEST_METADATA, 'scope': random_string()}
with pytest.raises(InvalidOAuth2Scopes):
create_oauth_client(client_metadata, user_1)
def test_oauth_client_has_expected_token_endpoint_auth_method(
self, app: Flask, user_1: User
) -> None:
@ -165,19 +174,23 @@ class TestOAuthCheckScopes:
@pytest.mark.parametrize(
'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']]
)
def test_it_returns_read_if_scope_is_invalid(
def test_it_raises_error_when_scope_is_invalid(
self, input_scope: Any
) -> None:
assert check_scope(input_scope) == 'read'
with pytest.raises(InvalidOAuth2Scopes):
check_scope(input_scope)
@pytest.mark.parametrize(
'input_scope,expected_scope',
[
('read', 'read'),
('read ' + random_string(), 'read'),
('write', 'write'),
('write read', 'write read'),
('write read ' + random_string(), 'write read'),
('profile:read', 'profile:read'),
('profile:read ' + random_string(), 'profile:read'),
('profile:write', 'profile:write'),
('profile:read profile:write', 'profile:read profile:write'),
(
'profile:write profile:read ' + random_string(),
'profile:write profile:read',
),
],
)
def test_it_return_only_valid_scopes(

View File

@ -83,6 +83,30 @@ class TestOAuthClientCreation(ApiTestCaseMixin):
error_message=f'OAuth client metadata missing keys: {missing_key}',
)
def test_it_returns_error_when_scope_is_invalid(
self, app: Flask, user_1: User
) -> None:
invalid_scope = self.random_string()
metadata: Dict = {
**TEST_OAUTH_CLIENT_METADATA,
'scope': invalid_scope,
}
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
self.route,
data=json.dumps(metadata),
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_400(
response,
error_message=('OAuth client invalid scopes'),
)
def test_it_creates_oauth_client(self, app: Flask, user_1: User) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email

View File

@ -1,58 +1,28 @@
from json import dumps
import pytest
from flask import Flask
from werkzeug.test import TestResponse
from fittrackee.users.models import User
from ..mixins import ApiTestCaseMixin
from ..utils import random_short_id, random_string
class OAuth2ScopesTestCase(ApiTestCaseMixin):
def assert_expected_response(
self, response: TestResponse, client_scope: str, endpoint_scope: str
) -> None:
if client_scope == endpoint_scope:
self.assert_not_insufficient_scope_error(response)
else:
self.assert_insufficient_scope(response)
class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
scope = 'read'
class TestOAuth2Scopes(ApiTestCaseMixin):
@pytest.mark.parametrize(
'endpoint_url',
'endpoint_url,scope',
[
'/api/auth/profile',
'/api/records',
'/api/sports',
'/api/sports/1',
f'/api/stats/{random_string()}/by_sport',
f'/api/stats/{random_string()}/by_time',
'/api/users/test',
'/api/workouts',
f'/api/workouts/{random_short_id()}',
f'/api/workouts/{random_short_id()}/chart_data',
f'/api/workouts/{random_short_id()}/chart_data/segment/1',
f'/api/workouts/{random_short_id()}/gpx',
f'/api/workouts/{random_short_id()}/gpx/download',
f'/api/workouts/{random_short_id()}/gpx/segment/1',
('/api/auth/profile', 'profile:read'),
('/api/workouts', 'workouts:read'),
],
)
def test_access_to_get_endpoints(
self, app: Flask, user_1: User, endpoint_url: str
def test_oauth_client_can_access_authorized_endpoints(
self, app: Flask, user_1: User, endpoint_url: str, scope: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope)
response = client.get(
endpoint_url,
@ -60,208 +30,29 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='read'
)
@pytest.mark.parametrize(
'endpoint_url',
['/api/users'],
)
def test_access_to_endpoints_as_admin(
self, app: Flask, user_1_admin: User, endpoint_url: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=self.scope
)
response = client.get(
endpoint_url,
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='read'
)
@pytest.mark.parametrize(
'endpoint_url',
[
'/api/auth/picture',
'/api/auth/profile/edit',
'/api/auth/profile/edit/preferences',
'/api/auth/profile/edit/sports',
'/api/workouts',
'/api/workouts/no_gpx',
],
)
def test_access_post_endpoints(
self, app: Flask, user_1: User, endpoint_url: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
response = client.post(
endpoint_url,
data=dumps(dict()),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='write'
)
@pytest.mark.parametrize(
'endpoint_url',
[
'/api/auth/profile/edit/account',
'/api/workouts/0',
],
)
def test_access_to_patch_endpoints(
self, app: Flask, user_1: User, endpoint_url: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
response = client.patch(
endpoint_url,
data=dumps(dict()),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='write'
)
@pytest.mark.parametrize(
'endpoint_url',
[
'/api/config',
'/api/sports/1',
f'/api/users/{random_string()}',
],
)
def test_access_to_patch_endpoints_as_admin(
self, app: Flask, user_1_admin: User, endpoint_url: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=self.scope
)
response = client.patch(
endpoint_url,
data=dumps(dict()),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='write'
)
@pytest.mark.parametrize(
'endpoint_url',
[
'/api/auth/picture',
'/api/auth/profile/reset/sports/1',
f'/api/users/{random_string()}',
'/api/workouts/0',
],
)
def test_access_to_delete_endpoints(
self, app: Flask, user_1: User, endpoint_url: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
user_1.picture = random_string()
response = client.delete(
endpoint_url,
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_expected_response(
response, client_scope=self.scope, endpoint_scope='write'
)
class TestOAuth2ScopesWithWriteAccess(TestOAuth2ScopesWithReadAccess):
scope = 'write'
class TestOAuth2ScopesWithReadAndWriteAccess(ApiTestCaseMixin):
scope = 'read write'
def test_client_can_access_endpoint_with_read_scope(
self, app: Flask, user_1: User
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
response = client.get(
'/api/auth/profile',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_not_insufficient_scope_error(response)
def test_client_with_read_can_access_endpoints_with_write_scope(
self, app: Flask, user_1: User
@pytest.mark.parametrize(
'endpoint_url,scope',
[
('/api/auth/profile', 'workouts:read'),
('/api/workouts', 'profile:read'),
],
)
def test_oauth_client_can_not_access_unauthorized_endpoints(
self, app: Flask, user_1: User, endpoint_url: str, scope: str
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=self.scope
)
) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope)
response = client.post(
'/api/auth/picture',
data=dumps(dict()),
response = client.get(
endpoint_url,
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_not_insufficient_scope_error(response)
self.assert_insufficient_scope(response)

View File

@ -494,6 +494,38 @@ class TestUserProfile(ApiTestCaseMixin):
assert data['status'] == 'success'
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', True),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self, app: Flask, user_1: User, client_scope: str, can_access: bool
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
'/api/auth/profile',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserProfileUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty(
@ -559,6 +591,42 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
assert data['message'] == 'user profile updated'
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/auth/profile/edit',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserAccountUpdate(ApiTestCaseMixin):
@staticmethod
@ -1193,6 +1261,42 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
password_change_email_mock,
)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserPreferencesUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty(
@ -1254,6 +1358,42 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
assert data['message'] == 'user preferences updated'
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/auth/profile/edit/preferences',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty(
@ -1436,6 +1576,42 @@ class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
assert data['data']['is_active']
assert data['data']['stopped_speed_threshold'] == 0.5
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/auth/profile/edit/sports',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserSportPreferencesReset(ApiTestCaseMixin):
def test_it_returns_error_if_sport_does_not_exist(
@ -1491,6 +1667,44 @@ class TestUserSportPreferencesReset(ApiTestCaseMixin):
assert response.status_code == 204
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
sport_1_cycling: Sport,
user_sport_1_preference: UserSportPreference,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.delete(
f'/api/auth/profile/reset/sports/{sport_1_cycling.id}',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUserPicture(ApiTestCaseMixin):
def test_it_returns_error_if_file_is_missing(
@ -1620,6 +1834,42 @@ class TestUserPicture(ApiTestCaseMixin):
assert 'avatar.png' not in user_1.picture
assert 'avatar2.png' in user_1.picture
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', True),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/auth/picture',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestRegistrationConfiguration(ApiTestCaseMixin):
def test_it_returns_error_if_it_exceeds_max_users(

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from io import BytesIO
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from fittrackee.users.models import User, UserSportPreference
@ -131,6 +132,42 @@ class TestGetUser(ApiTestCaseMixin):
self.assert_404_with_entity(response, 'user')
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', True),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.get(
'/api/users/not_existing',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetUsers(ApiTestCaseMixin):
def test_it_returns_error_if_user_has_no_admin_rights(
@ -885,6 +922,42 @@ class TestGetUsers(ApiTestCaseMixin):
'total': 3,
}
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', True),
('users:write', False),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.get(
'/api/users',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetUserPicture(ApiTestCaseMixin):
def test_it_return_error_if_user_has_no_picture(
@ -1360,6 +1433,43 @@ class TestUpdateUser(ApiTestCaseMixin):
assert user['is_active'] is True
assert user_2.confirmation_token is None
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', True),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
user_2: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.patch(
f'/api/users/{user_2.username}',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestDeleteUser(ApiTestCaseMixin):
def test_user_can_delete_its_own_account(
@ -1573,3 +1683,40 @@ class TestDeleteUser(ApiTestCaseMixin):
)
self.assert_403(response, 'error, registration is disabled')
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', True),
('workouts:read', False),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
user_2: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.delete(
f'/api/users/{user_2.username}',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -47,5 +47,5 @@ TEST_OAUTH_CLIENT_METADATA = {
'client_name': random_string(),
'client_uri': random_domain(),
'redirect_uris': [random_domain()],
'scope': 'read write',
'scope': 'profile:read workouts:read',
}

View File

@ -1,5 +1,6 @@
import json
import pytest
from flask import Flask
from fittrackee.users.models import User
@ -897,3 +898,40 @@ class TestGetRecords(ApiTestCaseMixin):
response = client.get('/api/records')
self.assert_401(response)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
user_2: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.get(
'/api/records',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -1,5 +1,6 @@
import json
import pytest
from flask import Flask
from fittrackee import db
@ -138,6 +139,43 @@ class TestGetSports(ApiTestCaseMixin):
sport_2_running.serialize(is_admin=True)
)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
'/api/sports',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetSport(ApiTestCaseMixin):
def test_it_gets_a_sport(
@ -241,6 +279,43 @@ class TestGetSport(ApiTestCaseMixin):
sport_1_cycling_inactive.serialize(is_admin=True)
)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
f'/api/sports/{sport_1_cycling.id}',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestUpdateSport(ApiTestCaseMixin):
def test_it_disables_a_sport(
@ -442,3 +517,41 @@ class TestUpdateSport(ApiTestCaseMixin):
data = self.assert_404(response)
assert len(data['data']['sports']) == 0
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', True),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
user_2: User,
sport_1_cycling: Sport,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.patch(
f'/api/sports/{sport_1_cycling.id}',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -1,5 +1,6 @@
import json
import pytest
from flask import Flask
from fittrackee.users.models import User
@ -862,6 +863,42 @@ class TestGetStatsByTime(ApiTestCaseMixin):
}
}
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
f'/api/stats/{user_1.username}/by_time',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetStatsBySport(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated(
@ -1007,6 +1044,42 @@ class TestGetStatsBySport(ApiTestCaseMixin):
self.assert_500(response)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
f'/api/stats/{user_1.username}/by_sport',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetAllStats(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated(
@ -1089,3 +1162,39 @@ class TestGetAllStats(ApiTestCaseMixin):
)
self.assert_403(response)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1_admin: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1_admin, scope=client_scope
)
response = client.get(
'/api/stats/all',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -3,6 +3,7 @@ from typing import List
from unittest.mock import patch
from uuid import uuid4
import pytest
from flask import Flask
from fittrackee.users.models import User
@ -101,6 +102,42 @@ class TestGetWorkouts(ApiTestCaseMixin):
self.assert_401(response, 'provide a valid auth token')
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
'/api/workouts',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestGetWorkoutsWithPagination(ApiTestCaseMixin):
def test_it_gets_workouts_with_default_pagination(
@ -1158,6 +1195,55 @@ class TestGetWorkout(ApiTestCaseMixin):
self.assert_404_with_message(response, 'Map does not exist')
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
@pytest.mark.parametrize(
'endpoint',
[
'/api/workouts/{workout_short_id}',
'/api/workouts/{workout_short_id}/gpx',
'/api/workouts/{workout_short_id}/chart_data',
'/api/workouts/{workout_short_id}/gpx/segment/1',
'/api/workouts/{workout_short_id}/chart_data/segment/1',
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
client_scope: str,
can_access: bool,
endpoint: str,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
endpoint.format(workout_short_id=workout_cycling_user_1.short_id),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestDownloadWorkoutGpx(ApiTestCaseMixin):
def test_it_returns_404_if_workout_does_not_exist(
@ -1242,3 +1328,41 @@ class TestDownloadWorkoutGpx(ApiTestCaseMixin):
mimetype='application/gpx+xml',
as_attachment=True,
)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', True),
('workouts:write', False),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.get(
f'/api/workouts/{workout_cycling_user_1.short_id}/gpx/download',
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)

View File

@ -621,6 +621,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
)
assert 'data' not in data
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', True),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/workouts',
data=dict(),
headers=dict(
content_type='multipart/form-data',
Authorization=f'Bearer {access_token}',
),
)
self.assert_response_scope(response, can_access)
class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated(
@ -763,6 +802,45 @@ class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
assert len(data['data']['workouts'][0]['segments']) == 0
assert len(data['data']['workouts'][0]['records']) == 0
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', True),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.post(
'/api/workouts/no_gpx',
data=dict(),
headers=dict(
content_type='multipart/form-data',
Authorization=f'Bearer {access_token}',
),
)
self.assert_response_scope(response, can_access)
class TestPostWorkoutWithZipArchive(ApiTestCaseMixin):
def test_it_adds_workouts_with_zip_archive(

View File

@ -221,6 +221,45 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
self.assert_500(response)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', True),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.patch(
f'/api/workouts/{workout_cycling_user_1.short_id}',
data=dict(),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_updates_a_workout_wo_gpx(

View File

@ -1,5 +1,6 @@
import os
import pytest
from flask import Flask
from fittrackee.files import get_absolute_file_path
@ -80,6 +81,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
self.assert_500(response)
@pytest.mark.parametrize(
'client_scope, can_access',
[
('application:write', False),
('profile:read', False),
('profile:write', False),
('users:read', False),
('users:write', False),
('workouts:read', False),
('workouts:write', True),
],
)
def test_expected_scopes_are_defined(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
client_scope: str,
can_access: bool,
) -> None:
(
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(
app, user_1, scope=client_scope
)
response = client.delete(
f'/api/workouts/{workout_cycling_user_1.short_id}',
data=dict(),
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_response_scope(response, can_access)
class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_deletes_a_workout_wo_gpx(