From 2778c2fd7e2f58914c8770484c5c56b4af333e73 Mon Sep 17 00:00:00 2001 From: Thiago Cardoso Date: Thu, 18 Jul 2024 14:49:02 -0300 Subject: [PATCH] feat: add missing fields to register_feedback --- incognia/api.py | 20 +++++++++++--- incognia/datetime_util.py | 6 ++++- tests/test_api.py | 57 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/incognia/api.py b/incognia/api.py index 1d56ff7..2d97d22 100644 --- a/incognia/api.py +++ b/incognia/api.py @@ -1,7 +1,7 @@ import datetime as dt from typing import Optional, List -from .datetime_util import total_milliseconds_since_epoch +from .datetime_util import total_milliseconds_since_epoch, has_timezone from .endpoints import Endpoints from .exceptions import IncogniaHTTPError, IncogniaError from .json_util import encode @@ -57,9 +57,18 @@ def register_feedback(self, signup_id: Optional[str] = None, account_id: Optional[str] = None, installation_id: Optional[str] = None, - session_token: Optional[str] = None) -> None: + session_token: Optional[str] = None, + request_token: Optional[str] = None, + occurred_at: dt.datetime = None, + expires_at: dt.datetime = None) -> None: if not event: raise IncogniaError('event is required.') + if timestamp is not None and not has_timezone(timestamp): + raise IncogniaError('timestamp must have timezone') + if occurred_at is not None and not has_timezone(occurred_at): + raise IncogniaError('occurred_at must have timezone') + if expires_at is not None and not has_timezone(expires_at): + raise IncogniaError('expires_at must have timezone') try: headers = self.__get_authorization_header() @@ -72,10 +81,15 @@ def register_feedback(self, 'signup_id': signup_id, 'account_id': account_id, 'installation_id': installation_id, - 'session_token': session_token + 'session_token': session_token, + 'request_token': request_token } if timestamp is not None: body['timestamp'] = total_milliseconds_since_epoch(timestamp) + if occurred_at is not None: + body['occurred_at'] = occurred_at.isoformat() + if expires_at is not None: + body['expires_at'] = expires_at.isoformat() data = encode(body) return self.__request.post(Endpoints.FEEDBACKS, headers=headers, data=data) diff --git a/incognia/datetime_util.py b/incognia/datetime_util.py index bb27a20..f2f7d33 100644 --- a/incognia/datetime_util.py +++ b/incognia/datetime_util.py @@ -2,4 +2,8 @@ def total_milliseconds_since_epoch(t: dt.datetime) -> int: - return int((t - dt.datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) + return int((t - dt.datetime.fromtimestamp(0, dt.timezone.utc)).total_seconds() * 1000.0) + + +def has_timezone(d: dt.datetime) -> bool: + return d.tzinfo is not None and d.tzinfo.utcoffset(d) is not None diff --git a/tests/test_api.py b/tests/test_api.py index 18ccc30..8cdcfd0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,6 +16,7 @@ class TestIncogniaAPI(TestCase): CLIENT_SECRET: Final[str] = 'ANY_SECRET' INSTALLATION_ID: Final[str] = 'ANY_INSTALLATION_ID' SESSION_TOKEN: Final[str] = 'ANY_SESSION_TOKEN' + REQUEST_TOKEN: Final[str] = 'ANY_REQUEST_TOKEN' INVALID_INSTALLATION_ID: Final[str] = 'INVALID_INSTALLATION_ID' INVALID_SESSION_TOKEN: Final[str] = 'INVALID_SESSION_TOKEN' ACCOUNT_ID: Final[str] = 'ANY_ACCOUNT_ID' @@ -71,7 +72,8 @@ class TestIncogniaAPI(TestCase): CLIENT_ERROR_CODE: Final[int] = 400 VALID_EVENT_FEEDBACK_TYPE: Final[str] = 'valid_event_feedback_type' INVALID_EVENT_FEEDBACK_TYPE: Final[str] = 'invalid_event_feedback_type' - TIMESTAMP: Final[dt.datetime] = dt.datetime.now() + TIMESTAMP: Final[dt.datetime] = dt.datetime.now(dt.timezone.utc) + TIMESTAMP_WITHOUT_TIMEZONE: Final[dt.datetime] = dt.datetime.now() LOGIN_ID: Final[str] = 'ANY_LOGIN_ID' PAYMENT_ID: Final[str] = 'ANY_PAYMENT_ID' REGISTER_VALID_FEEDBACK_DATA: Final[bytes] = encode({ @@ -86,7 +88,11 @@ class TestIncogniaAPI(TestCase): 'account_id': f'{ACCOUNT_ID}', 'installation_id': f'{INSTALLATION_ID}', 'session_token': f'{SESSION_TOKEN}', - 'timestamp': int((TIMESTAMP - dt.datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) + 'request_token': f'{REQUEST_TOKEN}', + 'timestamp': int(( + TIMESTAMP - dt.datetime.fromtimestamp(0, dt.timezone.utc)).total_seconds() * 1000.0), + 'occurred_at': TIMESTAMP.isoformat(), + 'expires_at': TIMESTAMP.isoformat(), }) REGISTER_INVALID_FEEDBACK_DATA: Final[bytes] = encode({ 'event': f'{INVALID_EVENT_FEEDBACK_TYPE}' @@ -211,13 +217,16 @@ def test_register_feedback_when_all_fields_are_valid_should_work( api.register_feedback(self.VALID_EVENT_FEEDBACK_TYPE, timestamp=self.TIMESTAMP, + occurred_at=self.TIMESTAMP, + expires_at=self.TIMESTAMP, external_id=self.EXTERNAL_ID, login_id=self.LOGIN_ID, payment_id=self.PAYMENT_ID, signup_id=self.SIGNUP_ID, account_id=self.ACCOUNT_ID, installation_id=self.INSTALLATION_ID, - session_token=self.SESSION_TOKEN) + session_token=self.SESSION_TOKEN, + request_token=self.REQUEST_TOKEN) mock_token_manager_get.assert_called() mock_base_request_post.assert_called_with(Endpoints.FEEDBACKS, @@ -235,6 +244,48 @@ def test_register_feedback_when_event_is_empty_should_raise_an_IncogniaError( mock_token_manager_get.assert_not_called() mock_base_request_post.assert_not_called() + @patch.object(BaseRequest, 'post') + @patch.object(TokenManager, 'get', return_value=TOKEN_VALUES) + def test_register_feedback_when_timestamp_does_not_have_timezone_should_raise_IncogniaError( + self, mock_token_manager_get: Mock, mock_base_request_post: Mock): + api = IncogniaAPI(self.CLIENT_ID, self.CLIENT_SECRET) + + self.assertRaises(IncogniaError, + api.register_feedback, + event=self.VALID_EVENT_FEEDBACK_TYPE, + timestamp=self.TIMESTAMP_WITHOUT_TIMEZONE) + + mock_token_manager_get.assert_not_called() + mock_base_request_post.assert_not_called() + + @patch.object(BaseRequest, 'post') + @patch.object(TokenManager, 'get', return_value=TOKEN_VALUES) + def test_register_feedback_when_occurred_at_does_not_have_timezone_should_raise_IncogniaError( + self, mock_token_manager_get: Mock, mock_base_request_post: Mock): + api = IncogniaAPI(self.CLIENT_ID, self.CLIENT_SECRET) + + self.assertRaises(IncogniaError, + api.register_feedback, + event=self.VALID_EVENT_FEEDBACK_TYPE, + occurred_at=self.TIMESTAMP_WITHOUT_TIMEZONE) + + mock_token_manager_get.assert_not_called() + mock_base_request_post.assert_not_called() + + @patch.object(BaseRequest, 'post') + @patch.object(TokenManager, 'get', return_value=TOKEN_VALUES) + def test_register_feedback_when_expires_at_does_not_have_timezone_should_raise_IncogniaError( + self, mock_token_manager_get: Mock, mock_base_request_post: Mock): + api = IncogniaAPI(self.CLIENT_ID, self.CLIENT_SECRET) + + self.assertRaises(IncogniaError, + api.register_feedback, + event=self.VALID_EVENT_FEEDBACK_TYPE, + expires_at=self.TIMESTAMP_WITHOUT_TIMEZONE) + + mock_token_manager_get.assert_not_called() + mock_base_request_post.assert_not_called() + @patch.object(BaseRequest, 'post') @patch.object(TokenManager, 'get', return_value=TOKEN_VALUES) def test_register_feedback_when_required_fields_are_invalid_should_raise_an_IncogniaHTTPError(