diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index b3eb763b..20e97cee 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -111,6 +111,8 @@ class MockHub(JupyterHub): db_file = None confirm_no_ssl = True + last_activity_interval = 2 + def _subdomain_host_default(self): return os.environ.get('JUPYTERHUB_TEST_SUBDOMAIN_HOST', '') @@ -128,7 +130,8 @@ class MockHub(JupyterHub): def start(self, argv=None): self.db_file = NamedTemporaryFile() - self.db_url = 'sqlite:///' + self.db_file.name + self.pid_file = NamedTemporaryFile(delete=False).name + self.db_url = self.db_file.name evt = threading.Event() @@ -173,6 +176,7 @@ class MockHub(JupyterHub): }, allow_redirects=False, ) + r.raise_for_status() assert r.cookies return r.cookies diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index bd5408d7..d722d10c 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -3,7 +3,7 @@ import json import time from queue import Queue -from urllib.parse import urlparse +from urllib.parse import urlparse, quote import requests @@ -383,6 +383,7 @@ def test_slow_spawn(app, io_loop): name = 'zoe' user = add_user(db, app=app, name=name) r = api_request(app, 'users', name, 'server', method='post') + app.tornado_settings['spawner_class'] = mocking.MockSpawner r.raise_for_status() assert r.status_code == 202 app_user = get_app_user(app, name) @@ -432,6 +433,7 @@ def test_never_spawn(app, io_loop): name = 'badger' user = add_user(db, app=app, name=name) r = api_request(app, 'users', name, 'server', method='post') + app.tornado_settings['spawner_class'] = mocking.MockSpawner app_user = get_app_user(app, name) assert app_user.spawner is not None assert app_user.spawn_pending @@ -454,6 +456,55 @@ def test_get_proxy(app, io_loop): assert list(reply.keys()) == ['/'] +def test_cookie(app): + db = app.db + name = 'patience' + user = add_user(db, app=app, name=name) + r = api_request(app, 'users', name, 'server', method='post') + assert r.status_code == 201 + assert 'pid' in user.state + app_user = get_app_user(app, name) + + cookies = app.login_user(name) + # cookie jar gives '"cookie-value"', we want 'cookie-value' + cookie = cookies[user.server.cookie_name][1:-1] + r = api_request(app, 'authorizations/cookie', user.server.cookie_name, "nothintoseehere") + assert r.status_code == 404 + + r = api_request(app, 'authorizations/cookie', user.server.cookie_name, quote(cookie, safe='')) + r.raise_for_status() + reply = r.json() + assert reply['name'] == name + + # deprecated cookie in body: + r = api_request(app, 'authorizations/cookie', user.server.cookie_name, data=cookie) + r.raise_for_status() + reply = r.json() + assert reply['name'] == name + +def test_token(app): + name = 'book' + user = add_user(app.db, app=app, name=name) + token = user.new_api_token() + r = api_request(app, 'authorizations/token', token) + r.raise_for_status() + user_model = r.json() + assert user_model['name'] == name + r = api_request(app, 'authorizations/token', 'notauthorized') + assert r.status_code == 404 + + +def test_options(app): + r = api_request(app, 'users', method='options') + r.raise_for_status() + assert 'Access-Control-Allow-Headers' in r.headers + + +def test_bad_json_body(app): + r = api_request(app, 'users', method='post', data='notjson') + assert r.status_code == 400 + + def test_shutdown(app): r = api_request(app, 'shutdown', method='post', data=json.dumps({ 'servers': True, diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index fcd03ff4..280c2e4b 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -3,8 +3,9 @@ import os import re import sys -from subprocess import check_output +from subprocess import check_output, Popen, PIPE from tempfile import NamedTemporaryFile, TemporaryDirectory +from .mocking import MockHub def test_help_all(): out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace') @@ -23,10 +24,23 @@ def test_token_app(): def test_generate_config(): with NamedTemporaryFile(prefix='jupyterhub_config', suffix='.py') as tf: cfg_file = tf.name - - out = check_output([sys.executable, '-m', 'jupyterhub', - '--generate-config', '-f', cfg_file] - ).decode('utf8', 'replace') + with open(cfg_file, 'w') as f: + f.write("c.A = 5") + p = Popen([sys.executable, '-m', 'jupyterhub', + '--generate-config', '-f', cfg_file], + stdout=PIPE, stdin=PIPE) + out, _ = p.communicate(b'n') + out = out.decode('utf8', 'replace') + assert os.path.exists(cfg_file) + with open(cfg_file) as f: + cfg_text = f.read() + assert cfg_text == 'c.A = 5' + + p = Popen([sys.executable, '-m', 'jupyterhub', + '--generate-config', '-f', cfg_file], + stdout=PIPE, stdin=PIPE) + out, _ = p.communicate(b'x\ny') + out = out.decode('utf8', 'replace') assert os.path.exists(cfg_file) with open(cfg_file) as f: cfg_text = f.read() diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index c4a6957b..48085c62 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -63,6 +63,7 @@ def test_admin(app): r.raise_for_status() assert r.url.endswith('/admin') + def test_spawn_redirect(app, io_loop): name = 'wash' cookies = app.login_user(name) @@ -174,6 +175,53 @@ def test_user_redirect(app): assert query == urlencode({'next': '/hub/user/baduser/test.ipynb'}) +def test_login_fail(app): + name = 'wash' + base_url = public_url(app) + r = requests.post(base_url + 'hub/login', + data={ + 'username': name, + 'password': 'wrong', + }, + allow_redirects=False, + ) + assert not r.cookies + + +def test_login_redirect(app, io_loop): + cookies = app.login_user('river') + user = app.users['river'] + # no next_url, server running + io_loop.run_sync(user.spawn) + r = get_page('login', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert r.status_code == 302 + assert '/user/river' in r.headers['Location'] + + # no next_url, server not running + io_loop.run_sync(user.stop) + r = get_page('login', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert r.status_code == 302 + assert '/hub/' in r.headers['Location'] + + # next URL given, use it + r = get_page('login?next=/hub/admin', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert r.status_code == 302 + assert r.headers['Location'].endswith('/hub/admin') + + +def test_logout(app): + name = 'wash' + cookies = app.login_user(name) + r = requests.get(public_host(app) + app.tornado_settings['logout_url'], cookies=cookies) + r.raise_for_status() + login_url = public_host(app) + app.tornado_settings['login_url'] + assert r.url == login_url + assert r.cookies == {} + + def test_static_files(app): base_url = ujoin(public_url(app), app.hub.server.base_url) print(base_url) @@ -186,5 +234,3 @@ def test_static_files(app): r = requests.get(ujoin(base_url, 'static', 'css', 'style.min.css')) r.raise_for_status() assert r.headers['content-type'] == 'text/css' - - \ No newline at end of file diff --git a/jupyterhub/tests/test_proxy.py b/jupyterhub/tests/test_proxy.py index 9947d829..573b55dc 100644 --- a/jupyterhub/tests/test_proxy.py +++ b/jupyterhub/tests/test_proxy.py @@ -105,6 +105,8 @@ def test_external_proxy(request, io_loop): # tell the hub where the new proxy is r = api_request(app, 'proxy', method='patch', data=json.dumps({ 'port': proxy_port, + 'protocol': 'http', + 'ip': app.ip, 'auth_token': new_auth_token, })) r.raise_for_status() @@ -123,6 +125,7 @@ def test_external_proxy(request, io_loop): routes = io_loop.run_sync(app.proxy.get_routes) assert sorted(routes.keys()) == ['/', user_path] + def test_check_routes(app, io_loop): proxy = app.proxy r = api_request(app, 'users/zoe', method='post') @@ -142,3 +145,13 @@ def test_check_routes(app, io_loop): after = sorted(io_loop.run_sync(app.proxy.get_routes)) assert zoe.proxy_path in after assert before == after + + +def test_patch_proxy_bad_req(app): + r = api_request(app, 'proxy', method='patch') + assert r.status_code == 400 + r = api_request(app, 'proxy', method='patch', data='notjson') + assert r.status_code == 400 + r = api_request(app, 'proxy', method='patch', data=json.dumps([])) + assert r.status_code == 400 + \ No newline at end of file diff --git a/jupyterhub/tests/test_spawner.py b/jupyterhub/tests/test_spawner.py index 69ed7a5a..76fa5cea 100644 --- a/jupyterhub/tests/test_spawner.py +++ b/jupyterhub/tests/test_spawner.py @@ -4,9 +4,14 @@ # Distributed under the terms of the Modified BSD License. import logging +import os import signal import sys +import tempfile import time +from unittest import mock + +from tornado import gen from .. import spawner as spawnermod from ..spawner import LocalProcessSpawner @@ -39,6 +44,7 @@ def new_spawner(db, **kwargs): kwargs.setdefault('INTERRUPT_TIMEOUT', 1) kwargs.setdefault('TERM_TIMEOUT', 1) kwargs.setdefault('KILL_TIMEOUT', 1) + kwargs.setdefault('poll_interval', 1) return LocalProcessSpawner(db=db, **kwargs) @@ -110,3 +116,53 @@ def test_stop_spawner_stop_now(db, io_loop): status = io_loop.run_sync(spawner.poll) assert status == -signal.SIGTERM +def test_spawner_poll(db, io_loop): + first_spawner = new_spawner(db) + user = first_spawner.user + io_loop.run_sync(first_spawner.start) + proc = first_spawner.proc + status = io_loop.run_sync(first_spawner.poll) + assert status is None + user.state = first_spawner.get_state() + assert 'pid' in user.state + + # create a new Spawner, loading from state of previous + spawner = new_spawner(db, user=first_spawner.user) + spawner.start_polling() + + # wait for the process to get to the while True: loop + io_loop.run_sync(lambda : gen.sleep(1)) + status = io_loop.run_sync(spawner.poll) + assert status is None + + # kill the process + proc.terminate() + for i in range(10): + if proc.poll() is None: + time.sleep(1) + else: + break + assert proc.poll() is not None + + io_loop.run_sync(lambda : gen.sleep(2)) + status = io_loop.run_sync(spawner.poll) + assert status is not None + +def test_setcwd(): + cwd = os.getcwd() + with tempfile.TemporaryDirectory() as td: + td = os.path.realpath(os.path.abspath(td)) + spawnermod._try_setcwd(td) + assert os.path.samefile(os.getcwd(), td) + os.chdir(cwd) + chdir = os.chdir + temp_root = os.path.realpath(os.path.abspath(tempfile.gettempdir())) + def raiser(path): + path = os.path.realpath(os.path.abspath(path)) + if not path.startswith(temp_root): + raise OSError(path) + chdir(path) + with mock.patch('os.chdir', raiser): + spawnermod._try_setcwd(cwd) + assert os.getcwd().startswith(temp_root) + os.chdir(cwd)