API - update OAuth2 scopes
This commit is contained in:
parent
969a92b8d4
commit
8b2543eb61
@ -67,7 +67,7 @@ def get_application_config() -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@config_blueprint.route('/config', methods=['PATCH'])
|
@config_blueprint.route('/config', methods=['PATCH'])
|
||||||
@require_auth(scopes='write', as_admin=True)
|
@require_auth(scopes=['application:write'], as_admin=True)
|
||||||
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Update Application config
|
Update Application config
|
||||||
|
@ -3,28 +3,37 @@ from typing import Dict
|
|||||||
|
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
from fittrackee.oauth2.models import OAuth2Client
|
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
DEFAULT_SCOPE = 'read'
|
from .exceptions import InvalidOAuth2Scopes
|
||||||
VALID_SCOPES = ['read', 'write']
|
from .models import OAuth2Client
|
||||||
|
|
||||||
|
VALID_SCOPES = [
|
||||||
|
'application:write',
|
||||||
|
'profile:read',
|
||||||
|
'profile:write',
|
||||||
|
'users:read',
|
||||||
|
'users:write',
|
||||||
|
'workouts:read',
|
||||||
|
'workouts:write',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def check_scope(scope: str) -> str:
|
def check_scope(scope: str) -> str:
|
||||||
"""
|
"""
|
||||||
Verify if provided scope is valid.
|
Verify if provided scope is valid.
|
||||||
If not, it returns the default scope ('read').
|
|
||||||
"""
|
"""
|
||||||
valid_scopes = []
|
|
||||||
if not isinstance(scope, str) or not scope:
|
if not isinstance(scope, str) or not scope:
|
||||||
return DEFAULT_SCOPE
|
raise InvalidOAuth2Scopes()
|
||||||
|
|
||||||
|
valid_scopes = []
|
||||||
scopes = scope.split()
|
scopes = scope.split()
|
||||||
for value in scopes:
|
for value in scopes:
|
||||||
if value in VALID_SCOPES:
|
if value in VALID_SCOPES:
|
||||||
valid_scopes.append(value)
|
valid_scopes.append(value)
|
||||||
if len(valid_scopes) == 0:
|
|
||||||
valid_scopes.append(DEFAULT_SCOPE)
|
if not valid_scopes:
|
||||||
|
raise InvalidOAuth2Scopes()
|
||||||
|
|
||||||
return ' '.join(valid_scopes)
|
return ' '.join(valid_scopes)
|
||||||
|
|
||||||
|
2
fittrackee/oauth2/exceptions.py
Normal file
2
fittrackee/oauth2/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class InvalidOAuth2Scopes(Exception):
|
||||||
|
...
|
@ -5,8 +5,6 @@ from flask import Blueprint, Response, request
|
|||||||
from urllib3.util import parse_url
|
from urllib3.util import parse_url
|
||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
|
|
||||||
from fittrackee.oauth2.server import require_auth
|
|
||||||
from fittrackee.responses import (
|
from fittrackee.responses import (
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
InvalidPayloadErrorResponse,
|
InvalidPayloadErrorResponse,
|
||||||
@ -15,7 +13,9 @@ from fittrackee.responses import (
|
|||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
from .client import create_oauth_client
|
from .client import create_oauth_client
|
||||||
from .server import authorization_server
|
from .exceptions import InvalidOAuth2Scopes
|
||||||
|
from .models import OAuth2Client, OAuth2Token
|
||||||
|
from .server import authorization_server, require_auth
|
||||||
|
|
||||||
oauth_blueprint = Blueprint('oauth', __name__)
|
oauth_blueprint = Blueprint('oauth', __name__)
|
||||||
|
|
||||||
@ -86,7 +86,13 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
new_client = create_oauth_client(client_metadata, auth_user)
|
try:
|
||||||
|
new_client = create_oauth_client(client_metadata, auth_user)
|
||||||
|
except InvalidOAuth2Scopes:
|
||||||
|
return InvalidPayloadErrorResponse(
|
||||||
|
message=('OAuth client invalid scopes')
|
||||||
|
)
|
||||||
|
|
||||||
db.session.add(new_client)
|
db.session.add(new_client)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return (
|
return (
|
||||||
@ -134,10 +140,10 @@ def get_client_by_id(
|
|||||||
return get_client(auth_user, client_id=client_id, client_client_id=None)
|
return get_client(auth_user, client_id=client_id, client_client_id=None)
|
||||||
|
|
||||||
|
|
||||||
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['DELETE'])
|
@oauth_blueprint.route('/oauth/apps/<int:client_id>', methods=['DELETE'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def delete_client(
|
def delete_client(
|
||||||
auth_user: User, client_id: str
|
auth_user: User, client_id: int
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
client = OAuth2Client.query.filter_by(
|
client = OAuth2Client.query.filter_by(
|
||||||
id=client_id,
|
id=client_id,
|
||||||
|
@ -324,3 +324,39 @@ class TestUpdateConfig(ApiTestCaseMixin):
|
|||||||
data = json.loads(response.data.decode())
|
data = json.loads(response.data.decode())
|
||||||
assert 'success' in data['status']
|
assert 'success' in data['status']
|
||||||
assert data['data']['admin_contact'] is None
|
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)
|
||||||
|
@ -268,15 +268,14 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_not_insufficient_scope_error(response: TestResponse) -> None:
|
def assert_not_insufficient_scope_error(response: TestResponse) -> None:
|
||||||
assert response.status_code != 403
|
assert response.status_code != 403
|
||||||
if response.status_code != 204:
|
|
||||||
data = json.loads(response.data.decode())
|
def assert_response_scope(
|
||||||
if 'error' in data:
|
self, response: TestResponse, can_access: bool
|
||||||
assert 'insufficient_scope' not in data['error']
|
) -> None:
|
||||||
if 'error_description' in data:
|
if can_access:
|
||||||
assert (
|
self.assert_not_insufficient_scope_error(response)
|
||||||
'The request requires higher privileges than provided by '
|
else:
|
||||||
'the access token.'
|
self.assert_insufficient_scope(response)
|
||||||
) != data['error_description']
|
|
||||||
|
|
||||||
|
|
||||||
class CallArgsMixin:
|
class CallArgsMixin:
|
||||||
|
@ -6,6 +6,7 @@ import pytest
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.oauth2.client import check_scope, create_oauth_client
|
from fittrackee.oauth2.client import check_scope, create_oauth_client
|
||||||
|
from fittrackee.oauth2.exceptions import InvalidOAuth2Scopes
|
||||||
from fittrackee.oauth2.models import OAuth2Client
|
from fittrackee.oauth2.models import OAuth2Client
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ TEST_METADATA = {
|
|||||||
'client_name': random_string(),
|
'client_name': random_string(),
|
||||||
'client_uri': random_string(),
|
'client_uri': random_string(),
|
||||||
'redirect_uris': [random_domain()],
|
'redirect_uris': [random_domain()],
|
||||||
'scope': 'read write',
|
'scope': 'profile:read',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -127,13 +128,21 @@ class TestCreateOAuth2Client:
|
|||||||
def test_oauth_client_has_expected_scope(
|
def test_oauth_client_has_expected_scope(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
scope = 'write'
|
scope = 'workouts:write'
|
||||||
client_metadata: Dict = {**TEST_METADATA, 'scope': scope}
|
client_metadata: Dict = {**TEST_METADATA, 'scope': scope}
|
||||||
|
|
||||||
oauth_client = create_oauth_client(client_metadata, user_1)
|
oauth_client = create_oauth_client(client_metadata, user_1)
|
||||||
|
|
||||||
assert oauth_client.scope == scope
|
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(
|
def test_oauth_client_has_expected_token_endpoint_auth_method(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -165,19 +174,23 @@ class TestOAuthCheckScopes:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']]
|
'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
|
self, input_scope: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
assert check_scope(input_scope) == 'read'
|
with pytest.raises(InvalidOAuth2Scopes):
|
||||||
|
check_scope(input_scope)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'input_scope,expected_scope',
|
'input_scope,expected_scope',
|
||||||
[
|
[
|
||||||
('read', 'read'),
|
('profile:read', 'profile:read'),
|
||||||
('read ' + random_string(), 'read'),
|
('profile:read ' + random_string(), 'profile:read'),
|
||||||
('write', 'write'),
|
('profile:write', 'profile:write'),
|
||||||
('write read', 'write read'),
|
('profile:read profile:write', 'profile:read profile:write'),
|
||||||
('write read ' + random_string(), 'write read'),
|
(
|
||||||
|
'profile:write profile:read ' + random_string(),
|
||||||
|
'profile:write profile:read',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_return_only_valid_scopes(
|
def test_it_return_only_valid_scopes(
|
||||||
|
@ -83,6 +83,30 @@ class TestOAuthClientCreation(ApiTestCaseMixin):
|
|||||||
error_message=f'OAuth client metadata missing keys: {missing_key}',
|
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:
|
def test_it_creates_oauth_client(self, app: Flask, user_1: User) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user_1.email
|
app, user_1.email
|
||||||
|
@ -1,58 +1,28 @@
|
|||||||
from json import dumps
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from werkzeug.test import TestResponse
|
|
||||||
|
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
from ..mixins import ApiTestCaseMixin
|
from ..mixins import ApiTestCaseMixin
|
||||||
from ..utils import random_short_id, random_string
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2ScopesTestCase(ApiTestCaseMixin):
|
class TestOAuth2Scopes(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'
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'endpoint_url',
|
'endpoint_url,scope',
|
||||||
[
|
[
|
||||||
'/api/auth/profile',
|
('/api/auth/profile', 'profile:read'),
|
||||||
'/api/records',
|
('/api/workouts', 'workouts:read'),
|
||||||
'/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',
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_access_to_get_endpoints(
|
def test_oauth_client_can_access_authorized_endpoints(
|
||||||
self, app: Flask, user_1: User, endpoint_url: str
|
self, app: Flask, user_1: User, endpoint_url: str, scope: str
|
||||||
) -> None:
|
) -> None:
|
||||||
(
|
(
|
||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
_,
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope)
|
||||||
app, user_1, scope=self.scope
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
endpoint_url,
|
endpoint_url,
|
||||||
@ -60,208 +30,29 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
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)
|
self.assert_not_insufficient_scope_error(response)
|
||||||
|
|
||||||
def test_client_with_read_can_access_endpoints_with_write_scope(
|
@pytest.mark.parametrize(
|
||||||
self, app: Flask, user_1: User
|
'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:
|
) -> None:
|
||||||
(
|
(
|
||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
_,
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope)
|
||||||
app, user_1, scope=self.scope
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.post(
|
response = client.get(
|
||||||
'/api/auth/picture',
|
endpoint_url,
|
||||||
data=dumps(dict()),
|
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers=dict(Authorization=f'Bearer {access_token}'),
|
headers=dict(Authorization=f'Bearer {access_token}'),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_not_insufficient_scope_error(response)
|
self.assert_insufficient_scope(response)
|
||||||
|
@ -494,6 +494,38 @@ class TestUserProfile(ApiTestCaseMixin):
|
|||||||
assert data['status'] == 'success'
|
assert data['status'] == 'success'
|
||||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
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):
|
class TestUserProfileUpdate(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_payload_is_empty(
|
def test_it_returns_error_if_payload_is_empty(
|
||||||
@ -559,6 +591,42 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
|||||||
assert data['message'] == 'user profile updated'
|
assert data['message'] == 'user profile updated'
|
||||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
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):
|
class TestUserAccountUpdate(ApiTestCaseMixin):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1193,6 +1261,42 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
password_change_email_mock,
|
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):
|
class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_payload_is_empty(
|
def test_it_returns_error_if_payload_is_empty(
|
||||||
@ -1254,6 +1358,42 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
assert data['message'] == 'user preferences updated'
|
assert data['message'] == 'user preferences updated'
|
||||||
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
|
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):
|
class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_payload_is_empty(
|
def test_it_returns_error_if_payload_is_empty(
|
||||||
@ -1436,6 +1576,42 @@ class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
assert data['data']['is_active']
|
assert data['data']['is_active']
|
||||||
assert data['data']['stopped_speed_threshold'] == 0.5
|
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):
|
class TestUserSportPreferencesReset(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_sport_does_not_exist(
|
def test_it_returns_error_if_sport_does_not_exist(
|
||||||
@ -1491,6 +1667,44 @@ class TestUserSportPreferencesReset(ApiTestCaseMixin):
|
|||||||
|
|
||||||
assert response.status_code == 204
|
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):
|
class TestUserPicture(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_file_is_missing(
|
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 'avatar.png' not in user_1.picture
|
||||||
assert 'avatar2.png' 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):
|
class TestRegistrationConfiguration(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_it_exceeds_max_users(
|
def test_it_returns_error_if_it_exceeds_max_users(
|
||||||
|
@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.users.models import User, UserSportPreference
|
from fittrackee.users.models import User, UserSportPreference
|
||||||
@ -131,6 +132,42 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_404_with_entity(response, 'user')
|
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):
|
class TestGetUsers(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_user_has_no_admin_rights(
|
def test_it_returns_error_if_user_has_no_admin_rights(
|
||||||
@ -885,6 +922,42 @@ class TestGetUsers(ApiTestCaseMixin):
|
|||||||
'total': 3,
|
'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):
|
class TestGetUserPicture(ApiTestCaseMixin):
|
||||||
def test_it_return_error_if_user_has_no_picture(
|
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['is_active'] is True
|
||||||
assert user_2.confirmation_token is None
|
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):
|
class TestDeleteUser(ApiTestCaseMixin):
|
||||||
def test_user_can_delete_its_own_account(
|
def test_user_can_delete_its_own_account(
|
||||||
@ -1573,3 +1683,40 @@ class TestDeleteUser(ApiTestCaseMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assert_403(response, 'error, registration is disabled')
|
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)
|
||||||
|
@ -47,5 +47,5 @@ TEST_OAUTH_CLIENT_METADATA = {
|
|||||||
'client_name': random_string(),
|
'client_name': random_string(),
|
||||||
'client_uri': random_domain(),
|
'client_uri': random_domain(),
|
||||||
'redirect_uris': [random_domain()],
|
'redirect_uris': [random_domain()],
|
||||||
'scope': 'read write',
|
'scope': 'profile:read workouts:read',
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
@ -897,3 +898,40 @@ class TestGetRecords(ApiTestCaseMixin):
|
|||||||
response = client.get('/api/records')
|
response = client.get('/api/records')
|
||||||
|
|
||||||
self.assert_401(response)
|
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)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
@ -138,6 +139,43 @@ class TestGetSports(ApiTestCaseMixin):
|
|||||||
sport_2_running.serialize(is_admin=True)
|
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):
|
class TestGetSport(ApiTestCaseMixin):
|
||||||
def test_it_gets_a_sport(
|
def test_it_gets_a_sport(
|
||||||
@ -241,6 +279,43 @@ class TestGetSport(ApiTestCaseMixin):
|
|||||||
sport_1_cycling_inactive.serialize(is_admin=True)
|
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):
|
class TestUpdateSport(ApiTestCaseMixin):
|
||||||
def test_it_disables_a_sport(
|
def test_it_disables_a_sport(
|
||||||
@ -442,3 +517,41 @@ class TestUpdateSport(ApiTestCaseMixin):
|
|||||||
|
|
||||||
data = self.assert_404(response)
|
data = self.assert_404(response)
|
||||||
assert len(data['data']['sports']) == 0
|
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)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.users.models import User
|
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):
|
class TestGetStatsBySport(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_user_is_not_authenticated(
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
@ -1007,6 +1044,42 @@ class TestGetStatsBySport(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_500(response)
|
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):
|
class TestGetAllStats(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_user_is_not_authenticated(
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
@ -1089,3 +1162,39 @@ class TestGetAllStats(ApiTestCaseMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assert_403(response)
|
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)
|
||||||
|
@ -3,6 +3,7 @@ from typing import List
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
@ -101,6 +102,42 @@ class TestGetWorkouts(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_401(response, 'provide a valid auth token')
|
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):
|
class TestGetWorkoutsWithPagination(ApiTestCaseMixin):
|
||||||
def test_it_gets_workouts_with_default_pagination(
|
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')
|
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):
|
class TestDownloadWorkoutGpx(ApiTestCaseMixin):
|
||||||
def test_it_returns_404_if_workout_does_not_exist(
|
def test_it_returns_404_if_workout_does_not_exist(
|
||||||
@ -1242,3 +1328,41 @@ class TestDownloadWorkoutGpx(ApiTestCaseMixin):
|
|||||||
mimetype='application/gpx+xml',
|
mimetype='application/gpx+xml',
|
||||||
as_attachment=True,
|
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)
|
||||||
|
@ -621,6 +621,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
|||||||
)
|
)
|
||||||
assert 'data' not in data
|
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):
|
class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||||
def test_it_returns_error_if_user_is_not_authenticated(
|
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]['segments']) == 0
|
||||||
assert len(data['data']['workouts'][0]['records']) == 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):
|
class TestPostWorkoutWithZipArchive(ApiTestCaseMixin):
|
||||||
def test_it_adds_workouts_with_zip_archive(
|
def test_it_adds_workouts_with_zip_archive(
|
||||||
|
@ -221,6 +221,45 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_500(response)
|
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):
|
class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||||
def test_it_updates_a_workout_wo_gpx(
|
def test_it_updates_a_workout_wo_gpx(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.files import get_absolute_file_path
|
from fittrackee.files import get_absolute_file_path
|
||||||
@ -80,6 +81,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_500(response)
|
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):
|
class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||||
def test_it_deletes_a_workout_wo_gpx(
|
def test_it_deletes_a_workout_wo_gpx(
|
||||||
|
@ -252,7 +252,7 @@ def login_user() -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/profile', methods=['GET'])
|
@auth_blueprint.route('/auth/profile', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['profile:read'])
|
||||||
def get_authenticated_user_profile(
|
def get_authenticated_user_profile(
|
||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -354,7 +354,7 @@ def get_authenticated_user_profile(
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
|
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
edit authenticated user profile
|
edit authenticated user profile
|
||||||
@ -502,7 +502,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH'])
|
@auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
update authenticated user email and password
|
update authenticated user email and password
|
||||||
@ -712,7 +712,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST'])
|
@auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
edit authenticated user preferences
|
edit authenticated user preferences
|
||||||
@ -853,7 +853,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/profile/edit/sports', methods=['POST'])
|
@auth_blueprint.route('/auth/profile/edit/sports', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_user_sport_preferences(
|
def edit_user_sport_preferences(
|
||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -959,7 +959,7 @@ def edit_user_sport_preferences(
|
|||||||
@auth_blueprint.route(
|
@auth_blueprint.route(
|
||||||
'/auth/profile/reset/sports/<sport_id>', methods=['DELETE']
|
'/auth/profile/reset/sports/<sport_id>', methods=['DELETE']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def reset_user_sport_preferences(
|
def reset_user_sport_preferences(
|
||||||
auth_user: User, sport_id: int
|
auth_user: User, sport_id: int
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
@ -1014,7 +1014,7 @@ def reset_user_sport_preferences(
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
update authenticated user picture
|
update authenticated user picture
|
||||||
@ -1102,7 +1102,7 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
|
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['profile:write'])
|
||||||
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
"""
|
"""
|
||||||
delete authenticated user picture
|
delete authenticated user picture
|
||||||
|
@ -34,7 +34,7 @@ USER_PER_PAGE = 10
|
|||||||
|
|
||||||
|
|
||||||
@users_blueprint.route('/users', methods=['GET'])
|
@users_blueprint.route('/users', methods=['GET'])
|
||||||
@require_auth(scopes='read', as_admin=True)
|
@require_auth(scopes=['users:read'], as_admin=True)
|
||||||
def get_users(auth_user: User) -> Dict:
|
def get_users(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all users (regardless their account status), if authenticated user
|
Get all users (regardless their account status), if authenticated user
|
||||||
@ -235,7 +235,7 @@ def get_users(auth_user: User) -> Dict:
|
|||||||
|
|
||||||
|
|
||||||
@users_blueprint.route('/users/<user_name>', methods=['GET'])
|
@users_blueprint.route('/users/<user_name>', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['users:read'])
|
||||||
def get_single_user(
|
def get_single_user(
|
||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -394,7 +394,7 @@ def get_picture(user_name: str) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
|
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
|
||||||
@require_auth(scopes='write', as_admin=True)
|
@require_auth(scopes=['users:write'], as_admin=True)
|
||||||
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Update user account
|
Update user account
|
||||||
@ -593,7 +593,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
|
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['users:write'])
|
||||||
def delete_user(
|
def delete_user(
|
||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
|
@ -11,7 +11,7 @@ records_blueprint = Blueprint('records', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@records_blueprint.route('/records', methods=['GET'])
|
@records_blueprint.route('/records', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_records(auth_user: User) -> Dict:
|
def get_records(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all records for authenticated user.
|
Get all records for authenticated user.
|
||||||
|
@ -19,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@sports_blueprint.route('/sports', methods=['GET'])
|
@sports_blueprint.route('/sports', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_sports(auth_user: User) -> Dict:
|
def get_sports(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all sports
|
Get all sports
|
||||||
@ -195,7 +195,7 @@ def get_sports(auth_user: User) -> Dict:
|
|||||||
|
|
||||||
|
|
||||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
|
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get a sport
|
Get a sport
|
||||||
@ -304,7 +304,7 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
|
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
|
||||||
@require_auth(scopes='write', as_admin=True)
|
@require_auth(scopes=['workouts:write'], as_admin=True)
|
||||||
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Update a sport
|
Update a sport
|
||||||
|
@ -174,7 +174,7 @@ def get_workouts(
|
|||||||
|
|
||||||
|
|
||||||
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
|
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workouts_by_time(
|
def get_workouts_by_time(
|
||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -281,7 +281,7 @@ def get_workouts_by_time(
|
|||||||
|
|
||||||
|
|
||||||
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
|
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workouts_by_sport(
|
def get_workouts_by_sport(
|
||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -377,7 +377,7 @@ def get_workouts_by_sport(
|
|||||||
|
|
||||||
|
|
||||||
@stats_blueprint.route('/stats/all', methods=['GET'])
|
@stats_blueprint.route('/stats/all', methods=['GET'])
|
||||||
@require_auth(as_admin=True)
|
@require_auth(scopes=['workouts:read'], as_admin=True)
|
||||||
def get_application_stats(auth_user: User) -> Dict:
|
def get_application_stats(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all application statistics
|
Get all application statistics
|
||||||
|
@ -56,7 +56,7 @@ MAX_WORKOUTS_PER_PAGE = 100
|
|||||||
|
|
||||||
|
|
||||||
@workouts_blueprint.route('/workouts', methods=['GET'])
|
@workouts_blueprint.route('/workouts', methods=['GET'])
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
|
def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get workouts for the authenticated user.
|
Get workouts for the authenticated user.
|
||||||
@ -298,7 +298,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>', methods=['GET']
|
'/workouts/<string:workout_short_id>', methods=['GET']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workout(
|
def get_workout(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -462,7 +462,7 @@ def get_workout_data(
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>/gpx', methods=['GET']
|
'/workouts/<string:workout_short_id>/gpx', methods=['GET']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workout_gpx(
|
def get_workout_gpx(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -512,7 +512,7 @@ def get_workout_gpx(
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>/chart_data', methods=['GET']
|
'/workouts/<string:workout_short_id>/chart_data', methods=['GET']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_workout_chart_data(
|
def get_workout_chart_data(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -582,7 +582,7 @@ def get_workout_chart_data(
|
|||||||
'/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>',
|
'/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>',
|
||||||
methods=['GET'],
|
methods=['GET'],
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_segment_gpx(
|
def get_segment_gpx(
|
||||||
auth_user: User, workout_short_id: str, segment_id: int
|
auth_user: User, workout_short_id: str, segment_id: int
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -634,7 +634,7 @@ def get_segment_gpx(
|
|||||||
'<int:segment_id>',
|
'<int:segment_id>',
|
||||||
methods=['GET'],
|
methods=['GET'],
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def get_segment_chart_data(
|
def get_segment_chart_data(
|
||||||
auth_user: User, workout_short_id: str, segment_id: int
|
auth_user: User, workout_short_id: str, segment_id: int
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -705,7 +705,7 @@ def get_segment_chart_data(
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>/gpx/download', methods=['GET']
|
'/workouts/<string:workout_short_id>/gpx/download', methods=['GET']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='read')
|
@require_auth(scopes=['workouts:read'])
|
||||||
def download_workout_gpx(
|
def download_workout_gpx(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[HttpResponse, Response]:
|
) -> Union[HttpResponse, Response]:
|
||||||
@ -848,7 +848,7 @@ def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
|
|||||||
|
|
||||||
|
|
||||||
@workouts_blueprint.route('/workouts', methods=['POST'])
|
@workouts_blueprint.route('/workouts', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['workouts:write'])
|
||||||
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Post a workout with a gpx file
|
Post a workout with a gpx file
|
||||||
@ -1016,7 +1016,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
|
|
||||||
|
|
||||||
@workouts_blueprint.route('/workouts/no_gpx', methods=['POST'])
|
@workouts_blueprint.route('/workouts/no_gpx', methods=['POST'])
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['workouts:write'])
|
||||||
def post_workout_no_gpx(
|
def post_workout_no_gpx(
|
||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
@ -1165,7 +1165,7 @@ def post_workout_no_gpx(
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>', methods=['PATCH']
|
'/workouts/<string:workout_short_id>', methods=['PATCH']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['workouts:write'])
|
||||||
def update_workout(
|
def update_workout(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
@ -1313,7 +1313,7 @@ def update_workout(
|
|||||||
@workouts_blueprint.route(
|
@workouts_blueprint.route(
|
||||||
'/workouts/<string:workout_short_id>', methods=['DELETE']
|
'/workouts/<string:workout_short_id>', methods=['DELETE']
|
||||||
)
|
)
|
||||||
@require_auth(scopes='write')
|
@require_auth(scopes=['workouts:write'])
|
||||||
def delete_workout(
|
def delete_workout(
|
||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
|
Loading…
Reference in New Issue
Block a user