1import getpass 2import logging 3import os 4import pathlib 5from unittest.mock import patch 6 7import pytest 8from jupyter_core.application import NoStart 9from traitlets import TraitError 10from traitlets.tests.utils import check_help_all_output 11 12from jupyter_server.auth.security import passwd_check 13from jupyter_server.serverapp import JupyterPasswordApp 14from jupyter_server.serverapp import list_running_servers 15from jupyter_server.serverapp import ServerApp 16 17 18def test_help_output(): 19 """jupyter server --help-all works""" 20 check_help_all_output("jupyter_server") 21 22 23def test_server_info_file(tmp_path, jp_configurable_serverapp): 24 app = jp_configurable_serverapp(log=logging.getLogger()) 25 26 app.write_server_info_file() 27 servers = list(list_running_servers(app.runtime_dir)) 28 29 assert len(servers) == 1 30 sinfo = servers[0] 31 32 assert sinfo["port"] == app.port 33 assert sinfo["url"] == app.connection_url 34 assert sinfo["version"] == app.version 35 36 app.remove_server_info_file() 37 38 assert list(list_running_servers(app.runtime_dir)) == [] 39 app.remove_server_info_file 40 41 42def test_root_dir(tmp_path, jp_configurable_serverapp): 43 app = jp_configurable_serverapp(root_dir=str(tmp_path)) 44 assert app.root_dir == str(tmp_path) 45 46 47# Build a list of invalid paths 48@pytest.fixture(params=[("notebooks",), ("root", "dir", "is", "missing"), ("test.txt",)]) 49def invalid_root_dir(tmp_path, request): 50 path = tmp_path.joinpath(*request.param) 51 # If the path is a file, create it. 52 if os.path.splitext(str(path))[1] != "": 53 path.write_text("") 54 return str(path) 55 56 57def test_invalid_root_dir(invalid_root_dir, jp_configurable_serverapp): 58 app = jp_configurable_serverapp() 59 with pytest.raises(TraitError): 60 app.root_dir = invalid_root_dir 61 62 63@pytest.fixture(params=[("/",), ("first-level",), ("first-level", "second-level")]) 64def valid_root_dir(tmp_path, request): 65 path = tmp_path.joinpath(*request.param) 66 if not path.exists(): 67 # Create path in temporary directory 68 path.mkdir(parents=True) 69 return str(path) 70 71 72def test_valid_root_dir(valid_root_dir, jp_configurable_serverapp): 73 app = jp_configurable_serverapp(root_dir=valid_root_dir) 74 root_dir = valid_root_dir 75 # If nested path, the last slash should 76 # be stripped by the root_dir trait. 77 if root_dir != "/": 78 root_dir = valid_root_dir.rstrip("/") 79 assert app.root_dir == root_dir 80 81 82def test_generate_config(tmp_path, jp_configurable_serverapp): 83 app = jp_configurable_serverapp(config_dir=str(tmp_path)) 84 app.initialize(["--generate-config", "--allow-root"]) 85 with pytest.raises(NoStart): 86 app.start() 87 assert tmp_path.joinpath("jupyter_server_config.py").exists() 88 89 90def test_server_password(tmp_path, jp_configurable_serverapp): 91 password = "secret" 92 with patch.dict("os.environ", {"JUPYTER_CONFIG_DIR": str(tmp_path)}), patch.object( 93 getpass, "getpass", return_value=password 94 ): 95 app = JupyterPasswordApp(log_level=logging.ERROR) 96 app.initialize([]) 97 app.start() 98 sv = jp_configurable_serverapp() 99 sv.load_config_file() 100 assert sv.password != "" 101 passwd_check(sv.password, password) 102 103 104def test_list_running_servers(jp_serverapp, jp_web_app): 105 servers = list(list_running_servers(jp_serverapp.runtime_dir)) 106 assert len(servers) >= 1 107 108 109@pytest.fixture 110def prefix_path(jp_root_dir, tmp_path): 111 """If a given path is prefixed with the literal 112 strings `/jp_root_dir` or `/tmp_path`, replace those 113 strings with these fixtures. 114 115 Returns a pathlib Path object. 116 """ 117 118 def _inner(rawpath): 119 path = pathlib.PurePosixPath(rawpath) 120 if rawpath.startswith("/jp_root_dir"): 121 path = jp_root_dir.joinpath(*path.parts[2:]) 122 elif rawpath.startswith("/tmp_path"): 123 path = tmp_path.joinpath(*path.parts[2:]) 124 return pathlib.Path(path) 125 126 return _inner 127 128 129@pytest.mark.parametrize( 130 "root_dir,file_to_run,expected_output", 131 [ 132 (None, "notebook.ipynb", "notebook.ipynb"), 133 (None, "/tmp_path/path/to/notebook.ipynb", "notebook.ipynb"), 134 ("/jp_root_dir", "/tmp_path/path/to/notebook.ipynb", SystemExit), 135 ("/tmp_path", "/tmp_path/path/to/notebook.ipynb", "path/to/notebook.ipynb"), 136 ("/jp_root_dir", "notebook.ipynb", "notebook.ipynb"), 137 ("/jp_root_dir", "path/to/notebook.ipynb", "path/to/notebook.ipynb"), 138 ], 139) 140def test_resolve_file_to_run_and_root_dir(prefix_path, root_dir, file_to_run, expected_output): 141 # Verify that the Singleton instance is cleared before the test runs. 142 ServerApp.clear_instance() 143 144 # Setup the file_to_run path, in case the server checks 145 # if the directory exists before initializing the server. 146 file_to_run = prefix_path(file_to_run) 147 if file_to_run.is_absolute(): 148 file_to_run.parent.mkdir(parents=True, exist_ok=True) 149 kwargs = {"file_to_run": str(file_to_run)} 150 151 # Setup the root_dir path, in case the server checks 152 # if the directory exists before initializing the server. 153 if root_dir: 154 root_dir = prefix_path(root_dir) 155 if root_dir.is_absolute(): 156 root_dir.parent.mkdir(parents=True, exist_ok=True) 157 kwargs["root_dir"] = str(root_dir) 158 159 # Create the notebook in the given location 160 serverapp = ServerApp.instance(**kwargs) 161 162 if expected_output is SystemExit: 163 with pytest.raises(SystemExit): 164 serverapp._resolve_file_to_run_and_root_dir() 165 else: 166 relpath = serverapp._resolve_file_to_run_and_root_dir() 167 assert relpath == str(pathlib.Path(expected_output)) 168 169 # Clear the singleton instance after each run. 170 ServerApp.clear_instance() 171 172 173# Test the URLs returned by ServerApp. The `<generated>` piece 174# in urls shown below will be replaced with the token 175# generated by the ServerApp on instance creation. 176@pytest.mark.parametrize( 177 "config,public_url,local_url,connection_url", 178 [ 179 # Token is hidden when configured. 180 ( 181 {"token": "test"}, 182 "http://localhost:8888/?token=...", 183 "http://127.0.0.1:8888/?token=...", 184 "http://localhost:8888/", 185 ), 186 # Verify port number has changed 187 ( 188 {"port": 9999}, 189 "http://localhost:9999/?token=<generated>", 190 "http://127.0.0.1:9999/?token=<generated>", 191 "http://localhost:9999/", 192 ), 193 ( 194 {"ip": "1.1.1.1"}, 195 "http://1.1.1.1:8888/?token=<generated>", 196 "http://127.0.0.1:8888/?token=<generated>", 197 "http://1.1.1.1:8888/", 198 ), 199 # Verify that HTTPS is returned when certfile is given 200 ( 201 {"certfile": "/path/to/dummy/file"}, 202 "https://localhost:8888/?token=<generated>", 203 "https://127.0.0.1:8888/?token=<generated>", 204 "https://localhost:8888/", 205 ), 206 # Verify changed port and a custom display URL 207 ( 208 {"port": 9999, "custom_display_url": "http://test.org"}, 209 "http://test.org/?token=<generated>", 210 "http://127.0.0.1:9999/?token=<generated>", 211 "http://localhost:9999/", 212 ), 213 ( 214 {"base_url": "/", "default_url": "/test/"}, 215 "http://localhost:8888/test/?token=<generated>", 216 "http://127.0.0.1:8888/test/?token=<generated>", 217 "http://localhost:8888/", 218 ), 219 # Verify unix socket URLs are handled properly 220 ( 221 {"sock": "/tmp/jp-test.sock"}, 222 "http+unix://%2Ftmp%2Fjp-test.sock/?token=<generated>", 223 "http+unix://%2Ftmp%2Fjp-test.sock/?token=<generated>", 224 "http+unix://%2Ftmp%2Fjp-test.sock/", 225 ), 226 ( 227 {"base_url": "/", "default_url": "/test/", "sock": "/tmp/jp-test.sock"}, 228 "http+unix://%2Ftmp%2Fjp-test.sock/test/?token=<generated>", 229 "http+unix://%2Ftmp%2Fjp-test.sock/test/?token=<generated>", 230 "http+unix://%2Ftmp%2Fjp-test.sock/", 231 ), 232 ], 233) 234def test_urls(config, public_url, local_url, connection_url): 235 # Verify we're working with a clean instance. 236 ServerApp.clear_instance() 237 serverapp = ServerApp.instance(**config) 238 # If a token is generated (not set by config), update 239 # expected_url with token. 240 if serverapp._token_generated: 241 public_url = public_url.replace("<generated>", serverapp.token) 242 local_url = local_url.replace("<generated>", serverapp.token) 243 connection_url = connection_url.replace("<generated>", serverapp.token) 244 assert serverapp.public_url == public_url 245 assert serverapp.local_url == local_url 246 assert serverapp.connection_url == connection_url 247 # Cleanup singleton after test. 248 ServerApp.clear_instance() 249 250 251# Preferred dir tests 252# ---------------------------------------------------------------------------- 253def test_valid_preferred_dir(tmp_path, jp_configurable_serverapp): 254 path = str(tmp_path) 255 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path) 256 assert app.root_dir == path 257 assert app.preferred_dir == path 258 assert app.root_dir == app.preferred_dir 259 260 261def test_valid_preferred_dir_is_root_subdir(tmp_path, jp_configurable_serverapp): 262 path = str(tmp_path) 263 path_subdir = str(tmp_path / "subdir") 264 os.makedirs(path_subdir, exist_ok=True) 265 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path_subdir) 266 assert app.root_dir == path 267 assert app.preferred_dir == path_subdir 268 assert app.preferred_dir.startswith(app.root_dir) 269 270 271def test_valid_preferred_dir_does_not_exist(tmp_path, jp_configurable_serverapp): 272 path = str(tmp_path) 273 path_subdir = str(tmp_path / "subdir") 274 with pytest.raises(TraitError) as error: 275 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path_subdir) 276 277 assert "No such preferred dir:" in str(error) 278 279 280def test_invalid_preferred_dir_does_not_exist(tmp_path, jp_configurable_serverapp): 281 path = str(tmp_path) 282 path_subdir = str(tmp_path / "subdir") 283 with pytest.raises(TraitError) as error: 284 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path_subdir) 285 286 assert "No such preferred dir:" in str(error) 287 288 289def test_invalid_preferred_dir_does_not_exist_set(tmp_path, jp_configurable_serverapp): 290 path = str(tmp_path) 291 path_subdir = str(tmp_path / "subdir") 292 293 app = jp_configurable_serverapp(root_dir=path) 294 with pytest.raises(TraitError) as error: 295 app.preferred_dir = path_subdir 296 297 assert "No such preferred dir:" in str(error) 298 299 300def test_invalid_preferred_dir_not_root_subdir(tmp_path, jp_configurable_serverapp): 301 path = str(tmp_path / "subdir") 302 os.makedirs(path, exist_ok=True) 303 not_subdir_path = str(tmp_path) 304 305 with pytest.raises(TraitError) as error: 306 app = jp_configurable_serverapp(root_dir=path, preferred_dir=not_subdir_path) 307 308 assert "preferred_dir must be equal or a subdir of root_dir:" in str(error) 309 310 311def test_invalid_preferred_dir_not_root_subdir_set(tmp_path, jp_configurable_serverapp): 312 path = str(tmp_path / "subdir") 313 os.makedirs(path, exist_ok=True) 314 not_subdir_path = str(tmp_path) 315 316 app = jp_configurable_serverapp(root_dir=path) 317 with pytest.raises(TraitError) as error: 318 app.preferred_dir = not_subdir_path 319 320 assert "preferred_dir must be equal or a subdir of root_dir:" in str(error) 321 322 323def test_observed_root_dir_updates_preferred_dir(tmp_path, jp_configurable_serverapp): 324 path = str(tmp_path) 325 new_path = str(tmp_path / "subdir") 326 os.makedirs(new_path, exist_ok=True) 327 328 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path) 329 app.root_dir = new_path 330 assert app.preferred_dir == new_path 331 332 333def test_observed_root_dir_does_not_update_preferred_dir(tmp_path, jp_configurable_serverapp): 334 path = str(tmp_path) 335 new_path = str(tmp_path.parent) 336 app = jp_configurable_serverapp(root_dir=path, preferred_dir=path) 337 app.root_dir = new_path 338 assert app.preferred_dir == path 339