diff --git a/.gitignore b/.gitignore index 533854a..decff03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ # Created by .ignore support plugin (hsz.mobi) ### Project files # auth data -/tests/auth_data.py -/tests/captcha.jpg +.pytest_cache/ ### Python template # Byte-compiled / optimized / DLL files diff --git a/README.rst b/README.rst index 30b18f2..89cce4f 100644 --- a/README.rst +++ b/README.rst @@ -8,13 +8,13 @@ Features * support python 3.5+ versions * have only one dependency - ``aiohttp 3+`` * support two-factor authentication -* support socks proxy with ``aiosocksy`` +* support socks proxy with ``aiohttp-socks`` * support rate limit of requests * support Long Poll connection TODO ---- -* replace ``aiosocksy`` to ``aiohttp-socks`` +* need refactoring tests for ``AsyncVkExecuteRequestPool`` Install ------- @@ -109,9 +109,9 @@ Drivers .. code-block:: python - >>> driver = Socks5Driver(PROXY_ADDRESS, PORT) # 1234 is port - >>> driver = Socks5Driver(PROXY_ADDRESS, PORT, timeout=10) - >>> driver = Socks5Driver(PROXY_ADDRESS, PORT, PROXY_LOGIN, PROXY_PASSWORD, timeout=10) + >>> driver = ProxyDriver(PROXY_ADDRESS, PORT) # 1234 is port + >>> driver = ProxyDriver(PROXY_ADDRESS, PORT, timeout=10) + >>> driver = ProxyDriver(PROXY_ADDRESS, PORT, PROXY_LOGIN, PROXY_PASSWORD, timeout=10) How to use custom driver with session: @@ -125,7 +125,7 @@ How to use driver with own loop: >>> loop = asyncio.get_event_loop() >>> asyncio.set_event_loop(None) - >>> session = TokenSession(driver=HttpDriver(loop=loop)) # or Socks5Driver + >>> session = TokenSession(driver=HttpDriver(loop=loop)) # or ProxyDriver How to use driver with custom http session object: @@ -211,6 +211,14 @@ Use Session object >>> await lp.get_pts(need_ts=True) # return pts, ts 191231223, 1820350345 +You can iterate over events + +.. code-block:: python + + >>> async for event in lp.iter(): + ... print(event) + {"type":..., "object": {...}} + Notice that ``wait`` value only for long pool connection. Real pause could be more ``wait`` time because of need time @@ -225,7 +233,7 @@ Use exist API object .. code-block:: python >>> api = API(session) - >>> lp = BotsLongPoll(api, mode=2, group_id=1) # default wait=25 + >>> lp = BotsLongPoll(api, group_id=1) # default wait=25 >>> await lp.wait() {"ts":345,"updates":[...]} >>> await lp.wait() @@ -235,7 +243,7 @@ Use Session object .. code-block:: python - >>> lp = BotsLongPoll(session, mode=2, group_id=1) # default wait=25 + >>> lp = BotsLongPoll(session, group_id=1) # default wait=25 >>> await lp.wait() {"ts":78455,"updates":[...]} >>> await lp.get_pts() # return pts @@ -243,7 +251,56 @@ Use Session object >>> await lp.get_pts(need_ts=True) # return pts, ts 191231223, 1820350345 +BotsLongPoll supports iterating too + +.. code-block:: python + + >>> async for event in lp.iter(): + ... print(event) + {"type":..., "object": {...}} + Notice that ``wait`` value only for long pool connection. Real pause could be more ``wait`` time because of need time for authorization (if needed), reconnect and etc. + +Async execute request pool +-------------- +For documentation, see: https://vk.com/dev/execute + +.. code-block:: python + + from aiovk.pools import AsyncVkExecuteRequestPool + + async with AsyncVkExecuteRequestPool() as pool: + response = pool.add_call('users.get', 'YOUR_TOKEN', {'user_ids': 1}) + response2 = pool.add_call('users.get', 'YOUR_TOKEN', {'user_ids': 2}) + response3 = pool.add_call('users.get', 'ANOTHER_TOKEN', {'user_ids': 1}) + response4 = pool.add_call('users.get', 'ANOTHER_TOKEN', {'user_ids': -1}) + + >>> print(response.ok) + True + >>> print(response.result) + [{'id': 1, 'first_name': 'Павел', 'last_name': 'Дуров'}] + >>> print(response2.result) + [{'id': 2, 'first_name': 'Александра', 'last_name': 'Владимирова'}] + >>> print(response3.result) + [{'id': 1, 'first_name': 'Павел', 'last_name': 'Дуров'}] + >>> print(response4.ok) + False + >>> print(response4.error) + {'method': 'users.get', 'error_code': 113, 'error_msg': 'Invalid user id'} + +or + +.. code-block:: python + + from aiovk.pools import AsyncVkExecuteRequestPool + + pool = AsyncVkExecuteRequestPool() + response = pool.add_call('users.get', 'YOUR_TOKEN', {'user_ids': 1}) + response2 = pool.add_call('users.get', 'YOUR_TOKEN', {'user_ids': 2}) + response3 = pool.add_call('users.get', 'ANOTHER_TOKEN', {'user_ids': 1}) + response4 = pool.add_call('users.get', 'ANOTHER_TOKEN', {'user_ids': -1}) + await pool.execute() + ... diff --git a/aiovk/__init__.py b/aiovk/__init__.py index 377c7a3..04c28b1 100644 --- a/aiovk/__init__.py +++ b/aiovk/__init__.py @@ -1,4 +1,4 @@ -__version__ = '3.0.0' +__version__ = '4.0.0' from aiovk.sessions import ImplicitSession, TokenSession, AuthorizationCodeSession from aiovk.api import API diff --git a/aiovk/api.py b/aiovk/api.py index 92f1801..28d6929 100644 --- a/aiovk/api.py +++ b/aiovk/api.py @@ -24,12 +24,9 @@ def __getattr__(self, method_name): async def __call__(self, **method_args): timeout = method_args.pop('timeout', None) + need_raw_response = method_args.pop('raw_response', False) self._method_args = method_args - return await self._api._session.send_api_request( - self._method_name, - method_args, - timeout, - ) + return await self._api._session.send_api_request(self._method_name, method_args, timeout, need_raw_response) class LazyAPI: diff --git a/aiovk/drivers.py b/aiovk/drivers.py index 25b186b..a1c5032 100644 --- a/aiovk/drivers.py +++ b/aiovk/drivers.py @@ -1,8 +1,7 @@ import aiohttp try: - import aiosocksy - from aiosocksy.connector import ProxyConnector + from aiohttp_socks.connector import ProxyConnector except ImportError as e: ProxyConnector = None @@ -12,32 +11,32 @@ def __init__(self, timeout=10, loop=None): self.timeout = timeout self._loop = loop - async def json(self, url, params, timeout=None): - ''' + async def post_json(self, url, params, timeout=None): + """ :param params: dict of query params - :return: dict from json response - ''' + :return: http status code, dict from json response + """ raise NotImplementedError - async def get_text(self, url, params, timeout=None): - ''' + async def get_bin(self, url, params, timeout=None): + """ :param params: dict of query params - :return: http status code, text body of response - ''' + :return: http status code, binary body of response + """ raise NotImplementedError - async def get_bin(self, url, params, timeout=None): - ''' + async def get_text(self, url, params, timeout=None): + """ :param params: dict of query params - :return: http status code, binary body of response - ''' + :return: http status code, text body of response and redirect_url + """ raise NotImplementedError async def post_text(self, url, data, timeout=None): - ''' + """ :param data: dict pr string - :return: redirect url and text body of response - ''' + :return: http status code, text body of response and redirect url + """ raise NotImplementedError async def close(self): @@ -52,37 +51,36 @@ def __init__(self, timeout=10, loop=None, session=None): else: self.session = session - async def json(self, url, params, timeout=None): - # timeouts - https://docs.aiohttp.org/en/v3.0.0/client_quickstart.html#timeouts - async with self.session.get(url, params=params, timeout=timeout or self.timeout) as response: - return await response.json() + async def post_json(self, url, params, timeout=None): + async with self.session.post(url, data=params, timeout=timeout or self.timeout) as response: + return response.status, await response.json() - async def get_text(self, url, params, timeout=None): + async def get_bin(self, url, params, timeout=None): async with self.session.get(url, params=params, timeout=timeout or self.timeout) as response: - return response.status, await response.text() + return response.status, await response.read() - async def get_bin(self, url, params, timeout=None): + async def get_text(self, url, params, timeout=None): async with self.session.get(url, params=params, timeout=timeout or self.timeout) as response: - return await response.read() + return response.status, await response.text(), response.real_url async def post_text(self, url, data, timeout=None): async with self.session.post(url, data=data, timeout=timeout or self.timeout) as response: - return response._real_url, await response.text() + return response.status, await response.text(), response.real_url async def close(self): await self.session.close() -if ProxyConnector: - class Socks5Driver(HttpDriver): - connector = ProxyConnector +class ProxyDriver(HttpDriver): + connector = ProxyConnector - def __init__(self, address, port, login=None, password=None, timeout=10, loop=None): - addr = aiosocksy.Socks5Addr(address, port) - if login and password: - auth = aiosocksy.Socks5Auth(login, password=password) - else: - auth = None - conn = self.connector(proxy=addr, proxy_auth=auth, loop=loop) - session = aiohttp.ClientSession(connector=conn) - super().__init__(timeout, loop, session) + def __init__(self, address, port, login=None, password=None, timeout=10, **kwargs): + connector = ProxyConnector( + host=address, + port=port, + username=login, + password=password, + **kwargs + ) + session = aiohttp.ClientSession(connector=connector) + super().__init__(timeout, kwargs.get('loop'), session) diff --git a/aiovk/exceptions.py b/aiovk/exceptions.py index c56585a..c553de8 100644 --- a/aiovk/exceptions.py +++ b/aiovk/exceptions.py @@ -1,5 +1,4 @@ -import urllib.parse -from aiovk.utils import get_request_params +from urllib.parse import urlencode CAPTCHA_IS_NEEDED = 14 @@ -14,7 +13,7 @@ class VkAuthError(VkException): def __init__(self, error, description, url='', params=''): self.error = error self.description = description - self.url = "{}?{}".format(url, urllib.parse.urlencode(params)) + self.url = "{}?{}".format(url, urlencode(params)) def __str__(self): return self.description @@ -39,7 +38,7 @@ class VkAPIError(VkException): def __init__(self, error, url): self.error_code = error.get('error_code') self.error_msg = error.get('error_msg') - self.params = get_request_params(error.get('request_params')) + self.params = {param['key']: param['value'] for param in error.get('request_params', [])} self.url = url @@ -47,7 +46,7 @@ class VkLongPollError(VkException): def __init__(self, error, description, url='', params=''): self.error = error self.description = description - self.url = "{}?{}".format(url, urllib.parse.urlencode(params)) + self.url = "{}?{}".format(url, urlencode(params)) def __str__(self): return self.description diff --git a/aiovk/longpoll.py b/aiovk/longpoll.py index df55a17..f2df9f3 100644 --- a/aiovk/longpoll.py +++ b/aiovk/longpoll.py @@ -1,5 +1,6 @@ import json from abc import ABC, abstractmethod +from typing import Union, Optional from aiovk import API from aiovk.api import LazyAPI @@ -8,7 +9,8 @@ class BaseLongPoll(ABC): """Interface for all types of Longpoll API""" - def __init__(self, session_or_api, mode: int or list, wait: int=25, version: int=2, timeout: int=None): + def __init__(self, session_or_api, mode: Optional[Union[int, list]], + wait: int = 25, version: int = 2, timeout: int = None): """ :param session_or_api: session object or data for creating a new session :type session_or_api: BaseSession or API or LazyAPI @@ -30,16 +32,19 @@ def __init__(self, session_or_api, mode: int or list, wait: int=25, version: int self.base_params = { 'version': version, 'wait': wait, - 'mode': mode, 'act': 'a_check' } + + if mode is not None: + self.base_params['mode'] = mode + self.pts = None self.ts = None self.key = None self.base_url = None @abstractmethod - async def _get_long_poll_server(self, need_pts: bool=False) -> None: + async def _get_long_poll_server(self, need_pts: bool = False) -> None: """Send *.getLongPollServer request and update internal data :param need_pts: need return the pts field @@ -59,12 +64,12 @@ async def wait(self, need_pts=False) -> dict: } params.update(self.base_params) # invalid mimetype from server - code, response = await self.api._session.driver.get_text( + status, response, _ = await self.api._session.driver.get_text( self.base_url, params, timeout=2 * self.base_params['wait'] ) - if code == 403: + if status == 403: raise VkLongPollError(403, 'smth weth wrong', self.base_url + '/', params) response = json.loads(response) @@ -87,6 +92,12 @@ async def wait(self, need_pts=False) -> dict: self.base_url = None return await self.wait() + + async def iter(self): + while True: + response = await self.wait() + for event in response['updates']: + yield event async def get_pts(self, need_ts=False): if not self.base_url or not self.pts: @@ -99,6 +110,8 @@ async def get_pts(self, need_ts=False): class UserLongPoll(BaseLongPoll): """Implements https://vk.com/dev/using_longpoll""" + # False for testing + use_https = True async def _get_long_poll_server(self, need_pts=False): response = await self.api('messages.getLongPollServer', need_pts=int(need_pts), timeout=self.timeout) @@ -106,7 +119,7 @@ async def _get_long_poll_server(self, need_pts=False): self.ts = response['ts'] self.key = response['key'] # fucking differences between long poll methods in vk api! - self.base_url = 'https://{}'.format(response['server']) + self.base_url = f'http{"s" if self.use_https else ""}://{response["server"]}' class LongPoll(UserLongPoll): @@ -118,8 +131,8 @@ class LongPoll(UserLongPoll): class BotsLongPoll(BaseLongPoll): """Implements https://vk.com/dev/bots_longpoll""" - def __init__(self, session_or_api, mode, group_id, wait=25, version=1, timeout=None): - super().__init__(session_or_api, mode, wait, version, timeout) + def __init__(self, session_or_api, group_id, wait=25, version=1, timeout=None): + super().__init__(session_or_api, None, wait, version, timeout) self.group_id = group_id async def _get_long_poll_server(self, need_pts=False): diff --git a/aiovk/mixins.py b/aiovk/mixins.py index 3eb6638..5f17d38 100644 --- a/aiovk/mixins.py +++ b/aiovk/mixins.py @@ -1,33 +1,31 @@ -from aiovk.utils import TaskQueue, wait_free_slot +from aiovk.drivers import BaseDriver +from aiovk.shaping import TaskQueue, wait_free_slot -class LimitRateDriverMixin: - requests_per_period = 3 - period = 1 # seconds - - def __init__(self, *args, **kwargs): +class LimitRateDriverMixin(BaseDriver): + def __init__(self, *args, requests_per_period=3, period=1, **kwargs): super().__init__(*args, **kwargs) - self._queue = TaskQueue(self.requests_per_period, self.period) - - @wait_free_slot - async def json(self, *args, **kwargs): - return await super().json(*args, **kwargs) + self._queue = TaskQueue(requests_per_period, period) @wait_free_slot - async def get_text(self, *args, **kwargs): - return await super().get_text(*args, **kwargs) + async def post_json(self, *args, **kwargs): + return await super().post_json(*args, **kwargs) @wait_free_slot async def get_bin(self, *args, **kwargs): return await super().get_bin(*args, **kwargs) + @wait_free_slot + async def get_text(self, *args, **kwargs): + return await super().get_text(*args, **kwargs) + @wait_free_slot async def post_text(self, *args, **kwargs): return await super().post_text(*args, **kwargs) async def close(self): await super().close() - self._queue.canel() + self._queue.cancel() class SimpleImplicitSessionMixin: diff --git a/aiovk/pools.py b/aiovk/pools.py new file mode 100644 index 0000000..47aab1a --- /dev/null +++ b/aiovk/pools.py @@ -0,0 +1,143 @@ +import asyncio +import json +from collections import defaultdict +from dataclasses import dataclass +from typing import List, Dict, Optional + +from aiovk import TokenSession, API +from aiovk.exceptions import VkAuthError + + +class AsyncResult: + def __init__(self): + self._result = None + self.ready = False + self.error = None + + @property + def result(self): + return self._result + + @result.setter + def result(self, val): + self._result = val + self.ready = True + + @property + def ok(self): + return self.ready and not self.error + + +@dataclass +class VkCall: + method: str + method_args: dict + result: AsyncResult + + def get_execute_representation(self) -> str: + return f"API.{self.method}({json.dumps(self.method_args, ensure_ascii=False)})" + + +class AsyncVkExecuteRequestPool: + """ + Allows concatenation of api calls using one token into groups and execute each group of hits in + one request using `execute` method + """ + + def __init__(self, call_number_per_request=25, token_session_class=TokenSession): + self.token_session_class = token_session_class + self.call_number_per_request = call_number_per_request + self.pool: Dict[str, List[VkCall]] = defaultdict(list) + self.sessions = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, *args, **kwargs): + await self.execute() + + async def execute(self): + try: + await self._execute() + await asyncio.gather(*[session.close() for session in self.sessions]) + finally: + self.pool.clear() + self.sessions.clear() + + async def _execute(self): + """ + Groups hits and executes them using the execute method, after execution the pool is cleared + """ + executed_pools = [] + for token, calls in self.pool.items(): + session = self.token_session_class(token) + self.sessions.append(session) + api = API(session) + + for methods_pool in chunks(calls, self.call_number_per_request): + executed_pools.append(VkExecuteMethodsPool(methods_pool).execute(api)) + await asyncio.gather(*executed_pools) + + def add_call(self, method, token, method_args=None) -> AsyncResult: + """ + Adds an any api method call to the execute pool + + :param method: api vk method name + :param token: session token + :param method_args: params + :return: object that will contain the result after the pool is closed + """ + if method_args is None: + method_args = {} + + result = None + # searching already added calls with equal token, method and values + for call in self.pool[token]: + if call.method == method and call.method_args == method_args: + result = call.result + break + if result: + return result + result = AsyncResult() + self.pool[token].append(VkCall(method=method, method_args=method_args, result=result)) + return result + + +class VkExecuteMethodsPool: + def __init__(self, pool: Optional[VkCall] = None): + if not pool: + pool = [] + self.pool: List[VkCall] = pool + + async def execute(self, api: API): + """ + Executes calls to the pool using the execute method and stores the results for each call + + :param api: API object to make the request + """ + methods = [call.get_execute_representation() for call in self.pool] + code = f"return [{','.join(methods)}];" + try: + response = await api.execute(code=code, raw_response=True) + except VkAuthError as e: + for call in self.pool: + call.result.error = { + 'method': call.method, + 'error_code': 5, + 'error_msg': e.description + } + return + errors = response.pop('execute_errors', [])[::-1] + response = response['response'] + + for call, result in zip(self.pool, response): + if result is False: + call.result.error = errors.pop() + else: + call.result.result = result + + +def chunks(lst, n): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i: i + n] diff --git a/aiovk/sessions.py b/aiovk/sessions.py index e06a22e..2f80798 100644 --- a/aiovk/sessions.py +++ b/aiovk/sessions.py @@ -1,5 +1,6 @@ import json from abc import ABC, abstractmethod +from typing import Tuple from urllib.parse import parse_qsl from yarl import URL @@ -25,13 +26,15 @@ async def close(self) -> None: """Perform the actions associated with the completion of the current session""" @abstractmethod - async def send_api_request(self, method_name: str, params: dict = None, timeout: int = None) -> dict: + async def send_api_request(self, method_name: str, params: dict = None, timeout: int = None, + raw_response: bool = False) -> dict: """Method that use API instance for sending request to vk server :param method_name: any value from the left column of the methods table from `https://vk.com/dev/methods` :param params: dict of params that available for current method. For example see `Parameters` block from: `https://vk.com/dev/account.getInfo` :param timeout: timeout for response from the server + :param raw_response: return full response :return: dict that contain data from `Result` block. Example see here: `https://vk.com/dev/account.getInfo` """ @@ -59,7 +62,8 @@ async def __aenter__(self) -> BaseSession: async def __aexit__(self, exc_type, exc_val, exc_tb): return await self.close() - async def send_api_request(self, method_name: str, params: dict = None, timeout: int = None) -> dict: + async def send_api_request(self, method_name: str, params: dict = None, timeout: int = None, + raw_response: bool = False) -> dict: # Prepare request if not timeout: timeout = self.timeout @@ -67,10 +71,11 @@ async def send_api_request(self, method_name: str, params: dict = None, timeout: params = {} if self.access_token: params['access_token'] = self.access_token - params['v'] = self.API_VERSION + if 'v' not in params: + params['v'] = self.API_VERSION # Send request - response = await self.driver.json(self.REQUEST_URL + method_name, params, timeout) + _, response = await self.driver.post_json(self.REQUEST_URL + method_name, params, timeout) # Process response # Checking the section with errors @@ -85,16 +90,18 @@ async def send_api_request(self, method_name: str, params: dict = None, timeout: params['captcha_sid'] = captcha_sid # Send request again # Provide one attempt to repeat the request - return await self.send_api_request(method_name, params, timeout) + return await self.send_api_request(method_name, params, timeout, raw_response) elif err_code == AUTHORIZATION_FAILED: await self.authorize() # Send request again # Provide one attempt to repeat the request - return await self.send_api_request(method_name, params, timeout) + return await self.send_api_request(method_name, params, timeout, raw_response) else: # Other errors is not related with security raise VkAPIError(error, self.REQUEST_URL + method_name) - # Must return only useful data + if raw_response: + return response + # Return only useful data return response['response'] async def authorize(self) -> None: @@ -146,12 +153,15 @@ def __init__(self, login: str, password: str, app_id: int, scope: str or int or async def authorize(self) -> None: """Getting a new token from server""" - html = await self._get_auth_page() - url = URL('/authorize?email') + url, html = await self._get_auth_page() for step in range(self.num_of_attempts): - if url.path == '/authorize' and 'email' in url.query: - # Invalid login or password and 'email' in q.query - url, html = await self._process_auth_form(html) + if url.path == '/authorize': + if '__q_hash' in url.query: + # Give rights for app + url, html = await self._process_access_form(html) + else: + # Invalid login or password and 'email' in q.query + url, html = await self._process_auth_form(html) if url.path == '/login' and url.query.get('act', '') == 'authcheck': # Entering 2auth code url, html = await self._process_2auth_form(html) @@ -168,10 +178,10 @@ async def authorize(self) -> None: return raise VkAuthError('Something went wrong', 'Exceeded the number of attempts to log in') - async def _get_auth_page(self) -> str: + async def _get_auth_page(self) -> Tuple[str, str]: """ Get authorization mobile page without js - :return: html page + :return: redirect_url, html page """ # Prepare request params = { @@ -185,13 +195,13 @@ async def _get_auth_page(self) -> str: params['scope'] = self.scope # Send request - status, response = await self.driver.get_text(self.AUTH_URL, params) + status, response, redirect_url = await self.driver.get_text(self.AUTH_URL, params) # Process response if status != 200: error_dict = json.loads(response) raise VkAuthError(error_dict['error'], error_dict['error_description'], self.AUTH_URL, params) - return response + return redirect_url, response async def _process_auth_form(self, html: str) -> (str, str): """ @@ -221,8 +231,8 @@ async def _process_auth_form(self, html: str) -> (str, str): form_url = "https://m.vk.com{}".format(form_url) # Send request - url, html = await self.driver.post_text(form_url, form_data) - return url, html + _, html, redirect_url = await self.driver.post_text(form_url, form_data) + return redirect_url, html async def _process_2auth_form(self, html: str) -> (str, str): """ @@ -245,8 +255,8 @@ async def _process_2auth_form(self, html: str) -> (str, str): form_data['code'] = await self.enter_confirmation_code() # Send request - url, html = await self.driver.post_text(form_url, form_data) - return url, html + _, html, redirect_url = await self.driver.post_text(form_url, form_data) + return redirect_url, html async def _process_access_form(self, html: str) -> (str, str): """ @@ -264,8 +274,8 @@ async def _process_access_form(self, html: str) -> (str, str): form_data = dict(p.inputs) # Send request - url, html = await self.driver.post_text(form_url, form_data) - return url, html + _, html, redirect_url = await self.driver.post_text(form_url, form_data) + return redirect_url, html async def enter_confirmation_code(self) -> str: """ @@ -306,7 +316,7 @@ async def authorize(self, code: str = None) -> None: 'redirect_uri': self.redirect_uri, 'code': code } - response = await self.driver.json(self.CODE_URL, params, self.timeout) + _, response = await self.driver.post_json(self.CODE_URL, params, self.timeout) if 'error' in response: raise VkAuthError(response['error'], response['error_description'], self.CODE_URL, params) self.access_token = response['access_token'] diff --git a/aiovk/utils.py b/aiovk/shaping.py similarity index 85% rename from aiovk/utils.py rename to aiovk/shaping.py index 064f847..713c520 100644 --- a/aiovk/utils.py +++ b/aiovk/shaping.py @@ -18,7 +18,7 @@ async def dispatcher(self, maxsize): for i in range(maxsize - self.qsize()): self.put_nowait(1) - def canel(self): + def cancel(self): self.task.cancel() @@ -27,7 +27,3 @@ async def wrapper(self, *args, **kwargs): await self._queue.get() return await func(self, *args, **kwargs) return wrapper - - -def get_request_params(request_params): - return {param['key']: param['value'] for param in request_params} diff --git a/requirements-dev.txt b/requirements-dev.txt index 7dc1c58..28bc534 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,8 @@ -r requirements.txt -jinja2==2.10.1 -pyotp==2.2.7 +aiohttp-jinja2==1.4.2 +pyotp==2.4.1 +proxy.py==2.2.0 +pytest==6.2.1 +pytest-asyncio==0.14.0 +pytest-aiohttp==0.3.0 +python-dotenv \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 09f137d..1ff3fe7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,2 @@ -aiohttp>=3.2.0 -aiosocksy==0.1.2 -appdirs==1.4.3 -async-timeout==2.0.1 -chardet==3.0.4 -idna==2.6 -idna-ssl==1.0.1 -multidict==4.1.0 -packaging==16.8 -pyparsing==2.2.0 -six==1.10.0 -yarl==1.1.1 +aiohttp>=3.7.3 +aiohttp-socks>=0.5.5 diff --git a/setup.py b/setup.py index d83a8af..726e291 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,9 @@ with open('README.rst', 'r') as f: readme = f.read() +with open('requirements.txt') as f: + requirements = list(map(lambda x: x.strip(), f.readlines())) + with codecs.open(os.path.join(os.path.abspath(os.path.dirname( __file__)), 'aiovk', '__init__.py'), 'r', 'latin1') as fp: try: @@ -27,16 +30,19 @@ long_description=readme, packages=find_packages(), - install_requires='aiohttp>=3.2.0', + install_requires=requirements, license='MIT License', classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords='vk.com api vk wrappper asyncio', + test_suite="tests", + python_requires='>=3.6' ) diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem deleted file mode 100644 index f126db7..0000000 --- a/tests/certs/cert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICpTCCAg6gAwIBAgIJALIUVtKHFoiqMA0GCSqGSIb3DQEBCwUAMGoxCzAJBgNV -BAYTAlVTMQ8wDQYDVQQIDAZPcmVnb24xETAPBgNVBAcMCFBvcnRsYW5kMRUwEwYD -VQQKDAxDb21wYW55IE5hbWUxDDAKBgNVBAsMA09yZzESMBAGA1UEAwwJbG9jYWxo -b3N0MB4XDTE4MTIxNjEyMDAyNFoXDTI0MDYwNzEyMDAyNFowajELMAkGA1UEBhMC -VVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9ydGxhbmQxFTATBgNVBAoM -DENvbXBhbnkgTmFtZTEMMAoGA1UECwwDT3JnMRIwEAYDVQQDDAlsb2NhbGhvc3Qw -gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOPT1EkOPwRtscX3WCI1Qd8dwoOL -nh0LvQSi1OBDuHFvlcfoEtAZgAFSoXXK+rofhYEcB2fwCSXVmRUzVqA8CINNMFCJ -ue/G+e2XUTKzJNZyOOTR5FNH3Uh/m3DVtmDK6aV39rQYxk0AvW9SDsfUXil3lCSy -bJvIW5knYU4g53/zAgMBAAGjUzBRMB0GA1UdDgQWBBSukcOPn66h0WXEOPaSVJ41 -lH6ZbzAfBgNVHSMEGDAWgBSukcOPn66h0WXEOPaSVJ41lH6ZbzAPBgNVHRMBAf8E -BTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAIbO7sMaOVnbEaNe19EUSuzhEmTMdSgk -llyqDTus7skH5ZDMI9rutGqfmZPcjym2/swC0stScPJ/iQym7rZvPd0tWKPYzLhw -h/Ltj0rxXCd/3Qn3Te3QGMHXlHkq65hF4YcqXaqiufZKaXIGn+V5yU7vt6osiMiT -auv4U+l0Bgas ------END CERTIFICATE----- diff --git a/tests/certs/generate_cert.sh b/tests/certs/generate_cert.sh deleted file mode 100755 index 8f3f123..0000000 --- a/tests/certs/generate_cert.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -openssl req -x509 -nodes -newkey rsa:1024 -keyout key.pem -out cert.pem -days 2000 -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=localhost" diff --git a/tests/certs/key.pem b/tests/certs/key.pem deleted file mode 100644 index e4e9ba3..0000000 --- a/tests/certs/key.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOPT1EkOPwRtscX3 -WCI1Qd8dwoOLnh0LvQSi1OBDuHFvlcfoEtAZgAFSoXXK+rofhYEcB2fwCSXVmRUz -VqA8CINNMFCJue/G+e2XUTKzJNZyOOTR5FNH3Uh/m3DVtmDK6aV39rQYxk0AvW9S -DsfUXil3lCSybJvIW5knYU4g53/zAgMBAAECgYAVLX+yOszI0JmR2Wgr5d91qgKG -z9emw4ySfcBkCGeAw+qcZoo5JeWsOc4gcPnVi1CRq9VUaA+xoKVWPytyQaJYxVAW -LtIMerzVMquqaY8U+6ejxQF1TCACIeamUpBnYm+jvjmMWsayM20gw/U18pVYsbWO -FFEXYgCHlqD1LYb+YQJBAPX9R0GcysNrJbi1kmx2s2/oX9ygH9OAA91hZ70jqn1M -yV8QZzwKsAj7rx4XpeNliB5Qmz77ehX+2DuC9AV4jw8CQQDtGVb8ILQxZBa5bIP8 -UmvYRBrd2bQk+Kfk7N+G5Khmh+zT7cGAqOoIFvbJUE7UbPp5x1PrfhZOBEWURWYa -7ADdAkBhnaPkRqs0B1YNyYgUoLouQ4GfFK/sh1WBSYEYTon+dTVIE0NUUU1wEyh4 -AZxj88ujdAtXYAYfqmT2oM3jSedDAkEA0ZfK60rFv7uo0vV4n38E0sMxtNgUhXJC -iP5UgtxzeV/DHX1ZxzCK4efa9Q9HAEXuDeUE7HIjqYfhMjc/EaF7CQJBANb/WGsK -4tqn27wjvqdnSk/mCEAEhPpwZUqP+TUYKSL1+sPmQtgjHEuT55oEpC/T9Y+NMpmG -8bqwPfzzpvDVmhw= ------END PRIVATE KEY----- diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py new file mode 100644 index 0000000..30031c8 --- /dev/null +++ b/tests/functional/conftest.py @@ -0,0 +1,81 @@ +from pathlib import Path + +import aiohttp_jinja2 +import jinja2 +import pytest +from aiohttp import web + + +@pytest.fixture() +def user_1_data(): + return [{'id': 1, 'first_name': 'Павел', 'last_name': 'Дуров'}] + + +@pytest.fixture() +def valid_token(): + return 'token' + + +@pytest.fixture() +async def vk_server(aiohttp_server, user_1_data, valid_token): + @aiohttp_jinja2.template('authorize_page.jinja2') + async def authorize(request): + if 'client_id' in request.query: + return {'base_url': request.host} + else: + raise web.HTTPNotImplemented + + @aiohttp_jinja2.template('blank.jinja2') + async def blank(request): + return + + async def longpoolwait(request): + return web.json_response({'ts': 1857669127, 'updates': []}) + + async def user_get_error(request): + return web.json_response({'error': {'error_code': 5, + 'error_msg': 'User authorization failed: invalid access_token (4).', + 'request_params': [{'key': 'oauth', 'value': '1'}, + {'key': 'method', 'value': 'users.get'}, + {'key': 'user_ids', 'value': '1'}, + {'key': 'v', 'value': '5.74'}]}}) + + async def user_get(request): + return web.json_response({'response': user_1_data}) + + async def get_long_poll_server(request): + data = await request.post() + if data.get('access_token') == valid_token: + return web.json_response( + {'response': {'key': 'long_pool_key', 'server': f'{request.host}/im1774', 'ts': 1857669095}}) + else: + return web.json_response({'error': {'error_code': 5, + 'error_msg': 'User authorization failed: no access_token passed.', + 'request_params': [{'key': 'oauth', 'value': '1'}, {'key': 'method', + 'value': 'messages.getLongPollServer'}, + {'key': 'need_pts', 'value': '0'}, + {'key': 'v', 'value': '5.74'}]}}) + + async def root(request): + data = await request.post() + if data.get('email') == 'login': + location = request.app.router['blank'].url_for() + raise web.HTTPFound(location=f'{location}#access_token={valid_token}') + else: + response = aiohttp_jinja2.render_template('blank.jinja2', request, None) + return response + + app = web.Application() + template_dir = Path(__file__).parent.parent / 'responses' + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(template_dir)) + app.add_routes([ + web.get('/authorize', authorize), + web.get('/blank.html', blank, name='blank'), + web.get('/im1774', longpoolwait), + web.post('/method/users.get.error', user_get_error), + web.post('/method/users.get', user_get), + web.post('/method/messages.getLongPollServer', get_long_poll_server), + web.post('/', root), + ]) + server = await aiohttp_server(app) + yield server diff --git a/tests/functional/test_drivers.py b/tests/functional/test_drivers.py new file mode 100644 index 0000000..512deae --- /dev/null +++ b/tests/functional/test_drivers.py @@ -0,0 +1,128 @@ +import json + +import proxy +import pytest +from aiohttp import web +from aiohttp.test_utils import unused_port +from python_socks import ProxyType +from yarl import URL + +from aiovk.drivers import HttpDriver, ProxyDriver + +pytestmark = pytest.mark.asyncio + + +@pytest.fixture() +def simple_response_data(): + return {'a': 1} + + +@pytest.fixture() +async def vk_server(aiohttp_server, simple_response_data): + async def simple_response_data_handler(request): + return web.json_response(simple_response_data) + + app = web.Application() + app.add_routes([web.route('*', r'/{name:.*}', simple_response_data_handler)]) + server = await aiohttp_server(app) + yield server + + +@pytest.fixture(scope='module') +def proxy_server(): + with proxy.start(['--hostname', '127.0.0.1', '--num-workers', '1', '--port', str(unused_port())]) as p: + yield p + + +@pytest.mark.parametrize( + 'driver_class, use_proxy', + [ + (HttpDriver, False), + (ProxyDriver, True), + ] +) +async def test_post_json(simple_response_data, vk_server, proxy_server, driver_class, use_proxy): + url = f'http://{vk_server.host}:{vk_server.port}/' + params = {} + if use_proxy: + params['address'] = str(proxy_server.flags.hostname) + params['port'] = proxy_server.flags.port + params['proxy_type'] = ProxyType.HTTP + driver = driver_class(**params) + status, jsn = await driver.post_json(url, {}) + await driver.close() + + assert status == 200 + assert jsn == simple_response_data + + +@pytest.mark.parametrize( + 'driver_class, use_proxy', + [ + (HttpDriver, False), + (ProxyDriver, True), + ] +) +async def test_get_bin(simple_response_data, vk_server, proxy_server, driver_class, use_proxy): + url = f'http://{vk_server.host}:{vk_server.port}/' + params = {} + if use_proxy: + params['address'] = str(proxy_server.flags.hostname) + params['port'] = proxy_server.flags.port + params['proxy_type'] = ProxyType.HTTP + driver = driver_class(**params) + status, text = await driver.get_bin(url, {}) + await driver.close() + + assert status == 200 + assert text == json.dumps(simple_response_data).encode() + + +@pytest.mark.parametrize( + 'driver_class, use_proxy', + [ + (HttpDriver, False), + (ProxyDriver, True), + ] +) +async def test_get_text(simple_response_data, vk_server, proxy_server, driver_class, use_proxy): + url = f'http://{vk_server.host}:{vk_server.port}/' + params = {} + if use_proxy: + params['address'] = str(proxy_server.flags.hostname) + params['port'] = proxy_server.flags.port + params['proxy_type'] = ProxyType.HTTP + driver = driver_class(**params) + status, text, redirect_url = await driver.get_text(url, {}) + await driver.close() + + assert status == 200 + assert text == json.dumps(simple_response_data) + assert redirect_url == URL(url) + + +@pytest.mark.parametrize( + 'driver_class, use_proxy', + [ + (HttpDriver, False), + (ProxyDriver, True), + ] +) +async def test_post_text(simple_response_data, vk_server, proxy_server, driver_class, use_proxy): + data = { + 'login': 'test', + 'password': 'test' + } + url = f'http://{vk_server.host}:{vk_server.port}/' + params = {} + if use_proxy: + params['address'] = str(proxy_server.flags.hostname) + params['port'] = proxy_server.flags.port + params['proxy_type'] = ProxyType.HTTP + driver = driver_class(**params) + status, text, redirect_url = await driver.post_text(url, data=data) + await driver.close() + + assert status == 200 + assert text == json.dumps(simple_response_data) + assert redirect_url == URL(url) diff --git a/tests/functional/test_longpool.py b/tests/functional/test_longpool.py new file mode 100644 index 0000000..c0f4fdb --- /dev/null +++ b/tests/functional/test_longpool.py @@ -0,0 +1,52 @@ +import pytest + +from aiovk import ImplicitSession, API, LongPoll, TokenSession + +pytestmark = pytest.mark.asyncio + + +async def test_wait_valid_with_token_session(vk_server, valid_token): + url = f'http://{vk_server.host}:{vk_server.port}' + t = TokenSession(valid_token, timeout=1000) + t.BASE_URL = url + t.REQUEST_URL = f'{url}/method/' + api = API(t) + lp = LongPoll(api, mode=2, wait=2) + lp.use_https = False + + response = await lp.wait() + await t.close() + assert 'ts' in response + assert 'updates' in response + + +async def test_wait_valid_with_session_authorised(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = ImplicitSession(login='login', password='pass', app_id='123', scope='messages') + s.REQUEST_URL = f'{url}/method/' + s.AUTH_URL = f'{url}/authorize' + await s.authorize() + + lp = LongPoll(s, mode=2, wait=2) + lp.use_https = False + + response = await lp.wait() + await s.close() + assert 'ts' in response + assert 'updates' in response + + +async def test_wait_valid_with_session_auto_auth(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = ImplicitSession(login='login', password='pass', app_id='123', scope='messages') + s.REQUEST_URL = f'{url}/method/' + s.AUTH_URL = f'{url}/authorize' + + api = API(s) + lp = LongPoll(api, mode=2, wait=2) + lp.use_https = False + + response = await lp.wait() + await s.close() + assert 'ts' in response + assert 'updates' in response diff --git a/tests/functional/test_sessions.py b/tests/functional/test_sessions.py new file mode 100644 index 0000000..eaa71ee --- /dev/null +++ b/tests/functional/test_sessions.py @@ -0,0 +1,70 @@ +import pytest + +from aiovk import ImplicitSession, TokenSession +from aiovk.exceptions import VkAuthError + +pytestmark = pytest.mark.asyncio + + +async def test_token_session_auth_with_empty_token(vk_server): + s = TokenSession() + with pytest.raises(VkAuthError): + await s.authorize() + await s.close() + + +async def test_token_session_auth_with_token(vk_server): + s = TokenSession('token') + with pytest.raises(VkAuthError): + await s.authorize() + await s.close() + + +async def test_token_session_auth_token_free_request_without_token(vk_server, user_1_data): + url = f'http://{vk_server.host}:{vk_server.port}' + s = TokenSession() + s.REQUEST_URL = f'{url}/method/' + result = await s.send_api_request('users.get', {'user_ids': 1}) + await s.close() + assert result == user_1_data + + +async def test_token_session_auth_token_request_without_token(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = TokenSession('token') + s.REQUEST_URL = f'{url}/method/' + with pytest.raises(VkAuthError): + await s.send_api_request('users.get.error', {'user_ids': 1}) + await s.close() + + +async def test_implicit_session_auth_with_empty_data(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = ImplicitSession(login='', password='', app_id='') + s.REQUEST_URL = f'{url}/method/' + s.AUTH_URL = f'{url}/authorize' + + with pytest.raises(VkAuthError): + await s.authorize() + await s.close() + + +async def test_implicit_session_auth_with_2factor(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = ImplicitSession(login='login', password='pass', app_id='123') + s.REQUEST_URL = f'{url}/method/' + s.AUTH_URL = f'{url}/authorize' + + await s.authorize() + await s.close() + + +@pytest.mark.skip('TODO add captcha test') +async def test_implicit_session_auth_process_captcha_without(vk_server): + url = f'http://{vk_server.host}:{vk_server.port}' + s = ImplicitSession(login='login', password='pass', app_id='123') + s.REQUEST_URL = f'{url}/method/' + s.AUTH_URL = f'{url}/authorize' + + await s.authorize() + await s.close() diff --git a/tests/responses/2auth_form.html b/tests/responses/2auth_form.html deleted file mode 100644 index 9412ab8..0000000 --- a/tests/responses/2auth_form.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - Вход | ВКонтакте - - - - - - - - - - - - - - - -
- - -
-
-
 
-
-
-
-
- -
-
-
-
- -
- выйти -
-
- -
-

Проверка безопасности

-
-
Пожалуйста, введите код из личного сообщения от Администрации или из приложения для генерации кодов, чтобы подтвердить, что Вы — владелец страницы.
-
-
-
Код подтверждения:
-
-
-
-
-
-
- - -
-
-
- - -
- - - - -
-
-
- -
-
- Регистрация - - -
-
-
-
- - - \ No newline at end of file diff --git a/tests/responses/LongPoolWait.json b/tests/responses/LongPoolWait.json deleted file mode 100644 index de2c936..0000000 --- a/tests/responses/LongPoolWait.json +++ /dev/null @@ -1 +0,0 @@ -{"ts":1857669127,"updates":[]} diff --git a/tests/responses/auth_form_captcha.html b/tests/responses/auth_form_captcha.html deleted file mode 100644 index 7083afc..0000000 --- a/tests/responses/auth_form_captcha.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - Подтверждение - - - - - - - - - - - - -
- - -
-
-
 
-
-
-
-
- -
-
-
-
- -
- выйти -
-
- -
-
-

Введите код с картинки

-
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
-
- - -
- - - - -
-
-
- -
-
- Регистрация - - -
-
-
-
- - - diff --git a/tests/responses/auth_form_captcha_invalid.html b/tests/responses/auth_form_captcha_invalid.html deleted file mode 100644 index 1d54220..0000000 --- a/tests/responses/auth_form_captcha_invalid.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - Вход | ВКонтакте - - - - - - - - - - - - - - - -
- - -
-
-
 
-
-
-
-
- -
-
-
-
- -
- выйти -
-
- -
-

Проверка безопасности

-
-
Пожалуйста, введите код из личного сообщения от Администрации или из приложения для генерации кодов, чтобы подтвердить, что Вы — владелец страницы.
-
Неверный код.
Пожалуйста, введите код, который Вы только что получили.
-
-
-
-
Код подтверждения:
-
-
-
-
-
-
- - -
-
-
- - -
- - - - -
-
-
- -
-
- Регистрация - - -
-
-
-
- - - \ No newline at end of file diff --git a/tests/responses/authorize_page.html b/tests/responses/authorize_page.jinja2 similarity index 99% rename from tests/responses/authorize_page.html rename to tests/responses/authorize_page.jinja2 index 63501da..ece6ac3 100644 --- a/tests/responses/authorize_page.html +++ b/tests/responses/authorize_page.jinja2 @@ -87,7 +87,7 @@

 

-
+ diff --git a/tests/responses/blank.html b/tests/responses/blank.jinja2 similarity index 100% rename from tests/responses/blank.html rename to tests/responses/blank.jinja2 diff --git a/tests/responses/getLongPollServer.error.json b/tests/responses/getLongPollServer.error.json deleted file mode 100644 index abb8a5a..0000000 --- a/tests/responses/getLongPollServer.error.json +++ /dev/null @@ -1 +0,0 @@ -{"error": {"error_code": 5, "error_msg": "User authorization failed: no access_token passed.", "request_params": [{"key": "oauth", "value": "1"}, {"key": "method", "value": "messages.getLongPollServer"}, {"key": "need_pts", "value": "0"}, {"key": "v", "value": "5.74"}]}} \ No newline at end of file diff --git a/tests/responses/getLongPollServer.success.json b/tests/responses/getLongPollServer.success.json deleted file mode 100644 index da20f86..0000000 --- a/tests/responses/getLongPollServer.success.json +++ /dev/null @@ -1 +0,0 @@ -{"response": {"key": "long_pool_key", "server": "{{ base_url }}/im1774", "ts": 1857669095}} \ No newline at end of file diff --git a/tests/responses/testdata.json b/tests/responses/testdata.json deleted file mode 100644 index 4a036f5..0000000 --- a/tests/responses/testdata.json +++ /dev/null @@ -1 +0,0 @@ -{"a": 1} \ No newline at end of file diff --git a/tests/responses/users.get.error.json b/tests/responses/users.get.error.json deleted file mode 100644 index bc0cbf7..0000000 --- a/tests/responses/users.get.error.json +++ /dev/null @@ -1 +0,0 @@ -{"error": {"error_code": 5, "error_msg": "User authorization failed: invalid access_token (4).", "request_params": [{"key": "oauth", "value": "1"}, {"key": "method", "value": "users.get"}, {"key": "user_ids", "value": "1"}, {"key": "v", "value": "5.74"}]}} \ No newline at end of file diff --git a/tests/responses/users.get.json b/tests/responses/users.get.json deleted file mode 100644 index 35f8394..0000000 --- a/tests/responses/users.get.json +++ /dev/null @@ -1 +0,0 @@ -{"response": [{"id": 1, "first_name": "Павел", "last_name": "Дуров"}]} diff --git a/tests/smoke/test_drivers.py b/tests/smoke/test_drivers.py deleted file mode 100644 index 128fb69..0000000 --- a/tests/smoke/test_drivers.py +++ /dev/null @@ -1,142 +0,0 @@ -import json -import os -from http.server import HTTPServer -from threading import Thread -from unittest import mock -import asyncio -import aiosocksy -from aiohttp import TCPConnector -from aiohttp.test_utils import unittest_run_loop -from yarl import URL - -from aiovk.drivers import Socks5Driver, HttpDriver -from tests.utils import AioTestCase, TEST_DIR -from tests.smoke.utils import get_free_port, MockServerRequestHandler - - -class TestMethodsMixin: - driver_class = None - json_filepath = os.path.join(TEST_DIR, 'responses', "testdata.json") - driver_kwargs = {} - - @classmethod - def setUpClass(cls): - super().setUpClass() - # Configure mock server. - cls.mock_server_port = get_free_port() - cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), MockServerRequestHandler) - - # Start running mock server in a separate thread. - # Daemon threads automatically shut down when the main process exits. - cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever) - cls.mock_server_thread.setDaemon(True) - cls.mock_server_thread.start() - - cls.json_url = 'http://localhost:{port}/'.format(port=cls.mock_server_port) - - async def json(self, loop=None): - driver = self.driver_class(loop=loop, **self.driver_kwargs) - jsn = await driver.json(self.json_url, {}) - await driver.close() - original = {} - with open(self.json_filepath) as f: - original = json.load(f) - self.assertDictEqual(jsn, original) - - @unittest_run_loop - async def test_json_default_loop(self): - await self.json() - - @unittest_run_loop - async def test_json_custom_loop(self): - loop = asyncio.get_event_loop() - await self.json(loop) - - async def get_text(self, loop=None): - driver = self.driver_class(loop=loop, **self.driver_kwargs) - status, text = await driver.get_text(self.json_url, {}) - await driver.close() - self.assertEqual(status, 200) - original = '' - with open(self.json_filepath) as f: - original = f.read() - self.assertEqual(text, original) - - @unittest_run_loop - async def test_get_text_default_loop(self): - await self.get_text() - - @unittest_run_loop - async def test_get_text_custom_loop(self): - loop = asyncio.get_event_loop() - await self.get_text(loop) - - async def get_bin(self, loop=None): - driver = self.driver_class(loop=loop, **self.driver_kwargs) - text = await driver.get_bin(self.json_url, {}) - await driver.close() - original = '' - with open(self.json_filepath, 'rb') as f: - original = f.read() - self.assertEqual(text, original) - - @unittest_run_loop - async def test_get_bin_default_loop(self): - await self.get_bin() - - @unittest_run_loop - async def test_get_bin_custom_loop(self): - loop = asyncio.get_event_loop() - await self.get_bin(loop) - - async def post_text(self, loop=None): - data = { - "login": 'test', - "password": "test" - } - driver = self.driver_class(loop=loop, **self.driver_kwargs) - request_url = self.json_url - url, text = await driver.post_text(request_url, data=data) - await driver.close() - self.assertEqual(url, URL(request_url)) - self.assertEqual(text, 'OK') - - @unittest_run_loop - async def test_post_text_default_loop(self): - await self.post_text() - - @unittest_run_loop - async def test_post_text_custom_loop(self): - loop = asyncio.get_event_loop() - await self.post_text(loop) - - -class HttpDirverTestCase(TestMethodsMixin, AioTestCase): - driver_class = HttpDriver - - -class TestSocksConnector(TCPConnector): - def __init__(self, proxy, proxy_auth, loop): - super().__init__(loop=loop) - assert type(proxy) == aiosocksy.Socks5Addr - assert type(proxy_auth) == aiosocksy.Socks5Auth or proxy_auth is None - - -@mock.patch('aiovk.drivers.Socks5Driver.connector', TestSocksConnector) -class SOCKS5DriverANONTestCase(TestMethodsMixin, AioTestCase): - driver_class = Socks5Driver - driver_kwargs = { - "address": '127.0.0.1', - "port": get_free_port() - } - - -@mock.patch('aiovk.drivers.Socks5Driver.connector', TestSocksConnector) -class SOCKS5DriverAUTHTestCase(TestMethodsMixin, AioTestCase): - driver_class = Socks5Driver - driver_kwargs = { - "address": '127.0.0.1', - "port": get_free_port(), - "login": 'test', - "password": 'test' - } diff --git a/tests/smoke/test_longpoll.py b/tests/smoke/test_longpoll.py deleted file mode 100644 index b27c159..0000000 --- a/tests/smoke/test_longpoll.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import ssl -from http.server import HTTPServer -from threading import Thread - -from aiohttp.test_utils import unittest_run_loop - -from aiovk import API -from aiovk.longpoll import LongPoll -# from tests.auth_data import USER_LOGIN, USER_PASSWORD, APP_ID -from tests.utils import AioTestCase, TEST_DIR -from tests.smoke.utils import disable_cert_verification, VKRequestHandler, TestTokenSession, TestInternalAuthSession, \ - get_free_port - - -class LongPollRealTestCase(AioTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Configure mock server. - cls.mock_server_port = get_free_port() - cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), VKRequestHandler) - cls.mock_server.socket = ssl.wrap_socket(cls.mock_server.socket, server_side=True, - certfile=os.path.join(TEST_DIR, 'certs', 'cert.pem'), - keyfile=os.path.join(TEST_DIR, 'certs', 'key.pem')) - - # Start running mock server in a separate thread. - # Daemon threads automatically shut down when the main process exits. - cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever) - cls.mock_server_thread.setDaemon(True) - cls.mock_server_thread.start() - cls.base_url = 'localhost:{port}'.format(port=cls.mock_server_port) - - @disable_cert_verification - @unittest_run_loop - async def test_wait_valid_with_token_session(self): - s = TestInternalAuthSession(login='login', password='pass', app_id='123', scope='messages') - s.BASE_URL = self.base_url - await s.authorize() - await s.close() - - t = TestTokenSession(s.access_token, timeout=1000) - t.BASE_URL = self.base_url - api = API(t) - lp = LongPoll(api, mode=2, wait=2) - - response = await lp.wait() - await t.close() - self.assertTrue('ts' in response) - self.assertTrue('updates' in response) - - @disable_cert_verification - @unittest_run_loop - async def test_wait_valid_with_session_authorised(self): - s = TestInternalAuthSession(login='login', password='pass', app_id='123', scope='messages') - s.BASE_URL = self.base_url - await s.authorize() - - lp = LongPoll(s, mode=2, wait=2) - - response = await lp.wait() - await s.close() - self.assertTrue('ts' in response) - self.assertTrue('updates' in response) - - @disable_cert_verification - @unittest_run_loop - async def test_wait_valid_with_session_auto_auth(self): - s = TestInternalAuthSession(login='login', password='pass', app_id='123', scope='messages') - s.BASE_URL = self.base_url - - api = API(s) - lp = LongPoll(api, mode=2, wait=2) - - response = await lp.wait() - await s.close() - self.assertTrue('ts' in response) - self.assertTrue('updates' in response) diff --git a/tests/smoke/test_sessions.py b/tests/smoke/test_sessions.py deleted file mode 100644 index 93f1380..0000000 --- a/tests/smoke/test_sessions.py +++ /dev/null @@ -1,119 +0,0 @@ -import os -import ssl -import unittest -from http.server import HTTPServer -from threading import Thread - -from aiohttp.test_utils import unittest_run_loop - -from aiovk import ImplicitSession, TokenSession -from aiovk.exceptions import VkAuthError -from tests.smoke.utils import disable_cert_verification, VKRequestHandler, get_free_port -from tests.utils import AioTestCase, TEST_DIR - - -class TokenSessionTestCase(AioTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Configure mock server. - cls.mock_server_port = get_free_port() - cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), VKRequestHandler) - cls.mock_server.socket = ssl.wrap_socket(cls.mock_server.socket, server_side=True, - certfile=os.path.join(TEST_DIR, 'certs', 'cert.pem'), - keyfile=os.path.join(TEST_DIR, 'certs', 'key.pem')) - - # Start running mock server in a separate thread. - # Daemon threads automatically shut down when the main process exits. - cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever) - cls.mock_server_thread.setDaemon(True) - cls.mock_server_thread.start() - cls.base_url = 'localhost:{port}'.format(port=cls.mock_server_port) - - cls.json_url = 'https://{}/'.format(cls.base_url) - - @unittest_run_loop - async def test_auth_with_empty_token(self): - s = TokenSession() - with self.assertRaises(VkAuthError): - await s.authorize() - await s.close() - - @disable_cert_verification - @unittest_run_loop - async def test_auth_with_token(self): - s = TokenSession('token') - with self.assertRaises(VkAuthError): - await s.authorize() - await s.close() - - @disable_cert_verification - @unittest_run_loop - async def test_auth_token_free_request_without_token(self): - s = TokenSession() - s.REQUEST_URL = 'https://{}/method/'.format(self.base_url) - result = await s.send_api_request('users.get', {'user_ids': 1}) - await s.close() - self.assertListEqual(result, [{'id': 1, 'last_name': 'Дуров', 'first_name': 'Павел'}]) - - @disable_cert_verification - @unittest_run_loop - async def test_auth_token_request_without_token(self): - s = TokenSession('token') - s.REQUEST_URL = 'https://{}/method/'.format(self.base_url) - with self.assertRaises(VkAuthError): - await s.send_api_request('users.get.error', {'user_ids': 1}) - await s.close() - - -class ImplicitSessionTestCase(AioTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Configure mock server. - cls.mock_server_port = get_free_port() - cls.mock_server = HTTPServer(('localhost', cls.mock_server_port), VKRequestHandler) - cls.mock_server.socket = ssl.wrap_socket(cls.mock_server.socket, server_side=True, - certfile=os.path.join(TEST_DIR, 'certs', 'cert.pem'), - keyfile=os.path.join(TEST_DIR, 'certs', 'key.pem')) - - # Start running mock server in a separate thread. - # Daemon threads automatically shut down when the main process exits. - cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever) - cls.mock_server_thread.setDaemon(True) - cls.mock_server_thread.start() - cls.base_url = 'localhost:{port}'.format(port=cls.mock_server_port) - - cls.json_url = 'https://{}/'.format(cls.base_url) - - @disable_cert_verification - @unittest_run_loop - async def test_auth_with_empty_data(self): - s = ImplicitSession(login='', password='', app_id='') - s.REQUEST_URL = 'https://{}/method/'.format(self.base_url) - s.AUTH_URL = 'https://{}/authorize'.format(self.base_url) - - with self.assertRaises(VkAuthError): - await s.authorize() - await s.close() - - @disable_cert_verification - @unittest_run_loop - async def test_auth_with_2factor(self): - s = ImplicitSession(login='login', password='pass', app_id='123') - s.REQUEST_URL = 'https://{}/method/'.format(self.base_url) - s.AUTH_URL = 'https://{}/authorize'.format(self.base_url) - - await s.authorize() - await s.close() - - @unittest.skip("TODO add captcha test") - @disable_cert_verification - @unittest_run_loop - async def test_auth_process_captcha_without(self): - s = ImplicitSession(login='login', password='pass', app_id='123') - s.REQUEST_URL = 'https://{}/method/'.format(self.base_url) - s.AUTH_URL = 'https://{}/authorize'.format(self.base_url) - - await s.authorize() - await s.close() diff --git a/tests/smoke/utils.py b/tests/smoke/utils.py deleted file mode 100644 index 8fc39ea..0000000 --- a/tests/smoke/utils.py +++ /dev/null @@ -1,145 +0,0 @@ -import os -import socket -import ssl -from http.server import BaseHTTPRequestHandler -from string import Template -from unittest.mock import patch -from urllib.parse import parse_qs - -from jinja2 import Environment, FileSystemLoader -from yarl import URL - -from aiovk import TokenSession, ImplicitSession -from tests.utils import TEST_DIR - - -def disable_cert_verification(func): - @patch('ssl.create_default_context', ssl._create_unverified_context) - def new_func(*inner_args, **inner_kwargs): - func(*inner_args, **inner_kwargs) - return new_func - - -class HtmlTemplate(Template): - pattern = r""" - %(delim)s(?: - (?P%(delim)s) | # Escape sequence of two delimiters - {{ (?P%(id)s) }} | # delimiter and a braced identifier - (?P) # Other ill-formed delimiter exprs - ) - """ - - -class VKRequestHandler(BaseHTTPRequestHandler): - base_file_dir = os.path.join(TEST_DIR, 'responses') - templates = Environment(loader=FileSystemLoader(base_file_dir)) - token = 'token' - - def redirect(self, url): - self.send_response(301) - self.send_header('Location', url) - self.end_headers() - - def html_pages(self, filename, mime_type=None, context=None): - if mime_type is None: - mime_type = 'text/html' - if context is None: - context = {} - self.send_response(200) - self.send_header('Content-type', mime_type) - self.end_headers() - template = self.templates.get_template(filename) - self.wfile.write(template.render(**context).encode()) - - def json_response(self, filename, context=None): - self.html_pages(filename, 'application/json', context=context) - - def do_GET(self): - url = URL(self.path) - if url.path == '/authorize' and 'client_id' in url.query: - self.html_pages('authorize_page.html', context={'base_url': self.headers['Host']}) - return - elif url.path == '/blank.html': - self.html_pages('blank.html') - return - elif url.path == '/method/messages.getLongPollServer': - if url.query.get('access_token') == self.token: - self.json_response('getLongPollServer.success.json', context={'base_url': self.headers['Host']}) - return - else: - self.json_response('getLongPollServer.error.json') - return - elif url.path == '/im1774': - self.json_response('LongPoolWait.json') - return - elif url.path == '/method/users.get': - self.json_response('users.get.json') - return - elif url.path == '/method/users.get.error': - self.json_response('users.get.error.json') - return - else: - pass - - def do_POST(self): - url = URL(self.path) - if url.query.get('act', None) == 'login': - content_length = int(self.headers['Content-Length']) # <--- Gets the size of data - post_data = self.rfile.read(content_length) # <--- Gets the data itself - post_data = parse_qs(post_data.decode()) - if 'email' in post_data: - redirect_url = 'https://{}/blank.html#access_token={}'.format(self.headers['Host'], self.token) - self.redirect(redirect_url) - else: - self.html_pages('blank.html') - return - else: - pass - - -class BaseUnittestSession: - BASE_URL = '127.0.0.1:8000' - - @property - def AUTH_URL(self): - return 'https://' + self.BASE_URL + '/authorize' - - @property - def REQUEST_URL(self): - return 'https://' + self.BASE_URL + '/method/' - - -class TestTokenSession(BaseUnittestSession, TokenSession): - pass - - -class TestInternalAuthSession(BaseUnittestSession, ImplicitSession): - pass - - -def get_free_port(): - s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) - s.bind(('localhost', 0)) - address, port = s.getsockname() - s.close() - return port - - -class MockServerRequestHandler(BaseHTTPRequestHandler): - json_filepath = os.path.join(TEST_DIR, 'responses', "testdata.json") - - def log_message(self, format, *args): - # Disable logging - return - - def do_GET(self): - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - with open(self.json_filepath) as f: - self.wfile.write(f.read().encode()) - - def do_POST(self): - self.send_response(200) - self.end_headers() - self.wfile.write("OK".encode()) \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index d1153b1..9705cbb 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,286 +1,99 @@ -from aiohttp.test_utils import unittest_run_loop +import pytest from aiovk import API from aiovk.api import Request, LazyRequest, LazyAPI -from tests.utils import AioTestCase +from aiovk.sessions import BaseSession +pytestmark = pytest.mark.asyncio -class TestSession: - async def send_api_request(self, method_name, params=None, timeout=None): - out = {'method_name': method_name, - 'params': params, - 'timeout': timeout, - } - return out - - -class RequestTestCase(AioTestCase): - @unittest_run_loop - async def test_request_without_args(self): - api = API(TestSession()) - request = Request(api, 'test') - first = await request() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_args(self): - api = API(TestSession()) - request = Request(api, 'test') - first = await request(arg=1) - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_timeout(self): - api = API(TestSession()) - request = Request(api, 'test') - first = await request(timeout=1) - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - -class LazyRequestTestCase(AioTestCase): - @unittest_run_loop - async def test_request_without_args(self): - api = API(TestSession()) - request = LazyRequest(api, 'test') - message = request() - first = await message() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_args(self): - api = API(TestSession()) - request = LazyRequest(api, 'test') - message = request(arg=1) - first = await message() - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_timeout(self): - api = API(TestSession()) - request = LazyRequest(api, 'test') - message = request(timeout=1) - first = await message() - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - -class APITestCase(AioTestCase): - @unittest_run_loop - async def test_request_without_args(self): - api = API(TestSession()) - first = await api.test() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_args(self): - api = API(TestSession()) - first = await api.test(arg=1) - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_timeout(self): - api = API(TestSession()) - first = await api.test(timeout=1) - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_complex_request_without_args(self): - api = API(TestSession()) - first = await api.test1.test2() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) - @unittest_run_loop - async def test_complex_request_with_args(self): - api = API(TestSession()) - first = await api.test1.test2(arg=1) - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) +class TestSession(BaseSession): + async def __aenter__(self): + pass - @unittest_run_loop - async def test_complex_request(self): - api = API(TestSession()) - first = await api.test1.test2(timeout=1) - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_method_name(self): - api = API(TestSession()) - first = await api('test') - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_method_name_and_args(self): - api = API(TestSession()) - first = await api('test', arg=1) - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_method_name_and_timeout(self): - api = API(TestSession()) - first = await api('test', timeout=1) - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - -class LazyAPITestCase(AioTestCase): - @unittest_run_loop - async def test_request_without_args(self): - api = LazyAPI(TestSession()) - message = api.test() - first = await message() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_as_non_lazy_api(self): - api = LazyAPI(TestSession()) - with self.assertRaises(TypeError): - await api.test() - - @unittest_run_loop - async def test_request_with_args(self): - api = LazyAPI(TestSession()) - message = api.test(arg=1) - first = await message() - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_timeout(self): - api = LazyAPI(TestSession()) - message = api.test(timeout=1) - first = await message() - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_complex_request_without_args(self): - api = LazyAPI(TestSession()) - message = api.test1.test2() - first = await message() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_complex_request_with_args(self): - api = LazyAPI(TestSession()) - message = api.test1.test2(arg=1) - first = await message() - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_complex_request(self): - api = LazyAPI(TestSession()) - message = api.test1.test2(timeout=1) - first = await message() - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test1.test2' - } - self.assertDictEqual(first, second) - - @unittest_run_loop - async def test_request_with_method_name(self): - api = LazyAPI(TestSession()) - message = api('test') - first = await message() - second = {'params': {}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) + async def send_api_request(self, method_name, params=None, timeout=None, raw_response=None): + out = { + 'method_name': method_name, + 'params': params, + 'timeout': timeout, + } + return out - @unittest_run_loop - async def test_request_with_method_name_and_args(self): - api = LazyAPI(TestSession()) - message = api('test', arg=1) - first = await message() - second = {'params': {'arg': 1}, - 'timeout': None, - 'method_name': 'test' - } - self.assertDictEqual(first, second) - @unittest_run_loop - async def test_request_with_method_name_and_timeout(self): - api = LazyAPI(TestSession()) - message = api('test', timeout=1) - first = await message() - second = {'params': {}, - 'timeout': 1, - 'method_name': 'test' - } - self.assertDictEqual(first, second) +@pytest.mark.parametrize( + 'request_method', [ + 'test', + 'test.test', +]) +@pytest.mark.parametrize( + 'request_params, timeout', [ + ({}, None), + ({'arg': 1}, None), + ({'timeout': 1}, 1), + ({'arg': 1, 'timeout': 1}, 1), +]) +@pytest.mark.parametrize( + 'request_class, is_lazy', [ + (Request, False), + (LazyRequest, True), +]) +@pytest.mark.parametrize( + 'api', [ + API(TestSession()), + LazyAPI(TestSession()), + ] +) +async def test_request(api, request_class, request_method, request_params, timeout, is_lazy): + request = request_class(api, request_method) + if is_lazy: + lazy_obj = request(**request_params) + response = await lazy_obj() + else: + response = await request(**request_params) + + params = request_params.copy() + params.pop('timeout', None) + expected = { + 'params': params, + 'timeout': timeout, + 'method_name': request_method + } + assert response == expected + + +@pytest.mark.parametrize( + 'request_params, timeout', [ + ({}, None), + ({'arg': 1}, None), + ({'timeout': 1}, 1), + ({'arg': 1, 'timeout': 1}, 1), +]) +@pytest.mark.parametrize( + 'method, method_name', [ + (lambda x, **kwargs: x.test(**kwargs), 'test'), + (lambda x, **kwargs: x.test1.test2(**kwargs), 'test1.test2'), + (lambda x, **kwargs: x('test', **kwargs), 'test'), + (lambda x, **kwargs: x('test1.test2', **kwargs), 'test1.test2'), + ] +) +@pytest.mark.parametrize( + 'api, is_lazy', [ + (LazyAPI(TestSession()), True), + (API(TestSession()), False), + ] +) +async def test_api(api, method, request_params, timeout, method_name, is_lazy): + if is_lazy: + lazy_obj = method(api, **request_params) + response = await lazy_obj() + else: + response = await method(api, **request_params) + + params = request_params.copy() + params.pop('timeout', None) + expected = { + 'params': params, + 'timeout': timeout, + 'method_name': method_name + } + assert response == expected diff --git a/tests/unit/test_drivers.py b/tests/unit/test_drivers.py deleted file mode 100644 index b558299..0000000 --- a/tests/unit/test_drivers.py +++ /dev/null @@ -1,45 +0,0 @@ -import math -import time - -from aiohttp.test_utils import unittest_run_loop - -from aiovk.drivers import BaseDriver -from aiovk.mixins import LimitRateDriverMixin -from tests.utils import AioTestCase - - -class LimitRateBaseTestDriver(BaseDriver): - async def json(self, *args, **kwargs): - return time.time() - - async def close(self): - pass - - -class LimitRateTestDriver(LimitRateDriverMixin, LimitRateBaseTestDriver): - period = 1 - requests_per_period = 1 - - -class LimitRateDriverMixinTestCase(AioTestCase): - period = 1 - requests_per_period = 1 - - def get_driver(self): - return LimitRateTestDriver() - - @unittest_run_loop - async def test_json_fast(self): - driver = self.get_driver() - t0 = time.time() - t1 = await driver.json() - self.assertEqual(math.floor(t1 - t0), 0) - await driver.close() - - @unittest_run_loop - async def test_json_slow(self): - driver = self.get_driver() - t1 = await driver.json() - t2 = await driver.json() - self.assertEqual(math.floor(t2 - t1), self.period) - await driver.close() diff --git a/tests/unit/test_longpool.py b/tests/unit/test_longpool.py index a920625..a7a68ba 100644 --- a/tests/unit/test_longpool.py +++ b/tests/unit/test_longpool.py @@ -1,118 +1,177 @@ import json -from aiohttp.test_utils import unittest_run_loop +import pytest from aiovk import LongPoll, API from aiovk.drivers import BaseDriver from aiovk.exceptions import VkLongPollError -from tests.utils import AioTestCase +from aiovk.longpoll import BotsLongPoll +from aiovk.sessions import BaseSession +pytestmark = pytest.mark.asyncio -class TestDriver(BaseDriver): + +class Driver(BaseDriver): counter = 0 - message = {} + message = None + original_event = {"type": "group_join", "object": {"user_id": 1, "join_type": "approved"}, "group_id": 1} + expected_version = None + expected_ts = None + expected_mode = None async def get_text(self, url, params, timeout=None): - if self.counter > 5: - self.counter = 0 - return 200, json.dumps({'ts': -TestSession.TS}) + message = json.dumps(self.messages[self.counter]) self.counter += 1 - return 200, json.dumps(self.message) + if self.expected_mode is not None: + assert params['mode'] == self.expected_mode + + if self.expected_version is not None: + assert params['version'] == self.expected_version + + if self.expected_ts is not None: + assert params['ts'] == self.expected_ts + + return 200, message, url + +class Session(BaseSession): + async def __aenter__(self): + pass -class TestSession: timeout = 10 TS = 1 PTS = 2 KEY = 'key' - SERVER = 'localhost' + SERVER = 'superhost' - driver = TestDriver() + def __init__(self): + self.driver = Driver() - async def send_api_request(self, method_name: str, params: dict = None, timeout: int = None) -> dict: + async def send_api_request(self, *args, **kwargs) -> dict: return {"key": self.KEY, "server": self.SERVER, "ts": self.TS, "pts": self.PTS} -class LongPollTestCase(AioTestCase): - @unittest_run_loop - async def test_init_with_session(self): - session = TestSession() - lp = LongPoll(session, mode=0) - await lp._get_long_poll_server() - - @unittest_run_loop - async def test_init_with_api(self): - session = API(TestSession()) - lp = LongPoll(session, mode=0) - await lp._get_long_poll_server() - - @unittest_run_loop - async def test_get_pts_first_call(self): - session = API(TestSession()) - lp = LongPoll(session, mode=0) - pts = await lp.get_pts() - self.assertIsInstance(pts, type(TestSession.PTS)) - self.assertEqual(pts, TestSession.PTS) - - @unittest_run_loop - async def test_get_pts_cached_value(self): - session = API(TestSession()) - lp = LongPoll(session, mode=0) - await lp.get_pts() - pts = await lp.get_pts() - self.assertIsInstance(pts, type(TestSession.PTS)) - self.assertEqual(pts, TestSession.PTS) - - @unittest_run_loop - async def test_get_pts_need_ts(self): - session = API(TestSession()) - lp = LongPoll(session, mode=0) - result = await lp.get_pts(need_ts=True) - self.assertEqual(len(result), 2) - self.assertEqual(result[0], TestSession.PTS) - self.assertEqual(result[1], TestSession.TS) - - @unittest_run_loop - async def test_wait_valid(self): - TestDriver.message = {'ts': -TestSession.TS} - session = API(TestSession()) - lp = LongPoll(session, mode=0) - - response = await lp.wait() - self.assertDictEqual(response, TestDriver.message) - - @unittest_run_loop - async def test_wait_error_code_1(self): - TestDriver.message = {'failed': 1, 'ts': 42} - session = API(TestSession()) - lp = LongPoll(session, mode=0) - +@pytest.mark.parametrize( + 'session', + [ + Session(), + API(Session()) + ] +) +async def test_longpoll_get_pts(session): + lp = LongPoll(session, mode=0) + pts = await lp.get_pts() + assert pts == Session.PTS + + +@pytest.mark.parametrize( + 'session', + [ + Session(), + API(Session()) + ] +) +async def test_longpoll_get_cached_pts(session): + lp = LongPoll(session, mode=0) + await lp.get_pts() + pts = await lp.get_pts() + assert pts == Session.PTS + + +@pytest.mark.parametrize( + 'session', + [ + Session(), + API(Session()) + ] +) +async def test_longpoll_get_pts_need_ts(session): + lp = LongPoll(session, mode=0) + result = await lp.get_pts(need_ts=True) + assert len(result) == 2 + assert result[0] == Session.PTS + assert result[1] == Session.TS + + +@pytest.mark.parametrize( + 'messages, exception, ts', + [ + ([{'ts': 23}], None, 23), + ([{'failed': 1, 'ts': 42}, {'ts': 45, 'updates': [Driver.original_event]}], None, 45), + ([{'failed': 2}, {'ts': 63, 'updates': [Driver.original_event]}], None, 63), + ([{'failed': 3}, {'ts': 56, 'updates': [Driver.original_event]}], None, 56), + ([{'failed': 4}, {'ts': 11, 'updates': [Driver.original_event]}], VkLongPollError, None), + ] +) +@pytest.mark.parametrize( + 'longpoll', + [ + lambda session: LongPoll(session, mode=0), + lambda session: LongPoll(session, mode=1), + lambda session: LongPoll(session, mode=1, version=1), + lambda session: BotsLongPoll(session, group_id=1), + lambda session: BotsLongPoll(session, group_id=1, version=3), + ] +) +async def test_longpoll_wait(longpoll, messages, exception, ts): + session = Session() + session.driver.messages = messages + session = API(session) + lp = longpoll(session) + session._session.driver.expected_mode = lp.base_params.get('mode') + session._session.driver.expected_version = lp.base_params.get('version') + + + if exception is None: response = await lp.wait() - self.assertDictEqual(response, {'ts': -TestSession.TS}) - - @unittest_run_loop - async def test_wait_error_code_2(self): - TestDriver.message = {'failed': 2} - session = API(TestSession()) - lp = LongPoll(session, mode=0) - - response = await lp.wait() - self.assertDictEqual(response, {'ts': -TestSession.TS}) - - @unittest_run_loop - async def test_wait_error_code_3(self): - TestDriver.message = {'failed': 3} - session = API(TestSession()) - lp = LongPoll(session, mode=0) - - response = await lp.wait() - self.assertDictEqual(response, {'ts': -TestSession.TS}) + assert response == messages[-1] + _, real_ts = await lp.get_pts(need_ts=True) + assert real_ts == ts + else: + with pytest.raises(exception): + await lp.wait() - @unittest_run_loop - async def test_wait_error_code_4(self): - TestDriver.message = {'failed': 4} - session = API(TestSession()) - lp = LongPoll(session, mode=0) - with self.assertRaises(VkLongPollError): - await lp.wait() +@pytest.mark.parametrize( + 'messages, exception, ts', + [ + ([{'failed': 1, 'ts': 42}, {'ts': 45, 'updates': [Driver.original_event]}], None, 45), + ([{'failed': 2}, {'ts': 63, 'updates': [Driver.original_event]}], None, 63), + ([{'failed': 3}, {'ts': 56, 'updates': [Driver.original_event]}], None, 56), + ([{'failed': 4}, {'ts': 11, 'updates': [Driver.original_event]}], VkLongPollError, None), + ([{'failed': 3}, {'ts': 56, 'updates': [Driver.original_event]}, + {'ts': 58, 'updates': [Driver.original_event]}], None, 58), + ] +) +@pytest.mark.parametrize( + 'longpoll', + [ + lambda session: LongPoll(session, mode=0), + lambda session: LongPoll(session, mode=1), + lambda session: LongPoll(session, mode=1, version=1), + lambda session: BotsLongPoll(session, group_id=1), + lambda session: BotsLongPoll(session, group_id=1, version=3), + ] +) +async def test_longpoll_iter(longpoll, messages, exception, ts): + session = Session() + session.driver.messages = messages + session = API(session) + lp = longpoll(session) + session._session.driver.expected_mode = lp.base_params.get('mode') + session._session.driver.expected_version = lp.base_params.get('version') + + if exception is None: + num_of_iterations = len([m for m in messages if 'updates' in m]) + i = 0 + async for event in lp.iter(): + assert event == messages[-1]['updates'][0] + i += 1 + if i >= num_of_iterations: + break + _, real_ts = await lp.get_pts(need_ts=True) + assert real_ts == ts + else: + with pytest.raises(exception): + async for _ in lp.iter(): + pass diff --git a/tests/unit/test_pools.py b/tests/unit/test_pools.py new file mode 100644 index 0000000..98105d7 --- /dev/null +++ b/tests/unit/test_pools.py @@ -0,0 +1,194 @@ +import os +from unittest import IsolatedAsyncioTestCase + +# from dotenv import load_dotenv + +from aiovk.pools import AsyncResult, AsyncVkExecuteRequestPool + +# load_dotenv( +# os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".env") +# ) +token1 = os.getenv('TEST_TOKEN_1') +token2 = os.getenv('TEST_TOKEN_2') + + +# TODO need refactoring +class ExecutePoolTestCase(IsolatedAsyncioTestCase): + async def test_one_call_per_request(self): + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call('users.get', token1, {'user_ids': 1}) + self.assertIsInstance(result, AsyncResult) + self.assertIsNotNone(result.result) + self.assertEqual(1, result.result[0]['id']) + + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call('users.get', token1, {'user_ids': 1}) + self.assertIsInstance(result, AsyncResult) + result2 = pool.add_call('users.get', token2, {'user_ids': 2}) + self.assertIsInstance(result2, AsyncResult) + + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(1, result.result[0]['id']) + self.assertTrue(result2.ok) + self.assertIsNotNone(result2.result) + self.assertEqual(2, result2.result[0]['id']) + + async def test_less_or_equal_than_25_calls_per_token(self): + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 2): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 25): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 26): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i in range(26, 51): + result = pool.add_call('users.get', token2, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + async def test_greater_than_25_calls_per_token(self): + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 26): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 50): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + users = [] + async with AsyncVkExecuteRequestPool() as pool: + for i in range(1, 51): + result = pool.add_call('users.get', token1, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i in range(51, 99): + result = pool.add_call('users.get', token2, {'user_ids': i}) + users.append(result) + self.assertIsInstance(result, AsyncResult) + + for i, result in enumerate(users, start=1): + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(i, result.result[0]['id']) + + async def test_error_requests(self): + async with AsyncVkExecuteRequestPool() as pool: + error_result = pool.add_call('users.get', token1, {'user_ids': -1}) + self.assertIsInstance(error_result, AsyncResult) + + self.assertFalse(error_result.ok) + self.assertIsNone(error_result.result) + self.assertIsNotNone(error_result.error) + self.assertDictEqual({ + 'method': 'users.get', 'error_code': 113, 'error_msg': 'Invalid user id' + }, error_result.error) + + async with AsyncVkExecuteRequestPool() as pool: + error_result = pool.add_call('users.get', token1, {'user_ids': -1}) + success_result = pool.add_call('users.get', token2, {'user_ids': 1}) + + self.assertFalse(error_result.ok) + self.assertIsNone(error_result.result) + self.assertIsNotNone(error_result.error) + self.assertDictEqual({ + 'method': 'users.get', 'error_code': 113, 'error_msg': 'Invalid user id' + }, error_result.error) + + self.assertTrue(success_result.ok) + self.assertIsNotNone(success_result.result) + self.assertEqual(1, success_result.result[0]['id']) + + async def test_request_without_values(self): + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call('users.get', token1) + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + + async def test_false_cast_response(self): + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call("groups.isMember", token1, {"user_id": 1, "group_id": 1}) + self.assertTrue(result.ok) + self.assertIsNotNone(result.result) + self.assertEqual(0, result.result) + + async def test_equal_requests(self): + """Тестирование того, что одинаковые запросы для одного токена будут выполняться только один раз""" + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call("groups.isMember", token1, {"user_id": 1, "group_id": 1}) + result2 = pool.add_call("groups.isMember", token1, {"user_id": 1, "group_id": 1}) + result3 = pool.add_call("groups.isMember", token1, {"user_id": 1, "group_id": 1}) + self.assertEqual(1, len(pool.pool[token1])) + self.assertIs(result, result2) + self.assertIs(result, result3) + + async def test_invalid_token(self): + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call("groups.isMember", 'invalid_token', {"user_id": 1, "group_id": 1}) + self.assertEqual(5, result.error["error_code"]) + self.assertEqual("groups.isMember", result.error["method"]) + + async def test_invalid_call(self): + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call("groups.isMember", token1, {"user_id": -1, "group_id": 1}) + self.assertEqual(100, result.error['error_code']) + + async def test_invalid_token_type(self): + """Вызов метода, который доступен только с токеном пользователя, с токеном группы""" + async with AsyncVkExecuteRequestPool() as pool: + result = pool.add_call("likes.isLiked", token1, { + "user_id": 1, + "owner_id": -1, + "type": "post", + "item_id": 396449, + }) + self.assertIsNone(result.result) + self.assertIsNotNone(result.error) + self.assertEqual(27, result.error['error_code']) + self.assertEqual('likes.isLiked', result.error['method']) diff --git a/tests/unit/test_shaping.py b/tests/unit/test_shaping.py new file mode 100644 index 0000000..3de98e1 --- /dev/null +++ b/tests/unit/test_shaping.py @@ -0,0 +1,39 @@ +import asyncio +import time + +import pytest + +from aiovk.drivers import BaseDriver +from aiovk.mixins import LimitRateDriverMixin + +pytestmark = pytest.mark.asyncio + + +class LimitRateBaseTestDriver(BaseDriver): + async def post_json(self, *args, **kwargs): + return 200, time.time() + + async def close(self): + pass + + +class LimitRateTestDriver(LimitRateDriverMixin, LimitRateBaseTestDriver): + pass + + +@pytest.mark.parametrize( + 'period, requests_per_period, rps, lower, upper', [ + (1, 1, 1, 0, 0.01), + (1, 1, 2, 1, 1.01), + (1, 1, 2, 1, 1.01), + (2, 2, 2, 0, 0.01), + (2, 1, 2, 0, 2.01), + ] +) +async def test_request_shaper_mixin(period, requests_per_period, rps, lower, upper): + driver = LimitRateTestDriver(period=period, requests_per_period=requests_per_period) + t0 = time.time() + data = await asyncio.gather(*(driver.post_json() for _ in range(rps))) + await driver.close() + max_time = max(v[1] for v in data) + assert lower < max_time - t0 < upper diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py deleted file mode 100644 index 28e1368..0000000 --- a/tests/unit/test_utils.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -from unittest.mock import Mock - -import time -import math - -from aiohttp.test_utils import unittest_run_loop - -from aiovk.utils import TaskQueue, wait_free_slot -from tests.utils import AioTestCase - - -class TaskQueueTestCase(AioTestCase): - @unittest_run_loop - async def test_long_init(self): - size = 1 - period = 1 - q = TaskQueue(size, period) - self.assertEqual(q.qsize(), size) - await asyncio.sleep(period + 1) - self.assertEqual(q.qsize(), size) - q.canel() - - @unittest_run_loop - async def test_get_elem_and_pause(self): - size = 1 - period = 1 - q = TaskQueue(size, period) - self.assertEqual(q.qsize(), size) - await q.get() - self.assertEqual(q.qsize(), size - 1) - await asyncio.sleep(period + 1) - self.assertEqual(q.qsize(), size) - q.canel() - - @unittest_run_loop - async def test_get_all_plus_one_elem(self): - size = 1 - period = 1 - q = TaskQueue(size, period) - self.assertEqual(q.qsize(), size) - await q.get() - - start = time.time() - await q.get() - stop = time.time() - - self.assertEqual(math.floor(stop - start), period) - self.assertEqual(q.qsize(), 0) - q.canel() - - -class WaitFreeSlotTestCase(AioTestCase): - @unittest_run_loop - async def test_simple_usage(self): - @wait_free_slot - async def foo(a): - pass - - size = 1 - period = 1 - q = TaskQueue(size, period) - obj = Mock() - obj._queue = q - - await foo(obj) - self.assertEqual(q.qsize(), size - 1) - q.canel() - - @unittest_run_loop - async def test_with_pause(self): - @wait_free_slot - async def foo(a): - pass - - size = 1 - period = 1 - q = TaskQueue(size, period) - obj = Mock() - obj._queue = q - await q.get() - - start = time.time() - await foo(obj) - stop = time.time() - - self.assertEqual(math.floor(stop - start), period) - self.assertEqual(q.qsize(), 0) - q.canel()