test_api passes without threads

just put requests in a thread via `utils.async_requests`

eliminates db threads issue
This commit is contained in:
Min RK
2017-07-27 10:34:54 +02:00
parent 58f072e5af
commit f05aecf5f9
5 changed files with 165 additions and 122 deletions

View File

@@ -38,8 +38,7 @@ def db():
@fixture(scope='module') @fixture(scope='module')
def io_loop(request): def io_loop(request):
"""Same as pytest-tornado.gen""" """Same as pytest-tornado.io_loop, but re-scoped to module-level"""
print("my io_loop fixture")
io_loop = ioloop.IOLoop() io_loop = ioloop.IOLoop()
io_loop.make_current() io_loop.make_current()
@@ -78,7 +77,7 @@ class MockServiceSpawner(jupyterhub.services.service._ServiceSpawner):
_mock_service_counter = 0 _mock_service_counter = 0
def _mockservice(request, app, io_loop, url=False): def _mockservice(request, app, url=False):
global _mock_service_counter global _mock_service_counter
_mock_service_counter += 1 _mock_service_counter += 1
name = 'mock-service-%i' % _mock_service_counter name = 'mock-service-%i' % _mock_service_counter
@@ -90,6 +89,8 @@ def _mockservice(request, app, io_loop, url=False):
if url: if url:
spec['url'] = 'http://127.0.0.1:%i' % random_port() spec['url'] = 'http://127.0.0.1:%i' % random_port()
io_loop = app.io_loop
with mock.patch.object(jupyterhub.services.service, '_ServiceSpawner', MockServiceSpawner): with mock.patch.object(jupyterhub.services.service, '_ServiceSpawner', MockServiceSpawner):
app.services = [spec] app.services = [spec]
app.init_services() 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 # wait for proxy to be updated before starting the service
yield app.proxy.add_all_services(app._service_map) yield app.proxy.add_all_services(app._service_map)
service.start() service.start()
app.io_loop.add_callback(start) io_loop.run_sync(start)
def cleanup(): def cleanup():
service.stop() service.stop()
app.services[:] = [] app.services[:] = []
app._service_map.clear() app._service_map.clear()
request.addfinalizer(cleanup) request.addfinalizer(cleanup)
for i in range(20):
if not getattr(service, 'proc', False):
time.sleep(0.2)
# ensure process finishes starting # ensure process finishes starting
with raises(TimeoutExpired): with raises(TimeoutExpired):
service.proc.wait(1) service.proc.wait(1)
if url: if url:
ioloop.IOLoop().run_sync(service.server.wait_up) io_loop.run_sync(service.server.wait_up)
return service return service

View File

@@ -22,6 +22,7 @@ from ..objects import Server
from ..spawner import LocalProcessSpawner from ..spawner import LocalProcessSpawner
from ..singleuser import SingleUserNotebookApp from ..singleuser import SingleUserNotebookApp
from ..utils import random_port, url_path_join from ..utils import random_port, url_path_join
from .utils import async_requests
from pamela import PAMError from pamela import PAMError
@@ -178,10 +179,11 @@ class MockHub(JupyterHub):
self.cleanup = lambda : None self.cleanup = lambda : None
self.db_file.close() self.db_file.close()
@gen.coroutine
def login_user(self, name): def login_user(self, name):
"""Login a user by name, returning her cookies.""" """Login a user by name, returning her cookies."""
base_url = public_url(self) base_url = public_url(self)
r = requests.post(base_url + 'hub/login', r = yield async_requests.post(base_url + 'hub/login',
data={ data={
'username': name, 'username': name,
'password': name, 'password': name,

View File

@@ -2,12 +2,11 @@
import json import json
import time import time
from queue import Queue
import sys import sys
from unittest import mock from unittest import mock
from urllib.parse import urlparse, quote from urllib.parse import urlparse, quote
from pytest import mark, yield_fixture from pytest import mark
import requests import requests
from tornado import gen from tornado import gen
@@ -18,6 +17,7 @@ from ..user import User
from ..utils import url_path_join as ujoin from ..utils import url_path_join as ujoin
from . import mocking from . import mocking
from .mocking import public_host, public_url from .mocking import public_host, public_url
from .utils import async_requests
def check_db_locks(func): def check_db_locks(func):
@@ -81,6 +81,7 @@ def auth_header(db, name):
@check_db_locks @check_db_locks
@gen.coroutine
def api_request(app, *api_path, **kwargs): def api_request(app, *api_path, **kwargs):
"""Make an API request""" """Make an API request"""
base_url = app.hub.url base_url = app.hub.url
@@ -91,8 +92,8 @@ def api_request(app, *api_path, **kwargs):
url = ujoin(base_url, 'api', *api_path) url = ujoin(base_url, 'api', *api_path)
method = kwargs.pop('method', 'get') method = kwargs.pop('method', 'get')
f = getattr(requests, method) f = getattr(async_requests, method)
resp = f(url, **kwargs) resp = yield f(url, **kwargs)
assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy'] 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 ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy']
assert 'http' not 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): def test_auth_api(app):
db = app.db db = app.db
r = api_request(app, 'authorizations', 'gobbledygook') r = yield api_request(app, 'authorizations', 'gobbledygook')
assert r.status_code == 404 assert r.status_code == 404
# make a new cookie token # make a new cookie token
@@ -114,36 +116,37 @@ def test_auth_api(app):
api_token = user.new_api_token() api_token = user.new_api_token()
# check success: # check success:
r = api_request(app, 'authorizations/token', api_token) r = yield api_request(app, 'authorizations/token', api_token)
assert r.status_code == 200 assert r.status_code == 200
reply = r.json() reply = r.json()
assert reply['name'] == user.name assert reply['name'] == user.name
# check fail # check fail
r = api_request(app, 'authorizations/token', api_token, r = yield api_request(app, 'authorizations/token', api_token,
headers={'Authorization': 'no sir'}, headers={'Authorization': 'no sir'},
) )
assert r.status_code == 403 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}, headers={'Authorization': 'token: %s' % user.cookie_id},
) )
assert r.status_code == 403 assert r.status_code == 403
@mark.gen_test
def test_referer_check(app, io_loop): def test_referer_check(app, io_loop):
url = ujoin(public_host(app), app.hub.base_url) url = ujoin(public_host(app), app.hub.base_url)
host = urlparse(url).netloc host = urlparse(url).netloc
user = find_user(app.db, 'admin') user = find_user(app.db, 'admin')
if user is None: if user is None:
user = add_user(app.db, name='admin', admin=True) user = add_user(app.db, name='admin', admin=True)
cookies = app.login_user('admin') cookies = yield app.login_user('admin')
app_user = get_app_user(app, 'admin') app_user = app.users[user]
# stop the admin's server so we don't mess up future tests # stop the admin's server so we don't mess up future tests
io_loop.run_sync(lambda: app.proxy.delete_user(app_user)) yield app.proxy.delete_user(app_user)
io_loop.run_sync(app_user.stop) yield app_user.stop()
r = api_request(app, 'users', r = yield api_request(app, 'users',
headers={ headers={
'Authorization': '', 'Authorization': '',
'Referer': 'null', 'Referer': 'null',
@@ -151,7 +154,7 @@ def test_referer_check(app, io_loop):
) )
assert r.status_code == 403 assert r.status_code == 403
r = api_request(app, 'users', r = yield api_request(app, 'users',
headers={ headers={
'Authorization': '', 'Authorization': '',
'Referer': 'http://attack.com/csrf/vulnerability', 'Referer': 'http://attack.com/csrf/vulnerability',
@@ -159,7 +162,7 @@ def test_referer_check(app, io_loop):
) )
assert r.status_code == 403 assert r.status_code == 403
r = api_request(app, 'users', r = yield api_request(app, 'users',
headers={ headers={
'Authorization': '', 'Authorization': '',
'Referer': url, 'Referer': url,
@@ -168,7 +171,7 @@ def test_referer_check(app, io_loop):
) )
assert r.status_code == 200 assert r.status_code == 200
r = api_request(app, 'users', r = yield api_request(app, 'users',
headers={ headers={
'Authorization': '', 'Authorization': '',
'Referer': ujoin(url, 'foo/bar/baz/bat'), 'Referer': ujoin(url, 'foo/bar/baz/bat'),
@@ -184,9 +187,10 @@ def test_referer_check(app, io_loop):
@mark.user @mark.user
@mark.gen_test
def test_get_users(app): def test_get_users(app):
db = app.db db = app.db
r = api_request(app, 'users') r = yield api_request(app, 'users')
assert r.status_code == 200 assert r.status_code == 200
users = sorted(r.json(), key=lambda d: d['name']) 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'), headers=auth_header(db, 'user'),
) )
assert r.status_code == 403 assert r.status_code == 403
@mark.user @mark.user
@mark.gen_test
def test_add_user(app): def test_add_user(app):
db = app.db db = app.db
name = 'newuser' name = 'newuser'
r = api_request(app, 'users', name, method='post') r = yield api_request(app, 'users', name, method='post')
assert r.status_code == 201 assert r.status_code == 201
user = find_user(db, name) user = find_user(db, name)
assert user is not None assert user is not None
@@ -230,9 +235,10 @@ def test_add_user(app):
@mark.user @mark.user
@mark.gen_test
def test_get_user(app): def test_get_user(app):
name = 'user' name = 'user'
r = api_request(app, 'users', name) r = yield api_request(app, 'users', name)
assert r.status_code == 200 assert r.status_code == 200
user = r.json() user = r.json()
user.pop('last_activity') user.pop('last_activity')
@@ -247,19 +253,21 @@ def test_get_user(app):
@mark.user @mark.user
@mark.gen_test
def test_add_multi_user_bad(app): 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 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 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 assert r.status_code == 400
@mark.user @mark.user
@mark.gen_test
def test_add_multi_user_invalid(app): def test_add_multi_user_invalid(app):
app.authenticator.username_pattern = r'w.*' 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']}) data=json.dumps({'usernames': ['Willow', 'Andrew', 'Tara']})
) )
app.authenticator.username_pattern = '' app.authenticator.username_pattern = ''
@@ -268,10 +276,11 @@ def test_add_multi_user_invalid(app):
@mark.user @mark.user
@mark.gen_test
def test_add_multi_user(app): def test_add_multi_user(app):
db = app.db db = app.db
names = ['a', 'b'] names = ['a', 'b']
r = api_request(app, 'users', method='post', r = yield api_request(app, 'users', method='post',
data=json.dumps({'usernames': names}), data=json.dumps({'usernames': names}),
) )
assert r.status_code == 201 assert r.status_code == 201
@@ -286,7 +295,7 @@ def test_add_multi_user(app):
assert not user.admin assert not user.admin
# try to create the same users again # 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}), data=json.dumps({'usernames': names}),
) )
assert r.status_code == 400 assert r.status_code == 400
@@ -294,7 +303,7 @@ def test_add_multi_user(app):
names = ['a', 'b', 'ab'] names = ['a', 'b', 'ab']
# try to create the same users again # 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}), data=json.dumps({'usernames': names}),
) )
assert r.status_code == 201 assert r.status_code == 201
@@ -304,10 +313,11 @@ def test_add_multi_user(app):
@mark.user @mark.user
@mark.gen_test
def test_add_multi_user_admin(app): def test_add_multi_user_admin(app):
db = app.db db = app.db
names = ['c', 'd'] 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}), data=json.dumps({'usernames': names, 'admin': True}),
) )
assert r.status_code == 201 assert r.status_code == 201
@@ -323,20 +333,22 @@ def test_add_multi_user_admin(app):
@mark.user @mark.user
@mark.gen_test
def test_add_user_bad(app): def test_add_user_bad(app):
db = app.db db = app.db
name = 'dne_newuser' 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 assert r.status_code == 400
user = find_user(db, name) user = find_user(db, name)
assert user is None assert user is None
@mark.user @mark.user
@mark.gen_test
def test_add_admin(app): def test_add_admin(app):
db = app.db db = app.db
name = 'newadmin' name = 'newadmin'
r = api_request(app, 'users', name, method='post', r = yield api_request(app, 'users', name, method='post',
data=json.dumps({'admin': True}), data=json.dumps({'admin': True}),
) )
assert r.status_code == 201 assert r.status_code == 201
@@ -347,25 +359,27 @@ def test_add_admin(app):
@mark.user @mark.user
@mark.gen_test
def test_delete_user(app): def test_delete_user(app):
db = app.db db = app.db
mal = add_user(db, name='mal') 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 assert r.status_code == 204
@mark.user @mark.user
@mark.gen_test
def test_make_admin(app): def test_make_admin(app):
db = app.db db = app.db
name = 'admin2' name = 'admin2'
r = api_request(app, 'users', name, method='post') r = yield api_request(app, 'users', name, method='post')
assert r.status_code == 201 assert r.status_code == 201
user = find_user(db, name) user = find_user(db, name)
assert user is not None assert user is not None
assert user.name == name assert user.name == name
assert not user.admin 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}) data=json.dumps({'admin': True})
) )
assert r.status_code == 200 assert r.status_code == 200
@@ -375,23 +389,7 @@ def test_make_admin(app):
assert user.admin assert user.admin
def get_app_user(app, name): @mark.gen_test
"""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]
def test_spawn(app, io_loop): def test_spawn(app, io_loop):
db = app.db db = app.db
name = 'wash' name = 'wash'
@@ -401,41 +399,41 @@ def test_spawn(app, io_loop):
'i': 5, 'i': 5,
} }
before_servers = sorted(db.query(orm.Server), key=lambda s: s.url) 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), data=json.dumps(options),
) )
assert r.status_code == 201 assert r.status_code == 201
assert 'pid' in user.orm_spawners[''].state 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 assert app_user.spawner is not None
spawner = app_user.spawner spawner = app_user.spawner
assert app_user.spawner.user_options == options assert app_user.spawner.user_options == options
assert not app_user.spawner._spawn_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 None assert status is None
assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/' assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/'
url = public_url(app, user) url = public_url(app, user)
r = requests.get(url) r = yield async_requests.get(url)
assert r.status_code == 200 assert r.status_code == 200
assert r.text == spawner.server.base_url 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 assert r.status_code == 200
argv = r.json() argv = r.json()
assert '--port' in ' '.join(argv) assert '--port' in ' '.join(argv)
r = requests.get(ujoin(url, 'env')) r = yield async_requests.get(ujoin(url, 'env'))
env = r.json() env = r.json()
for expected in ['JUPYTERHUB_USER', 'JUPYTERHUB_BASE_URL', 'JUPYTERHUB_API_TOKEN']: for expected in ['JUPYTERHUB_USER', 'JUPYTERHUB_BASE_URL', 'JUPYTERHUB_API_TOKEN']:
assert expected in env assert expected in env
if app.subdomain_host: if app.subdomain_host:
assert env['JUPYTERHUB_HOST'] == 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 r.status_code == 204
assert 'pid' not in user.orm_spawners[''].state 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 assert status == 0
# check that we cleaned up after ourselves # check that we cleaned up after ourselves
@@ -446,6 +444,7 @@ def test_spawn(app, io_loop):
assert tokens == [] assert tokens == []
@mark.gen_test
def test_slow_spawn(app, io_loop, no_patience, request): def test_slow_spawn(app, io_loop, no_patience, request):
patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.SlowSpawner}) patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.SlowSpawner})
patch.start() patch.start()
@@ -453,10 +452,10 @@ def test_slow_spawn(app, io_loop, no_patience, request):
db = app.db db = app.db
name = 'zoe' name = 'zoe'
user = add_user(db, app=app, name=name) 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() r.raise_for_status()
assert r.status_code == 202 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 is not None
assert app_user.spawner._spawn_pending assert app_user.spawner._spawn_pending
assert not app_user.spawner._stop_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: while app_user.spawner._spawn_pending:
yield gen.sleep(0.1) yield gen.sleep(0.1)
io_loop.run_sync(wait_spawn) yield wait_spawn()
assert not app_user.spawner._spawn_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 None assert status is None
@gen.coroutine @gen.coroutine
@@ -476,25 +475,26 @@ def test_slow_spawn(app, io_loop, no_patience, request):
while app_user.spawner._stop_pending: while app_user.spawner._stop_pending:
yield gen.sleep(0.1) 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() r.raise_for_status()
assert r.status_code == 202 assert r.status_code == 202
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawner._stop_pending 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() r.raise_for_status()
assert r.status_code == 202 assert r.status_code == 202
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawner._stop_pending assert app_user.spawner._stop_pending
io_loop.run_sync(wait_stop) yield wait_stop()
assert not app_user.spawner._stop_pending assert not app_user.spawner._stop_pending
assert app_user.spawner is not None 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 assert r.status_code == 400
@mark.gen_test
def test_never_spawn(app, io_loop, no_patience, request): def test_never_spawn(app, io_loop, no_patience, request):
patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.NeverSpawner}) patch = mock.patch.dict(app.tornado_settings, {'spawner_class': mocking.NeverSpawner})
patch.start() patch.start()
@@ -503,8 +503,8 @@ def test_never_spawn(app, io_loop, no_patience, request):
db = app.db db = app.db
name = 'badger' name = 'badger'
user = add_user(db, app=app, name=name) 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')
app_user = get_app_user(app, name) app_user = app.users[name]
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawner._spawn_pending 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: while app_user.spawner._spawn_pending:
yield gen.sleep(0.1) yield gen.sleep(0.1)
io_loop.run_sync(wait_pending) yield wait_pending()
assert not app_user.spawner._spawn_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 assert status is not None
@mark.gen_test
def test_get_proxy(app, io_loop): def test_get_proxy(app, io_loop):
r = api_request(app, 'proxy') r = yield api_request(app, 'proxy')
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert list(reply.keys()) == ['/'] assert list(reply.keys()) == ['/']
@mark.gen_test
def test_cookie(app): def test_cookie(app):
db = app.db db = app.db
name = 'patience' name = 'patience'
user = add_user(db, app=app, name=name) 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 r.status_code == 201
assert 'pid' in user.orm_spawners[''].state 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_name = app.hub.cookie_name
# cookie jar gives '"cookie-value"', we want 'cookie-value' # cookie jar gives '"cookie-value"', we want 'cookie-value'
cookie = cookies[cookie_name][1:-1] cookie = cookies[cookie_name][1:-1]
r = api_request(app, 'authorizations/cookie', r = yield api_request(app, 'authorizations/cookie',
cookie_name, "nothintoseehere", cookie_name, "nothintoseehere",
) )
assert r.status_code == 404 assert r.status_code == 404
r = api_request(app, 'authorizations/cookie', r = yield api_request(app, 'authorizations/cookie',
cookie_name, quote(cookie, safe=''), cookie_name, quote(cookie, safe=''),
) )
r.raise_for_status() r.raise_for_status()
@@ -552,7 +554,7 @@ def test_cookie(app):
assert reply['name'] == name assert reply['name'] == name
# deprecated cookie in body: # deprecated cookie in body:
r = api_request(app, 'authorizations/cookie', r = yield api_request(app, 'authorizations/cookie',
cookie_name, data=cookie, cookie_name, data=cookie,
) )
r.raise_for_status() r.raise_for_status()
@@ -560,18 +562,20 @@ def test_cookie(app):
assert reply['name'] == name assert reply['name'] == name
@mark.gen_test
def test_token(app): def test_token(app):
name = 'book' name = 'book'
user = add_user(app.db, app=app, name=name) user = add_user(app.db, app=app, name=name)
token = user.new_api_token() token = user.new_api_token()
r = api_request(app, 'authorizations/token', token) r = yield api_request(app, 'authorizations/token', token)
r.raise_for_status() r.raise_for_status()
user_model = r.json() user_model = r.json()
assert user_model['name'] == name 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 assert r.status_code == 404
@mark.gen_test
@mark.parametrize("headers, data, status", [ @mark.parametrize("headers, data, status", [
({}, None, 200), ({}, None, 200),
({'Authorization': ''}, None, 403), ({'Authorization': ''}, None, 403),
@@ -581,13 +585,13 @@ def test_get_new_token(app, headers, data, status):
if data: if data:
data = json.dumps(data) data = json.dumps(data)
# request a new token # 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 assert r.status_code == status
if status != 200: if status != 200:
return return
reply = r.json() reply = r.json()
assert 'token' in reply 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() r.raise_for_status()
assert 'name' in r.json() assert 'name' in r.json()
@@ -598,8 +602,9 @@ def test_get_new_token(app, headers, data, status):
@mark.group @mark.group
@mark.gen_test
def test_groups_list(app): def test_groups_list(app):
r = api_request(app, 'groups') r = yield api_request(app, 'groups')
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == [] assert reply == []
@@ -609,7 +614,7 @@ def test_groups_list(app):
app.db.add(group) app.db.add(group)
app.db.commit() app.db.commit()
r = api_request(app, 'groups') r = yield api_request(app, 'groups')
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == [{ assert reply == [{
@@ -620,16 +625,17 @@ def test_groups_list(app):
@mark.group @mark.group
@mark.gen_test
def test_group_get(app): def test_group_get(app):
group = orm.Group.find(app.db, name='alphaflight') group = orm.Group.find(app.db, name='alphaflight')
user = add_user(app.db, app=app, name='sasquatch') user = add_user(app.db, app=app, name='sasquatch')
group.users.append(user) group.users.append(user)
app.db.commit() app.db.commit()
r = api_request(app, 'groups/runaways') r = yield api_request(app, 'groups/runaways')
assert r.status_code == 404 assert r.status_code == 404
r = api_request(app, 'groups/alphaflight') r = yield api_request(app, 'groups/alphaflight')
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == { assert reply == {
@@ -640,18 +646,19 @@ def test_group_get(app):
@mark.group @mark.group
@mark.gen_test
def test_group_create_delete(app): def test_group_create_delete(app):
db = app.db 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 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']}), data=json.dumps({'users': ['doesntexist']}),
) )
assert r.status_code == 400 assert r.status_code == 400
assert orm.Group.find(db, name='new') is None 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']}), data=json.dumps({'users': ['sasquatch']}),
) )
r.raise_for_status() r.raise_for_status()
@@ -662,29 +669,30 @@ def test_group_create_delete(app):
assert sasquatch in omegaflight.users assert sasquatch in omegaflight.users
# create duplicate raises 400 # 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 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 r.status_code == 204
assert omegaflight not in sasquatch.groups assert omegaflight not in sasquatch.groups
assert orm.Group.find(db, name='omegaflight') is None assert orm.Group.find(db, name='omegaflight') is None
# delete nonexistent gives 404 # 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 assert r.status_code == 404
@mark.group @mark.group
@mark.gen_test
def test_group_add_users(app): def test_group_add_users(app):
db = app.db db = app.db
# must specify users # 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 assert r.status_code == 400
names = ['aurora', 'guardian', 'northstar', 'sasquatch', 'shaman', 'snowbird'] 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 ] 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, 'users': names,
})) }))
r.raise_for_status() r.raise_for_status()
@@ -698,15 +706,16 @@ def test_group_add_users(app):
@mark.group @mark.group
@mark.gen_test
def test_group_delete_users(app): def test_group_delete_users(app):
db = app.db db = app.db
# must specify users # 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 assert r.status_code == 400
names = ['aurora', 'guardian', 'northstar', 'sasquatch', 'shaman', 'snowbird'] names = ['aurora', 'guardian', 'northstar', 'sasquatch', 'shaman', 'snowbird']
users = [ find_user(db, name=name) for name in names ] 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], 'users': names[:2],
})) }))
r.raise_for_status() r.raise_for_status()
@@ -726,10 +735,11 @@ def test_group_delete_users(app):
@mark.services @mark.services
@mark.gen_test
def test_get_services(app, mockservice_url): def test_get_services(app, mockservice_url):
mockservice = mockservice_url mockservice = mockservice_url
db = app.db db = app.db
r = api_request(app, 'services') r = yield api_request(app, 'services')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 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'), headers=auth_header(db, 'user'),
) )
assert r.status_code == 403 assert r.status_code == 403
@mark.services @mark.services
@mark.gen_test
def test_get_service(app, mockservice_url): def test_get_service(app, mockservice_url):
mockservice = mockservice_url mockservice = mockservice_url
db = app.db db = app.db
r = api_request(app, 'services/%s' % mockservice.name) r = yield api_request(app, 'services/%s' % mockservice.name)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
@@ -769,22 +780,23 @@ def test_get_service(app, mockservice_url):
'url': mockservice.url, 'url': mockservice.url,
} }
r = api_request(app, 'services/%s' % mockservice.name, r = yield api_request(app, 'services/%s' % mockservice.name,
headers={ headers={
'Authorization': 'token %s' % mockservice.api_token 'Authorization': 'token %s' % mockservice.api_token
} }
) )
r.raise_for_status() 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'), headers=auth_header(db, 'user'),
) )
assert r.status_code == 403 assert r.status_code == 403
@mark.gen_test
def test_root_api(app): def test_root_api(app):
base_url = app.hub.url base_url = app.hub.url
url = ujoin(base_url, 'api') url = ujoin(base_url, 'api')
r = requests.get(url) r = yield async_requests.get(url)
r.raise_for_status() r.raise_for_status()
expected = { expected = {
'version': jupyterhub.__version__ 'version': jupyterhub.__version__
@@ -792,8 +804,9 @@ def test_root_api(app):
assert r.json() == expected assert r.json() == expected
@mark.gen_test
def test_info(app): def test_info(app):
r = api_request(app, 'info') r = yield api_request(app, 'info')
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
assert data['version'] == jupyterhub.__version__ assert data['version'] == jupyterhub.__version__
@@ -821,14 +834,16 @@ def test_info(app):
# ----------------- # -----------------
@mark.gen_test
def test_options(app): def test_options(app):
r = api_request(app, 'users', method='options') r = yield api_request(app, 'users', method='options')
r.raise_for_status() r.raise_for_status()
assert 'Access-Control-Allow-Headers' in r.headers assert 'Access-Control-Allow-Headers' in r.headers
@mark.gen_test
def test_bad_json_body(app): 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 assert r.status_code == 400
@@ -838,14 +853,24 @@ def test_bad_json_body(app):
def test_shutdown(app): def test_shutdown(app):
r = api_request(app, 'shutdown', method='post', 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,}), 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() r.raise_for_status()
reply = r.json() reply = r.json()
for i in range(100): assert stop.called
if app.io_loop._running:
time.sleep(0.1)
else:
break
assert not app.io_loop._running

18
jupyterhub/tests/utils.py Normal file
View File

@@ -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()