From 8a59d74813cdda217729031284e481754b3a8c76 Mon Sep 17 00:00:00 2001 From: Peter Parente Date: Tue, 28 Nov 2017 23:57:05 -0500 Subject: [PATCH] Refactor to share fixtures with option tests --- base-notebook/test/test_container_options.py | 36 +++++-- conftest.py | 101 +++++++++++++++++++ test/test_notebook.py | 55 ++-------- 3 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 conftest.py diff --git a/base-notebook/test/test_container_options.py b/base-notebook/test/test_container_options.py index 1e1918bc73..57b82c9ffc 100644 --- a/base-notebook/test/test_container_options.py +++ b/base-notebook/test/test_container_options.py @@ -1,16 +1,38 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import pytest +import time +import pytest -@pytest.mark.skip('placeholder') -def test_cli_args(): - pass +def test_cli_args(container, http_client): + """Container should respect notebook server command line args + (e.g., disabling token security)""" + container.run( + command=['start-notebook.sh', '--NotebookApp.token=""'] + ) + resp = http_client.get('http://localhost:8888') + resp.raise_for_status() + assert 'login_submit' not in resp.text + + +@pytest.mark.filterwarnings('ignore:Unverified HTTPS request') +def test_unsigned_ssl(container, http_client): + """Container should generate a self-signed SSL certificate + and notebook server should use it to enable HTTPS. + """ + c = container.run( + environment=['GEN_CERT=yes'] + ) + # NOTE: The requests.Session backing the http_client fixture does not retry + # properly while the server is booting up. An SSL handshake error seems to + # abort the retry logic. Forcing a long sleep for the moment until I have + # time to dig more. + time.sleep(5) + resp = http_client.get('https://localhost:8888', verify=False) + resp.raise_for_status() + assert 'login_submit' in resp.text -@pytest.mark.skip('placeholder') -def test_unsigned_ssl(): - pass @pytest.mark.skip('placeholder') def test_uid_change(): diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..8b0d76ac20 --- /dev/null +++ b/conftest.py @@ -0,0 +1,101 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +import os + +import docker +import pytest +import requests + +from requests.packages.urllib3.util.retry import Retry +from requests.adapters import HTTPAdapter + + +@pytest.fixture(scope='session') +def http_client(): + """Requests session with retries and backoff.""" + s = requests.Session() + retries = Retry(total=5, backoff_factor=1) + s.mount('http://', HTTPAdapter(max_retries=retries)) + s.mount('https://', HTTPAdapter(max_retries=retries)) + return s + + +@pytest.fixture(scope='session') +def docker_client(): + """Docker client configured based on the host environment""" + return docker.from_env() + + +@pytest.fixture(scope='session') +def image_name(): + """Image name to test""" + return os.getenv('TEST_IMAGE', 'jupyter/base-notebook') + + +class TrackedContainer(object): + """Wrapper that collects docker container configuration and delays + container creation/execution. + + Parameters + ---------- + docker_client: docker.DockerClient + Docker client instance + image_name: str + Name of the docker image to launch + **kwargs: dict, optional + Default keyword arguments to pass to docker.DockerClient.containers.run + """ + def __init__(self, docker_client, image_name, **kwargs): + self.container = None + self.docker_client = docker_client + self.image_name = image_name + self.kwargs = kwargs + + def run(self, **kwargs): + """Runs a docker container using the preconfigured image name + and a mix of the preconfigured container options and those passed + to this method. + + Keeps track of the docker.Container instance spawned to kill it + later. + + Parameters + ---------- + **kwargs: dict, optional + Keyword arguments to pass to docker.DockerClient.containers.run + extending and/or overriding key/value pairs passed to the constructor + + Returns + ------- + docker.Container + """ + all_kwargs = {} + all_kwargs.update(self.kwargs) + all_kwargs.update(kwargs) + self.container = self.docker_client.containers.run(self.image_name, **all_kwargs) + return self.container + + def kill(self): + """Kills the tracked docker container.""" + if self.container: + self.container.kill() + + +@pytest.fixture(scope='function') +def container(docker_client, image_name): + """Notebook container with initial configuration appropriate for testing + (e.g., HTTP port exposed to the host for HTTP calls). + + Yields the container instance and kills it when the caller is done with it. + """ + container = TrackedContainer( + docker_client, + image_name, + detach=True, + auto_remove=False, + ports={ + '8888/tcp': 8888 + } + ) + yield container + container.kill() \ No newline at end of file diff --git a/test/test_notebook.py b/test/test_notebook.py index ed3c0ebb9e..51746fed50 100644 --- a/test/test_notebook.py +++ b/test/test_notebook.py @@ -1,52 +1,9 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import os -import time -import docker -import pytest -import requests - - -@pytest.fixture(scope='session') -def docker_client(): - """Docker client to use""" - return docker.from_env() - - -@pytest.fixture(scope='session') -def image_name(): - """Image name to test""" - return os.getenv('TEST_IMAGE', 'jupyter/base-notebook') - - -@pytest.fixture(scope='function') -def nb_container(docker_client, image_name): - """Notebook container to test""" - container = docker_client.containers.run( - image_name, - detach=True, - auto_remove=True, - ports={ - '8888/tcp': 8888 - } - ) - yield container - container.kill() - - -def test_server_liveliness(nb_container): - """Notebook server should eventually respond with HTTP 200 OK.""" - for i in range(10): - try: - resp = requests.get('http://localhost:8888') - except requests.exceptions.ConnectionError: - # Wait a bit and try again. Just because the docker container - # is running doesn't mean the notebook server is ready to accept - # connections inside it. - time.sleep(i) - else: - assert resp.status_code == 200 - break - else: - assert False, 'could not connect to server' +def test_secured_server(container, http_client): + """Notebook server should eventually request user login.""" + container.run() + resp = http_client.get('http://localhost:8888') + resp.raise_for_status() + assert 'login_submit' in resp.text