diff --git a/examples/simple/setup.py b/examples/simple/setup.py index 9040c55e8..c15b12456 100755 --- a/examples/simple/setup.py +++ b/examples/simple/setup.py @@ -40,7 +40,7 @@ def add_data_files(path): 'jinja2', ], extras_require = { - 'test': ['pytest-jupyter'], + 'test': ['pytest'], }, include_package_data=True, cmdclass = cmdclass, diff --git a/examples/simple/tests/conftest.py b/examples/simple/tests/conftest.py new file mode 100644 index 000000000..87c6aff30 --- /dev/null +++ b/examples/simple/tests/conftest.py @@ -0,0 +1,3 @@ +pytest_plugins = [ + 'jupyter_server.pytest_plugin' +] diff --git a/jupyter_server/pytest_plugin.py b/jupyter_server/pytest_plugin.py new file mode 100644 index 000000000..dee6d3f0d --- /dev/null +++ b/jupyter_server/pytest_plugin.py @@ -0,0 +1,450 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import os +import sys +import json +import pytest +import shutil +import urllib.parse + +from binascii import hexlify + +import tornado +from tornado.escape import url_escape +import jupyter_core.paths +import nbformat +from traitlets.config import Config + +from jupyter_server.extension import serverextension +from jupyter_server.serverapp import ServerApp +from jupyter_server.utils import url_path_join +from jupyter_server.services.contents.filemanager import FileContentsManager +from jupyter_server.services.contents.largefilemanager import LargeFileManager + + +# List of dependencies needed for this plugin. +pytest_plugins = [ + "pytest_tornasync", + # Once the chunk below moves to Jupyter Core, we'll uncomment + # This plugin and use the fixtures directly from Jupyter Core. + # "jupyter_core.pytest_plugin" +] + +# ============ Move to Jupyter Core ============= + +def mkdir(tmp_path, *parts): + path = tmp_path.joinpath(*parts) + if not path.exists(): + path.mkdir(parents=True) + return path + + +@pytest.fixture +def jp_home_dir(tmp_path): + """Provides a temporary HOME directory value.""" + return mkdir(tmp_path, "home") + + +@pytest.fixture +def jp_data_dir(tmp_path): + """Provides a temporary Jupyter data dir directory value.""" + return mkdir(tmp_path, "data") + + +@pytest.fixture +def jp_config_dir(tmp_path): + """Provides a temporary Jupyter config dir directory value.""" + return mkdir(tmp_path, "config") + + +@pytest.fixture +def jp_runtime_dir(tmp_path): + """Provides a temporary Jupyter runtime dir directory value.""" + return mkdir(tmp_path, "runtime") + + +@pytest.fixture +def jp_system_jupyter_path(tmp_path): + """Provides a temporary Jupyter system path value.""" + return mkdir(tmp_path, "share", "jupyter") + + +@pytest.fixture +def jp_env_jupyter_path(tmp_path): + """Provides a temporary Jupyter env system path value.""" + return mkdir(tmp_path, "env", "share", "jupyter") + + +@pytest.fixture +def jp_system_config_path(tmp_path): + """Provides a temporary Jupyter config path value.""" + return mkdir(tmp_path, "etc", "jupyter") + + +@pytest.fixture +def jp_env_config_path(tmp_path): + """Provides a temporary Jupyter env config path value.""" + return mkdir(tmp_path, "env", "etc", "jupyter") + + +@pytest.fixture +def jp_environ( + monkeypatch, + tmp_path, + jp_home_dir, + jp_data_dir, + jp_config_dir, + jp_runtime_dir, + jp_system_jupyter_path, + jp_system_config_path, + jp_env_jupyter_path, + jp_env_config_path, +): + """Configures a temporary environment based on Jupyter-specific environment variables. """ + monkeypatch.setenv("HOME", str(jp_home_dir)) + monkeypatch.setenv("PYTHONPATH", os.pathsep.join(sys.path)) + # monkeypatch.setenv("JUPYTER_NO_CONFIG", "1") + monkeypatch.setenv("JUPYTER_CONFIG_DIR", str(jp_config_dir)) + monkeypatch.setenv("JUPYTER_DATA_DIR", str(jp_data_dir)) + monkeypatch.setenv("JUPYTER_RUNTIME_DIR", str(jp_runtime_dir)) + monkeypatch.setattr( + jupyter_core.paths, "SYSTEM_JUPYTER_PATH", [str(jp_system_jupyter_path)] + ) + monkeypatch.setattr(jupyter_core.paths, "ENV_JUPYTER_PATH", [str(jp_env_jupyter_path)]) + monkeypatch.setattr( + jupyter_core.paths, "SYSTEM_CONFIG_PATH", [str(jp_system_config_path)] + ) + monkeypatch.setattr(jupyter_core.paths, "ENV_CONFIG_PATH", [str(jp_env_config_path)]) + + +# ================= End: Move to Jupyter core ================ + +# NOTE: This is a temporary fix for Windows 3.8 +# We have to override the io_loop fixture with an +# asyncio patch. This will probably be removed in +# the future. +@pytest.fixture +def jp_asyncio_patch(): + """Appropriately configures the event loop policy if running on Windows w/ Python >= 3.8.""" + ServerApp()._init_asyncio_patch() + + +@pytest.fixture +def io_loop(jp_asyncio_patch): + """Returns an ioloop instance that includes the asyncio patch for Windows 3.8 platforms.""" + loop = tornado.ioloop.IOLoop() + loop.make_current() + yield loop + loop.clear_current() + loop.close(all_fds=True) + + +@pytest.fixture +def jp_server_config(): + """Allows tests to setup their specific configuration values. """ + return {} + + +@pytest.fixture +def jp_root_dir(tmp_path): + """Provides a temporary Jupyter root directory value.""" + return mkdir(tmp_path, "root_dir") + + +@pytest.fixture +def jp_template_dir(tmp_path): + """Provides a temporary Jupyter templates directory value.""" + return mkdir(tmp_path, "templates") + + +@pytest.fixture +def jp_argv(): + """Allows tests to setup specific argv values. """ + return [] + + +@pytest.fixture +def jp_extension_environ(jp_env_config_path, monkeypatch): + """Monkeypatch a Jupyter Extension's config path into each test's environment variable""" + monkeypatch.setattr(serverextension, "ENV_CONFIG_PATH", [str(jp_env_config_path)]) + + +@pytest.fixture +def jp_http_port(http_server_port): + """Returns the port value from the http_server_port fixture. """ + return http_server_port[-1] + + +@pytest.fixture +def jp_nbconvert_templates(jp_data_dir): + """Setups up a temporary directory consisting of the nbconvert templates.""" + + # Get path to nbconvert template directory *before* + # monkeypatching the paths env variable via the jp_environ fixture. + possible_paths = jupyter_core.paths.jupyter_path('nbconvert', 'templates') + nbconvert_path = None + for path in possible_paths: + if os.path.exists(path): + nbconvert_path = path + break + + nbconvert_target = jp_data_dir / 'nbconvert' / 'templates' + + # copy nbconvert templates to new tmp data_dir. + if nbconvert_path: + shutil.copytree(nbconvert_path, str(nbconvert_target)) + + +@pytest.fixture(scope='function') +def jp_configurable_serverapp( + jp_nbconvert_templates, # this fixture must preceed jp_environ + jp_environ, + jp_server_config, + jp_argv, + jp_http_port, + jp_base_url, + tmp_path, + jp_root_dir, + io_loop, +): + """Starts a Jupyter Server instance based on + the provided configuration values. + + The fixture is a factory; it can be called like + a function inside a unit test. Here's a basic + example of how use this fixture: + + .. code-block:: python + + def my_test(jp_configurable_serverapp): + + app = jp_configurable_serverapp(...) + ... + """ + ServerApp.clear_instance() + + def _configurable_serverapp( + config=jp_server_config, + base_url=jp_base_url, + argv=jp_argv, + environ=jp_environ, + http_port=jp_http_port, + tmp_path=tmp_path, + root_dir=jp_root_dir, + **kwargs + ): + c = Config(config) + c.NotebookNotary.db_file = ":memory:" + token = hexlify(os.urandom(4)).decode("ascii") + app = ServerApp.instance( + # Set the log level to debug for testing purposes + log_level='DEBUG', + port=http_port, + port_retries=0, + open_browser=False, + root_dir=str(root_dir), + base_url=base_url, + config=c, + allow_root=True, + token=token, + **kwargs + ) + + app.init_signal = lambda: None + app.log.propagate = True + app.log.handlers = [] + # Initialize app without httpserver + app.initialize(argv=argv, new_httpserver=False) + app.log.propagate = True + app.log.handlers = [] + # Start app without ioloop + app.start_app() + return app + + return _configurable_serverapp + + +@pytest.fixture +def jp_ensure_app_fixture(request): + """Ensures that the 'app' fixture used by pytest-tornasync + is set to `jp_web_app`, the Tornado Web Application returned + by the ServerApp in Jupyter Server, provided by the jp_web_app + fixture in this module. + + Note, this hardcodes the `app_fixture` option from + pytest-tornasync to `jp_web_app`. If this value is configured + to something other than the default, it will raise an exception. + """ + app_option = request.config.getoption("app_fixture") + if app_option not in ["app", "jp_web_app"]: + raise Exception("jp_serverapp requires the `app-fixture` option " + "to be set to 'jp_web_app`. Try rerunning the " + "current tests with the option `--app-fixture " + "jp_web_app`.") + elif app_option == "app": + # Manually set the app_fixture to `jp_web_app` if it's + # not set already. + request.config.option.app_fixture = "jp_web_app" + + +@pytest.fixture(scope="function") +def jp_serverapp( + jp_ensure_app_fixture, + jp_server_config, + jp_argv, + jp_configurable_serverapp +): + """Starts a Jupyter Server instance based on the established configuration values.""" + app = jp_configurable_serverapp(config=jp_server_config, argv=jp_argv) + yield app + app.remove_server_info_file() + app.remove_browser_open_file() + app.cleanup_kernels() + + +@pytest.fixture +def jp_web_app(jp_serverapp): + """app fixture is needed by pytest_tornasync plugin""" + return jp_serverapp.web_app + + +@pytest.fixture +def jp_auth_header(jp_serverapp): + """Configures an authorization header using the token from the serverapp fixture.""" + return {"Authorization": "token {token}".format(token=jp_serverapp.token)} + + +@pytest.fixture +def jp_base_url(): + """Returns the base url to use for the test.""" + return "/a%40b/" + + +@pytest.fixture +def jp_fetch(jp_serverapp, http_server_client, jp_auth_header, jp_base_url): + """Sends an (asynchronous) HTTP request to a test server. + + The fixture is a factory; it can be called like + a function inside a unit test. Here's a basic + example of how use this fixture: + + .. code-block:: python + + async def my_test(jp_fetch): + + response = await jp_fetch("api", "spec.yaml") + ... + """ + def client_fetch(*parts, headers={}, params={}, **kwargs): + # Handle URL strings + path_url = url_escape(url_path_join(*parts), plus=False) + base_path_url = url_path_join(jp_base_url, path_url) + params_url = urllib.parse.urlencode(params) + url = base_path_url + "?" + params_url + # Add auth keys to header + headers.update(jp_auth_header) + # Make request. + return http_server_client.fetch( + url, headers=headers, request_timeout=20, **kwargs + ) + return client_fetch + + +@pytest.fixture +def jp_ws_fetch(jp_serverapp, jp_auth_header, jp_http_port, jp_base_url): + """Sends a websocket request to a test server. + + The fixture is a factory; it can be called like + a function inside a unit test. Here's a basic + example of how use this fixture: + + .. code-block:: python + + async def my_test(jp_fetch, jp_ws_fetch): + # Start a kernel + r = await jp_fetch( + 'api', 'kernels', + method='POST', + body=json.dumps({ + 'name': "python3" + }) + ) + kid = json.loads(r.body.decode())['id'] + + # Open a websocket connection. + ws = await jp_ws_fetch( + 'api', 'kernels', kid, 'channels' + ) + ... + """ + def client_fetch(*parts, headers={}, params={}, **kwargs): + # Handle URL strings + path_url = url_escape(url_path_join(*parts), plus=False) + base_path_url = url_path_join(jp_base_url, path_url) + urlparts = urllib.parse.urlparse('ws://localhost:{}'.format(jp_http_port)) + urlparts = urlparts._replace( + path=base_path_url, + query=urllib.parse.urlencode(params) + ) + url = urlparts.geturl() + # Add auth keys to header + headers.update(jp_auth_header) + # Make request. + req = tornado.httpclient.HTTPRequest( + url, + headers=jp_auth_header, + connect_timeout=120 + ) + return tornado.websocket.websocket_connect(req) + return client_fetch + + +some_resource = u"The very model of a modern major general" +sample_kernel_json = { + 'argv':['cat', '{connection_file}'], + 'display_name': 'Test kernel', +} +@pytest.fixture +def jp_kernelspecs(jp_data_dir): + """Configures some sample kernelspecs in the Jupyter data directory.""" + spec_names = ['sample', 'sample 2'] + for name in spec_names: + sample_kernel_dir = jp_data_dir.joinpath('kernels', name) + sample_kernel_dir.mkdir(parents=True) + # Create kernel json file + sample_kernel_file = sample_kernel_dir.joinpath('kernel.json') + sample_kernel_file.write_text(json.dumps(sample_kernel_json)) + # Create resources text + sample_kernel_resources = sample_kernel_dir.joinpath('resource.txt') + sample_kernel_resources.write_text(some_resource) + + +@pytest.fixture(params=[True, False]) +def jp_contents_manager(request, tmp_path): + """Returns a FileContentsManager instance based on the use_atomic_writing parameter value.""" + return FileContentsManager(root_dir=str(tmp_path), use_atomic_writing=request.param) + + +@pytest.fixture +def jp_large_contents_manager(tmp_path): + """Returns a LargeFileManager instance.""" + return LargeFileManager(root_dir=str(tmp_path)) + + +@pytest.fixture +def jp_create_notebook(jp_root_dir): + """Creates a notebook in the test's home directory.""" + def inner(nbpath): + nbpath = jp_root_dir.joinpath(nbpath) + # Check that the notebook has the correct file extension. + if nbpath.suffix != '.ipynb': + raise Exception("File extension for notebook must be .ipynb") + # If the notebook path has a parent directory, make sure it's created. + parent = nbpath.parent + parent.mkdir(parents=True, exist_ok=True) + # Create a notebook string and write to file. + nb = nbformat.v4.new_notebook() + nbtext = nbformat.writes(nb, version=4) + nbpath.write_text(nbtext) + return inner diff --git a/setup.py b/setup.py index c79672055..063764d2a 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,8 @@ ], extras_require = { 'test': ['coverage', 'requests', - 'pytest', 'pytest-cov', 'pytest-jupyter', + 'pytest', 'pytest-cov', + 'pytest-tornasync', 'pytest-console-scripts', 'ipykernel'], }, python_requires = '>=3.6', diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..bdac3802b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +pytest_plugins = [ + "jupyter_server.pytest_plugin" +] diff --git a/tests/extension/test_handler.py b/tests/extension/test_handler.py index 1f5b52e64..1e4a47c78 100644 --- a/tests/extension/test_handler.py +++ b/tests/extension/test_handler.py @@ -51,7 +51,7 @@ async def test_handler_template(jp_fetch, mock_template): } ] ) -async def test_handler_setting(jp_fetch): +async def test_handler_setting(jp_fetch, jp_server_config): # Test that the extension trait was picked up by the webapp. r = await jp_fetch( 'mock', @@ -64,7 +64,7 @@ async def test_handler_setting(jp_fetch): @pytest.mark.parametrize( 'jp_argv', (['--MockExtensionApp.mock_trait=test mock trait'],) ) -async def test_handler_argv(jp_fetch): +async def test_handler_argv(jp_fetch, jp_argv): # Test that the extension trait was picked up by the webapp. r = await jp_fetch( 'mock', @@ -75,28 +75,31 @@ async def test_handler_argv(jp_fetch): @pytest.mark.parametrize( - 'jp_server_config', + 'jp_server_config,jp_base_url', [ - { - "ServerApp": { - "jpserver_extensions": { - "tests.extension.mockextensions": True + ( + { + "ServerApp": { + "jpserver_extensions": { + "tests.extension.mockextensions": True + }, + # Move extension handlers behind a url prefix + "base_url": "test_prefix" }, - # Move extension handlers behind a url prefix - "base_url": "test_prefix" + "MockExtensionApp": { + # Change a trait in the MockExtensionApp using + # the following config value. + "mock_trait": "test mock trait" + } }, - "MockExtensionApp": { - # Change a trait in the MockExtensionApp using - # the following config value. - "mock_trait": "test mock trait" - } - } + '/test_prefix/' + ) ] ) -async def test_base_url(jp_fetch): +async def test_base_url(jp_fetch, jp_server_config, jp_base_url): # Test that the extension's handlers were properly prefixed r = await jp_fetch( - 'test_prefix', 'mock', + 'mock', method='GET' ) assert r.code == 200 @@ -104,7 +107,6 @@ async def test_base_url(jp_fetch): # Test that the static namespace was prefixed by base_url r = await jp_fetch( - 'test_prefix', 'static', 'mockextension', 'mock.txt', method='GET' ) diff --git a/tests/extension/test_serverextension.py b/tests/extension/test_serverextension.py index cc2fb80dd..2034d3279 100644 --- a/tests/extension/test_serverextension.py +++ b/tests/extension/test_serverextension.py @@ -106,7 +106,7 @@ def test_merge_config( } ] ) -def test_load_ordered(jp_serverapp): +def test_load_ordered(jp_serverapp, jp_server_config): assert jp_serverapp.mockII is True, "Mock II should have been loaded" assert jp_serverapp.mockI is True, "Mock I should have been loaded" assert jp_serverapp.mock_shared == 'II', "Mock II should be loaded after Mock I" diff --git a/tests/services/contents/test_api.py b/tests/services/contents/test_api.py index ee8b57fb7..fea04db7b 100644 --- a/tests/services/contents/test_api.py +++ b/tests/services/contents/test_api.py @@ -264,24 +264,25 @@ async def test_get_bad_type(jp_fetch, contents): ) assert expected_http_error(e, 400, '%s is not a directory' % path) - -def _check_created(r, contents_dir, path, name, type='notebook'): - fpath = path+'/'+name - assert r.code == 201 - location = '/api/contents/' + tornado.escape.url_escape(fpath, plus=False) - assert r.headers['Location'] == location - model = json.loads(r.body.decode()) - assert model['name'] == name - assert model['path'] == fpath - assert model['type'] == type - path = contents_dir + '/' + fpath - if type == 'directory': - assert pathlib.Path(path).is_dir() - else: - assert pathlib.Path(path).is_file() - - -async def test_create_untitled(jp_fetch, contents, contents_dir): +@pytest.fixture +def _check_created(jp_base_url): + def _inner(r, contents_dir, path, name, type='notebook'): + fpath = path+'/'+name + assert r.code == 201 + location = jp_base_url + 'api/contents/' + tornado.escape.url_escape(fpath, plus=False) + assert r.headers['Location'] == location + model = json.loads(r.body.decode()) + assert model['name'] == name + assert model['path'] == fpath + assert model['type'] == type + path = contents_dir + '/' + fpath + if type == 'directory': + assert pathlib.Path(path).is_dir() + else: + assert pathlib.Path(path).is_file() + return _inner + +async def test_create_untitled(jp_fetch, contents, contents_dir, _check_created): path = 'å b' name = 'Untitled.ipynb' r = await jp_fetch( @@ -309,7 +310,7 @@ async def test_create_untitled(jp_fetch, contents, contents_dir): _check_created(r, str(contents_dir), path, name, type='notebook') -async def test_create_untitled_txt(jp_fetch, contents, contents_dir): +async def test_create_untitled_txt(jp_fetch, contents, contents_dir, _check_created): name = 'untitled.txt' path = 'foo/bar' r = await jp_fetch( @@ -329,7 +330,7 @@ async def test_create_untitled_txt(jp_fetch, contents, contents_dir): assert model['content'] == '' -async def test_upload(jp_fetch, contents, contents_dir): +async def test_upload(jp_fetch, contents, contents_dir, _check_created): nb = new_notebook() nbmodel = {'content': nb, 'type': 'notebook'} path = 'å b' @@ -342,7 +343,7 @@ async def test_upload(jp_fetch, contents, contents_dir): _check_created(r, str(contents_dir), path, name) -async def test_mkdir_untitled(jp_fetch, contents, contents_dir): +async def test_mkdir_untitled(jp_fetch, contents, contents_dir, _check_created): name = 'Untitled Folder' path = 'å b' r = await jp_fetch( @@ -370,7 +371,7 @@ async def test_mkdir_untitled(jp_fetch, contents, contents_dir): _check_created(r, str(contents_dir), path, name, type='directory') -async def test_mkdir(jp_fetch, contents, contents_dir): +async def test_mkdir(jp_fetch, contents, contents_dir, _check_created): name = 'New ∂ir' path = 'å b' r = await jp_fetch( @@ -391,7 +392,7 @@ async def test_mkdir_hidden_400(jp_fetch): assert expected_http_error(e, 400) -async def test_upload_txt(jp_fetch, contents, contents_dir): +async def test_upload_txt(jp_fetch, contents, contents_dir, _check_created): body = 'ünicode téxt' model = { 'content' : body, @@ -418,7 +419,7 @@ async def test_upload_txt(jp_fetch, contents, contents_dir): assert model['content'] == body -async def test_upload_b64(jp_fetch, contents, contents_dir): +async def test_upload_b64(jp_fetch, contents, contents_dir, _check_created): body = b'\xFFblob' b64body = encodebytes(body).decode('ascii') model = { @@ -446,7 +447,7 @@ async def test_upload_b64(jp_fetch, contents, contents_dir): assert decoded == body -async def test_copy(jp_fetch, contents, contents_dir): +async def test_copy(jp_fetch, contents, contents_dir, _check_created): path = 'å b' name = 'ç d.ipynb' copy = 'ç d-Copy1.ipynb' @@ -476,7 +477,7 @@ async def test_copy(jp_fetch, contents, contents_dir): _check_created(r, str(contents_dir), path, copy3, type='notebook') -async def test_copy_path(jp_fetch, contents, contents_dir): +async def test_copy_path(jp_fetch, contents, contents_dir, _check_created): path1 = 'foo' path2 = 'å b' name = 'a.ipynb' @@ -496,7 +497,7 @@ async def test_copy_path(jp_fetch, contents, contents_dir): _check_created(r, str(contents_dir), path2, copy, type='notebook') -async def test_copy_put_400(jp_fetch, contents, contents_dir): +async def test_copy_put_400(jp_fetch, contents, contents_dir, _check_created): with pytest.raises(tornado.httpclient.HTTPClientError) as e: await jp_fetch( 'api', 'contents', 'å b/cøpy.ipynb', @@ -506,7 +507,7 @@ async def test_copy_put_400(jp_fetch, contents, contents_dir): assert expected_http_error(e, 400) -async def test_copy_dir_400(jp_fetch, contents, contents_dir): +async def test_copy_dir_400(jp_fetch, contents, contents_dir, _check_created): with pytest.raises(tornado.httpclient.HTTPClientError) as e: await jp_fetch( 'api', 'contents', 'foo', @@ -517,7 +518,7 @@ async def test_copy_dir_400(jp_fetch, contents, contents_dir): @pytest.mark.parametrize('path,name', dirs) -async def test_delete(jp_fetch, contents, contents_dir, path, name): +async def test_delete(jp_fetch, contents, contents_dir, path, name, _check_created): nbname = name+'.ipynb' nbpath = (path + '/' + nbname).lstrip('/') r = await jp_fetch( @@ -567,7 +568,7 @@ async def test_delete_non_empty_dir(jp_fetch, contents): assert expected_http_error(e, 404) -async def test_rename(jp_fetch, contents, contents_dir): +async def test_rename(jp_fetch, jp_base_url, contents, contents_dir): path = 'foo' name = 'a.ipynb' new_name = 'z.ipynb' @@ -579,7 +580,7 @@ async def test_rename(jp_fetch, contents, contents_dir): ) fpath = path+'/'+new_name assert r.code == 200 - location = '/api/contents/' + fpath + location = url_path_join(jp_base_url, 'api/contents/', fpath) assert r.headers['Location'] == location model = json.loads(r.body.decode()) assert model['name'] == new_name diff --git a/tests/services/kernels/test_api.py b/tests/services/kernels/test_api.py index 712ea8551..bc2ade403 100644 --- a/tests/services/kernels/test_api.py +++ b/tests/services/kernels/test_api.py @@ -31,18 +31,18 @@ async def test_no_kernels(jp_fetch): assert kernels == [] -async def test_default_kernels(jp_fetch): +async def test_default_kernels(jp_fetch, jp_base_url): r = await jp_fetch( 'api', 'kernels', method='POST', allow_nonstandard_methods=True ) kernel = json.loads(r.body.decode()) - assert r.headers['location'] == '/api/kernels/' + kernel['id'] + assert r.headers['location'] == url_path_join(jp_base_url, '/api/kernels/', kernel['id']) assert r.code == 201 assert isinstance(kernel, dict) - report_uri = '/api/security/csp-report' + report_uri = url_path_join(jp_base_url, '/api/security/csp-report') expected_csp = '; '.join([ "frame-ancestors 'self'", 'report-uri ' + report_uri, @@ -51,7 +51,7 @@ async def test_default_kernels(jp_fetch): assert r.headers['Content-Security-Policy'] == expected_csp -async def test_main_kernel_handler(jp_fetch): +async def test_main_kernel_handler(jp_fetch, jp_base_url): # Start the first kernel r = await jp_fetch( 'api', 'kernels', @@ -61,11 +61,11 @@ async def test_main_kernel_handler(jp_fetch): }) ) kernel1 = json.loads(r.body.decode()) - assert r.headers['location'] == '/api/kernels/' + kernel1['id'] + assert r.headers['location'] == url_path_join(jp_base_url, '/api/kernels/', kernel1['id']) assert r.code == 201 assert isinstance(kernel1, dict) - report_uri = '/api/security/csp-report' + report_uri = url_path_join(jp_base_url, '/api/security/csp-report') expected_csp = '; '.join([ "frame-ancestors 'self'", 'report-uri ' + report_uri, diff --git a/tests/services/kernelspecs/test_api.py b/tests/services/kernelspecs/test_api.py index e95e57f63..f5cc3a9a0 100644 --- a/tests/services/kernelspecs/test_api.py +++ b/tests/services/kernelspecs/test_api.py @@ -3,11 +3,8 @@ import tornado -from pytest_jupyter.jupyter_server import some_resource - from jupyter_client.kernelspec import NATIVE_KERNEL_NAME - -from ...utils import expected_http_error +from ...utils import expected_http_error, some_resource async def test_list_kernelspecs_bad(jp_fetch, jp_kernelspecs, jp_data_dir): diff --git a/tests/services/sessions/test_api.py b/tests/services/sessions/test_api.py index 6ffd01354..065e392a6 100644 --- a/tests/services/sessions/test_api.py +++ b/tests/services/sessions/test_api.py @@ -10,6 +10,8 @@ from nbformat import writes from ...utils import expected_http_error +from jupyter_server.utils import url_path_join + j = lambda r: json.loads(r.body.decode()) @@ -148,7 +150,7 @@ def assert_session_equality(actual, expected): assert_kernel_equality(actual['kernel'], expected['kernel']) -async def test_create(session_client): +async def test_create(session_client, jp_base_url): # Make sure no sessions exist. resp = await session_client.list() sessions = j(resp) @@ -161,7 +163,7 @@ async def test_create(session_client): assert 'id' in new_session assert new_session['path'] == 'foo/nb1.ipynb' assert new_session['type'] == 'notebook' - assert resp.headers['Location'] == '/api/sessions/' + new_session['id'] + assert resp.headers['Location'] == url_path_join(jp_base_url, '/api/sessions/', new_session['id']) # Check that the new session appears in list. resp = await session_client.list() @@ -209,7 +211,7 @@ async def test_create_deprecated(session_client): await session_client.cleanup() -async def test_create_with_kernel_id(session_client, jp_fetch): +async def test_create_with_kernel_id(session_client, jp_fetch, jp_base_url): # create a new kernel resp = await jp_fetch('api/kernels', method='POST', allow_nonstandard_methods=True) kernel = j(resp) @@ -220,7 +222,7 @@ async def test_create_with_kernel_id(session_client, jp_fetch): assert 'id' in new_session assert new_session['path'] == 'foo/nb1.ipynb' assert new_session['kernel']['id'] == kernel['id'] - assert resp.headers['Location'] == '/api/sessions/{0}'.format(new_session['id']) + assert resp.headers['Location'] == url_path_join(jp_base_url, '/api/sessions/{0}'.format(new_session['id'])) resp = await session_client.list() sessions = j(resp) diff --git a/tests/test_paths.py b/tests/test_paths.py index 155426d06..60c2951a1 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -2,7 +2,7 @@ import pytest import tornado from jupyter_server.base.handlers import path_regex - +from jupyter_server.utils import url_path_join # build regexps that tornado uses: path_pat = re.compile('^' + '/x%s' % path_regex + '$') @@ -45,7 +45,7 @@ async def test_trailing_slash(jp_ensure_app_fixture, uri, expected, http_server_ # http_server_client raises an exception when follow_redirects=False with pytest.raises(tornado.httpclient.HTTPClientError) as err: await http_server_client.fetch( - uri, + url_path_join(jp_base_url, uri), headers=jp_auth_header, request_timeout=20, follow_redirects=False @@ -54,4 +54,4 @@ async def test_trailing_slash(jp_ensure_app_fixture, uri, expected, http_server_ response = err.value.response assert response.code == 302 assert "Location" in response.headers - assert response.headers["Location"] == expected + assert response.headers["Location"] == url_path_join(jp_base_url, expected) diff --git a/tests/utils.py b/tests/utils.py index 819d62aa5..8e7376897 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,13 @@ import json import tornado +some_resource = u"The very model of a modern major general" + +sample_kernel_json = { + 'argv':['cat', '{connection_file}'], + 'display_name': 'Test kernel', +} + def mkdir(tmp_path, *parts): path = tmp_path.joinpath(*parts)