API - add enpoints to request user data export and get export status

This commit is contained in:
Sam 2023-03-01 12:39:35 +01:00
parent 8d8bb2efb9
commit e4fc8849be
2 changed files with 287 additions and 2 deletions

View File

@ -9,7 +9,12 @@ from flask import Flask
from freezegun import freeze_time from freezegun import freeze_time
from fittrackee import db from fittrackee import db
from fittrackee.users.models import BlacklistedToken, User, UserSportPreference from fittrackee.users.models import (
BlacklistedToken,
User,
UserDataExport,
UserSportPreference,
)
from fittrackee.users.utils.token import get_user_token from fittrackee.users.utils.token import get_user_token
from fittrackee.workouts.models import Sport from fittrackee.workouts.models import Sport
@ -2822,3 +2827,154 @@ class TestUserPrivacyPolicyUpdate(ApiTestCaseMixin):
) )
assert response.status_code == 400 assert response.status_code == 400
class TestPostUserDataExportRequest(ApiTestCaseMixin):
def test_it_returns_data_export_info_when_no_ongoing_request_exists_for_user( # noqa
self,
app: Flask,
user_1: User,
user_2: User,
) -> None:
db.session.add(UserDataExport(user_id=user_2.id))
db.session.commit()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
'/api/auth/profile/export/request',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
data_export_request = UserDataExport.query.filter_by(
user_id=user_1.id
).first()
assert data["status"] == "success"
assert data["request"] == jsonify_dict(data_export_request.serialize())
def test_it_returns_error_if_ongoing_request_exist(
self,
app: Flask,
user_1: User,
) -> None:
db.session.add(UserDataExport(user_id=user_1.id))
db.session.commit()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
'/api/auth/profile/export/request',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_400(response, "ongoing request exists")
def test_it_returns_error_if_existing_request_is_completed_for_less_than_24_hours( # noqa
self,
app: Flask,
user_1: User,
) -> None:
completed_export_request = UserDataExport(user_id=user_1.id)
db.session.add(completed_export_request)
completed_export_request.completed = True
db.session.commit()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
'/api/auth/profile/export/request',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_400(response, "completed request already exists")
def test_it_returns_new_request_if_existing_request_is_completed_for_more_than_more_24_hours( # noqa
self,
app: Flask,
user_1: User,
) -> None:
completed_export_request = UserDataExport(
user_id=user_1.id,
created_at=datetime.utcnow() - timedelta(hours=24),
)
db.session.add(completed_export_request)
completed_export_request.completed = True
db.session.commit()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
'/api/auth/profile/export/request',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
data_export_request = UserDataExport.query.filter_by(
user_id=user_1.id
).first()
assert data_export_request.id != completed_export_request.id
assert data["status"] == "success"
assert data["request"] == jsonify_dict(data_export_request.serialize())
class TestGetUserDataExportRequest(ApiTestCaseMixin):
def test_it_returns_none_if_no_request(
self,
app: Flask,
user_1: User,
user_2: User,
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
'/api/auth/profile/export',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert data["status"] == "success"
assert data["request"] is None
def test_it_returns_existing_request(
self,
app: Flask,
user_1: User,
user_2: User,
) -> None:
completed_export_request = UserDataExport(
user_id=user_1.id,
created_at=datetime.utcnow() - timedelta(hours=24),
)
db.session.add(completed_export_request)
db.session.commit()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
'/api/auth/profile/export',
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert data["status"] == "success"
assert data["request"] == jsonify_dict(
completed_export_request.serialize()
)

View File

@ -33,7 +33,7 @@ from fittrackee.responses import (
from fittrackee.utils import get_readable_duration from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Sport from fittrackee.workouts.models import Sport
from .models import BlacklistedToken, User, UserSportPreference from .models import BlacklistedToken, User, UserDataExport, UserSportPreference
from .utils.controls import check_password, is_valid_email, register_controls from .utils.controls import check_password, is_valid_email, register_controls
from .utils.token import decode_user_token from .utils.token import decode_user_token
@ -1684,3 +1684,132 @@ def accept_privacy_policy(auth_user: User) -> Union[Dict, HttpResponse]:
return InvalidPayloadErrorResponse() return InvalidPayloadErrorResponse()
except (exc.IntegrityError, exc.OperationalError, ValueError) as e: except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
return handle_error_and_return_response(e, db=db) return handle_error_and_return_response(e, db=db)
@auth_blueprint.route('/auth/profile/export/request', methods=['POST'])
@require_auth()
def request_user_data_export(auth_user: User) -> Union[Dict, HttpResponse]:
"""
Request a data export for authenticated user.
**Example request**:
.. sourcecode:: http
POST /auth/profile/export/request HTTP/1.1
Content-Type: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"request": {
"created_at": "Wed, 01 Mar 2023 12:31:17 GMT",
"status": "in_progress",
"file_name": null,
"file_size": null
}
}
:reqheader Authorization: OAuth 2.0 Bearer Token
:statuscode 200: success
:statuscode 400:
- ongoing request exists
- completed request already exists
:statuscode 401:
- provide a valid auth token
- signature expired, please log in again
- invalid token, please log in again
:statuscode 500: internal server error
"""
existing_export_request = UserDataExport.query.filter_by(
user_id=auth_user.id
).first()
if existing_export_request:
if not existing_export_request.completed:
return InvalidPayloadErrorResponse("ongoing request exists")
if (
existing_export_request.created_at
> datetime.datetime.utcnow() - datetime.timedelta(hours=24)
):
return InvalidPayloadErrorResponse(
"completed request already exists"
)
try:
if existing_export_request:
db.session.delete(existing_export_request)
db.session.flush()
export_request = UserDataExport(user_id=auth_user.id)
db.session.add(export_request)
db.session.commit()
return {"status": "success", "request": export_request.serialize()}
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
return handle_error_and_return_response(e, db=db)
@auth_blueprint.route('/auth/profile/export', methods=['GET'])
@require_auth()
def get_user_data_export(auth_user: User) -> Union[Dict, HttpResponse]:
"""
Get a data export info for authenticated user if a request exists.
It returns:
- export creation date
- export status ("in_progress", "successful" and "errored")
- file name and size (in bytes) when export is successful
**Example request**:
.. sourcecode:: http
GET /auth/profile/export HTTP/1.1
Content-Type: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
- if a request exists:
{
"status": "success",
"request": {
"created_at": "Wed, 01 Mar 2023 12:31:17 GMT",
"status": "successful",
"file_name": "archive_rgjsR3fHt295ywNQr5Yp.zip",
"file_size": 924
}
}
- if no request:
{
"status": "success",
"request": null
}
:reqheader Authorization: OAuth 2.0 Bearer Token
:statuscode 200: success
:statuscode 401:
- provide a valid auth token
- signature expired, please log in again
- invalid token, please log in again
"""
export_request = UserDataExport.query.filter_by(
user_id=auth_user.id
).first()
return {
"status": "success",
"request": export_request.serialize() if export_request else None,
}