mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 12:03:00 +00:00
Merge branch 'master' into master
This commit is contained in:
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
========
|
========
|
||||||
|
@@ -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)
|
||||||
|
@@ -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:
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
@@ -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
@@ -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
|
||||||
|
@@ -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',
|
||||||
})
|
})
|
||||||
|
@@ -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')
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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',
|
||||||
})
|
})
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user