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