diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index 1724f7a5..33e96351 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -38,8 +38,7 @@ def db(): @fixture(scope='module') def io_loop(request): - """Same as pytest-tornado.gen""" - print("my io_loop fixture") + """Same as pytest-tornado.io_loop, but re-scoped to module-level""" io_loop = ioloop.IOLoop() io_loop.make_current() @@ -78,7 +77,7 @@ class MockServiceSpawner(jupyterhub.services.service._ServiceSpawner): _mock_service_counter = 0 -def _mockservice(request, app, io_loop, url=False): +def _mockservice(request, app, url=False): global _mock_service_counter _mock_service_counter += 1 name = 'mock-service-%i' % _mock_service_counter @@ -90,6 +89,8 @@ def _mockservice(request, app, io_loop, url=False): if url: spec['url'] = 'http://127.0.0.1:%i' % random_port() + io_loop = app.io_loop + with mock.patch.object(jupyterhub.services.service, '_ServiceSpawner', MockServiceSpawner): app.services = [spec] app.init_services() @@ -100,20 +101,17 @@ def _mockservice(request, app, io_loop, url=False): # wait for proxy to be updated before starting the service yield app.proxy.add_all_services(app._service_map) service.start() - app.io_loop.add_callback(start) + io_loop.run_sync(start) def cleanup(): service.stop() app.services[:] = [] app._service_map.clear() request.addfinalizer(cleanup) - for i in range(20): - if not getattr(service, 'proc', False): - time.sleep(0.2) # ensure process finishes starting with raises(TimeoutExpired): service.proc.wait(1) if url: - ioloop.IOLoop().run_sync(service.server.wait_up) + io_loop.run_sync(service.server.wait_up) return service diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 8b773890..11d6472c 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -22,6 +22,7 @@ from ..objects import Server from ..spawner import LocalProcessSpawner from ..singleuser import SingleUserNotebookApp from ..utils import random_port, url_path_join +from .utils import async_requests from pamela import PAMError @@ -178,10 +179,11 @@ class MockHub(JupyterHub): self.cleanup = lambda : None self.db_file.close() + @gen.coroutine def login_user(self, name): """Login a user by name, returning her cookies.""" base_url = public_url(self) - r = requests.post(base_url + 'hub/login', + r = yield async_requests.post(base_url + 'hub/login', data={ 'username': name, 'password': name, diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index a0dfc720..a3e3e843 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -2,12 +2,11 @@ import json import time -from queue import Queue import sys from unittest import mock from urllib.parse import urlparse, quote -from pytest import mark, yield_fixture +from pytest import mark import requests from tornado import gen @@ -18,6 +17,7 @@ from ..user import User from ..utils import url_path_join as ujoin from . import mocking from .mocking import public_host, public_url +from .utils import async_requests def check_db_locks(func): @@ -81,6 +81,7 @@ def auth_header(db, name): @check_db_locks +@gen.coroutine def api_request(app, *api_path, **kwargs): """Make an API request""" base_url = app.hub.url @@ -91,8 +92,8 @@ def api_request(app, *api_path, **kwargs): url = ujoin(base_url, 'api', *api_path) method = kwargs.pop('method', 'get') - f = getattr(requests, method) - resp = f(url, **kwargs) + f = getattr(async_requests, method) + resp = yield f(url, **kwargs) assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy'] assert ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy'] assert 'http' not in resp.headers['Content-Security-Policy'] @@ -104,9 +105,10 @@ def api_request(app, *api_path, **kwargs): # -------------------- +@mark.gen_test def test_auth_api(app): db = app.db - r = api_request(app, 'authorizations', 'gobbledygook') + r = yield api_request(app, 'authorizations', 'gobbledygook') assert r.status_code == 404 # make a new cookie token @@ -114,36 +116,37 @@ def test_auth_api(app): api_token = user.new_api_token() # check success: - r = api_request(app, 'authorizations/token', api_token) + r = yield api_request(app, 'authorizations/token', api_token) assert r.status_code == 200 reply = r.json() assert reply['name'] == user.name # check fail - r = api_request(app, 'authorizations/token', api_token, + r = yield api_request(app, 'authorizations/token', api_token, headers={'Authorization': 'no sir'}, ) assert r.status_code == 403 - r = api_request(app, 'authorizations/token', api_token, + r = yield api_request(app, 'authorizations/token', api_token, headers={'Authorization': 'token: %s' % user.cookie_id}, ) assert r.status_code == 403 +@mark.gen_test def test_referer_check(app, io_loop): url = ujoin(public_host(app), app.hub.base_url) host = urlparse(url).netloc user = find_user(app.db, 'admin') if user is None: user = add_user(app.db, name='admin', admin=True) - cookies = app.login_user('admin') - app_user = get_app_user(app, 'admin') + cookies = yield app.login_user('admin') + app_user = app.users[user] # stop the admin's server so we don't mess up future tests - io_loop.run_sync(lambda: app.proxy.delete_user(app_user)) - io_loop.run_sync(app_user.stop) + yield app.proxy.delete_user(app_user) + yield app_user.stop() - r = api_request(app, 'users', + r = yield api_request(app, 'users', headers={ 'Authorization': '', 'Referer': 'null', @@ -151,7 +154,7 @@ def test_referer_check(app, io_loop): ) assert r.status_code == 403 - r = api_request(app, 'users', + r = yield api_request(app, 'users', headers={ 'Authorization': '', 'Referer': 'http://attack.com/csrf/vulnerability', @@ -159,7 +162,7 @@ def test_referer_check(app, io_loop): ) assert r.status_code == 403 - r = api_request(app, 'users', + r = yield api_request(app, 'users', headers={ 'Authorization': '', 'Referer': url, @@ -168,7 +171,7 @@ def test_referer_check(app, io_loop): ) assert r.status_code == 200 - r = api_request(app, 'users', + r = yield api_request(app, 'users', headers={ 'Authorization': '', 'Referer': ujoin(url, 'foo/bar/baz/bat'), @@ -184,9 +187,10 @@ def test_referer_check(app, io_loop): @mark.user +@mark.gen_test def test_get_users(app): db = app.db - r = api_request(app, 'users') + r = yield api_request(app, 'users') assert r.status_code == 200 users = sorted(r.json(), key=lambda d: d['name']) @@ -211,17 +215,18 @@ def test_get_users(app): } ] - r = api_request(app, 'users', + r = yield api_request(app, 'users', headers=auth_header(db, 'user'), ) assert r.status_code == 403 @mark.user +@mark.gen_test def test_add_user(app): db = app.db name = 'newuser' - r = api_request(app, 'users', name, method='post') + r = yield api_request(app, 'users', name, method='post') assert r.status_code == 201 user = find_user(db, name) assert user is not None @@ -230,9 +235,10 @@ def test_add_user(app): @mark.user +@mark.gen_test def test_get_user(app): name = 'user' - r = api_request(app, 'users', name) + r = yield api_request(app, 'users', name) assert r.status_code == 200 user = r.json() user.pop('last_activity') @@ -247,19 +253,21 @@ def test_get_user(app): @mark.user +@mark.gen_test def test_add_multi_user_bad(app): - r = api_request(app, 'users', method='post') + r = yield api_request(app, 'users', method='post') assert r.status_code == 400 - r = api_request(app, 'users', method='post', data='{}') + r = yield api_request(app, 'users', method='post', data='{}') assert r.status_code == 400 - r = api_request(app, 'users', method='post', data='[]') + r = yield api_request(app, 'users', method='post', data='[]') assert r.status_code == 400 @mark.user +@mark.gen_test def test_add_multi_user_invalid(app): app.authenticator.username_pattern = r'w.*' - r = api_request(app, 'users', method='post', + r = yield api_request(app, 'users', method='post', data=json.dumps({'usernames': ['Willow', 'Andrew', 'Tara']}) ) app.authenticator.username_pattern = '' @@ -268,10 +276,11 @@ def test_add_multi_user_invalid(app): @mark.user +@mark.gen_test def test_add_multi_user(app): db = app.db names = ['a', 'b'] - r = api_request(app, 'users', method='post', + r = yield api_request(app, 'users', method='post', data=json.dumps({'usernames': names}), ) assert r.status_code == 201 @@ -286,7 +295,7 @@ def test_add_multi_user(app): assert not user.admin # try to create the same users again - r = api_request(app, 'users', method='post', + r = yield api_request(app, 'users', method='post', data=json.dumps({'usernames': names}), ) assert r.status_code == 400 @@ -294,7 +303,7 @@ def test_add_multi_user(app): names = ['a', 'b', 'ab'] # try to create the same users again - r = api_request(app, 'users', method='post', + r = yield api_request(app, 'users', method='post', data=json.dumps({'usernames': names}), ) assert r.status_code == 201 @@ -304,10 +313,11 @@ def test_add_multi_user(app): @mark.user +@mark.gen_test def test_add_multi_user_admin(app): db = app.db names = ['c', 'd'] - r = api_request(app, 'users', method='post', + r = yield api_request(app, 'users', method='post', data=json.dumps({'usernames': names, 'admin': True}), ) assert r.status_code == 201 @@ -323,20 +333,22 @@ def test_add_multi_user_admin(app): @mark.user +@mark.gen_test def test_add_user_bad(app): db = app.db name = 'dne_newuser' - r = api_request(app, 'users', name, method='post') + r = yield api_request(app, 'users', name, method='post') assert r.status_code == 400 user = find_user(db, name) assert user is None @mark.user +@mark.gen_test def test_add_admin(app): db = app.db name = 'newadmin' - r = api_request(app, 'users', name, method='post', + r = yield api_request(app, 'users', name, method='post', data=json.dumps({'admin': True}), ) assert r.status_code == 201 @@ -347,25 +359,27 @@ def test_add_admin(app): @mark.user +@mark.gen_test def test_delete_user(app): db = app.db mal = add_user(db, name='mal') - r = api_request(app, 'users', 'mal', method='delete') + r = yield api_request(app, 'users', 'mal', method='delete') assert r.status_code == 204 @mark.user +@mark.gen_test def test_make_admin(app): db = app.db name = 'admin2' - r = api_request(app, 'users', name, method='post') + r = yield api_request(app, 'users', name, method='post') assert r.status_code == 201 user = find_user(db, name) assert user is not None assert user.name == name assert not user.admin - r = api_request(app, 'users', name, method='patch', + r = yield api_request(app, 'users', name, method='patch', data=json.dumps({'admin': True}) ) assert r.status_code == 200 @@ -375,23 +389,7 @@ def test_make_admin(app): assert user.admin -def get_app_user(app, name): - """Helper to get the User object from the main thread. - - Needed for access to the Spawner during testing. - - No ORM methods should be called on the result. - """ - q = Queue() - - def get_user_id(): - user = find_user(app.db, name) - q.put(user.id) - app.io_loop.add_callback(get_user_id) - user_id = q.get(timeout=2) - return app.users[user_id] - - +@mark.gen_test def test_spawn(app, io_loop): db = app.db name = 'wash' @@ -401,41 +399,41 @@ def test_spawn(app, io_loop): 'i': 5, } before_servers = sorted(db.query(orm.Server), key=lambda s: s.url) - r = api_request(app, 'users', name, 'server', method='post', + r = yield api_request(app, 'users', name, 'server', method='post', data=json.dumps(options), ) assert r.status_code == 201 assert 'pid' in user.orm_spawners[''].state - app_user = get_app_user(app, name) + app_user = app.users[name] assert app_user.spawner is not None spawner = app_user.spawner assert app_user.spawner.user_options == options assert not app_user.spawner._spawn_pending - status = io_loop.run_sync(app_user.spawner.poll) + status = yield app_user.spawner.poll() assert status is None assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/' url = public_url(app, user) - r = requests.get(url) + r = yield async_requests.get(url) assert r.status_code == 200 assert r.text == spawner.server.base_url - r = requests.get(ujoin(url, 'args')) + r = yield async_requests.get(ujoin(url, 'args')) assert r.status_code == 200 argv = r.json() assert '--port' in ' '.join(argv) - r = requests.get(ujoin(url, 'env')) + r = yield async_requests.get(ujoin(url, 'env')) env = r.json() for expected in ['JUPYTERHUB_USER', 'JUPYTERHUB_BASE_URL', 'JUPYTERHUB_API_TOKEN']: assert expected in env if app.subdomain_host: assert env['JUPYTERHUB_HOST'] == app.subdomain_host - r = api_request(app, 'users', name, 'server', method='delete') + r = yield api_request(app, 'users', name, 'server', method='delete') assert r.status_code == 204 assert 'pid' not in user.orm_spawners[''].state - status = io_loop.run_sync(app_user.spawner.poll) + status = yield app_user.spawner.poll() assert status == 0 # check that we cleaned up after ourselves @@ -446,6 +444,7 @@ def test_spawn(app, io_loop): assert tokens == [] +@mark.gen_test def test_slow_spawn(app, io_loop, no_patience, request): patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.SlowSpawner}) patch.start() @@ -453,10 +452,10 @@ def test_slow_spawn(app, io_loop, no_patience, request): db = app.db name = 'zoe' user = add_user(db, app=app, name=name) - r = api_request(app, 'users', name, 'server', method='post') + r = yield api_request(app, 'users', name, 'server', method='post') r.raise_for_status() assert r.status_code == 202 - app_user = get_app_user(app, name) + app_user = app.users[name] assert app_user.spawner is not None assert app_user.spawner._spawn_pending assert not app_user.spawner._stop_pending @@ -466,9 +465,9 @@ def test_slow_spawn(app, io_loop, no_patience, request): while app_user.spawner._spawn_pending: yield gen.sleep(0.1) - io_loop.run_sync(wait_spawn) + yield wait_spawn() assert not app_user.spawner._spawn_pending - status = io_loop.run_sync(app_user.spawner.poll) + status = yield app_user.spawner.poll() assert status is None @gen.coroutine @@ -476,25 +475,26 @@ def test_slow_spawn(app, io_loop, no_patience, request): while app_user.spawner._stop_pending: yield gen.sleep(0.1) - r = api_request(app, 'users', name, 'server', method='delete') + r = yield api_request(app, 'users', name, 'server', method='delete') r.raise_for_status() assert r.status_code == 202 assert app_user.spawner is not None assert app_user.spawner._stop_pending - r = api_request(app, 'users', name, 'server', method='delete') + r = yield api_request(app, 'users', name, 'server', method='delete') r.raise_for_status() assert r.status_code == 202 assert app_user.spawner is not None assert app_user.spawner._stop_pending - io_loop.run_sync(wait_stop) + yield wait_stop() assert not app_user.spawner._stop_pending assert app_user.spawner is not None - r = api_request(app, 'users', name, 'server', method='delete') + r = yield api_request(app, 'users', name, 'server', method='delete') assert r.status_code == 400 +@mark.gen_test def test_never_spawn(app, io_loop, no_patience, request): patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.NeverSpawner}) patch.start() @@ -503,8 +503,8 @@ def test_never_spawn(app, io_loop, no_patience, request): db = app.db name = 'badger' user = add_user(db, app=app, name=name) - r = api_request(app, 'users', name, 'server', method='post') - app_user = get_app_user(app, name) + r = yield api_request(app, 'users', name, 'server', method='post') + app_user = app.users[name] assert app_user.spawner is not None assert app_user.spawner._spawn_pending @@ -513,38 +513,40 @@ def test_never_spawn(app, io_loop, no_patience, request): while app_user.spawner._spawn_pending: yield gen.sleep(0.1) - io_loop.run_sync(wait_pending) + yield wait_pending() assert not app_user.spawner._spawn_pending - status = io_loop.run_sync(app_user.spawner.poll) + status = yield app_user.spawner.poll() assert status is not None +@mark.gen_test def test_get_proxy(app, io_loop): - r = api_request(app, 'proxy') + r = yield api_request(app, 'proxy') r.raise_for_status() reply = r.json() assert list(reply.keys()) == ['/'] +@mark.gen_test def test_cookie(app): db = app.db name = 'patience' user = add_user(db, app=app, name=name) - r = api_request(app, 'users', name, 'server', method='post') + r = yield api_request(app, 'users', name, 'server', method='post') assert r.status_code == 201 assert 'pid' in user.orm_spawners[''].state - app_user = get_app_user(app, name) + app_user = app.users[name] - cookies = app.login_user(name) + cookies = yield app.login_user(name) cookie_name = app.hub.cookie_name # cookie jar gives '"cookie-value"', we want 'cookie-value' cookie = cookies[cookie_name][1:-1] - r = api_request(app, 'authorizations/cookie', + r = yield api_request(app, 'authorizations/cookie', cookie_name, "nothintoseehere", ) assert r.status_code == 404 - r = api_request(app, 'authorizations/cookie', + r = yield api_request(app, 'authorizations/cookie', cookie_name, quote(cookie, safe=''), ) r.raise_for_status() @@ -552,7 +554,7 @@ def test_cookie(app): assert reply['name'] == name # deprecated cookie in body: - r = api_request(app, 'authorizations/cookie', + r = yield api_request(app, 'authorizations/cookie', cookie_name, data=cookie, ) r.raise_for_status() @@ -560,18 +562,20 @@ def test_cookie(app): assert reply['name'] == name +@mark.gen_test def test_token(app): name = 'book' user = add_user(app.db, app=app, name=name) token = user.new_api_token() - r = api_request(app, 'authorizations/token', token) + r = yield api_request(app, 'authorizations/token', token) r.raise_for_status() user_model = r.json() assert user_model['name'] == name - r = api_request(app, 'authorizations/token', 'notauthorized') + r = yield api_request(app, 'authorizations/token', 'notauthorized') assert r.status_code == 404 +@mark.gen_test @mark.parametrize("headers, data, status", [ ({}, None, 200), ({'Authorization': ''}, None, 403), @@ -581,13 +585,13 @@ def test_get_new_token(app, headers, data, status): if data: data = json.dumps(data) # request a new token - r = api_request(app, 'authorizations', 'token', method='post', data=data, headers=headers) + r = yield api_request(app, 'authorizations', 'token', method='post', data=data, headers=headers) assert r.status_code == status if status != 200: return reply = r.json() assert 'token' in reply - r = api_request(app, 'authorizations', 'token', reply['token']) + r = yield api_request(app, 'authorizations', 'token', reply['token']) r.raise_for_status() assert 'name' in r.json() @@ -598,8 +602,9 @@ def test_get_new_token(app, headers, data, status): @mark.group +@mark.gen_test def test_groups_list(app): - r = api_request(app, 'groups') + r = yield api_request(app, 'groups') r.raise_for_status() reply = r.json() assert reply == [] @@ -609,7 +614,7 @@ def test_groups_list(app): app.db.add(group) app.db.commit() - r = api_request(app, 'groups') + r = yield api_request(app, 'groups') r.raise_for_status() reply = r.json() assert reply == [{ @@ -620,16 +625,17 @@ def test_groups_list(app): @mark.group +@mark.gen_test def test_group_get(app): group = orm.Group.find(app.db, name='alphaflight') user = add_user(app.db, app=app, name='sasquatch') group.users.append(user) app.db.commit() - r = api_request(app, 'groups/runaways') + r = yield api_request(app, 'groups/runaways') assert r.status_code == 404 - r = api_request(app, 'groups/alphaflight') + r = yield api_request(app, 'groups/alphaflight') r.raise_for_status() reply = r.json() assert reply == { @@ -640,18 +646,19 @@ def test_group_get(app): @mark.group +@mark.gen_test def test_group_create_delete(app): db = app.db - r = api_request(app, 'groups/runaways', method='delete') + r = yield api_request(app, 'groups/runaways', method='delete') assert r.status_code == 404 - r = api_request(app, 'groups/new', method='post', + r = yield api_request(app, 'groups/new', method='post', data=json.dumps({'users': ['doesntexist']}), ) assert r.status_code == 400 assert orm.Group.find(db, name='new') is None - r = api_request(app, 'groups/omegaflight', method='post', + r = yield api_request(app, 'groups/omegaflight', method='post', data=json.dumps({'users': ['sasquatch']}), ) r.raise_for_status() @@ -662,29 +669,30 @@ def test_group_create_delete(app): assert sasquatch in omegaflight.users # create duplicate raises 400 - r = api_request(app, 'groups/omegaflight', method='post') + r = yield api_request(app, 'groups/omegaflight', method='post') assert r.status_code == 400 - r = api_request(app, 'groups/omegaflight', method='delete') + r = yield api_request(app, 'groups/omegaflight', method='delete') assert r.status_code == 204 assert omegaflight not in sasquatch.groups assert orm.Group.find(db, name='omegaflight') is None # delete nonexistent gives 404 - r = api_request(app, 'groups/omegaflight', method='delete') + r = yield api_request(app, 'groups/omegaflight', method='delete') assert r.status_code == 404 @mark.group +@mark.gen_test def test_group_add_users(app): db = app.db # must specify users - r = api_request(app, 'groups/alphaflight/users', method='post', data='{}') + r = yield api_request(app, 'groups/alphaflight/users', method='post', data='{}') assert r.status_code == 400 names = ['aurora', 'guardian', 'northstar', 'sasquatch', 'shaman', 'snowbird'] users = [ find_user(db, name=name) or add_user(db, app=app, name=name) for name in names ] - r = api_request(app, 'groups/alphaflight/users', method='post', data=json.dumps({ + r = yield api_request(app, 'groups/alphaflight/users', method='post', data=json.dumps({ 'users': names, })) r.raise_for_status() @@ -698,15 +706,16 @@ def test_group_add_users(app): @mark.group +@mark.gen_test def test_group_delete_users(app): db = app.db # must specify users - r = api_request(app, 'groups/alphaflight/users', method='delete', data='{}') + r = yield api_request(app, 'groups/alphaflight/users', method='delete', data='{}') assert r.status_code == 400 names = ['aurora', 'guardian', 'northstar', 'sasquatch', 'shaman', 'snowbird'] users = [ find_user(db, name=name) for name in names ] - r = api_request(app, 'groups/alphaflight/users', method='delete', data=json.dumps({ + r = yield api_request(app, 'groups/alphaflight/users', method='delete', data=json.dumps({ 'users': names[:2], })) r.raise_for_status() @@ -726,10 +735,11 @@ def test_group_delete_users(app): @mark.services +@mark.gen_test def test_get_services(app, mockservice_url): mockservice = mockservice_url db = app.db - r = api_request(app, 'services') + r = yield api_request(app, 'services') r.raise_for_status() assert r.status_code == 200 @@ -745,17 +755,18 @@ def test_get_services(app, mockservice_url): } } - r = api_request(app, 'services', + r = yield api_request(app, 'services', headers=auth_header(db, 'user'), ) assert r.status_code == 403 @mark.services +@mark.gen_test def test_get_service(app, mockservice_url): mockservice = mockservice_url db = app.db - r = api_request(app, 'services/%s' % mockservice.name) + r = yield api_request(app, 'services/%s' % mockservice.name) r.raise_for_status() assert r.status_code == 200 @@ -769,22 +780,23 @@ def test_get_service(app, mockservice_url): 'url': mockservice.url, } - r = api_request(app, 'services/%s' % mockservice.name, + r = yield api_request(app, 'services/%s' % mockservice.name, headers={ 'Authorization': 'token %s' % mockservice.api_token } ) r.raise_for_status() - r = api_request(app, 'services/%s' % mockservice.name, + r = yield api_request(app, 'services/%s' % mockservice.name, headers=auth_header(db, 'user'), ) assert r.status_code == 403 +@mark.gen_test def test_root_api(app): base_url = app.hub.url url = ujoin(base_url, 'api') - r = requests.get(url) + r = yield async_requests.get(url) r.raise_for_status() expected = { 'version': jupyterhub.__version__ @@ -792,8 +804,9 @@ def test_root_api(app): assert r.json() == expected +@mark.gen_test def test_info(app): - r = api_request(app, 'info') + r = yield api_request(app, 'info') r.raise_for_status() data = r.json() assert data['version'] == jupyterhub.__version__ @@ -821,14 +834,16 @@ def test_info(app): # ----------------- +@mark.gen_test def test_options(app): - r = api_request(app, 'users', method='options') + r = yield api_request(app, 'users', method='options') r.raise_for_status() assert 'Access-Control-Allow-Headers' in r.headers +@mark.gen_test def test_bad_json_body(app): - r = api_request(app, 'users', method='post', data='notjson') + r = yield api_request(app, 'users', method='post', data='notjson') assert r.status_code == 400 @@ -838,14 +853,24 @@ def test_bad_json_body(app): def test_shutdown(app): - r = api_request(app, 'shutdown', method='post', - data=json.dumps({'servers': True, 'proxy': True,}), - ) + loop = app.io_loop + + # have to do things a little funky since we are going to stop the loop, + # which makes gen_test unhappy. So we run the loop ourselves. + + @gen.coroutine + def shutdown(): + r = yield api_request(app, 'shutdown', method='post', + data=json.dumps({'servers': True, 'proxy': True,}), + ) + return r + + real_stop = loop.stop + def stop(): + stop.called = True + loop.call_later(1, real_stop) + with mock.patch.object(loop, 'stop', stop): + r = loop.run_sync(shutdown, timeout=5) r.raise_for_status() reply = r.json() - for i in range(100): - if app.io_loop._running: - time.sleep(0.1) - else: - break - assert not app.io_loop._running + assert stop.called diff --git a/jupyterhub/tests/test_utils.py b/jupyterhub/tests/test_utils.py deleted file mode 100644 index e69de29b..00000000 diff --git a/jupyterhub/tests/utils.py b/jupyterhub/tests/utils.py new file mode 100644 index 00000000..58d9222a --- /dev/null +++ b/jupyterhub/tests/utils.py @@ -0,0 +1,18 @@ +from concurrent.futures import ThreadPoolExecutor +import requests + +class _AsyncRequests: + """Wrapper around requests to return a Future from request methods + + A single thread is allocated to avoid blocking the IOLoop thread. + """ + def __init__(self): + self.executor = ThreadPoolExecutor(1) + + def __getattr__(self, name): + requests_method = getattr(requests, name) + return lambda *args, **kwargs: self.executor.submit(requests_method, *args, **kwargs) + +# async_requests.get = requests.get returning a Future, etc. +async_requests = _AsyncRequests() +