Merge branch 'master' into master

This commit is contained in:
Min RK
2018-12-14 10:51:47 +01:00
committed by GitHub
28 changed files with 805 additions and 867 deletions

View File

@@ -1,5 +1,5 @@
# Contributing # Contributing
See the [contribution guide](https://jupyterhub.readthedocs.io/en/latest/index.html#contributor) section See the [contribution guide](https://jupyterhub.readthedocs.io/en/latest/index.html#contributing) section
at the JupyterHub documentation. at the JupyterHub documentation.

View File

@@ -6,7 +6,7 @@ coverage
cryptography cryptography
html5lib # needed for beautifulsoup html5lib # needed for beautifulsoup
pytest-cov pytest-cov
pytest-tornado pytest-asyncio
pytest>=3.3 pytest>=3.3
notebook notebook
requests-mock requests-mock

View File

@@ -185,6 +185,15 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- options:
description: |
Spawn options can be passed as a JSON body
when spawning via the API instead of spawn form.
The structure of the options
will depend on the Spawner's configuration.
in: body
required: false
type: object
responses: responses:
'201': '201':
description: The user's notebook server has started description: The user's notebook server has started
@@ -217,13 +226,15 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: remove - options:
description: | description: |
Whether to fully remove the server, rather than just stop it. Spawn options can be passed as a JSON body
Removing a server deletes things like the state of the stopped server. when spawning via the API instead of spawn form.
The structure of the options
will depend on the Spawner's configuration.
in: body in: body
required: false required: false
type: boolean type: object
responses: responses:
'201': '201':
description: The user's notebook named-server has started description: The user's notebook named-server has started
@@ -242,6 +253,13 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: remove
description: |
Whether to fully remove the server, rather than just stop it.
Removing a server deletes things like the state of the stopped server.
in: body
required: false
type: boolean
responses: responses:
'204': '204':
description: The user's notebook named-server has stopped description: The user's notebook named-server has stopped

View File

@@ -30,7 +30,7 @@ For convenient administration of the Hub, its users, and services,
JupyterHub also provides a `REST API`_. JupyterHub also provides a `REST API`_.
The JupyterHub team and Project Jupyter value our community, and JupyterHub The JupyterHub team and Project Jupyter value our community, and JupyterHub
follows the Jupyter [Community Guides](https://jupyter.readthedocs.io/en/latest/community/content-community.html). follows the Jupyter `Community Guides <https://jupyter.readthedocs.io/en/latest/community/content-community.html>`_.
Contents Contents
======== ========

View File

@@ -1692,6 +1692,40 @@ class JupyterHub(Application):
spawner._log_name) spawner._log_name)
status = -1 status = -1
if status is None:
# poll claims it's running.
# Check if it's really there
url_in_db = spawner.server.url
url = await spawner.get_url()
if url != url_in_db:
self.log.warning(
"%s had invalid url %s. Updating to %s",
spawner._log_name, url_in_db, url,
)
urlinfo = urlparse(url)
spawner.server.protocol = urlinfo.scheme
spawner.server.ip = urlinfo.hostname
if urlinfo.port:
spawner.server.port = urlinfo.port
elif urlinfo.scheme == 'http':
spawner.server.port = 80
elif urlinfo.scheme == 'https':
spawner.server.port = 443
self.db.commit()
self.log.debug(
"Verifying that %s is running at %s",
spawner._log_name, url,
)
try:
await user._wait_up(spawner)
except TimeoutError:
self.log.error(
"%s does not appear to be running at %s, shutting it down.",
spawner._log_name, url,
)
status = -1
if status is None: if status is None:
self.log.info("%s still running", user.name) self.log.info("%s still running", user.name)
spawner.add_poll_callback(user_stopped, user, name) spawner.add_poll_callback(user_stopped, user, name)

View File

@@ -17,7 +17,6 @@ except Exception as e:
_pamela_error = e _pamela_error = e
from tornado.concurrent import run_on_executor from tornado.concurrent import run_on_executor
from tornado import gen
from traitlets.config import LoggingConfigurable from traitlets.config import LoggingConfigurable
from traitlets import Bool, Set, Unicode, Dict, Any, default, observe from traitlets import Bool, Set, Unicode, Dict, Any, default, observe
@@ -704,7 +703,7 @@ class PAMAuthenticator(LocalAuthenticator):
# (returning None instead of just the username) as this indicates some sort of system failure # (returning None instead of just the username) as this indicates some sort of system failure
admin_group_gids = {self._getgrnam(x).gr_gid for x in self.admin_groups} admin_group_gids = {self._getgrnam(x).gr_gid for x in self.admin_groups}
user_group_gids = {x.gr_gid for x in self._getgrouplist(username, self._getpwnam(username).pw_gid)} user_group_gids = set(self._getgrouplist(username, self._getpwnam(username).pw_gid))
admin_status = len(admin_group_gids & user_group_gids) != 0 admin_status = len(admin_group_gids & user_group_gids) != 0
except Exception as e: except Exception as e:

View File

@@ -19,7 +19,6 @@ from sqlalchemy import (
DateTime, Enum, Table, DateTime, Enum, Table,
) )
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.interfaces import PoolListener
from sqlalchemy.orm import ( from sqlalchemy.orm import (
Session, Session,
interfaces, object_session, relationship, sessionmaker, interfaces, object_session, relationship, sessionmaker,
@@ -559,10 +558,13 @@ class DatabaseSchemaMismatch(Exception):
""" """
class ForeignKeysListener(PoolListener): def register_foreign_keys(engine):
"""Enable foreign keys on sqlite""" """register PRAGMA foreign_keys=on on connection"""
def connect(self, dbapi_con, con_record): @event.listens_for(engine, "connect")
dbapi_con.execute('pragma foreign_keys=ON') def connect(dbapi_con, con_record):
cursor = dbapi_con.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def _expire_relationship(target, relationship_prop): def _expire_relationship(target, relationship_prop):
@@ -735,8 +737,6 @@ def new_session_factory(url="sqlite:///:memory:",
"""Create a new session at url""" """Create a new session at url"""
if url.startswith('sqlite'): if url.startswith('sqlite'):
kwargs.setdefault('connect_args', {'check_same_thread': False}) kwargs.setdefault('connect_args', {'check_same_thread': False})
listeners = kwargs.setdefault('listeners', [])
listeners.append(ForeignKeysListener())
elif url.startswith('mysql'): elif url.startswith('mysql'):
kwargs.setdefault('pool_recycle', 60) kwargs.setdefault('pool_recycle', 60)
@@ -747,6 +747,9 @@ def new_session_factory(url="sqlite:///:memory:",
kwargs.setdefault('poolclass', StaticPool) kwargs.setdefault('poolclass', StaticPool)
engine = create_engine(url, **kwargs) engine = create_engine(url, **kwargs)
if url.startswith('sqlite'):
register_foreign_keys(engine)
# enable pessimistic disconnect handling # enable pessimistic disconnect handling
register_ping_connection(engine) register_ping_connection(engine)

View File

@@ -852,8 +852,8 @@ class HubAuthenticated(object):
self._hub_auth_user_cache = None self._hub_auth_user_cache = None
raise raise
# store ?token=... tokens passed via url in a cookie for future requests # store tokens passed via url or header in a cookie for future requests
url_token = self.get_argument('token', '') url_token = self.hub_auth.get_token(self)
if ( if (
user_model user_model
and url_token and url_token

View File

@@ -660,6 +660,22 @@ class Spawner(LoggingConfigurable):
return env return env
async def get_url(self):
"""Get the URL to connect to the server
Sometimes JupyterHub may ask the Spawner for its url.
This can occur e.g. when JupyterHub has restarted while a server was not finished starting,
giving Spawners a chance to recover the URL where their server is running.
The default is to trust that JupyterHub has the right information.
Only override this method in Spawners that know how to
check the correct URL for the servers they start.
This will only be asked of Spawners that claim to be running
(`poll()` returns `None`).
"""
return self.server.url
def template_namespace(self): def template_namespace(self):
"""Return the template namespace for format-string formatting. """Return the template namespace for format-string formatting.

View File

@@ -29,6 +29,7 @@ Fixtures to add functionality or spawning behavior
import asyncio import asyncio
from getpass import getuser from getpass import getuser
import inspect
import logging import logging
import os import os
import sys import sys
@@ -55,6 +56,16 @@ import jupyterhub.services.service
_db = None _db = None
def pytest_collection_modifyitems(items):
"""add asyncio marker to all async tests"""
for item in items:
if inspect.iscoroutinefunction(item.obj):
item.add_marker('asyncio')
if hasattr(inspect, 'isasyncgenfunction'):
# double-check that we aren't mixing yield and async def
assert not inspect.isasyncgenfunction(item.obj)
@fixture(scope='module') @fixture(scope='module')
def ssl_tmpdir(tmpdir_factory): def ssl_tmpdir(tmpdir_factory):
return tmpdir_factory.mktemp('ssl') return tmpdir_factory.mktemp('ssl')
@@ -126,15 +137,21 @@ def db():
@fixture(scope='module') @fixture(scope='module')
def io_loop(request): def event_loop(request):
"""Same as pytest-asyncio.event_loop, but re-scoped to module-level"""
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
return event_loop
@fixture(scope='module')
def io_loop(event_loop, request):
"""Same as pytest-tornado.io_loop, but re-scoped to module-level""" """Same as pytest-tornado.io_loop, but re-scoped to module-level"""
ioloop.IOLoop.configure(AsyncIOMainLoop) ioloop.IOLoop.configure(AsyncIOMainLoop)
aio_loop = asyncio.new_event_loop() io_loop = AsyncIOMainLoop()
asyncio.set_event_loop(aio_loop)
io_loop = ioloop.IOLoop()
io_loop.make_current() io_loop.make_current()
assert asyncio.get_event_loop() is aio_loop assert asyncio.get_event_loop() is event_loop
assert io_loop.asyncio_loop is aio_loop assert io_loop.asyncio_loop is event_loop
def _close(): def _close():
io_loop.clear_current() io_loop.clear_current()

File diff suppressed because it is too large Load Diff

View File

@@ -63,8 +63,7 @@ def test_generate_config():
assert 'Authenticator.whitelist' in cfg_text assert 'Authenticator.whitelist' in cfg_text
@pytest.mark.gen_test async def test_init_tokens(request):
def test_init_tokens(request):
with TemporaryDirectory() as td: with TemporaryDirectory() as td:
db_file = os.path.join(td, 'jupyterhub.sqlite') db_file = os.path.join(td, 'jupyterhub.sqlite')
tokens = { tokens = {
@@ -77,7 +76,7 @@ def test_init_tokens(request):
if ssl_enabled: if ssl_enabled:
kwargs['internal_certs_location'] = td kwargs['internal_certs_location'] = td
app = MockHub(**kwargs) app = MockHub(**kwargs)
yield app.initialize([]) await app.initialize([])
db = app.db db = app.db
for token, username in tokens.items(): for token, username in tokens.items():
api_token = orm.APIToken.find(db, token) api_token = orm.APIToken.find(db, token)
@@ -87,7 +86,7 @@ def test_init_tokens(request):
# simulate second startup, reloading same tokens: # simulate second startup, reloading same tokens:
app = MockHub(**kwargs) app = MockHub(**kwargs)
yield app.initialize([]) await app.initialize([])
db = app.db db = app.db
for token, username in tokens.items(): for token, username in tokens.items():
api_token = orm.APIToken.find(db, token) api_token = orm.APIToken.find(db, token)
@@ -99,7 +98,7 @@ def test_init_tokens(request):
tokens['short'] = 'gman' tokens['short'] = 'gman'
app = MockHub(**kwargs) app = MockHub(**kwargs)
with pytest.raises(ValueError): with pytest.raises(ValueError):
yield app.initialize([]) await app.initialize([])
assert orm.User.find(app.db, 'gman') is None assert orm.User.find(app.db, 'gman') is None
@@ -169,8 +168,7 @@ def test_cookie_secret_env(tmpdir, request):
assert not os.path.exists(hub.cookie_secret_file) assert not os.path.exists(hub.cookie_secret_file)
@pytest.mark.gen_test async def test_load_groups(tmpdir, request):
def test_load_groups(tmpdir, request):
to_load = { to_load = {
'blue': ['cyclops', 'rogue', 'wolverine'], 'blue': ['cyclops', 'rogue', 'wolverine'],
'gold': ['storm', 'jean-grey', 'colossus'], 'gold': ['storm', 'jean-grey', 'colossus'],
@@ -181,8 +179,8 @@ def test_load_groups(tmpdir, request):
kwargs['internal_certs_location'] = str(tmpdir) kwargs['internal_certs_location'] = str(tmpdir)
hub = MockHub(**kwargs) hub = MockHub(**kwargs)
hub.init_db() hub.init_db()
yield hub.init_users() await hub.init_users()
yield hub.init_groups() await hub.init_groups()
db = hub.db db = hub.db
blue = orm.Group.find(db, name='blue') blue = orm.Group.find(db, name='blue')
assert blue is not None assert blue is not None
@@ -192,16 +190,15 @@ def test_load_groups(tmpdir, request):
assert sorted([ u.name for u in gold.users ]) == sorted(to_load['gold']) assert sorted([ u.name for u in gold.users ]) == sorted(to_load['gold'])
@pytest.mark.gen_test async def test_resume_spawners(tmpdir, request):
def test_resume_spawners(tmpdir, request):
if not os.getenv('JUPYTERHUB_TEST_DB_URL'): if not os.getenv('JUPYTERHUB_TEST_DB_URL'):
p = patch.dict(os.environ, { p = patch.dict(os.environ, {
'JUPYTERHUB_TEST_DB_URL': 'sqlite:///%s' % tmpdir.join('jupyterhub.sqlite'), 'JUPYTERHUB_TEST_DB_URL': 'sqlite:///%s' % tmpdir.join('jupyterhub.sqlite'),
}) })
p.start() p.start()
request.addfinalizer(p.stop) request.addfinalizer(p.stop)
@gen.coroutine
def new_hub(): async def new_hub():
kwargs = {} kwargs = {}
ssl_enabled = getattr(request.module, "ssl_enabled", False) ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled: if ssl_enabled:
@@ -209,26 +206,27 @@ def test_resume_spawners(tmpdir, request):
app = MockHub(test_clean_db=False, **kwargs) app = MockHub(test_clean_db=False, **kwargs)
app.config.ConfigurableHTTPProxy.should_start = False app.config.ConfigurableHTTPProxy.should_start = False
app.config.ConfigurableHTTPProxy.auth_token = 'unused' app.config.ConfigurableHTTPProxy.auth_token = 'unused'
yield app.initialize([]) await app.initialize([])
return app return app
app = yield new_hub()
app = await new_hub()
db = app.db db = app.db
# spawn a user's server # spawn a user's server
name = 'kurt' name = 'kurt'
user = add_user(db, app, name=name) user = add_user(db, app, name=name)
yield user.spawn() await user.spawn()
proc = user.spawner.proc proc = user.spawner.proc
assert proc is not None assert proc is not None
# stop the Hub without cleaning up servers # stop the Hub without cleaning up servers
app.cleanup_servers = False app.cleanup_servers = False
yield app.stop() app.stop()
# proc is still running # proc is still running
assert proc.poll() is None assert proc.poll() is None
# resume Hub, should still be running # resume Hub, should still be running
app = yield new_hub() app = await new_hub()
db = app.db db = app.db
user = app.users[name] user = app.users[name]
assert user.running assert user.running
@@ -236,7 +234,7 @@ def test_resume_spawners(tmpdir, request):
# stop the Hub without cleaning up servers # stop the Hub without cleaning up servers
app.cleanup_servers = False app.cleanup_servers = False
yield app.stop() app.stop()
# stop the server while the Hub is down. BAMF! # stop the server while the Hub is down. BAMF!
proc.terminate() proc.terminate()
@@ -244,7 +242,7 @@ def test_resume_spawners(tmpdir, request):
assert proc.poll() is not None assert proc.poll() is not None
# resume Hub, should be stopped # resume Hub, should be stopped
app = yield new_hub() app = await new_hub()
db = app.db db = app.db
user = app.users[name] user = app.users[name]
assert not user.running assert not user.running

View File

@@ -14,47 +14,44 @@ from jupyterhub import auth, crypto, orm
from .mocking import MockPAMAuthenticator, MockStructGroup, MockStructPasswd from .mocking import MockPAMAuthenticator, MockStructGroup, MockStructPasswd
from .test_api import add_user from .test_api import add_user
@pytest.mark.gen_test async def test_pam_auth():
def test_pam_auth():
authenticator = MockPAMAuthenticator() authenticator = MockPAMAuthenticator()
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'match', 'username': 'match',
'password': 'match', 'password': 'match',
}) })
assert authorized['name'] == 'match' assert authorized['name'] == 'match'
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'match', 'username': 'match',
'password': 'nomatch', 'password': 'nomatch',
}) })
assert authorized is None assert authorized is None
# Account check is on by default for increased security # Account check is on by default for increased security
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'notallowedmatch', 'username': 'notallowedmatch',
'password': 'notallowedmatch', 'password': 'notallowedmatch',
}) })
assert authorized is None assert authorized is None
@pytest.mark.gen_test async def test_pam_auth_account_check_disabled():
def test_pam_auth_account_check_disabled():
authenticator = MockPAMAuthenticator(check_account=False) authenticator = MockPAMAuthenticator(check_account=False)
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'allowedmatch', 'username': 'allowedmatch',
'password': 'allowedmatch', 'password': 'allowedmatch',
}) })
assert authorized['name'] == 'allowedmatch' assert authorized['name'] == 'allowedmatch'
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'notallowedmatch', 'username': 'notallowedmatch',
'password': 'notallowedmatch', 'password': 'notallowedmatch',
}) })
assert authorized['name'] == 'notallowedmatch' assert authorized['name'] == 'notallowedmatch'
@pytest.mark.gen_test async def test_pam_auth_admin_groups():
def test_pam_auth_admin_groups():
jh_users = MockStructGroup('jh_users', ['group_admin', 'also_group_admin', 'override_admin', 'non_admin'], 1234) jh_users = MockStructGroup('jh_users', ['group_admin', 'also_group_admin', 'override_admin', 'non_admin'], 1234)
jh_admins = MockStructGroup('jh_admins', ['group_admin'], 5678) jh_admins = MockStructGroup('jh_admins', ['group_admin'], 5678)
wheel = MockStructGroup('wheel', ['also_group_admin'], 9999) wheel = MockStructGroup('wheel', ['also_group_admin'], 9999)
@@ -67,10 +64,10 @@ def test_pam_auth_admin_groups():
system_users = [group_admin, also_group_admin, override_admin, non_admin] system_users = [group_admin, also_group_admin, override_admin, non_admin]
user_group_map = { user_group_map = {
'group_admin': [jh_users, jh_admins], 'group_admin': [jh_users.gr_gid, jh_admins.gr_gid],
'also_group_admin': [jh_users, wheel], 'also_group_admin': [jh_users.gr_gid, wheel.gr_gid],
'override_admin': [jh_users], 'override_admin': [jh_users.gr_gid],
'non_admin': [jh_users] 'non_admin': [jh_users.gr_gid]
} }
def getgrnam(name): def getgrnam(name):
@@ -90,7 +87,7 @@ def test_pam_auth_admin_groups():
_getgrnam=getgrnam, _getgrnam=getgrnam,
_getpwnam=getpwnam, _getpwnam=getpwnam,
_getgrouplist=getgrouplist): _getgrouplist=getgrouplist):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'group_admin', 'username': 'group_admin',
'password': 'group_admin' 'password': 'group_admin'
}) })
@@ -102,7 +99,7 @@ def test_pam_auth_admin_groups():
_getgrnam=getgrnam, _getgrnam=getgrnam,
_getpwnam=getpwnam, _getpwnam=getpwnam,
_getgrouplist=getgrouplist): _getgrouplist=getgrouplist):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'also_group_admin', 'username': 'also_group_admin',
'password': 'also_group_admin' 'password': 'also_group_admin'
}) })
@@ -114,7 +111,7 @@ def test_pam_auth_admin_groups():
_getgrnam=getgrnam, _getgrnam=getgrnam,
_getpwnam=getpwnam, _getpwnam=getpwnam,
_getgrouplist=getgrouplist): _getgrouplist=getgrouplist):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'override_admin', 'username': 'override_admin',
'password': 'override_admin' 'password': 'override_admin'
}) })
@@ -126,7 +123,7 @@ def test_pam_auth_admin_groups():
_getgrnam=getgrnam, _getgrnam=getgrnam,
_getpwnam=getpwnam, _getpwnam=getpwnam,
_getgrouplist=getgrouplist): _getgrouplist=getgrouplist):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'non_admin', 'username': 'non_admin',
'password': 'non_admin' 'password': 'non_admin'
}) })
@@ -134,55 +131,52 @@ def test_pam_auth_admin_groups():
assert authorized['admin'] is False assert authorized['admin'] is False
@pytest.mark.gen_test async def test_pam_auth_whitelist():
def test_pam_auth_whitelist():
authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'kaylee', 'username': 'kaylee',
'password': 'kaylee', 'password': 'kaylee',
}) })
assert authorized['name'] == 'kaylee' assert authorized['name'] == 'kaylee'
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'wash', 'username': 'wash',
'password': 'nomatch', 'password': 'nomatch',
}) })
assert authorized is None assert authorized is None
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'mal', 'username': 'mal',
'password': 'mal', 'password': 'mal',
}) })
assert authorized is None assert authorized is None
@pytest.mark.gen_test async def test_pam_auth_group_whitelist():
def test_pam_auth_group_whitelist():
def getgrnam(name): def getgrnam(name):
return MockStructGroup('grp', ['kaylee']) return MockStructGroup('grp', ['kaylee'])
authenticator = MockPAMAuthenticator(group_whitelist={'group'}) authenticator = MockPAMAuthenticator(group_whitelist={'group'})
with mock.patch.object(authenticator, '_getgrnam', getgrnam): with mock.patch.object(authenticator, '_getgrnam', getgrnam):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'kaylee', 'username': 'kaylee',
'password': 'kaylee', 'password': 'kaylee',
}) })
assert authorized['name'] == 'kaylee' assert authorized['name'] == 'kaylee'
with mock.patch.object(authenticator, '_getgrnam', getgrnam): with mock.patch.object(authenticator, '_getgrnam', getgrnam):
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'mal', 'username': 'mal',
'password': 'mal', 'password': 'mal',
}) })
assert authorized is None assert authorized is None
@pytest.mark.gen_test async def test_pam_auth_blacklist():
def test_pam_auth_blacklist():
# Null case compared to next case # Null case compared to next case
authenticator = MockPAMAuthenticator() authenticator = MockPAMAuthenticator()
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'wash', 'username': 'wash',
'password': 'wash', 'password': 'wash',
}) })
@@ -190,7 +184,7 @@ def test_pam_auth_blacklist():
# Blacklist basics # Blacklist basics
authenticator = MockPAMAuthenticator(blacklist={'wash'}) authenticator = MockPAMAuthenticator(blacklist={'wash'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'wash', 'username': 'wash',
'password': 'wash', 'password': 'wash',
}) })
@@ -198,7 +192,7 @@ def test_pam_auth_blacklist():
# User in both white and blacklists: default deny. Make error someday? # User in both white and blacklists: default deny. Make error someday?
authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'wash', 'username': 'wash',
'password': 'wash', 'password': 'wash',
}) })
@@ -206,7 +200,7 @@ def test_pam_auth_blacklist():
# User not in blacklist can log in # User not in blacklist can log in
authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'kaylee', 'username': 'kaylee',
'password': 'kaylee', 'password': 'kaylee',
}) })
@@ -214,7 +208,7 @@ def test_pam_auth_blacklist():
# User in whitelist, blacklist irrelevent # User in whitelist, blacklist irrelevent
authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'wash', 'username': 'wash',
'password': 'wash', 'password': 'wash',
}) })
@@ -222,7 +216,7 @@ def test_pam_auth_blacklist():
# User in neither list # User in neither list
authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'simon', 'username': 'simon',
'password': 'simon', 'password': 'simon',
}) })
@@ -230,34 +224,31 @@ def test_pam_auth_blacklist():
# blacklist == {} # blacklist == {}
authenticator = MockPAMAuthenticator(blacklist=set(), whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(blacklist=set(), whitelist={'wash', 'kaylee'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'kaylee', 'username': 'kaylee',
'password': 'kaylee', 'password': 'kaylee',
}) })
assert authorized['name'] == 'kaylee' assert authorized['name'] == 'kaylee'
@pytest.mark.gen_test async def test_pam_auth_no_such_group():
def test_pam_auth_no_such_group():
authenticator = MockPAMAuthenticator(group_whitelist={'nosuchcrazygroup'}) authenticator = MockPAMAuthenticator(group_whitelist={'nosuchcrazygroup'})
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'kaylee', 'username': 'kaylee',
'password': 'kaylee', 'password': 'kaylee',
}) })
assert authorized is None assert authorized is None
@pytest.mark.gen_test async def test_wont_add_system_user():
def test_wont_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(whitelist={'mal'})
authenticator.create_system_users = False authenticator.create_system_users = False
with pytest.raises(KeyError): with pytest.raises(KeyError):
yield authenticator.add_user(user) await authenticator.add_user(user)
@pytest.mark.gen_test async def test_cant_add_system_user():
def test_cant_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(whitelist={'mal'})
authenticator.add_user_cmd = ['jupyterhub-fake-command'] authenticator.add_user_cmd = ['jupyterhub-fake-command']
@@ -279,12 +270,11 @@ def test_cant_add_system_user():
with mock.patch.object(auth, 'Popen', DummyPopen): with mock.patch.object(auth, 'Popen', DummyPopen):
with pytest.raises(RuntimeError) as exc: with pytest.raises(RuntimeError) as exc:
yield authenticator.add_user(user) await authenticator.add_user(user)
assert str(exc.value) == 'Failed to create system user lioness4321: dummy error' assert str(exc.value) == 'Failed to create system user lioness4321: dummy error'
@pytest.mark.gen_test async def test_add_system_user():
def test_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(whitelist={'mal'})
authenticator.create_system_users = True authenticator.create_system_users = True
@@ -300,17 +290,16 @@ def test_add_system_user():
return return
with mock.patch.object(auth, 'Popen', DummyPopen): with mock.patch.object(auth, 'Popen', DummyPopen):
yield authenticator.add_user(user) await authenticator.add_user(user)
assert record['cmd'] == ['echo', '/home/lioness4321', 'lioness4321'] assert record['cmd'] == ['echo', '/home/lioness4321', 'lioness4321']
@pytest.mark.gen_test async def test_delete_user():
def test_delete_user():
user = orm.User(name='zoe') user = orm.User(name='zoe')
a = MockPAMAuthenticator(whitelist={'mal'}) a = MockPAMAuthenticator(whitelist={'mal'})
assert 'zoe' not in a.whitelist assert 'zoe' not in a.whitelist
yield a.add_user(user) await a.add_user(user)
assert 'zoe' in a.whitelist assert 'zoe' in a.whitelist
a.delete_user(user) a.delete_user(user)
assert 'zoe' not in a.whitelist assert 'zoe' not in a.whitelist
@@ -330,47 +319,43 @@ def test_handlers(app):
assert handlers[0][0] == '/login' assert handlers[0][0] == '/login'
@pytest.mark.gen_test async def test_auth_state(app, auth_state_enabled):
def test_auth_state(app, auth_state_enabled):
"""auth_state enabled and available""" """auth_state enabled and available"""
name = 'kiwi' name = 'kiwi'
user = add_user(app.db, app, name=name) user = add_user(app.db, app, name=name)
assert user.encrypted_auth_state is None assert user.encrypted_auth_state is None
cookies = yield app.login_user(name) cookies = await app.login_user(name)
auth_state = yield user.get_auth_state() auth_state = await user.get_auth_state()
assert auth_state == app.authenticator.auth_state assert auth_state == app.authenticator.auth_state
@pytest.mark.gen_test async def test_auth_admin_non_admin(app):
def test_auth_admin_non_admin(app):
"""admin should be passed through for non-admin users""" """admin should be passed through for non-admin users"""
name = 'kiwi' name = 'kiwi'
user = add_user(app.db, app, name=name, admin=False) user = add_user(app.db, app, name=name, admin=False)
assert user.admin is False assert user.admin is False
cookies = yield app.login_user(name) cookies = await app.login_user(name)
assert user.admin is False assert user.admin is False
@pytest.mark.gen_test async def test_auth_admin_is_admin(app):
def test_auth_admin_is_admin(app):
"""admin should be passed through for admin users""" """admin should be passed through for admin users"""
# Admin user defined in MockPAMAuthenticator. # Admin user defined in MockPAMAuthenticator.
name = 'admin' name = 'admin'
user = add_user(app.db, app, name=name, admin=False) user = add_user(app.db, app, name=name, admin=False)
assert user.admin is False assert user.admin is False
cookies = yield app.login_user(name) cookies = await app.login_user(name)
assert user.admin is True assert user.admin is True
@pytest.mark.gen_test async def test_auth_admin_retained_if_unset(app):
def test_auth_admin_retained_if_unset(app):
"""admin should be unchanged if authenticator doesn't return admin value""" """admin should be unchanged if authenticator doesn't return admin value"""
name = 'kiwi' name = 'kiwi'
# Add user as admin. # Add user as admin.
user = add_user(app.db, app, name=name, admin=True) user = add_user(app.db, app, name=name, admin=True)
assert user.admin is True assert user.admin is True
# User should remain unchanged. # User should remain unchanged.
cookies = yield app.login_user(name) cookies = await app.login_user(name)
assert user.admin is True assert user.admin is True
@@ -384,56 +369,53 @@ def auth_state_unavailable(auth_state_enabled):
yield yield
@pytest.mark.gen_test async def test_auth_state_disabled(app, auth_state_unavailable):
def test_auth_state_disabled(app, auth_state_unavailable):
name = 'driebus' name = 'driebus'
user = add_user(app.db, app, name=name) user = add_user(app.db, app, name=name)
assert user.encrypted_auth_state is None assert user.encrypted_auth_state is None
with pytest.raises(HTTPError): with pytest.raises(HTTPError):
cookies = yield app.login_user(name) cookies = await app.login_user(name)
auth_state = yield user.get_auth_state() auth_state = await user.get_auth_state()
assert auth_state is None assert auth_state is None
@pytest.mark.gen_test async def test_normalize_names():
def test_normalize_names():
a = MockPAMAuthenticator() a = MockPAMAuthenticator()
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'ZOE', 'username': 'ZOE',
'password': 'ZOE', 'password': 'ZOE',
}) })
assert authorized['name'] == 'zoe' assert authorized['name'] == 'zoe'
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'Glenn', 'username': 'Glenn',
'password': 'Glenn', 'password': 'Glenn',
}) })
assert authorized['name'] == 'glenn' assert authorized['name'] == 'glenn'
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'hExi', 'username': 'hExi',
'password': 'hExi', 'password': 'hExi',
}) })
assert authorized['name'] == 'hexi' assert authorized['name'] == 'hexi'
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'Test', 'username': 'Test',
'password': 'Test', 'password': 'Test',
}) })
assert authorized['name'] == 'test' assert authorized['name'] == 'test'
@pytest.mark.gen_test async def test_username_map():
def test_username_map():
a = MockPAMAuthenticator(username_map={'wash': 'alpha'}) a = MockPAMAuthenticator(username_map={'wash': 'alpha'})
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'WASH', 'username': 'WASH',
'password': 'WASH', 'password': 'WASH',
}) })
assert authorized['name'] == 'alpha' assert authorized['name'] == 'alpha'
authorized = yield a.get_authenticated_user(None, { authorized = await a.get_authenticated_user(None, {
'username': 'Inara', 'username': 'Inara',
'password': 'Inara', 'password': 'Inara',
}) })

View File

@@ -11,6 +11,7 @@ keys = [('%i' % i).encode('ascii') * 32 for i in range(3)]
hex_keys = [ b2a_hex(key).decode('ascii') for key in keys ] hex_keys = [ b2a_hex(key).decode('ascii') for key in keys ]
b64_keys = [ b2a_base64(key).decode('ascii').strip() for key in keys ] b64_keys = [ b2a_base64(key).decode('ascii').strip() for key in keys ]
@pytest.mark.parametrize("key_env, keys", [ @pytest.mark.parametrize("key_env, keys", [
(hex_keys[0], [keys[0]]), (hex_keys[0], [keys[0]]),
(';'.join([b64_keys[0], hex_keys[1]]), keys[:2]), (';'.join([b64_keys[0], hex_keys[1]]), keys[:2]),
@@ -52,30 +53,27 @@ def crypt_keeper():
ck.keys = save_keys ck.keys = save_keys
@pytest.mark.gen_test async def test_roundtrip(crypt_keeper):
def test_roundtrip(crypt_keeper):
data = {'key': 'value'} data = {'key': 'value'}
encrypted = yield encrypt(data) encrypted = await encrypt(data)
decrypted = yield decrypt(encrypted) decrypted = await decrypt(encrypted)
assert decrypted == data assert decrypted == data
@pytest.mark.gen_test async def test_missing_crypto(crypt_keeper):
def test_missing_crypto(crypt_keeper):
with patch.object(crypto, 'cryptography', None): with patch.object(crypto, 'cryptography', None):
with pytest.raises(crypto.CryptographyUnavailable): with pytest.raises(crypto.CryptographyUnavailable):
yield encrypt({}) await encrypt({})
with pytest.raises(crypto.CryptographyUnavailable): with pytest.raises(crypto.CryptographyUnavailable):
yield decrypt(b'whatever') await decrypt(b'whatever')
@pytest.mark.gen_test async def test_missing_keys(crypt_keeper):
def test_missing_keys(crypt_keeper):
crypt_keeper.keys = [] crypt_keeper.keys = []
with pytest.raises(crypto.NoEncryptionKeys): with pytest.raises(crypto.NoEncryptionKeys):
yield encrypt({}) await encrypt({})
with pytest.raises(crypto.NoEncryptionKeys): with pytest.raises(crypto.NoEncryptionKeys):
yield decrypt(b'whatever') await decrypt(b'whatever')

View File

@@ -40,8 +40,7 @@ def generate_old_db(env_dir, hub_version, db_url):
'0.8.1', '0.8.1',
], ],
) )
@pytest.mark.gen_test async def test_upgrade(tmpdir, hub_version):
def test_upgrade(tmpdir, hub_version):
db_url = os.getenv('JUPYTERHUB_TEST_DB_URL') db_url = os.getenv('JUPYTERHUB_TEST_DB_URL')
if db_url: if db_url:
db_url += '_upgrade_' + hub_version.replace('.', '') db_url += '_upgrade_' + hub_version.replace('.', '')
@@ -69,7 +68,7 @@ def test_upgrade(tmpdir, hub_version):
assert len(sqlite_files) == 1 assert len(sqlite_files) == 1
upgradeapp = UpgradeDB(config=cfg) upgradeapp = UpgradeDB(config=cfg)
yield upgradeapp.initialize([]) upgradeapp.initialize([])
upgradeapp.start() upgradeapp.start()
# check that backup was created: # check that backup was created:

View File

@@ -7,48 +7,45 @@ import pytest
from jupyterhub.auth import DummyAuthenticator from jupyterhub.auth import DummyAuthenticator
@pytest.mark.gen_test async def test_dummy_auth_without_global_password():
def test_dummy_auth_without_global_password():
authenticator = DummyAuthenticator() authenticator = DummyAuthenticator()
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'test_user', 'username': 'test_user',
'password': 'test_pass', 'password': 'test_pass',
}) })
assert authorized['name'] == 'test_user' assert authorized['name'] == 'test_user'
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'test_user', 'username': 'test_user',
'password': '', 'password': '',
}) })
assert authorized['name'] == 'test_user' assert authorized['name'] == 'test_user'
@pytest.mark.gen_test async def test_dummy_auth_without_username():
def test_dummy_auth_without_username():
authenticator = DummyAuthenticator() authenticator = DummyAuthenticator()
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': '', 'username': '',
'password': 'test_pass', 'password': 'test_pass',
}) })
assert authorized is None assert authorized is None
@pytest.mark.gen_test async def test_dummy_auth_with_global_password():
def test_dummy_auth_with_global_password():
authenticator = DummyAuthenticator() authenticator = DummyAuthenticator()
authenticator.password = "test_password" authenticator.password = "test_password"
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'test_user', 'username': 'test_user',
'password': 'test_password', 'password': 'test_password',
}) })
assert authorized['name'] == 'test_user' assert authorized['name'] == 'test_user'
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'test_user', 'username': 'test_user',
'password': 'qwerty', 'password': 'qwerty',
}) })
assert authorized is None assert authorized is None
authorized = yield authenticator.get_authenticated_user(None, { authorized = await authenticator.get_authenticated_user(None, {
'username': 'some_other_user', 'username': 'some_other_user',
'password': 'test_password', 'password': 'test_password',
}) })

View File

@@ -39,39 +39,36 @@ def wait_for_spawner(spawner, timeout=10):
yield wait() yield wait()
@pytest.mark.gen_test async def test_connection_hub_wrong_certs(app):
def test_connection_hub_wrong_certs(app):
"""Connecting to the internal hub url fails without correct certs""" """Connecting to the internal hub url fails without correct certs"""
with pytest.raises(SSLError): with pytest.raises(SSLError):
kwargs = {'verify': False} kwargs = {'verify': False}
r = yield async_requests.get(app.hub.url, **kwargs) r = await async_requests.get(app.hub.url, **kwargs)
r.raise_for_status() r.raise_for_status()
@pytest.mark.gen_test async def test_connection_proxy_api_wrong_certs(app):
def test_connection_proxy_api_wrong_certs(app):
"""Connecting to the proxy api fails without correct certs""" """Connecting to the proxy api fails without correct certs"""
with pytest.raises(SSLError): with pytest.raises(SSLError):
kwargs = {'verify': False} kwargs = {'verify': False}
r = yield async_requests.get(app.proxy.api_url, **kwargs) r = await async_requests.get(app.proxy.api_url, **kwargs)
r.raise_for_status() r.raise_for_status()
@pytest.mark.gen_test async def test_connection_notebook_wrong_certs(app):
def test_connection_notebook_wrong_certs(app):
"""Connecting to a notebook fails without correct certs""" """Connecting to a notebook fails without correct certs"""
with mock.patch.dict( with mock.patch.dict(
app.config.LocalProcessSpawner, app.config.LocalProcessSpawner,
{'cmd': [sys.executable, '-m', 'jupyterhub.tests.mocksu']} {'cmd': [sys.executable, '-m', 'jupyterhub.tests.mocksu']}
): ):
user = add_user(app.db, app, name='foo') user = add_user(app.db, app, name='foo')
yield user.spawn() await user.spawn()
yield wait_for_spawner(user.spawner) await wait_for_spawner(user.spawner)
spawner = user.spawner spawner = user.spawner
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
with pytest.raises(SSLError): with pytest.raises(SSLError):
kwargs = {'verify': False} kwargs = {'verify': False}
r = yield async_requests.get(spawner.server.url, **kwargs) r = await async_requests.get(spawner.server.url, **kwargs)
r.raise_for_status() r.raise_for_status()

View File

@@ -17,16 +17,15 @@ def named_servers(app):
yield yield
@pytest.mark.gen_test async def test_default_server(app, named_servers):
def test_default_server(app, named_servers):
"""Test the default /users/:user/server handler when named servers are enabled""" """Test the default /users/:user/server handler when named servers are enabled"""
username = 'rosie' username = 'rosie'
user = add_user(app.db, app, name=username) user = add_user(app.db, app, name=username)
r = yield api_request(app, 'users', username, 'server', method='post') r = await api_request(app, 'users', username, 'server', method='post')
assert r.status_code == 201 assert r.status_code == 201
assert r.text == '' assert r.text == ''
r = yield api_request(app, 'users', username) r = await api_request(app, 'users', username)
r.raise_for_status() r.raise_for_status()
user_model = normalize_user(r.json()) user_model = normalize_user(r.json())
@@ -51,11 +50,11 @@ def test_default_server(app, named_servers):
# now stop the server # now stop the server
r = yield api_request(app, 'users', username, 'server', method='delete') r = await api_request(app, 'users', username, 'server', method='delete')
assert r.status_code == 204 assert r.status_code == 204
assert r.text == '' assert r.text == ''
r = yield api_request(app, 'users', username) r = await api_request(app, 'users', username)
r.raise_for_status() r.raise_for_status()
user_model = normalize_user(r.json()) user_model = normalize_user(r.json())
@@ -66,20 +65,19 @@ def test_default_server(app, named_servers):
}) })
@pytest.mark.gen_test async def test_create_named_server(app, named_servers):
def test_create_named_server(app, named_servers):
username = 'walnut' username = 'walnut'
user = add_user(app.db, app, name=username) user = add_user(app.db, app, name=username)
# assert user.allow_named_servers == True # assert user.allow_named_servers == True
cookies = yield app.login_user(username) cookies = await app.login_user(username)
servername = 'trevor' servername = 'trevor'
r = yield api_request(app, 'users', username, 'servers', servername, method='post') r = await api_request(app, 'users', username, 'servers', servername, method='post')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 201 assert r.status_code == 201
assert r.text == '' assert r.text == ''
url = url_path_join(public_url(app, user), servername, 'env') url = url_path_join(public_url(app, user), servername, 'env')
r = yield async_requests.get(url, cookies=cookies) r = await async_requests.get(url, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.url == url assert r.url == url
env = r.json() env = r.json()
@@ -87,7 +85,7 @@ def test_create_named_server(app, named_servers):
assert prefix == user.spawners[servername].server.base_url assert prefix == user.spawners[servername].server.base_url
assert prefix.endswith('/user/%s/%s/' % (username, servername)) assert prefix.endswith('/user/%s/%s/' % (username, servername))
r = yield api_request(app, 'users', username) r = await api_request(app, 'users', username)
r.raise_for_status() r.raise_for_status()
user_model = normalize_user(r.json()) user_model = normalize_user(r.json())
@@ -111,22 +109,21 @@ def test_create_named_server(app, named_servers):
}) })
@pytest.mark.gen_test async def test_delete_named_server(app, named_servers):
def test_delete_named_server(app, named_servers):
username = 'donaar' username = 'donaar'
user = add_user(app.db, app, name=username) user = add_user(app.db, app, name=username)
assert user.allow_named_servers assert user.allow_named_servers
cookies = app.login_user(username) cookies = app.login_user(username)
servername = 'splugoth' servername = 'splugoth'
r = yield api_request(app, 'users', username, 'servers', servername, method='post') r = await api_request(app, 'users', username, 'servers', servername, method='post')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 201 assert r.status_code == 201
r = yield api_request(app, 'users', username, 'servers', servername, method='delete') r = await api_request(app, 'users', username, 'servers', servername, method='delete')
r.raise_for_status() r.raise_for_status()
assert r.status_code == 204 assert r.status_code == 204
r = yield api_request(app, 'users', username) r = await api_request(app, 'users', username)
r.raise_for_status() r.raise_for_status()
user_model = normalize_user(r.json()) user_model = normalize_user(r.json())
@@ -140,7 +137,7 @@ def test_delete_named_server(app, named_servers):
# low-level record still exists # low-level record still exists
assert servername in user.orm_spawners assert servername in user.orm_spawners
r = yield api_request( r = await api_request(
app, 'users', username, 'servers', servername, app, 'users', username, 'servers', servername,
method='delete', method='delete',
data=json.dumps({'remove': True}), data=json.dumps({'remove': True}),
@@ -151,11 +148,10 @@ def test_delete_named_server(app, named_servers):
assert servername not in user.orm_spawners assert servername not in user.orm_spawners
@pytest.mark.gen_test async def test_named_server_disabled(app):
def test_named_server_disabled(app):
username = 'user' username = 'user'
servername = 'okay' servername = 'okay'
r = yield api_request(app, 'users', username, 'servers', servername, method='post') r = await api_request(app, 'users', username, 'servers', servername, method='post')
assert r.status_code == 400 assert r.status_code == 400
r = yield api_request(app, 'users', username, 'servers', servername, method='delete') r = await api_request(app, 'users', username, 'servers', servername, method='delete')
assert r.status_code == 400 assert r.status_code == 400

View File

@@ -205,8 +205,7 @@ def test_token_find(db):
assert found is None assert found is None
@pytest.mark.gen_test async def test_spawn_fails(db):
def test_spawn_fails(db):
orm_user = orm.User(name='aeofel') orm_user = orm.User(name='aeofel')
db.add(orm_user) db.add(orm_user)
db.commit() db.commit()
@@ -223,7 +222,7 @@ def test_spawn_fails(db):
}) })
with pytest.raises(RuntimeError) as exc: with pytest.raises(RuntimeError) as exc:
yield user.spawn() await user.spawn()
assert user.spawners[''].server is None assert user.spawners[''].server is None
assert not user.running assert not user.running
@@ -246,8 +245,7 @@ def test_groups(db):
assert group.users == [] assert group.users == []
@pytest.mark.gen_test async def test_auth_state(db):
def test_auth_state(db):
orm_user = orm.User(name='eve') orm_user = orm.User(name='eve')
db.add(orm_user) db.add(orm_user)
db.commit() db.commit()
@@ -262,51 +260,51 @@ def test_auth_state(db):
state = {'key': 'value'} state = {'key': 'value'}
ck.keys = [] ck.keys = []
with pytest.raises(crypto.EncryptionUnavailable): with pytest.raises(crypto.EncryptionUnavailable):
yield user.save_auth_state(state) await user.save_auth_state(state)
assert user.encrypted_auth_state is None assert user.encrypted_auth_state is None
# saving/loading None doesn't require keys # saving/loading None doesn't require keys
yield user.save_auth_state(None) await user.save_auth_state(None)
current = yield user.get_auth_state() current = await user.get_auth_state()
assert current is None assert current is None
first_key = os.urandom(32) first_key = os.urandom(32)
second_key = os.urandom(32) second_key = os.urandom(32)
ck.keys = [first_key] ck.keys = [first_key]
yield user.save_auth_state(state) await user.save_auth_state(state)
assert user.encrypted_auth_state is not None assert user.encrypted_auth_state is not None
decrypted_state = yield user.get_auth_state() decrypted_state = await user.get_auth_state()
assert decrypted_state == state assert decrypted_state == state
# can't read auth_state without keys # can't read auth_state without keys
ck.keys = [] ck.keys = []
auth_state = yield user.get_auth_state() auth_state = await user.get_auth_state()
assert auth_state is None assert auth_state is None
# key rotation works # key rotation works
db.rollback() db.rollback()
ck.keys = [second_key, first_key] ck.keys = [second_key, first_key]
decrypted_state = yield user.get_auth_state() decrypted_state = await user.get_auth_state()
assert decrypted_state == state assert decrypted_state == state
new_state = {'key': 'newvalue'} new_state = {'key': 'newvalue'}
yield user.save_auth_state(new_state) await user.save_auth_state(new_state)
db.commit() db.commit()
ck.keys = [first_key] ck.keys = [first_key]
db.rollback() db.rollback()
# can't read anymore with new-key after encrypting with second-key # can't read anymore with new-key after encrypting with second-key
decrypted_state = yield user.get_auth_state() decrypted_state = await user.get_auth_state()
assert decrypted_state is None assert decrypted_state is None
yield user.save_auth_state(new_state) await user.save_auth_state(new_state)
decrypted_state = yield user.get_auth_state() decrypted_state = await user.get_auth_state()
assert decrypted_state == new_state assert decrypted_state == new_state
ck.keys = [] ck.keys = []
db.rollback() db.rollback()
decrypted_state = yield user.get_auth_state() decrypted_state = await user.get_auth_state()
assert decrypted_state is None assert decrypted_state is None

View File

@@ -30,91 +30,81 @@ def get_page(path, app, hub=True, **kw):
return async_requests.get(ujoin(base_url, path), **kw) return async_requests.get(ujoin(base_url, path), **kw)
@pytest.mark.gen_test async def test_root_no_auth(app):
def test_root_no_auth(app):
url = ujoin(public_host(app), app.hub.base_url) url = ujoin(public_host(app), app.hub.base_url)
r = yield async_requests.get(url) r = await async_requests.get(url)
r.raise_for_status() r.raise_for_status()
assert r.url == ujoin(url, 'login') assert r.url == ujoin(url, 'login')
@pytest.mark.gen_test async def test_root_auth(app):
def test_root_auth(app): cookies = await app.login_user('river')
cookies = yield app.login_user('river') r = await async_requests.get(public_url(app), cookies=cookies)
r = yield async_requests.get(public_url(app), cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.url.startswith(public_url(app, app.users['river'])) assert r.url.startswith(public_url(app, app.users['river']))
@pytest.mark.gen_test async def test_root_redirect(app):
def test_root_redirect(app):
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
next_url = ujoin(app.base_url, 'user/other/test.ipynb') next_url = ujoin(app.base_url, 'user/other/test.ipynb')
url = '/?' + urlencode({'next': next_url}) url = '/?' + urlencode({'next': next_url})
r = yield get_page(url, app, cookies=cookies) r = await get_page(url, app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'user/%s/test.ipynb' % name) assert path == ujoin(app.base_url, 'user/%s/test.ipynb' % name)
@pytest.mark.gen_test async def test_root_default_url_noauth(app):
def test_root_default_url_noauth(app):
with mock.patch.dict(app.tornado_settings, with mock.patch.dict(app.tornado_settings,
{'default_url': '/foo/bar'}): {'default_url': '/foo/bar'}):
r = yield get_page('/', app, allow_redirects=False) r = await get_page('/', app, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
url = r.headers.get('Location', '') url = r.headers.get('Location', '')
path = urlparse(url).path path = urlparse(url).path
assert path == '/foo/bar' assert path == '/foo/bar'
@pytest.mark.gen_test async def test_root_default_url_auth(app):
def test_root_default_url_auth(app):
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
with mock.patch.dict(app.tornado_settings, with mock.patch.dict(app.tornado_settings,
{'default_url': '/foo/bar'}): {'default_url': '/foo/bar'}):
r = yield get_page('/', app, cookies=cookies, allow_redirects=False) r = await get_page('/', app, cookies=cookies, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
url = r.headers.get('Location', '') url = r.headers.get('Location', '')
path = urlparse(url).path path = urlparse(url).path
assert path == '/foo/bar' assert path == '/foo/bar'
@pytest.mark.gen_test async def test_home_no_auth(app):
def test_home_no_auth(app): r = await get_page('home', app, allow_redirects=False)
r = yield get_page('home', app, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 302 assert r.status_code == 302
assert '/hub/login' in r.headers['Location'] assert '/hub/login' in r.headers['Location']
@pytest.mark.gen_test async def test_home_auth(app):
def test_home_auth(app): cookies = await app.login_user('river')
cookies = yield app.login_user('river') r = await get_page('home', app, cookies=cookies)
r = yield get_page('home', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.url.endswith('home') assert r.url.endswith('home')
@pytest.mark.gen_test async def test_admin_no_auth(app):
def test_admin_no_auth(app): r = await get_page('admin', app)
r = yield get_page('admin', app)
assert r.status_code == 403 assert r.status_code == 403
@pytest.mark.gen_test async def test_admin_not_admin(app):
def test_admin_not_admin(app): cookies = await app.login_user('wash')
cookies = yield app.login_user('wash') r = await get_page('admin', app, cookies=cookies)
r = yield get_page('admin', app, cookies=cookies)
assert r.status_code == 403 assert r.status_code == 403
@pytest.mark.gen_test async def test_admin(app):
def test_admin(app): cookies = await app.login_user('admin')
cookies = yield app.login_user('admin') r = await get_page('admin', app, cookies=cookies, allow_redirects=False)
r = yield get_page('admin', app, cookies=cookies, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.url.endswith('/admin') assert r.url.endswith('/admin')
@@ -125,127 +115,120 @@ def test_admin(app):
'admin', 'admin',
'name', 'name',
]) ])
@pytest.mark.gen_test async def test_admin_sort(app, sort):
def test_admin_sort(app, sort): cookies = await app.login_user('admin')
cookies = yield app.login_user('admin') r = await get_page('admin?sort=%s' % sort, app, cookies=cookies)
r = yield get_page('admin?sort=%s' % sort, app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
@pytest.mark.gen_test async def test_spawn_redirect(app):
def test_spawn_redirect(app):
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
u = app.users[orm.User.find(app.db, name)] u = app.users[orm.User.find(app.db, name)]
status = yield u.spawner.poll() status = await u.spawner.poll()
assert status is not None assert status is not None
# test spawn page when no server is running # test spawn page when no server is running
r = yield get_page('spawn', app, cookies=cookies) r = await get_page('spawn', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'user/%s/' % name) assert path == ujoin(app.base_url, 'user/%s/' % name)
# should have started server # should have started server
status = yield u.spawner.poll() status = await u.spawner.poll()
assert status is None assert status is None
# test spawn page when server is already running (just redirect) # test spawn page when server is already running (just redirect)
r = yield get_page('spawn', app, cookies=cookies) r = await get_page('spawn', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, '/user/%s/' % name) assert path == ujoin(app.base_url, '/user/%s/' % name)
# stop server to ensure /user/name is handled by the Hub # stop server to ensure /user/name is handled by the Hub
r = yield api_request(app, 'users', name, 'server', method='delete', cookies=cookies) r = await api_request(app, 'users', name, 'server', method='delete', cookies=cookies)
r.raise_for_status() r.raise_for_status()
# test handing of trailing slash on `/user/name` # test handing of trailing slash on `/user/name`
r = yield get_page('user/' + name, app, hub=False, cookies=cookies) r = await get_page('user/' + name, app, hub=False, cookies=cookies)
r.raise_for_status() r.raise_for_status()
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, '/user/%s/' % name) assert path == ujoin(app.base_url, '/user/%s/' % name)
@pytest.mark.gen_test async def test_spawn_handler_access(app):
def test_spawn_handler_access(app):
name = 'winston' name = 'winston'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
u = app.users[orm.User.find(app.db, name)] u = app.users[orm.User.find(app.db, name)]
status = yield u.spawner.poll() status = await u.spawner.poll()
assert status is not None assert status is not None
# spawn server via browser link with ?arg=value # spawn server via browser link with ?arg=value
r = yield get_page('spawn', app, cookies=cookies, params={'arg': 'value'}) r = await get_page('spawn', app, cookies=cookies, params={'arg': 'value'})
r.raise_for_status() r.raise_for_status()
# verify that request params got passed down # verify that request params got passed down
# implemented in MockSpawner # implemented in MockSpawner
r = yield async_requests.get(ujoin(public_url(app, u), 'env')) r = await async_requests.get(ujoin(public_url(app, u), 'env'))
env = r.json() env = r.json()
assert 'HANDLER_ARGS' in env assert 'HANDLER_ARGS' in env
assert env['HANDLER_ARGS'] == 'arg=value' assert env['HANDLER_ARGS'] == 'arg=value'
# stop server # stop server
r = yield api_request(app, 'users', name, 'server', method='delete') r = await api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status() r.raise_for_status()
@pytest.mark.gen_test async def test_spawn_admin_access(app, admin_access):
def test_spawn_admin_access(app, admin_access):
"""GET /user/:name as admin with admin-access spawns user's server""" """GET /user/:name as admin with admin-access spawns user's server"""
cookies = yield app.login_user('admin') cookies = await app.login_user('admin')
name = 'mariel' name = 'mariel'
user = add_user(app.db, app=app, name=name) user = add_user(app.db, app=app, name=name)
app.db.commit() app.db.commit()
r = yield get_page('user/' + name, app, cookies=cookies) r = await get_page('user/' + name, app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert (r.url.split('?')[0] + '/').startswith(public_url(app, user)) assert (r.url.split('?')[0] + '/').startswith(public_url(app, user))
r = yield get_page('user/{}/env'.format(name), app, hub=False, cookies=cookies) r = await get_page('user/{}/env'.format(name), app, hub=False, cookies=cookies)
r.raise_for_status() r.raise_for_status()
env = r.json() env = r.json()
assert env['JUPYTERHUB_USER'] == name assert env['JUPYTERHUB_USER'] == name
@pytest.mark.gen_test async def test_spawn_page(app):
def test_spawn_page(app):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}): with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
cookies = yield app.login_user('jones') cookies = await app.login_user('jones')
r = yield get_page('spawn', app, cookies=cookies) r = await get_page('spawn', app, cookies=cookies)
assert r.url.endswith('/spawn') assert r.url.endswith('/spawn')
assert FormSpawner.options_form in r.text assert FormSpawner.options_form in r.text
r = yield get_page('spawn?next=foo', app, cookies=cookies) r = await get_page('spawn?next=foo', app, cookies=cookies)
assert r.url.endswith('/spawn?next=foo') assert r.url.endswith('/spawn?next=foo')
assert FormSpawner.options_form in r.text assert FormSpawner.options_form in r.text
@pytest.mark.gen_test async def test_spawn_page_admin(app, admin_access):
def test_spawn_page_admin(app, admin_access):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}): with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
cookies = yield app.login_user('admin') cookies = await app.login_user('admin')
u = add_user(app.db, app=app, name='melanie') u = add_user(app.db, app=app, name='melanie')
r = yield get_page('spawn/' + u.name, app, cookies=cookies) r = await get_page('spawn/' + u.name, app, cookies=cookies)
assert r.url.endswith('/spawn/' + u.name) assert r.url.endswith('/spawn/' + u.name)
assert FormSpawner.options_form in r.text assert FormSpawner.options_form in r.text
assert "Spawning server for {}".format(u.name) in r.text assert "Spawning server for {}".format(u.name) in r.text
@pytest.mark.gen_test async def test_spawn_form(app):
def test_spawn_form(app):
with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}): with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url) base_url = ujoin(public_host(app), app.hub.base_url)
cookies = yield app.login_user('jones') cookies = await app.login_user('jones')
orm_u = orm.User.find(app.db, 'jones') orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u] u = app.users[orm_u]
yield u.stop() await u.stop()
next_url = ujoin(app.base_url, 'user/jones/tree') next_url = ujoin(app.base_url, 'user/jones/tree')
r = yield async_requests.post( r = await async_requests.post(
url_concat(ujoin(base_url, 'spawn'), {'next': next_url}), url_concat(ujoin(base_url, 'spawn'), {'next': next_url}),
cookies=cookies, cookies=cookies,
data={'bounds': ['-1', '1'], 'energy': '511keV'}, data={'bounds': ['-1', '1'], 'energy': '511keV'},
@@ -259,15 +242,14 @@ def test_spawn_form(app):
} }
@pytest.mark.gen_test async def test_spawn_form_admin_access(app, admin_access):
def test_spawn_form_admin_access(app, admin_access):
with mock.patch.dict(app.tornado_settings, {'spawner_class': FormSpawner}): with mock.patch.dict(app.tornado_settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url) base_url = ujoin(public_host(app), app.hub.base_url)
cookies = yield app.login_user('admin') cookies = await app.login_user('admin')
u = add_user(app.db, app=app, name='martha') u = add_user(app.db, app=app, name='martha')
next_url = ujoin(app.base_url, 'user', u.name, 'tree') next_url = ujoin(app.base_url, 'user', u.name, 'tree')
r = yield async_requests.post( r = await async_requests.post(
url_concat(ujoin(base_url, 'spawn', u.name), {'next': next_url}), url_concat(ujoin(base_url, 'spawn', u.name), {'next': next_url}),
cookies=cookies, cookies=cookies,
data={'bounds': ['-3', '3'], 'energy': '938MeV'}, data={'bounds': ['-3', '3'], 'energy': '938MeV'},
@@ -282,16 +264,15 @@ def test_spawn_form_admin_access(app, admin_access):
} }
@pytest.mark.gen_test async def test_spawn_form_with_file(app):
def test_spawn_form_with_file(app):
with mock.patch.dict(app.tornado_settings, {'spawner_class': FormSpawner}): with mock.patch.dict(app.tornado_settings, {'spawner_class': FormSpawner}):
base_url = ujoin(public_host(app), app.hub.base_url) base_url = ujoin(public_host(app), app.hub.base_url)
cookies = yield app.login_user('jones') cookies = await app.login_user('jones')
orm_u = orm.User.find(app.db, 'jones') orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u] u = app.users[orm_u]
yield u.stop() await u.stop()
r = yield async_requests.post(ujoin(base_url, 'spawn'), r = await async_requests.post(ujoin(base_url, 'spawn'),
cookies=cookies, cookies=cookies,
data={ data={
'bounds': ['-1', '1'], 'bounds': ['-1', '1'],
@@ -310,12 +291,11 @@ def test_spawn_form_with_file(app):
} }
@pytest.mark.gen_test async def test_user_redirect(app):
def test_user_redirect(app):
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
r = yield get_page('/user-redirect/tree/top/', app) r = await get_page('/user-redirect/tree/top/', app)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
@@ -325,32 +305,31 @@ def test_user_redirect(app):
'next': ujoin(app.hub.base_url, '/user-redirect/tree/top/') 'next': ujoin(app.hub.base_url, '/user-redirect/tree/top/')
}) })
r = yield get_page('/user-redirect/notebooks/test.ipynb', app, cookies=cookies) r = await get_page('/user-redirect/notebooks/test.ipynb', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, '/user/%s/notebooks/test.ipynb' % name) assert path == ujoin(app.base_url, '/user/%s/notebooks/test.ipynb' % name)
@pytest.mark.gen_test async def test_user_redirect_deprecated(app):
def test_user_redirect_deprecated(app):
"""redirecting from /user/someonelse/ URLs (deprecated)""" """redirecting from /user/someonelse/ URLs (deprecated)"""
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
r = yield get_page('/user/baduser', app, cookies=cookies, hub=False) r = await get_page('/user/baduser', app, cookies=cookies, hub=False)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, '/user/%s/' % name) assert path == ujoin(app.base_url, '/user/%s/' % name)
r = yield get_page('/user/baduser/test.ipynb', app, cookies=cookies, hub=False) r = await get_page('/user/baduser/test.ipynb', app, cookies=cookies, hub=False)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
assert path == ujoin(app.base_url, '/user/%s/test.ipynb' % name) assert path == ujoin(app.base_url, '/user/%s/test.ipynb' % name)
r = yield get_page('/user/baduser/test.ipynb', app, hub=False) r = await get_page('/user/baduser/test.ipynb', app, hub=False)
r.raise_for_status() r.raise_for_status()
print(urlparse(r.url)) print(urlparse(r.url))
path = urlparse(r.url).path path = urlparse(r.url).path
@@ -361,11 +340,10 @@ def test_user_redirect_deprecated(app):
}) })
@pytest.mark.gen_test async def test_login_fail(app):
def test_login_fail(app):
name = 'wash' name = 'wash'
base_url = public_url(app) base_url = public_url(app)
r = yield async_requests.post(base_url + 'hub/login', r = await async_requests.post(base_url + 'hub/login',
data={ data={
'username': name, 'username': name,
'password': 'wrong', 'password': 'wrong',
@@ -375,8 +353,7 @@ def test_login_fail(app):
assert not r.cookies assert not r.cookies
@pytest.mark.gen_test async def test_login_strip(app):
def test_login_strip(app):
"""Test that login form doesn't strip whitespace from passwords""" """Test that login form doesn't strip whitespace from passwords"""
form_data = { form_data = {
'username': 'spiff', 'username': 'spiff',
@@ -389,7 +366,7 @@ def test_login_strip(app):
called_with.append(data) called_with.append(data)
with mock.patch.object(app.authenticator, 'authenticate', mock_authenticate): with mock.patch.object(app.authenticator, 'authenticate', mock_authenticate):
yield async_requests.post(base_url + 'hub/login', await async_requests.post(base_url + 'hub/login',
data=form_data, data=form_data,
allow_redirects=False, allow_redirects=False,
) )
@@ -415,9 +392,8 @@ def test_login_strip(app):
(False, '//other.domain', ''), (False, '//other.domain', ''),
] ]
) )
@pytest.mark.gen_test async def test_login_redirect(app, running, next_url, location):
def test_login_redirect(app, running, next_url, location): cookies = await app.login_user('river')
cookies = yield app.login_user('river')
user = app.users['river'] user = app.users['river']
if location: if location:
location = ujoin(app.base_url, location) location = ujoin(app.base_url, location)
@@ -433,18 +409,17 @@ def test_login_redirect(app, running, next_url, location):
if running and not user.active: if running and not user.active:
# ensure running # ensure running
yield user.spawn() await user.spawn()
elif user.active and not running: elif user.active and not running:
# ensure not running # ensure not running
yield user.stop() await user.stop()
r = yield get_page(url, app, cookies=cookies, allow_redirects=False) r = await get_page(url, app, cookies=cookies, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 302 assert r.status_code == 302
assert location == r.headers['Location'] assert location == r.headers['Location']
@pytest.mark.gen_test async def test_auto_login(app, request):
def test_auto_login(app, request):
class DummyLoginHandler(BaseHandler): class DummyLoginHandler(BaseHandler):
def get(self): def get(self):
self.write('ok!') self.write('ok!')
@@ -453,7 +428,7 @@ def test_auto_login(app, request):
(ujoin(app.hub.base_url, 'dummy'), DummyLoginHandler), (ujoin(app.hub.base_url, 'dummy'), DummyLoginHandler),
]) ])
# no auto_login: end up at /hub/login # no auto_login: end up at /hub/login
r = yield async_requests.get(base_url) r = await async_requests.get(base_url)
assert r.url == public_url(app, path='hub/login') assert r.url == public_url(app, path='hub/login')
# enable auto_login: redirect from /hub/login to /hub/dummy # enable auto_login: redirect from /hub/login to /hub/dummy
authenticator = Authenticator(auto_login=True) authenticator = Authenticator(auto_login=True)
@@ -462,28 +437,28 @@ def test_auto_login(app, request):
with mock.patch.dict(app.tornado_settings, { with mock.patch.dict(app.tornado_settings, {
'authenticator': authenticator, 'authenticator': authenticator,
}): }):
r = yield async_requests.get(base_url) r = await async_requests.get(base_url)
assert r.url == public_url(app, path='hub/dummy') assert r.url == public_url(app, path='hub/dummy')
@pytest.mark.gen_test
def test_auto_login_logout(app): async def test_auto_login_logout(app):
name = 'burnham' name = 'burnham'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
with mock.patch.dict(app.tornado_settings, { with mock.patch.dict(app.tornado_settings, {
'authenticator': Authenticator(auto_login=True), 'authenticator': Authenticator(auto_login=True),
}): }):
r = yield async_requests.get(public_host(app) + app.tornado_settings['logout_url'], cookies=cookies) r = await async_requests.get(public_host(app) + app.tornado_settings['logout_url'], cookies=cookies)
r.raise_for_status() r.raise_for_status()
logout_url = public_host(app) + app.tornado_settings['logout_url'] logout_url = public_host(app) + app.tornado_settings['logout_url']
assert r.url == logout_url assert r.url == logout_url
assert r.cookies == {} assert r.cookies == {}
@pytest.mark.gen_test
def test_logout(app): async def test_logout(app):
name = 'wash' name = 'wash'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
r = yield async_requests.get(public_host(app) + app.tornado_settings['logout_url'], cookies=cookies) r = await async_requests.get(public_host(app) + app.tornado_settings['logout_url'], cookies=cookies)
r.raise_for_status() r.raise_for_status()
login_url = public_host(app) + app.tornado_settings['login_url'] login_url = public_host(app) + app.tornado_settings['login_url']
assert r.url == login_url assert r.url == login_url
@@ -491,7 +466,6 @@ def test_logout(app):
@pytest.mark.parametrize('shutdown_on_logout', [True, False]) @pytest.mark.parametrize('shutdown_on_logout', [True, False])
@pytest.mark.gen_test
async def test_shutdown_on_logout(app, shutdown_on_logout): async def test_shutdown_on_logout(app, shutdown_on_logout):
name = 'shutitdown' name = 'shutitdown'
cookies = await app.login_user(name) cookies = await app.login_user(name)
@@ -535,50 +509,46 @@ async def test_shutdown_on_logout(app, shutdown_on_logout):
assert spawner.ready == (not shutdown_on_logout) assert spawner.ready == (not shutdown_on_logout)
@pytest.mark.gen_test async def test_login_no_whitelist_adds_user(app):
def test_login_no_whitelist_adds_user(app):
auth = app.authenticator auth = app.authenticator
mock_add_user = mock.Mock() mock_add_user = mock.Mock()
with mock.patch.object(auth, 'add_user', mock_add_user): with mock.patch.object(auth, 'add_user', mock_add_user):
cookies = yield app.login_user('jubal') cookies = await app.login_user('jubal')
user = app.users['jubal'] user = app.users['jubal']
assert mock_add_user.mock_calls == [mock.call(user)] assert mock_add_user.mock_calls == [mock.call(user)]
@pytest.mark.gen_test async def test_static_files(app):
def test_static_files(app):
base_url = ujoin(public_host(app), app.hub.base_url) base_url = ujoin(public_host(app), app.hub.base_url)
r = yield async_requests.get(ujoin(base_url, 'logo')) r = await async_requests.get(ujoin(base_url, 'logo'))
r.raise_for_status() r.raise_for_status()
assert r.headers['content-type'] == 'image/png' assert r.headers['content-type'] == 'image/png'
r = yield async_requests.get(ujoin(base_url, 'static', 'images', 'jupyter.png')) r = await async_requests.get(ujoin(base_url, 'static', 'images', 'jupyter.png'))
r.raise_for_status() r.raise_for_status()
assert r.headers['content-type'] == 'image/png' assert r.headers['content-type'] == 'image/png'
r = yield async_requests.get(ujoin(base_url, 'static', 'css', 'style.min.css')) r = await async_requests.get(ujoin(base_url, 'static', 'css', 'style.min.css'))
r.raise_for_status() r.raise_for_status()
assert r.headers['content-type'] == 'text/css' assert r.headers['content-type'] == 'text/css'
@pytest.mark.gen_test async def test_token_auth(app):
def test_token_auth(app): cookies = await app.login_user('token')
cookies = yield app.login_user('token') r = await get_page('token', app, cookies=cookies)
r = yield get_page('token', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
@pytest.mark.gen_test async def test_oauth_token_page(app):
def test_oauth_token_page(app):
name = 'token' name = 'token'
cookies = yield app.login_user(name) cookies = await app.login_user(name)
user = app.users[orm.User.find(app.db, name)] user = app.users[orm.User.find(app.db, name)]
client = orm.OAuthClient(identifier='token') client = orm.OAuthClient(identifier='token')
app.db.add(client) app.db.add(client)
oauth_token = orm.OAuthAccessToken(client=client, user=user, grant_type=orm.GrantType.authorization_code) oauth_token = orm.OAuthAccessToken(client=client, user=user, grant_type=orm.GrantType.authorization_code)
app.db.add(oauth_token) app.db.add(oauth_token)
app.db.commit() app.db.commit()
r = yield get_page('token', app, cookies=cookies) r = await get_page('token', app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
@@ -587,14 +557,11 @@ def test_oauth_token_page(app):
503, 503,
404, 404,
]) ])
async def test_proxy_error(app, error_status):
@pytest.mark.gen_test r = await get_page('/error/%i' % error_status, app)
def test_proxy_error(app, error_status):
r = yield get_page('/error/%i' % error_status, app)
assert r.status_code == 200 assert r.status_code == 200
@pytest.mark.gen_test
@pytest.mark.parametrize( @pytest.mark.parametrize(
"announcements", "announcements",
[ [
@@ -604,7 +571,7 @@ def test_proxy_error(app, error_status):
"login,logout", "login,logout",
] ]
) )
def test_announcements(app, announcements): async def test_announcements(app, announcements):
"""Test announcements on various pages""" """Test announcements on various pages"""
# Default announcement - same on all pages # Default announcement - same on all pages
ann01 = "ANNOUNCE01" ann01 = "ANNOUNCE01"
@@ -620,26 +587,26 @@ def test_announcements(app, announcements):
else: else:
assert ann01 in text assert ann01 in text
cookies = yield app.login_user("jones") cookies = await app.login_user("jones")
with mock.patch.dict( with mock.patch.dict(
app.tornado_settings, app.tornado_settings,
{"template_vars": template_vars, "spawner_class": FormSpawner}, {"template_vars": template_vars, "spawner_class": FormSpawner},
): ):
r = yield get_page("login", app) r = await get_page("login", app)
r.raise_for_status() r.raise_for_status()
assert_announcement("login", r.text) assert_announcement("login", r.text)
r = yield get_page("spawn", app, cookies=cookies) r = await get_page("spawn", app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert_announcement("spawn", r.text) assert_announcement("spawn", r.text)
r = yield get_page("home", app, cookies=cookies) # hub/home r = await get_page("home", app, cookies=cookies) # hub/home
r.raise_for_status() r.raise_for_status()
assert_announcement("home", r.text) assert_announcement("home", r.text)
# need auto_login=True to get logout page # need auto_login=True to get logout page
auto_login = app.authenticator.auto_login auto_login = app.authenticator.auto_login
app.authenticator.auto_login = True app.authenticator.auto_login = True
try: try:
r = yield get_page("logout", app, cookies=cookies) r = await get_page("logout", app, cookies=cookies)
finally: finally:
app.authenticator.auto_login = auto_login app.authenticator.auto_login = auto_login
r.raise_for_status() r.raise_for_status()
@@ -654,18 +621,16 @@ def test_announcements(app, announcements):
"redirect_uri=ok&client_id=nosuchthing", "redirect_uri=ok&client_id=nosuchthing",
] ]
) )
@pytest.mark.gen_test async def test_bad_oauth_get(app, params):
def test_bad_oauth_get(app, params): cookies = await app.login_user("authorizer")
cookies = yield app.login_user("authorizer") r = await get_page("hub/api/oauth2/authorize?" + params, app, hub=False, cookies=cookies)
r = yield get_page("hub/api/oauth2/authorize?" + params, app, hub=False, cookies=cookies)
assert r.status_code == 400 assert r.status_code == 400
@pytest.mark.gen_test async def test_token_page(app):
def test_token_page(app):
name = "cake" name = "cake"
cookies = yield app.login_user(name) cookies = await app.login_user(name)
r = yield get_page("token", app, cookies=cookies) r = await get_page("token", app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert urlparse(r.url).path.endswith('/hub/token') assert urlparse(r.url).path.endswith('/hub/token')
def extract_body(r): def extract_body(r):
@@ -684,7 +649,7 @@ def test_token_page(app):
token = user.new_api_token(expires_in=60, note="my-test-token") token = user.new_api_token(expires_in=60, note="my-test-token")
app.db.commit() app.db.commit()
r = yield get_page("token", app, cookies=cookies) r = await get_page("token", app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
body = extract_body(r) body = extract_body(r)
assert "API Tokens" in body, body assert "API Tokens" in body, body
@@ -695,10 +660,10 @@ def test_token_page(app):
# spawn the user to trigger oauth, etc. # spawn the user to trigger oauth, etc.
# request an oauth token # request an oauth token
user.spawner.cmd = [sys.executable, '-m', 'jupyterhub.singleuser'] user.spawner.cmd = [sys.executable, '-m', 'jupyterhub.singleuser']
r = yield get_page("spawn", app, cookies=cookies) r = await get_page("spawn", app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
r = yield get_page("token", app, cookies=cookies) r = await get_page("token", app, cookies=cookies)
r.raise_for_status() r.raise_for_status()
body = extract_body(r) body = extract_body(r)
assert "API Tokens" in body, body assert "API Tokens" in body, body
@@ -706,31 +671,27 @@ def test_token_page(app):
assert "Authorized Applications" in body, body assert "Authorized Applications" in body, body
@pytest.mark.gen_test async def test_server_not_running_api_request(app):
def test_server_not_running_api_request(app): cookies = await app.login_user("bees")
cookies = yield app.login_user("bees") r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
r = yield get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 404 assert r.status_code == 404
assert r.headers["content-type"] == "application/json" assert r.headers["content-type"] == "application/json"
assert r.json() == {"message": "bees is not running"} assert r.json() == {"message": "bees is not running"}
@pytest.mark.gen_test async def test_metrics_no_auth(app):
def test_metrics_no_auth(app): r = await get_page("metrics", app)
r = yield get_page("metrics", app)
assert r.status_code == 403 assert r.status_code == 403
@pytest.mark.gen_test async def test_metrics_auth(app):
def test_metrics_auth(app): cookies = await app.login_user('river')
cookies = yield app.login_user('river')
metrics_url = ujoin(public_host(app), app.hub.base_url, 'metrics') metrics_url = ujoin(public_host(app), app.hub.base_url, 'metrics')
r = yield get_page("metrics", app, cookies=cookies) r = await get_page("metrics", app, cookies=cookies)
assert r.status_code == 200 assert r.status_code == 200
assert r.url == metrics_url assert r.url == metrics_url
@pytest.mark.gen_test async def test_health_check_request(app):
def test_health_check_request(app): r = await get_page('health', app)
r = yield get_page('health', app)
assert r.status_code == 200 assert r.status_code == 200

View File

@@ -16,6 +16,7 @@ from .mocking import MockHub
from .test_api import api_request, add_user from .test_api import api_request, add_user
from ..utils import wait_for_http_server, url_path_join as ujoin from ..utils import wait_for_http_server, url_path_join as ujoin
@pytest.fixture @pytest.fixture
def disable_check_routes(app): def disable_check_routes(app):
# disable periodic check_routes while we are testing # disable periodic check_routes while we are testing
@@ -25,9 +26,8 @@ def disable_check_routes(app):
finally: finally:
app.last_activity_callback.start() app.last_activity_callback.start()
@pytest.mark.gen_test
def test_external_proxy(request):
async def test_external_proxy(request):
auth_token = 'secret!' auth_token = 'secret!'
proxy_ip = '127.0.0.1' proxy_ip = '127.0.0.1'
proxy_port = 54321 proxy_port = 54321
@@ -71,23 +71,23 @@ def test_external_proxy(request):
def wait_for_proxy(): def wait_for_proxy():
return wait_for_http_server('http://%s:%i' % (proxy_ip, proxy_port)) return wait_for_http_server('http://%s:%i' % (proxy_ip, proxy_port))
yield wait_for_proxy() await wait_for_proxy()
yield app.initialize([]) await app.initialize([])
yield app.start() await app.start()
assert app.proxy.proxy_process is None assert app.proxy.proxy_process is None
# test if api service has a root route '/' # test if api service has a root route '/'
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
assert list(routes.keys()) == [app.hub.routespec] assert list(routes.keys()) == [app.hub.routespec]
# add user to the db and start a single user server # add user to the db and start a single user server
name = 'river' name = 'river'
add_user(app.db, app, name=name) add_user(app.db, app, name=name)
r = yield api_request(app, 'users', name, 'server', method='post') r = await api_request(app, 'users', name, 'server', method='post')
r.raise_for_status() r.raise_for_status()
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
# sets the desired path result # sets the desired path result
user_path = ujoin(app.base_url, 'user/river') + '/' user_path = ujoin(app.base_url, 'user/river') + '/'
print(app.base_url, user_path) print(app.base_url, user_path)
@@ -101,18 +101,18 @@ def test_external_proxy(request):
proxy.terminate() proxy.terminate()
proxy.wait(timeout=10) proxy.wait(timeout=10)
proxy = Popen(cmd, env=env) proxy = Popen(cmd, env=env)
yield wait_for_proxy() await wait_for_proxy()
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
assert list(routes.keys()) == [] assert list(routes.keys()) == []
# poke the server to update the proxy # poke the server to update the proxy
r = yield api_request(app, 'proxy', method='post') r = await api_request(app, 'proxy', method='post')
r.raise_for_status() r.raise_for_status()
# check that the routes are correct # check that the routes are correct
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
assert sorted(routes.keys()) == [app.hub.routespec, user_spec] assert sorted(routes.keys()) == [app.hub.routespec, user_spec]
# teardown the proxy, and start a new one with different auth and port # teardown the proxy, and start a new one with different auth and port
@@ -131,11 +131,11 @@ def test_external_proxy(request):
if app.subdomain_host: if app.subdomain_host:
cmd.append('--host-routing') cmd.append('--host-routing')
proxy = Popen(cmd, env=env) proxy = Popen(cmd, env=env)
yield wait_for_proxy() await wait_for_proxy()
# tell the hub where the new proxy is # tell the hub where the new proxy is
new_api_url = 'http://{}:{}'.format(proxy_ip, proxy_port) new_api_url = 'http://{}:{}'.format(proxy_ip, proxy_port)
r = yield api_request(app, 'proxy', method='patch', data=json.dumps({ r = await api_request(app, 'proxy', method='patch', data=json.dumps({
'api_url': new_api_url, 'api_url': new_api_url,
'auth_token': new_auth_token, 'auth_token': new_auth_token,
})) }))
@@ -145,11 +145,10 @@ def test_external_proxy(request):
assert app.proxy.auth_token == new_auth_token assert app.proxy.auth_token == new_auth_token
# check that the routes are correct # check that the routes are correct
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
assert sorted(routes.keys()) == [app.hub.routespec, user_spec] assert sorted(routes.keys()) == [app.hub.routespec, user_spec]
@pytest.mark.gen_test
@pytest.mark.parametrize("username", [ @pytest.mark.parametrize("username", [
'zoe', 'zoe',
'50fia', '50fia',
@@ -157,27 +156,27 @@ def test_external_proxy(request):
'~TestJH', '~TestJH',
'has@', 'has@',
]) ])
def test_check_routes(app, username, disable_check_routes): async def test_check_routes(app, username, disable_check_routes):
proxy = app.proxy proxy = app.proxy
test_user = add_user(app.db, app, name=username) test_user = add_user(app.db, app, name=username)
r = yield api_request(app, 'users/%s/server' % username, method='post') r = await api_request(app, 'users/%s/server' % username, method='post')
r.raise_for_status() r.raise_for_status()
# check a valid route exists for user # check a valid route exists for user
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
before = sorted(routes) before = sorted(routes)
assert test_user.proxy_spec in before assert test_user.proxy_spec in before
# check if a route is removed when user deleted # check if a route is removed when user deleted
yield app.proxy.check_routes(app.users, app._service_map) await app.proxy.check_routes(app.users, app._service_map)
yield proxy.delete_user(test_user) await proxy.delete_user(test_user)
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
during = sorted(routes) during = sorted(routes)
assert test_user.proxy_spec not in during assert test_user.proxy_spec not in during
# check if a route exists for user # check if a route exists for user
yield app.proxy.check_routes(app.users, app._service_map) await app.proxy.check_routes(app.users, app._service_map)
routes = yield app.proxy.get_all_routes() routes = await app.proxy.get_all_routes()
after = sorted(routes) after = sorted(routes)
assert test_user.proxy_spec in after assert test_user.proxy_spec in after
@@ -185,7 +184,6 @@ def test_check_routes(app, username, disable_check_routes):
assert before == after assert before == after
@pytest.mark.gen_test
@pytest.mark.parametrize("routespec", [ @pytest.mark.parametrize("routespec", [
'/has%20space/foo/', '/has%20space/foo/',
'/missing-trailing/slash', '/missing-trailing/slash',
@@ -194,11 +192,11 @@ def test_check_routes(app, username, disable_check_routes):
'host.name/path/', 'host.name/path/',
'other.host/path/no/slash', 'other.host/path/no/slash',
]) ])
def test_add_get_delete(app, routespec, disable_check_routes): async def test_add_get_delete(app, routespec, disable_check_routes):
arg = routespec arg = routespec
if not routespec.endswith('/'): if not routespec.endswith('/'):
routespec = routespec + '/' routespec = routespec + '/'
# host-routes when not host-routing raises an error # host-routes when not host-routing raises an error
# and vice versa # and vice versa
expect_value_error = bool(app.subdomain_host) ^ (not routespec.startswith('/')) expect_value_error = bool(app.subdomain_host) ^ (not routespec.startswith('/'))
@@ -213,26 +211,25 @@ def test_add_get_delete(app, routespec, disable_check_routes):
proxy = app.proxy proxy = app.proxy
target = 'https://localhost:1234' target = 'https://localhost:1234'
with context(): with context():
yield proxy.add_route(arg, target, {}) await proxy.add_route(arg, target, {})
routes = yield proxy.get_all_routes() routes = await proxy.get_all_routes()
if not expect_value_error: if not expect_value_error:
assert routespec in routes.keys() assert routespec in routes.keys()
with context(): with context():
route = yield proxy.get_route(arg) route = await proxy.get_route(arg)
assert route == { assert route == {
'target': target, 'target': target,
'routespec': routespec, 'routespec': routespec,
'data': route.get('data'), 'data': route.get('data'),
} }
with context(): with context():
yield proxy.delete_route(arg) await proxy.delete_route(arg)
with context(): with context():
route = yield proxy.get_route(arg) route = await proxy.get_route(arg)
assert route is None assert route is None
@pytest.mark.gen_test
@pytest.mark.parametrize("test_data", [None, 'notjson', json.dumps([])]) @pytest.mark.parametrize("test_data", [None, 'notjson', json.dumps([])])
def test_proxy_patch_bad_request_data(app, test_data): async def test_proxy_patch_bad_request_data(app, test_data):
r = yield api_request(app, 'proxy', method='patch', data=test_data) r = await api_request(app, 'proxy', method='patch', data=test_data)
assert r.status_code == 400 assert r.status_code == 400

View File

@@ -1,5 +1,6 @@
"""Tests for services""" """Tests for services"""
import asyncio
from binascii import hexlify from binascii import hexlify
from contextlib import contextmanager from contextlib import contextmanager
import os import os
@@ -8,13 +9,14 @@ import sys
from threading import Event from threading import Event
import time import time
from async_generator import asynccontextmanager, async_generator, yield_
import pytest import pytest
import requests import requests
from tornado import gen from tornado import gen
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from .mocking import public_url from .mocking import public_url
from ..utils import url_path_join, wait_for_http_server, random_port from ..utils import url_path_join, wait_for_http_server, random_port, maybe_future
from .utils import async_requests from .utils import async_requests
mockservice_path = os.path.dirname(os.path.abspath(__file__)) mockservice_path = os.path.dirname(os.path.abspath(__file__))
@@ -22,8 +24,9 @@ mockservice_py = os.path.join(mockservice_path, 'mockservice.py')
mockservice_cmd = [sys.executable, mockservice_py] mockservice_cmd = [sys.executable, mockservice_py]
@contextmanager @asynccontextmanager
def external_service(app, name='mockservice'): @async_generator
async def external_service(app, name='mockservice'):
env = { env = {
'JUPYTERHUB_API_TOKEN': hexlify(os.urandom(5)), 'JUPYTERHUB_API_TOKEN': hexlify(os.urandom(5)),
'JUPYTERHUB_SERVICE_NAME': name, 'JUPYTERHUB_SERVICE_NAME': name,
@@ -31,17 +34,14 @@ def external_service(app, name='mockservice'):
'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:%i' % random_port(), 'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:%i' % random_port(),
} }
proc = Popen(mockservice_cmd, env=env) proc = Popen(mockservice_cmd, env=env)
IOLoop().run_sync(
lambda: wait_for_http_server(env['JUPYTERHUB_SERVICE_URL'])
)
try: try:
yield env await wait_for_http_server(env['JUPYTERHUB_SERVICE_URL'])
await yield_(env)
finally: finally:
proc.terminate() proc.terminate()
@pytest.mark.gen_test async def test_managed_service(mockservice):
def test_managed_service(mockservice):
service = mockservice service = mockservice
proc = service.proc proc = service.proc
assert isinstance(proc.pid, object) assert isinstance(proc.pid, object)
@@ -58,19 +58,18 @@ def test_managed_service(mockservice):
if service.proc is not proc: if service.proc is not proc:
break break
else: else:
yield gen.sleep(0.2) await asyncio.sleep(0.2)
assert service.proc.pid != first_pid assert service.proc.pid != first_pid
assert service.proc.poll() is None assert service.proc.poll() is None
@pytest.mark.gen_test async def test_proxy_service(app, mockservice_url):
def test_proxy_service(app, mockservice_url):
service = mockservice_url service = mockservice_url
name = service.name name = service.name
yield app.proxy.get_all_routes() await app.proxy.get_all_routes()
url = public_url(app, service) + '/foo' url = public_url(app, service) + '/foo'
r = yield async_requests.get(url, allow_redirects=False) r = await async_requests.get(url, allow_redirects=False)
path = '/services/{}/foo'.format(name) path = '/services/{}/foo'.format(name)
r.raise_for_status() r.raise_for_status()
@@ -78,23 +77,22 @@ def test_proxy_service(app, mockservice_url):
assert r.text.endswith(path) assert r.text.endswith(path)
@pytest.mark.gen_test async def test_external_service(app):
def test_external_service(app):
name = 'external' name = 'external'
with external_service(app, name=name) as env: async with external_service(app, name=name) as env:
app.services = [{ app.services = [{
'name': name, 'name': name,
'admin': True, 'admin': True,
'url': env['JUPYTERHUB_SERVICE_URL'], 'url': env['JUPYTERHUB_SERVICE_URL'],
'api_token': env['JUPYTERHUB_API_TOKEN'], 'api_token': env['JUPYTERHUB_API_TOKEN'],
}] }]
yield app.init_services() await maybe_future(app.init_services())
yield app.init_api_tokens() await app.init_api_tokens()
yield app.proxy.add_all_services(app._service_map) await app.proxy.add_all_services(app._service_map)
service = app._service_map[name] service = app._service_map[name]
url = public_url(app, service) + '/api/users' url = public_url(app, service) + '/api/users'
r = yield async_requests.get(url, allow_redirects=False) r = await async_requests.get(url, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
resp = r.json() resp = r.json()

View File

@@ -227,11 +227,10 @@ def test_hub_authenticated(request):
assert r.status_code == 403 assert r.status_code == 403
@pytest.mark.gen_test async def test_hubauth_cookie(app, mockservice_url):
def test_hubauth_cookie(app, mockservice_url):
"""Test HubAuthenticated service with user cookies""" """Test HubAuthenticated service with user cookies"""
cookies = yield app.login_user('badger') cookies = await app.login_user('badger')
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/', cookies=cookies) r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/', cookies=cookies)
r.raise_for_status() r.raise_for_status()
print(r.text) print(r.text)
reply = r.json() reply = r.json()
@@ -242,15 +241,14 @@ def test_hubauth_cookie(app, mockservice_url):
} }
@pytest.mark.gen_test async def test_hubauth_token(app, mockservice_url):
def test_hubauth_token(app, mockservice_url):
"""Test HubAuthenticated service with user API tokens""" """Test HubAuthenticated service with user API tokens"""
u = add_user(app.db, name='river') u = add_user(app.db, name='river')
token = u.new_api_token() token = u.new_api_token()
app.db.commit() app.db.commit()
# token in Authorization header # token in Authorization header
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/', r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/',
headers={ headers={
'Authorization': 'token %s' % token, 'Authorization': 'token %s' % token,
}) })
@@ -262,7 +260,7 @@ def test_hubauth_token(app, mockservice_url):
} }
# token in ?token parameter # token in ?token parameter
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=%s' % token) r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=%s' % token)
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
sub_reply = { key: reply.get(key, 'missing') for key in ['name', 'admin']} sub_reply = { key: reply.get(key, 'missing') for key in ['name', 'admin']}
@@ -271,7 +269,7 @@ def test_hubauth_token(app, mockservice_url):
'admin': False, 'admin': False,
} }
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=no-such-token', r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=no-such-token',
allow_redirects=False, allow_redirects=False,
) )
assert r.status_code == 302 assert r.status_code == 302
@@ -281,17 +279,16 @@ def test_hubauth_token(app, mockservice_url):
assert path.endswith('/hub/login') assert path.endswith('/hub/login')
@pytest.mark.gen_test async def test_hubauth_service_token(app, mockservice_url):
def test_hubauth_service_token(app, mockservice_url):
"""Test HubAuthenticated service with service API tokens""" """Test HubAuthenticated service with service API tokens"""
token = hexlify(os.urandom(5)).decode('utf8') token = hexlify(os.urandom(5)).decode('utf8')
name = 'test-api-service' name = 'test-api-service'
app.service_tokens[token] = name app.service_tokens[token] = name
yield app.init_api_tokens() await app.init_api_tokens()
# token in Authorization header # token in Authorization header
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/', r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/',
headers={ headers={
'Authorization': 'token %s' % token, 'Authorization': 'token %s' % token,
}) })
@@ -305,7 +302,7 @@ def test_hubauth_service_token(app, mockservice_url):
assert not r.cookies assert not r.cookies
# token in ?token parameter # token in ?token parameter
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=%s' % token) r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=%s' % token)
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == { assert reply == {
@@ -314,7 +311,7 @@ def test_hubauth_service_token(app, mockservice_url):
'admin': False, 'admin': False,
} }
r = yield async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=no-such-token', r = await async_requests.get(public_url(app, mockservice_url) + '/whoami/?token=no-such-token',
allow_redirects=False, allow_redirects=False,
) )
assert r.status_code == 302 assert r.status_code == 302
@@ -324,16 +321,15 @@ def test_hubauth_service_token(app, mockservice_url):
assert path.endswith('/hub/login') assert path.endswith('/hub/login')
@pytest.mark.gen_test async def test_oauth_service(app, mockservice_url):
def test_oauth_service(app, mockservice_url):
service = mockservice_url service = mockservice_url
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/?arg=x') url = url_path_join(public_url(app, mockservice_url) + 'owhoami/?arg=x')
# first request is only going to login and get us to the oauth form page # first request is only going to login and get us to the oauth form page
s = AsyncSession() s = AsyncSession()
name = 'link' name = 'link'
s.cookies = yield app.login_user(name) s.cookies = await app.login_user(name)
r = yield s.get(url) r = await s.get(url)
r.raise_for_status() r.raise_for_status()
# we should be looking at the oauth confirmation page # we should be looking at the oauth confirmation page
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize' assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
@@ -341,7 +337,7 @@ def test_oauth_service(app, mockservice_url):
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name} assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
# submit the oauth form to complete authorization # submit the oauth form to complete authorization
r = yield s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url}) r = await s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url})
r.raise_for_status() r.raise_for_status()
assert r.url == url assert r.url == url
# verify oauth cookie is set # verify oauth cookie is set
@@ -350,7 +346,7 @@ def test_oauth_service(app, mockservice_url):
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys()) assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
# second request should be authenticated, which means no redirects # second request should be authenticated, which means no redirects
r = yield s.get(url, allow_redirects=False) r = await s.get(url, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
reply = r.json() reply = r.json()
@@ -363,7 +359,7 @@ def test_oauth_service(app, mockservice_url):
# token-authenticated request to HubOAuth # token-authenticated request to HubOAuth
token = app.users[name].new_api_token() token = app.users[name].new_api_token()
# token in ?token parameter # token in ?token parameter
r = yield async_requests.get(url_concat(url, {'token': token})) r = await async_requests.get(url_concat(url, {'token': token}))
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply['name'] == name assert reply['name'] == name
@@ -371,7 +367,7 @@ def test_oauth_service(app, mockservice_url):
# verify that ?token= requests set a cookie # verify that ?token= requests set a cookie
assert len(r.cookies) != 0 assert len(r.cookies) != 0
# ensure cookie works in future requests # ensure cookie works in future requests
r = yield async_requests.get( r = await async_requests.get(
url, url,
cookies=r.cookies, cookies=r.cookies,
allow_redirects=False, allow_redirects=False,
@@ -382,17 +378,16 @@ def test_oauth_service(app, mockservice_url):
assert reply['name'] == name assert reply['name'] == name
@pytest.mark.gen_test async def test_oauth_cookie_collision(app, mockservice_url):
def test_oauth_cookie_collision(app, mockservice_url):
service = mockservice_url service = mockservice_url
url = url_path_join(public_url(app, mockservice_url), 'owhoami/') url = url_path_join(public_url(app, mockservice_url), 'owhoami/')
print(url) print(url)
s = AsyncSession() s = AsyncSession()
name = 'mypha' name = 'mypha'
s.cookies = yield app.login_user(name) s.cookies = await app.login_user(name)
state_cookie_name = 'service-%s-oauth-state' % service.name state_cookie_name = 'service-%s-oauth-state' % service.name
service_cookie_name = 'service-%s' % service.name service_cookie_name = 'service-%s' % service.name
oauth_1 = yield s.get(url) oauth_1 = await s.get(url)
print(oauth_1.headers) print(oauth_1.headers)
print(oauth_1.cookies, oauth_1.url, url) print(oauth_1.cookies, oauth_1.url, url)
assert state_cookie_name in s.cookies assert state_cookie_name in s.cookies
@@ -402,7 +397,7 @@ def test_oauth_cookie_collision(app, mockservice_url):
state_1 = s.cookies[state_cookie_name] state_1 = s.cookies[state_cookie_name]
# start second oauth login before finishing the first # start second oauth login before finishing the first
oauth_2 = yield s.get(url) oauth_2 = await s.get(url)
state_cookies = [ c for c in s.cookies.keys() if c.startswith(state_cookie_name) ] state_cookies = [ c for c in s.cookies.keys() if c.startswith(state_cookie_name) ]
assert len(state_cookies) == 2 assert len(state_cookies) == 2
# get the random-suffix cookie name # get the random-suffix cookie name
@@ -412,7 +407,7 @@ def test_oauth_cookie_collision(app, mockservice_url):
# finish oauth 2 # finish oauth 2
# submit the oauth form to complete authorization # submit the oauth form to complete authorization
r = yield s.post( r = await s.post(
oauth_2.url, oauth_2.url,
data={'scopes': ['identify']}, data={'scopes': ['identify']},
headers={'Referer': oauth_2.url}, headers={'Referer': oauth_2.url},
@@ -426,7 +421,7 @@ def test_oauth_cookie_collision(app, mockservice_url):
service_cookie_2 = s.cookies[service_cookie_name] service_cookie_2 = s.cookies[service_cookie_name]
# finish oauth 1 # finish oauth 1
r = yield s.post( r = await s.post(
oauth_1.url, oauth_1.url,
data={'scopes': ['identify']}, data={'scopes': ['identify']},
headers={'Referer': oauth_1.url}, headers={'Referer': oauth_1.url},
@@ -445,8 +440,7 @@ def test_oauth_cookie_collision(app, mockservice_url):
assert state_cookies == [] assert state_cookies == []
@pytest.mark.gen_test async def test_oauth_logout(app, mockservice_url):
def test_oauth_logout(app, mockservice_url):
"""Verify that logout via the Hub triggers logout for oauth services """Verify that logout via the Hub triggers logout for oauth services
1. clears session id cookie 1. clears session id cookie
@@ -471,18 +465,18 @@ def test_oauth_logout(app, mockservice_url):
# ensure we start empty # ensure we start empty
assert auth_tokens() == [] assert auth_tokens() == []
s.cookies = yield app.login_user(name) s.cookies = await app.login_user(name)
assert 'jupyterhub-session-id' in s.cookies assert 'jupyterhub-session-id' in s.cookies
r = yield s.get(url) r = await s.get(url)
r.raise_for_status() r.raise_for_status()
assert urlparse(r.url).path.endswith('oauth2/authorize') assert urlparse(r.url).path.endswith('oauth2/authorize')
# submit the oauth form to complete authorization # submit the oauth form to complete authorization
r = yield s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url}) r = await s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url})
r.raise_for_status() r.raise_for_status()
assert r.url == url assert r.url == url
# second request should be authenticated # second request should be authenticated
r = yield s.get(url, allow_redirects=False) r = await s.get(url, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
reply = r.json() reply = r.json()
@@ -501,13 +495,13 @@ def test_oauth_logout(app, mockservice_url):
assert len(auth_tokens()) == 1 assert len(auth_tokens()) == 1
# hit hub logout URL # hit hub logout URL
r = yield s.get(public_url(app, path='hub/logout')) r = await s.get(public_url(app, path='hub/logout'))
r.raise_for_status() r.raise_for_status()
# verify that all cookies other than the service cookie are cleared # verify that all cookies other than the service cookie are cleared
assert list(s.cookies.keys()) == [service_cookie_name] assert list(s.cookies.keys()) == [service_cookie_name]
# verify that clearing session id invalidates service cookie # verify that clearing session id invalidates service cookie
# i.e. redirect back to login page # i.e. redirect back to login page
r = yield s.get(url) r = await s.get(url)
r.raise_for_status() r.raise_for_status()
assert r.url.split('?')[0] == public_url(app, path='hub/login') assert r.url.split('?')[0] == public_url(app, path='hub/login')
@@ -524,7 +518,7 @@ def test_oauth_logout(app, mockservice_url):
# check that we got the old session id back # check that we got the old session id back
assert session_id == s.cookies['jupyterhub-session-id'] assert session_id == s.cookies['jupyterhub-session-id']
r = yield s.get(url, allow_redirects=False) r = await s.get(url, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
reply = r.json() reply = r.json()

View File

@@ -13,42 +13,41 @@ from ..utils import url_path_join
from .utils import async_requests, AsyncSession from .utils import async_requests, AsyncSession
@pytest.mark.gen_test async def test_singleuser_auth(app):
def test_singleuser_auth(app):
# use StubSingleUserSpawner to launch a single-user app in a thread # use StubSingleUserSpawner to launch a single-user app in a thread
app.spawner_class = StubSingleUserSpawner app.spawner_class = StubSingleUserSpawner
app.tornado_settings['spawner_class'] = StubSingleUserSpawner app.tornado_settings['spawner_class'] = StubSingleUserSpawner
# login, start the server # login, start the server
cookies = yield app.login_user('nandy') cookies = await app.login_user('nandy')
user = app.users['nandy'] user = app.users['nandy']
if not user.running: if not user.running:
yield user.spawn() await user.spawn()
url = public_url(app, user) url = public_url(app, user)
# no cookies, redirects to login page # no cookies, redirects to login page
r = yield async_requests.get(url) r = await async_requests.get(url)
r.raise_for_status() r.raise_for_status()
assert '/hub/login' in r.url assert '/hub/login' in r.url
# with cookies, login successful # with cookies, login successful
r = yield async_requests.get(url, cookies=cookies) r = await async_requests.get(url, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert urlparse(r.url).path.rstrip('/').endswith('/user/nandy/tree') assert urlparse(r.url).path.rstrip('/').endswith('/user/nandy/tree')
assert r.status_code == 200 assert r.status_code == 200
# logout # logout
r = yield async_requests.get(url_path_join(url, 'logout'), cookies=cookies) r = await async_requests.get(url_path_join(url, 'logout'), cookies=cookies)
assert len(r.cookies) == 0 assert len(r.cookies) == 0
# accessing another user's server hits the oauth confirmation page # accessing another user's server hits the oauth confirmation page
cookies = yield app.login_user('burgess') cookies = await app.login_user('burgess')
s = AsyncSession() s = AsyncSession()
s.cookies = cookies s.cookies = cookies
r = yield s.get(url) r = await s.get(url)
assert urlparse(r.url).path.endswith('/oauth2/authorize') assert urlparse(r.url).path.endswith('/oauth2/authorize')
# submit the oauth form to complete authorization # submit the oauth form to complete authorization
r = yield s.post( r = await s.post(
r.url, r.url,
data={'scopes': ['identify']}, data={'scopes': ['identify']},
headers={'Referer': r.url}, headers={'Referer': r.url},
@@ -59,28 +58,27 @@ def test_singleuser_auth(app):
assert 'burgess' in r.text assert 'burgess' in r.text
@pytest.mark.gen_test async def test_disable_user_config(app):
def test_disable_user_config(app):
# use StubSingleUserSpawner to launch a single-user app in a thread # use StubSingleUserSpawner to launch a single-user app in a thread
app.spawner_class = StubSingleUserSpawner app.spawner_class = StubSingleUserSpawner
app.tornado_settings['spawner_class'] = StubSingleUserSpawner app.tornado_settings['spawner_class'] = StubSingleUserSpawner
# login, start the server # login, start the server
cookies = yield app.login_user('nandy') cookies = await app.login_user('nandy')
user = app.users['nandy'] user = app.users['nandy']
# stop spawner, if running: # stop spawner, if running:
if user.running: if user.running:
print("stopping") print("stopping")
yield user.stop() await user.stop()
# start with new config: # start with new config:
user.spawner.debug = True user.spawner.debug = True
user.spawner.disable_user_config = True user.spawner.disable_user_config = True
yield user.spawn() await user.spawn()
yield app.proxy.add_user(user) await app.proxy.add_user(user)
url = public_url(app, user) url = public_url(app, user)
# with cookies, login successful # with cookies, login successful
r = yield async_requests.get(url, cookies=cookies) r = await async_requests.get(url, cookies=cookies)
r.raise_for_status() r.raise_for_status()
assert r.url.rstrip('/').endswith('/user/nandy/tree') assert r.url.rstrip('/').endswith('/user/nandy/tree')
assert r.status_code == 200 assert r.status_code == 200
@@ -90,6 +88,7 @@ def test_help_output():
out = check_output([sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']).decode('utf8', 'replace') out = check_output([sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']).decode('utf8', 'replace')
assert 'JupyterHub' in out assert 'JupyterHub' in out
def test_version(): def test_version():
out = check_output([sys.executable, '-m', 'jupyterhub.singleuser', '--version']).decode('utf8', 'replace') out = check_output([sys.executable, '-m', 'jupyterhub.singleuser', '--version']).decode('utf8', 'replace')
assert jupyterhub.__version__ in out assert jupyterhub.__version__ in out

View File

@@ -60,10 +60,9 @@ def new_spawner(db, **kwargs):
return user._new_spawner('', spawner_class=LocalProcessSpawner, **kwargs) return user._new_spawner('', spawner_class=LocalProcessSpawner, **kwargs)
@pytest.mark.gen_test async def test_spawner(db, request):
def test_spawner(db, request):
spawner = new_spawner(db) spawner = new_spawner(db)
ip, port = yield spawner.start() ip, port = await spawner.start()
assert ip == '127.0.0.1' assert ip == '127.0.0.1'
assert isinstance(port, int) assert isinstance(port, int)
assert port > 0 assert port > 0
@@ -72,115 +71,110 @@ def test_spawner(db, request):
# wait for the process to get to the while True: loop # wait for the process to get to the while True: loop
time.sleep(1) time.sleep(1)
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
yield spawner.stop() await spawner.stop()
status = yield spawner.poll() status = await spawner.poll()
assert status == 1 assert status == 1
@gen.coroutine async def wait_for_spawner(spawner, timeout=10):
def wait_for_spawner(spawner, timeout=10):
"""Wait for an http server to show up """Wait for an http server to show up
polling at shorter intervals for early termination polling at shorter intervals for early termination
""" """
deadline = time.monotonic() + timeout deadline = time.monotonic() + timeout
def wait(): def wait():
return spawner.server.wait_up(timeout=1, http=True) return spawner.server.wait_up(timeout=1, http=True)
while time.monotonic() < deadline: while time.monotonic() < deadline:
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
try: try:
yield wait() await wait()
except TimeoutError: except TimeoutError:
continue continue
else: else:
break break
yield wait() await wait()
@pytest.mark.gen_test async def test_single_user_spawner(app, request):
def test_single_user_spawner(app, request):
user = next(iter(app.users.values()), None) user = next(iter(app.users.values()), None)
spawner = user.spawner spawner = user.spawner
spawner.cmd = ['jupyterhub-singleuser'] spawner.cmd = ['jupyterhub-singleuser']
yield user.spawn() await user.spawn()
assert spawner.server.ip == '127.0.0.1' assert spawner.server.ip == '127.0.0.1'
assert spawner.server.port > 0 assert spawner.server.port > 0
yield wait_for_spawner(spawner) await wait_for_spawner(spawner)
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
yield spawner.stop() await spawner.stop()
status = yield spawner.poll() status = await spawner.poll()
assert status == 0 assert status == 0
@pytest.mark.gen_test async def test_stop_spawner_sigint_fails(db):
def test_stop_spawner_sigint_fails(db):
spawner = new_spawner(db, cmd=[sys.executable, '-c', _uninterruptible]) spawner = new_spawner(db, cmd=[sys.executable, '-c', _uninterruptible])
yield spawner.start() await spawner.start()
# wait for the process to get to the while True: loop # wait for the process to get to the while True: loop
yield gen.sleep(1) await gen.sleep(1)
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
yield spawner.stop() await spawner.stop()
status = yield spawner.poll() status = await spawner.poll()
assert status == -signal.SIGTERM assert status == -signal.SIGTERM
@pytest.mark.gen_test async def test_stop_spawner_stop_now(db):
def test_stop_spawner_stop_now(db):
spawner = new_spawner(db) spawner = new_spawner(db)
yield spawner.start() await spawner.start()
# wait for the process to get to the while True: loop # wait for the process to get to the while True: loop
yield gen.sleep(1) await gen.sleep(1)
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
yield spawner.stop(now=True) await spawner.stop(now=True)
status = yield spawner.poll() status = await spawner.poll()
assert status == -signal.SIGTERM assert status == -signal.SIGTERM
@pytest.mark.gen_test async def test_spawner_poll(db):
def test_spawner_poll(db):
first_spawner = new_spawner(db) first_spawner = new_spawner(db)
user = first_spawner.user user = first_spawner.user
yield first_spawner.start() await first_spawner.start()
proc = first_spawner.proc proc = first_spawner.proc
status = yield first_spawner.poll() status = await first_spawner.poll()
assert status is None assert status is None
if user.state is None: if user.state is None:
user.state = {} user.state = {}
first_spawner.orm_spawner.state = first_spawner.get_state() first_spawner.orm_spawner.state = first_spawner.get_state()
assert 'pid' in first_spawner.orm_spawner.state assert 'pid' in first_spawner.orm_spawner.state
# create a new Spawner, loading from state of previous # create a new Spawner, loading from state of previous
spawner = new_spawner(db, user=first_spawner.user) spawner = new_spawner(db, user=first_spawner.user)
spawner.start_polling() spawner.start_polling()
# wait for the process to get to the while True: loop # wait for the process to get to the while True: loop
yield gen.sleep(1) await gen.sleep(1)
status = yield spawner.poll() status = await spawner.poll()
assert status is None assert status is None
# kill the process # kill the process
proc.terminate() proc.terminate()
for i in range(10): for i in range(10):
if proc.poll() is None: if proc.poll() is None:
yield gen.sleep(1) await gen.sleep(1)
else: else:
break break
assert proc.poll() is not None assert proc.poll() is not None
yield gen.sleep(2) await gen.sleep(2)
status = yield spawner.poll() status = await spawner.poll()
assert status is not None assert status is not None
@@ -213,8 +207,7 @@ def test_string_formatting(db):
assert s.format_string(s.default_url) == '/base/%s' % name assert s.format_string(s.default_url) == '/base/%s' % name
@pytest.mark.gen_test async def test_popen_kwargs(db):
def test_popen_kwargs(db):
mock_proc = mock.Mock(spec=Popen) mock_proc = mock.Mock(spec=Popen)
def mock_popen(*args, **kwargs): def mock_popen(*args, **kwargs):
mock_proc.args = args mock_proc.args = args
@@ -224,14 +217,13 @@ def test_popen_kwargs(db):
s = new_spawner(db, popen_kwargs={'shell': True}, cmd='jupyterhub-singleuser') s = new_spawner(db, popen_kwargs={'shell': True}, cmd='jupyterhub-singleuser')
with mock.patch.object(spawnermod, 'Popen', mock_popen): with mock.patch.object(spawnermod, 'Popen', mock_popen):
yield s.start() await s.start()
assert mock_proc.kwargs['shell'] == True assert mock_proc.kwargs['shell'] == True
assert mock_proc.args[0][:1] == (['jupyterhub-singleuser']) assert mock_proc.args[0][:1] == (['jupyterhub-singleuser'])
@pytest.mark.gen_test async def test_shell_cmd(db, tmpdir, request):
def test_shell_cmd(db, tmpdir, request):
f = tmpdir.join('bashrc') f = tmpdir.join('bashrc')
f.write('export TESTVAR=foo\n') f.write('export TESTVAR=foo\n')
s = new_spawner(db, s = new_spawner(db,
@@ -243,17 +235,17 @@ def test_shell_cmd(db, tmpdir, request):
db.commit() db.commit()
s.server = Server.from_orm(server) s.server = Server.from_orm(server)
db.commit() db.commit()
(ip, port) = yield s.start() (ip, port) = await s.start()
request.addfinalizer(s.stop) request.addfinalizer(s.stop)
s.server.ip = ip s.server.ip = ip
s.server.port = port s.server.port = port
db.commit() db.commit()
yield wait_for_spawner(s) await wait_for_spawner(s)
r = yield async_requests.get('http://%s:%i/env' % (ip, port)) r = await async_requests.get('http://%s:%i/env' % (ip, port))
r.raise_for_status() r.raise_for_status()
env = r.json() env = r.json()
assert env['TESTVAR'] == 'foo' assert env['TESTVAR'] == 'foo'
yield s.stop() await s.stop()
def test_inherit_overwrite(): def test_inherit_overwrite():
@@ -277,8 +269,7 @@ def test_inherit_ok():
pass pass
@pytest.mark.gen_test async def test_spawner_reuse_api_token(db, app):
def test_spawner_reuse_api_token(db, app):
# setup: user with no tokens, whose spawner has set the .will_resume flag # setup: user with no tokens, whose spawner has set the .will_resume flag
user = add_user(app.db, app, name='snoopy') user = add_user(app.db, app, name='snoopy')
spawner = user.spawner spawner = user.spawner
@@ -286,26 +277,25 @@ def test_spawner_reuse_api_token(db, app):
# will_resume triggers reuse of tokens # will_resume triggers reuse of tokens
spawner.will_resume = True spawner.will_resume = True
# first start: gets a new API token # first start: gets a new API token
yield user.spawn() await user.spawn()
api_token = spawner.api_token api_token = spawner.api_token
found = orm.APIToken.find(app.db, api_token) found = orm.APIToken.find(app.db, api_token)
assert found assert found
assert found.user.name == user.name assert found.user.name == user.name
assert user.api_tokens == [found] assert user.api_tokens == [found]
yield user.stop() await user.stop()
# stop now deletes unused spawners. # stop now deletes unused spawners.
# put back the mock spawner! # put back the mock spawner!
user.spawners[''] = spawner user.spawners[''] = spawner
# second start: should reuse the token # second start: should reuse the token
yield user.spawn() await user.spawn()
# verify re-use of API token # verify re-use of API token
assert spawner.api_token == api_token assert spawner.api_token == api_token
# verify that a new token was not created # verify that a new token was not created
assert user.api_tokens == [found] assert user.api_tokens == [found]
@pytest.mark.gen_test async def test_spawner_insert_api_token(app):
def test_spawner_insert_api_token(app):
"""Token provided by spawner is not in the db """Token provided by spawner is not in the db
Insert token into db as a user-provided token. Insert token into db as a user-provided token.
@@ -322,17 +312,16 @@ def test_spawner_insert_api_token(app):
# The spawner's provided API token would already be in the db # The spawner's provided API token would already be in the db
# unless there is a bug somewhere else (in the Spawner), # unless there is a bug somewhere else (in the Spawner),
# but handle it anyway. # but handle it anyway.
yield user.spawn() await user.spawn()
assert spawner.api_token == api_token assert spawner.api_token == api_token
found = orm.APIToken.find(app.db, api_token) found = orm.APIToken.find(app.db, api_token)
assert found assert found
assert found.user.name == user.name assert found.user.name == user.name
assert user.api_tokens == [found] assert user.api_tokens == [found]
yield user.stop() await user.stop()
@pytest.mark.gen_test async def test_spawner_bad_api_token(app):
def test_spawner_bad_api_token(app):
"""Tokens are revoked when a Spawner gets another user's token""" """Tokens are revoked when a Spawner gets another user's token"""
# we need two users for this one # we need two users for this one
user = add_user(app.db, app, name='antimone') user = add_user(app.db, app, name='antimone')
@@ -349,13 +338,12 @@ def test_spawner_bad_api_token(app):
# starting a user's server with another user's token # starting a user's server with another user's token
# should revoke it # should revoke it
with pytest.raises(ValueError): with pytest.raises(ValueError):
yield user.spawn() await user.spawn()
assert orm.APIToken.find(app.db, other_token) is None assert orm.APIToken.find(app.db, other_token) is None
assert other_user.api_tokens == [] assert other_user.api_tokens == []
@pytest.mark.gen_test async def test_spawner_delete_server(app):
def test_spawner_delete_server(app):
"""Test deleting spawner.server """Test deleting spawner.server
This can occur during app startup if their server has been deleted. This can occur during app startup if their server has been deleted.
@@ -393,22 +381,21 @@ def test_spawner_delete_server(app):
"has%40x", "has%40x",
] ]
) )
@pytest.mark.gen_test async def test_spawner_routing(app, name):
def test_spawner_routing(app, name):
"""Test routing of names with special characters""" """Test routing of names with special characters"""
db = app.db db = app.db
with mock.patch.dict(app.config.LocalProcessSpawner, {'cmd': [sys.executable, '-m', 'jupyterhub.tests.mocksu']}): with mock.patch.dict(app.config.LocalProcessSpawner, {'cmd': [sys.executable, '-m', 'jupyterhub.tests.mocksu']}):
user = add_user(app.db, app, name=name) user = add_user(app.db, app, name=name)
yield user.spawn() await user.spawn()
yield wait_for_spawner(user.spawner) await wait_for_spawner(user.spawner)
yield app.proxy.add_user(user) await app.proxy.add_user(user)
kwargs = {'allow_redirects': False} kwargs = {'allow_redirects': False}
if app.internal_ssl: if app.internal_ssl:
kwargs['cert'] = (app.internal_ssl_cert, app.internal_ssl_key) kwargs['cert'] = (app.internal_ssl_cert, app.internal_ssl_key)
kwargs["verify"] = app.internal_ssl_ca kwargs["verify"] = app.internal_ssl_ca
url = url_path_join(public_url(app, user), "test/url") url = url_path_join(public_url(app, user), "test/url")
r = yield async_requests.get(url, **kwargs) r = await async_requests.get(url, **kwargs)
r.raise_for_status() r.raise_for_status()
assert r.url == url assert r.url == url
assert r.text == urlparse(url).path assert r.text == urlparse(url).path
yield user.stop() await user.stop()

View File

@@ -26,7 +26,6 @@ def schedule_future(io_loop, *, delay, result=None):
return f return f
@pytest.mark.gen_test
@pytest.mark.parametrize("deadline, n, delay, expected", [ @pytest.mark.parametrize("deadline, n, delay, expected", [
(0, 3, 1, []), (0, 3, 1, []),
(0, 3, 0, [0, 1, 2]), (0, 3, 0, [0, 1, 2]),
@@ -43,7 +42,6 @@ async def test_iterate_until(io_loop, deadline, n, delay, expected):
assert yielded == expected assert yielded == expected
@pytest.mark.gen_test
async def test_iterate_until_ready_after_deadline(io_loop): async def test_iterate_until_ready_after_deadline(io_loop):
f = schedule_future(io_loop, delay=0) f = schedule_future(io_loop, delay=0)

View File

@@ -13,11 +13,15 @@ class _AsyncRequests:
def __init__(self): def __init__(self):
self.executor = ThreadPoolExecutor(1) self.executor = ThreadPoolExecutor(1)
real_submit = self.executor.submit
self.executor.submit = lambda *args, **kwargs: asyncio.wrap_future(
real_submit(*args, **kwargs)
)
def __getattr__(self, name): def __getattr__(self, name):
requests_method = getattr(requests, name) requests_method = getattr(requests, name)
return lambda *args, **kwargs: asyncio.wrap_future( return lambda *args, **kwargs: self.executor.submit(
self.executor.submit(requests_method, *args, **kwargs) requests_method, *args, **kwargs
) )

View File

@@ -531,7 +531,7 @@ class User:
self.settings['statsd'].incr('spawner.failure.error') self.settings['statsd'].incr('spawner.failure.error')
e.reason = 'error' e.reason = 'error'
try: try:
await self.stop() await self.stop(spawner.name)
except Exception: except Exception:
self.log.error("Failed to cleanup {user}'s server that failed to start".format( self.log.error("Failed to cleanup {user}'s server that failed to start".format(
user=self.name, user=self.name,
@@ -550,6 +550,15 @@ class User:
spawner.orm_spawner.state = spawner.get_state() spawner.orm_spawner.state = spawner.get_state()
db.commit() db.commit()
spawner._waiting_for_response = True spawner._waiting_for_response = True
await self._wait_up(spawner)
async def _wait_up(self, spawner):
"""Wait for a server to finish starting.
Shuts the server down if it doesn't respond within
spawner.http_timeout.
"""
server = spawner.server
key = self.settings.get('internal_ssl_key') key = self.settings.get('internal_ssl_key')
cert = self.settings.get('internal_ssl_cert') cert = self.settings.get('internal_ssl_cert')
ca = self.settings.get('internal_ssl_ca') ca = self.settings.get('internal_ssl_ca')
@@ -578,7 +587,7 @@ class User:
)) ))
self.settings['statsd'].incr('spawner.failure.http_error') self.settings['statsd'].incr('spawner.failure.http_error')
try: try:
await self.stop() await self.stop(spawner.name)
except Exception: except Exception:
self.log.error("Failed to cleanup {user}'s server that failed to start".format( self.log.error("Failed to cleanup {user}'s server that failed to start".format(
user=self.name, user=self.name,
@@ -594,7 +603,7 @@ class User:
finally: finally:
spawner._waiting_for_response = False spawner._waiting_for_response = False
spawner._start_pending = False spawner._start_pending = False
return self return spawner
async def stop(self, server_name=''): async def stop(self, server_name=''):
"""Stop the user's spawner """Stop the user's spawner