From 6d094b5ed9bf3d432bdd57f7b01242306ed2b0be Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka <49284924+YariKartoshe4ka@users.noreply.github.com> Date: Sun, 5 Jun 2022 18:52:05 +0300 Subject: [PATCH] Upgrading tests (#126) * Small tests refactoring, cover vk.exceptions * Tox fix * Empty ACCESS_TOKEN not a token * Cover vk.utils * Refactoring and covering vk.session.InteractiveMixin * Remove mocking requests objects and old vk.API (UserAPI) test --- .github/workflows/check.yml | 4 +- src/vk/exceptions.py | 34 ++++----- src/vk/session.py | 134 +++++++++++++++++++----------------- src/vk/utils.py | 30 +------- tests/conftest.py | 85 +++-------------------- tests/test_api.py | 27 +------- tests/test_exceptions.py | 35 ++++++++++ tests/test_user_api.py | 68 +++++------------- tests/test_utils.py | 20 ++++-- tox.ini | 2 + 10 files changed, 169 insertions(+), 270 deletions(-) create mode 100644 tests/test_exceptions.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c892dde..316eb3a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -5,7 +5,7 @@ on: push: schedule: - - cron: "0 12 * * *" + - cron: "0 12 */2 * *" jobs: test: @@ -38,6 +38,8 @@ jobs: run: tox -vv --notest - name: Run test suite run: tox --skip-pkg-install + env: + VK_ACCESS_TOKEN: ${{ secrets.VK_ACCESS_TOKEN }} - name: Upload coverage uses: codecov/codecov-action@v2 diff --git a/src/vk/exceptions.py b/src/vk/exceptions.py index c5763dc..1d36f69 100644 --- a/src/vk/exceptions.py +++ b/src/vk/exceptions.py @@ -1,10 +1,14 @@ +from enum import IntEnum -# API Error Codes -AUTHORIZATION_FAILED = 5 # Invalid access token -PERMISSION_IS_DENIED = 7 -CAPTCHA_IS_NEEDED = 14 -ACCESS_DENIED = 15 # No access to call this method -INVALID_USER_ID = 113 # User deactivated + +class ErrorCodes(IntEnum): + """VK API error codes""" + + AUTHORIZATION_FAILED = 5 # Invalid access token + PERMISSION_IS_DENIED = 7 + CAPTCHA_NEEDED = 14 + ACCESS_DENIED = 15 # No access to call this method + INVALID_USER_ID = 113 # User deactivated class VkException(Exception): @@ -18,9 +22,6 @@ class VkAuthError(VkException): class VkAPIError(VkException): __slots__ = ['error', 'code', 'message', 'request_params', 'redirect_uri'] - CAPTCHA_NEEDED = 14 - ACCESS_DENIED = 15 - def __init__(self, error_data): super(VkAPIError, self).__init__() self.error_data = error_data @@ -31,15 +32,16 @@ def __init__(self, error_data): @staticmethod def get_pretty_request_params(error_data): - request_params = error_data.get('request_params', ()) - request_params = {param['key']: param['value'] for param in request_params} - return request_params + return { + param['key']: param['value'] + for param in error_data.get('request_params', ()) + } def is_access_token_incorrect(self): - return self.code == self.ACCESS_DENIED and 'access_token' in self.message + return self.code in (ErrorCodes.AUTHORIZATION_FAILED, ErrorCodes.ACCESS_DENIED) def is_captcha_needed(self): - return self.code == self.CAPTCHA_NEEDED + return self.code == ErrorCodes.CAPTCHA_NEEDED @property def captcha_sid(self): @@ -50,7 +52,7 @@ def captcha_img(self): return self.error_data.get('captcha_img') def __str__(self): - error_message = '{self.code}. {self.message}. request_params = {self.request_params}'.format(self=self) + error_message = f'{self.code}. {self.message}. request_params = {self.request_params}' if self.redirect_uri: - error_message += ',\nredirect_uri = "{self.redirect_uri}"'.format(self=self) + error_message += f',\nredirect_uri = "{self.redirect_uri}"' return error_message diff --git a/src/vk/session.py b/src/vk/session.py index a8e2071..b4d971c 100644 --- a/src/vk/session.py +++ b/src/vk/session.py @@ -1,12 +1,14 @@ +import getpass import logging -import re import urllib +from json import loads +from re import findall import requests from .api import APINamespace from .exceptions import VkAPIError, VkAuthError -from .utils import json_iter_parse, stringify +from .utils import stringify logger = logging.getLogger('vk') @@ -47,27 +49,22 @@ def send(self, request): # todo Replace with something less exceptional response.raise_for_status() - # TODO: there are may be 2 dicts in one JSON - # for example: "{'error': ...}{'response': ...}" - for response_or_error in json_iter_parse(response.text): - request.response = response_or_error + response_or_error = loads(response.text) + request.response = response_or_error - if 'response' in response_or_error: - # todo Can we have error and response simultaneously - # for error in errors: - # logger.warning(str(error)) - return response_or_error['response'] + if 'response' in response_or_error: + # todo Can we have error and response simultaneously + # for error in errors: + # logger.warning(str(error)) + return response_or_error['response'] - elif 'error' in response_or_error: - api_error = VkAPIError(request.response['error']) - request.api_error = api_error - return self.handle_api_error(request) + elif 'error' in response_or_error: + api_error = VkAPIError(request.response['error']) + request.api_error = api_error + return self.handle_api_error(request) - def prepare_request(self, request): - request.method_params.setdefault('access_token', self.access_token) - - def get_access_token(self): - raise NotImplementedError + def prepare_request(self, request): # noqa: U100 + pass def handle_api_error(self, request): logger.error('Handle API error: %s', request.api_error) @@ -77,31 +74,15 @@ def handle_api_error(self, request): return api_error_handler(request) - def on_api_error_14(self, request): - """ - 14. Captcha needed - """ - request.method_params['captcha_key'] = self.get_captcha_key(request) - request.method_params['captcha_sid'] = request.api_error.captcha_sid - - return self.send(request) - - def on_api_error_15(self, request): - """ - 15. Access denied - - due to scope - """ - logger.error('Authorization failed. Access token will be dropped') - - del request.method_params['access_token'] - self.access_token = self.get_access_token() - - return self.send(request) - def on_api_error(self, request): - logger.error('API error: %s', request.api_error) raise request.api_error + +class API(APIBase): + def __init__(self, access_token, **kwargs): + super().__init__(**kwargs) + self.access_token = access_token + def get_captcha_key(self, request): """ Default behavior on CAPTCHA is to raise exception @@ -110,18 +91,24 @@ def get_captcha_key(self, request): # request.api_error.captcha_img raise request.api_error + def on_api_error_14(self, request): + """ + 14. Captcha needed + """ + request.method_params['captcha_key'] = self.get_captcha_key(request) + request.method_params['captcha_sid'] = request.api_error.captcha_sid -class API(APIBase): - def __init__(self, access_token, **kwargs): - super().__init__(**kwargs) - self.access_token = access_token + return self.send(request) + + def prepare_request(self, request): + request.method_params.setdefault('access_token', self.access_token) class UserAPI(APIBase): LOGIN_URL = 'https://m.vk.com' AUTHORIZE_URL = 'https://oauth.vk.com/authorize' - def __init__(self, user_login='', user_password='', app_id=None, scope='offline', **kwargs): + def __init__(self, user_login=None, user_password=None, app_id=None, scope='offline', **kwargs): super().__init__(**kwargs) self.user_login = user_login @@ -133,7 +120,7 @@ def __init__(self, user_login='', user_password='', app_id=None, scope='offline' @staticmethod def get_form_action(response): - form_action = re.findall(r'= 12: return '{}***{}'.format(access_token[:4], access_token[-4:]) - elif access_token: - return '***' - else: - return access_token + return '***' diff --git a/tests/conftest.py b/tests/conftest.py index 1b0ead4..b219cfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ -import pytest -import requests +import os -from vk.session import APIBase +import pytest @pytest.fixture(scope='session') @@ -9,80 +8,12 @@ def v(): """ Actual vk API version """ - return '5.80' - - -class Attributable(object): - def set_attrs(self, attributes): - for attr_name, attr_value in attributes.items(): - setattr(self, attr_name, attr_value) - - -class RequestData(Attributable): - def __init__(self, data): - self.set_attrs(data) - - def __repr__(self): - return ''.format(self.__dict__) - - -class Request(Attributable): - def __init__(self, method, url, **kwargs): - self.method = method - self.url = url - - self.data = RequestData(kwargs.pop('data', {})) - self.set_attrs(kwargs) - - -class Response(object): - def __init__(self, text='', status_code=200, url=None): - self.text = text - self.status_code = status_code - self.url = url - - def raise_for_status(self): - if self.status_code != 200: - raise ValueError(self.status_code) - + return '5.131' -@pytest.fixture -def response_class(): - return Response +@pytest.fixture(scope='session') +def access_token(): + if os.getenv('VK_ACCESS_TOKEN'): + return os.environ['VK_ACCESS_TOKEN'] -class MockedSessionBase(requests.Session): - - def __init__(self): - super(MockedSessionBase, self).__init__() - - self.history = [] - self.last_request = None - - def request(self, method, url, **kwargs): - self.last_request = Request(method, url, **kwargs) - - response = self.mocked_request(method, url, **kwargs) - if not response: - raise NotImplementedError - - return response - - -@pytest.fixture -def session_class(): - return MockedSessionBase - - -@pytest.fixture -def mock_requests_session(monkeypatch): - - class MockedSession(MockedSessionBase): - - def mocked_request(self, verb, url): - if verb == 'POST': - if url.startswith(APIBase.API_URL): - # method = url[len(vk.Session.API_URL):] - return Response() - - monkeypatch.setattr('requests.Session', MockedSession) + pytest.skip('VK_ACCESS_TOKEN env var not defined') diff --git a/tests/test_api.py b/tests/test_api.py index eb2245a..c5765f0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,36 +1,13 @@ -import os -import time - import pytest from vk import API -from vk.exceptions import VkAPIError - - -@pytest.fixture(scope='session') -def service_token(): - return os.environ['TEST_APP_SERVICE_TOKEN'] @pytest.fixture -def api(service_token, v): - return API(service_token, v=v, lang='en') - - -@pytest.mark.skip -def test_v_param(service_token, v): - """ - Missed version on API instance - """ - api = API(service_token) - - with pytest.raises(VkAPIError, match=r'8\. Invalid request: v \(version\) is required'): - api.getServerTime() - - assert api.getServerTime(v=v) > time.time() - 10 +def api(access_token, v): + return API(access_token, v=v, lang='en') -@pytest.mark.skip def test_durov(api): """ Get users diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..aa9b868 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,35 @@ +from time import time + +import pytest + +from vk import API +from vk.exceptions import VkAPIError + + +def test_missed_v_param(access_token, v): + """ + Missed version on API instance + """ + api = API(access_token) + + with pytest.raises(VkAPIError, match=r'8\. Invalid request: v is required'): + api.getServerTime() + + assert api.getServerTime(v=v) > time() - 10 + + +def test_incorrect_token(v): + """ + Incorrect token on API instance + """ + api = API('?', v=v) + + with pytest.raises(VkAPIError, match=r'5\. User authorization failed') as exc_info: + api.getServerTime() + + exc = exc_info.value + + assert exc.is_access_token_incorrect() + assert not exc.is_captcha_needed() + assert exc.captcha_sid is None + assert exc.captcha_img is None diff --git a/tests/test_user_api.py b/tests/test_user_api.py index b68e481..e09f04d 100644 --- a/tests/test_user_api.py +++ b/tests/test_user_api.py @@ -1,61 +1,25 @@ -import pytest +from io import StringIO -import vk +from vk.session import InteractiveMixin -@pytest.fixture -def user_login(): - return 'user-login' +def test_interactive_mixin(monkeypatch): + mixin = InteractiveMixin() + monkeypatch.setattr('sys.stdin', StringIO('test_login_321')) + assert mixin.user_login == 'test_login_321' -@pytest.fixture -def user_password(): - return 'user-password' + mixin.user_login = None + assert mixin.user_login == 'test_login_321' + monkeypatch.setattr('getpass.getpass', lambda *args, **kwargs: '123_test_password') # noqa: U100 + assert mixin.user_password == '123_test_password' -@pytest.fixture -def app_id(): - return 'app-id' + monkeypatch.setattr('sys.stdin', StringIO('test_access_token')) + assert mixin.access_token == 'test_access_token' + monkeypatch.setattr('sys.stdin', StringIO('SuperCaptcha')) + assert mixin.get_captcha_key('http://example.com') == 'SuperCaptcha' -@pytest.fixture -def scope(): - return 'scope' - - -@pytest.fixture(autouse=True) -def mock_requests_session(monkeypatch, user_login, user_password, access_token, response_class, session_class): - - class MockedSession(session_class): - - def mocked_request(self, method, url, **kwargs): - data = kwargs.get('data') - - if method == 'GET': - if url == 'https://m.vk.com': - return response_class('
') - - elif method == 'POST': - - if url == '/login': - if data == {'email': user_login, 'pass': user_password}: - self.cookies['remixsid'] = 'REMIX-SID' - - return response_class(url='/login') - - elif url == 'https://oauth.vk.com/authorize': - return response_class(url='/ANY#access_token={}'.format(access_token)) - - raise NotImplementedError - - monkeypatch.setattr('requests.Session', MockedSession) - - -@pytest.mark.skip -def test_login(user_login, user_password, app_id, scope, access_token): - - api = vk.API(user_login=user_login, user_password=user_password, app_id=app_id, scope=scope) - assert 'access_token' not in api._session.method_default_params - - api._session.update_access_token() - assert api._session.method_default_params.get('access_token') == access_token + monkeypatch.setattr('sys.stdin', StringIO('123789456')) + assert mixin.get_auth_check_code() == '123789456' diff --git a/tests/test_utils.py b/tests/test_utils.py index 0ef2a22..0fa62bf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,19 @@ -# coding=utf8 - -from vk.utils import stringify_values +from vk.utils import censor_access_token, stringify, stringify_values def test_stringify(): + assert stringify(['str', 'str2']) == 'str,str2' + assert stringify(['str', 'стр2']) == 'str,стр2' + assert stringify(['стр', 'стр2']) == 'стр,стр2' + + +def test_stringify_values(): assert stringify_values({1: ['str', 'str2']}) == {1: 'str,str2'} - assert stringify_values({1: ['str', u'стр2']}) == {1: u'str,стр2'} - assert stringify_values({1: [u'стр', u'стр2']}) == {1: u'стр,стр2'} + assert stringify_values({2: ['str', 'стр2']}) == {2: 'str,стр2'} + assert stringify_values({3: ['стр', 'стр2']}) == {3: 'стр,стр2'} + + +def test_censor_access_token(): + assert censor_access_token('abcdfoobartestwxyz') == 'abcd***wxyz' + assert censor_access_token('1234toadfamapgplrkpea4321') == '1234***4321' + assert censor_access_token('foobar') == '***' diff --git a/tox.ini b/tox.ini index e7c60ed..7320616 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,8 @@ skip_missing_interpreters = true description = run the unit tests with pytest under {basepython} extras = test +passenv = + VK_ACCESS_TOKEN commands = pytest {tty:--color=yes} {posargs: \ --no-cov-on-fail --cov-report xml --cov-report term-missing --cov-append \