New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reimplement checkrealtime as part of smokey #8
Merged
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
Jump to file or symbol
Failed to load files and symbols.
Diff settings
| @@ -5,15 +5,17 @@ | ||
| from behave.model import ScenarioOutline | ||
| logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', | ||
| datefmt='%Y-%m-%d %H:%M:%S', | ||
| level=logging.INFO) | ||
| log = logging.getLogger(__name__) | ||
| # Configuration default values | ||
| CONFIG_DEFAULTS = { | ||
| 'api_root': 'https://hypothes.is/api', | ||
| 'proxy_root': 'https://via.hypothes.is', | ||
| 'unsafe_disable_ssl_verification': False, | ||
| 'websocket_endpoint': 'wss://hypothes.is/ws', | ||
| } | ||
| # Configuration configurable from the environment | ||
| @@ -22,9 +24,20 @@ | ||
| 'proxy_root', | ||
| 'sauce_access_key', | ||
| 'sauce_username', | ||
| 'websocket_endpoint', | ||
| ] | ||
| # A list of test users that scenarios can use | ||
| TEST_USERS = [ | ||
| 'smokey' | ||
| ] | ||
| class UnsafeHTTPAdapter(requests.adapters.HTTPAdapter): | ||
| def cert_verify(self, conn, url, verify, cert): | ||
| return | ||
| def before_all(context): | ||
| # Load config from system environment | ||
| for k in CONFIG_ENV: | ||
| @@ -36,10 +49,27 @@ def before_all(context): | ||
| for k, v in CONFIG_DEFAULTS.items(): | ||
| context.config.userdata.setdefault(k, v) | ||
| # Load test user keys from environment | ||
| context.test_users = {} | ||
| for user in TEST_USERS: | ||
| token_key = 'TEST_USER_{user}_KEY'.format(user=user.upper()) | ||
| userid_key = 'TEST_USER_{user}_USERID'.format(user=user.upper()) | ||
Show comment
Hide comment
robertknight
Contributor
|
||
| if token_key in os.environ and userid_key in os.environ: | ||
| token = os.environ[token_key] | ||
| userid = os.environ[userid_key] | ||
| if not userid.startswith('acct:') or '@' not in userid: | ||
| log.warn('{var} should contain a full userid of the form ' | ||
| 'acct:<username>@<domain>'.format(var=userid_key)) | ||
| context.test_users[user] = {'token': token, 'userid': userid} | ||
| # Set up an HTTP client session with some reasonable defaults (including | ||
| # retrying requests that fail). | ||
| session = requests.Session() | ||
| adapter = requests.adapters.HTTPAdapter(max_retries=3) | ||
| if context.config.userdata['unsafe_disable_ssl_verification']: | ||
| adapter = UnsafeHTTPAdapter(max_retries=3) | ||
| session.mount('http://', adapter) | ||
| session.mount('https://', adapter) | ||
| @@ -91,11 +121,13 @@ def before_scenario(context, scenario): | ||
| if 'sauce' in scenario.tags and not _check_sauce_config(context): | ||
| scenario.skip("Sauce config not provided") | ||
| # Allow scenarios to register teardown tasks | ||
| context.teardown = [] | ||
| def after_scenario(context, scenario): | ||
| # Shut down any webdriver instances that were started by the scenario. | ||
| if hasattr(context, 'browser'): | ||
| context.browser.close() | ||
| for teardown_task in context.teardown: | ||
| teardown_task() | ||
| def _check_sauce_config(context): | ||
| @@ -0,0 +1,105 @@ | ||
| import asyncio | ||
| import logging | ||
| import json | ||
| import ssl | ||
| import uuid | ||
| from behave import * | ||
| import websockets | ||
| log = logging.getLogger(__name__) | ||
| def wait_for(timeout, func, *args, **kwargs): | ||
| """Block waiting for a a coroutine call to complete, with a timeout.""" | ||
| loop = asyncio.get_event_loop() | ||
| task = asyncio.ensure_future(func(*args, **kwargs)) | ||
| try: | ||
| loop.run_until_complete(asyncio.wait_for(task, timeout=timeout)) | ||
| except asyncio.TimeoutError as e: | ||
| task.cancel() | ||
| raise | ||
| async def connect(context): | ||
| """Establish a websocket connection and send a client_id message.""" | ||
| endpoint = context.config.userdata['websocket_endpoint'] | ||
| verify = True | ||
| if context.config.userdata['unsafe_disable_ssl_verification']: | ||
| verify = False | ||
| ssl_context = _ssl_context(verify=verify) | ||
| context.websocket = await websockets.connect(endpoint, ssl=ssl_context) | ||
| context.teardown.append(lambda: wait_for(10.0, context.websocket.close)) | ||
| async def send(websocket, message): | ||
| """JSON-encode and send a message over the websocket.""" | ||
| await websocket.send(json.dumps(message)) | ||
| async def await_annotation(websocket, id): | ||
| """Wait to see a notification about annotation with the given `id`""" | ||
| while True: | ||
| msg = await websocket.recv() | ||
| try: | ||
| data = json.loads(msg) | ||
| except ValueError: | ||
| log.warn('received non-JSON message: {!r}'.format(msg)) | ||
| continue | ||
| if data.get('type') != 'annotation-notification': | ||
| continue | ||
| if data.get('options') != {'action': 'create'}: | ||
| continue | ||
| if 'payload' not in data: | ||
| log.warn('saw annotation-notification lacking payload: {!r}'.format(msg)) | ||
| continue | ||
| if not isinstance(data['payload'], list): | ||
| log.warn('saw annotation-notification with bad payload format: {!r}'.format(msg)) | ||
| continue | ||
| for annotation in data['payload']: | ||
| if annotation.get('id') == id: | ||
| return | ||
| @given('I am connected to the websocket') | ||
| def connect_websocket(context): | ||
| wait_for(10.0, connect, context) | ||
| wait_for(2.0, send, context.websocket, { | ||
| 'messageType': 'client_id', | ||
| 'value': str(uuid.uuid4()), | ||
| }) | ||
| @given('I request to be notified of all annotation events') | ||
| def request_notification_all(context): | ||
| wait_for(2.0, send, context.websocket, { | ||
| 'filter': { | ||
| 'match_policy': 'include_all', | ||
| 'clauses': [], | ||
| 'actions': {'create': True, 'update': True, 'delete': True}, | ||
| } | ||
| }) | ||
| @then('I should receive notification of my test annotation on the websocket') | ||
| def wait_for_notification(context): | ||
| try: | ||
| getattr(context, 'last_test_annotation') | ||
| except AttributeError: | ||
| raise RuntimeError("you must create a test annotation first!") | ||
| id = context.last_test_annotation['id'] | ||
| wait_for(5.0, await_annotation, context.websocket, id) | ||
| def _ssl_context(verify=True): | ||
| ssl_context = ssl.create_default_context() | ||
| if not verify: | ||
| ssl_context.check_hostname = False | ||
| ssl_context.verify_mode = ssl.CERT_NONE | ||
| return ssl_context |
| @@ -0,0 +1,7 @@ | ||
| Feature: Real-time websocket streaming | ||
| Scenario: Receiving recently-created annotations over the websocket | ||
| Given I am acting as the test user "smokey" | ||
| And I am connected to the websocket | ||
| And I request to be notified of all annotation events | ||
| When I create a test annotation | ||
| Then I should receive notification of my test annotation on the websocket |
| @@ -1,3 +1,4 @@ | ||
| behave | ||
| requests | ||
| selenium | ||
| websockets |
ProTip!
Use n and p to navigate between commits in a pull request.
Based on my own screwups in testing this, it might be a good idea to validate the format of the user ID here.