Skip to content

Commit

Permalink
Update unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Mar 14, 2024
1 parent 3b1eb4f commit 410f046
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 40 deletions.
16 changes: 14 additions & 2 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from jupyterhub.auth import DummyAuthenticator
from tljh.configurer import apply_config, load_config
from tljh_repo2docker import tljh_custom_jupyterhub_config
from tljh_repo2docker import tljh_custom_jupyterhub_config, TLJH_R2D_ADMIN_SCOPE
import sys

c.JupyterHub.services = []
Expand Down Expand Up @@ -47,10 +47,18 @@
"6789",
],
"oauth_no_confirm": True,
"oauth_client_allowed_scopes": [
TLJH_R2D_ADMIN_SCOPE,
],
}
]
)

c.JupyterHub.custom_scopes = {
"custom:tljh_repo2docker:admin": {
"description": "Admin access to myservice",
},
}

c.JupyterHub.load_roles = [
{
Expand All @@ -59,7 +67,11 @@
"scopes": ["read:users", "read:servers", "read:roles:users"],
"services": ["tljh_repo2docker"],
},
{"name": "tljh-repo2docker-service-admin", "users": ["alice"]},
{
"name": 'tljh-repo2docker-service-admin',
"users": ["alice"],
"scopes": [TLJH_R2D_ADMIN_SCOPE],
},
{
"name": "user",
"scopes": [
Expand Down
2 changes: 1 addition & 1 deletion tljh_repo2docker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler
CPU_PERIOD = 100_000

TLJH_R2D_ADMIN_ROLE = 'tljh-repo2docker-service-admin'
TLJH_R2D_ADMIN_SCOPE = "custom:tljh_repo2docker:admin"


class SpawnerMixin(Configurable):
Expand Down
1 change: 1 addition & 0 deletions tljh_repo2docker/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
if __name__ == "__main__":
from .app import main

main()
66 changes: 58 additions & 8 deletions tljh_repo2docker/base.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import functools
import json
import os
from http.client import responses

from httpx import AsyncClient
from jinja2 import Template
from jupyterhub.services.auth import HubOAuthenticated
from jupyterhub.utils import url_path_join
from tornado import web
from tljh_repo2docker import TLJH_R2D_ADMIN_ROLE
import functools

from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE

from .model import UserModel
from jupyterhub.scopes import needs_scope


def require_admin_role(func):
"""decorator to require admin role to perform an action"""

@functools.wraps(func)
async def wrapped_func(self, *args, **kwargs):
user = await self.fetch_user()
if not user.admin:
raise web.HTTPError(status_code=404, reason="Unauthorized.")
else:
return await func(self, *args, **kwargs)

return wrapped_func


Expand Down Expand Up @@ -53,10 +57,7 @@ async def fetch_user(self) -> UserModel:
user_model.setdefault("admin", False)

if not user_model["admin"]:
if (
"admin" in user_model["roles"]
or TLJH_R2D_ADMIN_ROLE in user_model["roles"]
):
if "admin" in user_model["roles"] or TLJH_R2D_ADMIN_SCOPE in user["scopes"]:
user_model["admin"] = True

return UserModel.from_dict(user_model)
Expand Down Expand Up @@ -84,7 +85,9 @@ async def render_template(self, name: str, **kwargs) -> str:
service_prefix=self.settings.get("service_prefix", "/"),
hub_prefix=self.settings.get("hub_prefix", "/"),
base_url=base_url,
logout_url=self.settings.get("logout_url", url_path_join(base_url, 'logout')),
logout_url=self.settings.get(
"logout_url", url_path_join(base_url, "logout")
),
static_url=self.static_url,
xsrf_token=self.xsrf_token.decode("ascii"),
user=user,
Expand All @@ -106,3 +109,50 @@ def get_json_body(self):
self.log.error("Couldn't parse JSON", exc_info=True)
raise web.HTTPError(400, "Invalid JSON in body of request")
return model

def check_xsrf_cookie(self):
"""
Copy from https://github.com/jupyterhub/jupyterhub/blob/main/jupyterhub/apihandlers/base.py#L89
"""
if not hasattr(self, "_jupyterhub_user"):
return
if self._jupyterhub_user is None and "Origin" not in self.request.headers:
return
if getattr(self, "_token_authenticated", False):
# if token-authenticated, ignore XSRF
return
return super().check_xsrf_cookie()

def write_error(self, status_code, **kwargs):
"""Write JSON errors instead of HTML"""
exc_info = kwargs.get("exc_info")
message = ""
exception = None
status_message = responses.get(status_code, "Unknown Error")
if exc_info:
exception = exc_info[1]
# get the custom message, if defined
try:
message = exception.log_message % exception.args
except Exception:
pass

# construct the custom reason, if defined
reason = getattr(exception, "reason", "")
if reason:
status_message = reason

self.set_header("Content-Type", "application/json")
if isinstance(exception, web.HTTPError):
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, "headers", None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
# Content-Length must be recalculated.
self.clear_header("Content-Length")

self.write(
json.dumps({"status": status_code, "message": message or status_message})
)
2 changes: 2 additions & 0 deletions tljh_repo2docker/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async def delete(self):
raise web.HTTPError(e.status, e.message)

self.set_status(200)
self.set_header("content-type", "application/json")
self.finish(json.dumps({"status": "ok"}))

@web.authenticated
Expand Down Expand Up @@ -74,4 +75,5 @@ async def post(self):
)

self.set_status(200)
self.set_header("content-type", "application/json")
self.finish(json.dumps({"status": "ok"}))
1 change: 0 additions & 1 deletion tljh_repo2docker/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class EnvironmentsHandler(BaseHandler):
@web.authenticated
@require_admin_role
async def get(self):

images = await list_images()
containers = await list_containers()
result = self.render_template(
Expand Down
2 changes: 1 addition & 1 deletion tljh_repo2docker/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ async def _emit(self, msg):
await self.flush()
except StreamClosedError:
self.log.warning("Stream closed while handling %s", self.request.uri)
raise web.Finish()
raise web.Finish()
1 change: 0 additions & 1 deletion tljh_repo2docker/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ def all_spawners(self) -> list:
}
)
return sp

2 changes: 1 addition & 1 deletion tljh_repo2docker/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ async def get(self):
if isawaitable(result):
self.write(await result)
else:
self.write(result)
self.write(result)
38 changes: 34 additions & 4 deletions tljh_repo2docker/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

import pytest
from aiodocker import Docker, DockerError
from traitlets.config import Config
Expand All @@ -13,22 +15,22 @@ async def remove_docker_image(image_name):
pass


@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def minimal_repo():
return "https://github.com/plasmabio/tljh-repo2docker-test-binder"


@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def minimal_repo_uppercase():
return "https://github.com/plasmabio/TLJH-REPO2DOCKER-TEST-BINDER"


@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def generated_image_name():
return "plasmabio-tljh-repo2docker-test-binder:HEAD"


@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def image_name():
return "tljh-repo2docker-test:HEAD"

Expand All @@ -38,6 +40,34 @@ async def app(hub_app):
config = Config()
tljh_custom_jupyterhub_config(config)

config.JupyterHub.services.extend(
[
{
"name": "tljh_repo2docker",
"url": "http://127.0.0.1:6789",
"command": [
sys.executable,
"-m",
"tljh_repo2docker",
"--ip",
"127.0.0.1",
"--port",
"6789",
],
"oauth_no_confirm": True,
}
]
)

config.JupyterHub.load_roles = [
{
"description": "Role for tljh_repo2docker service",
"name": "tljh-repo2docker-service",
"scopes": ["read:users", "read:servers", "read:roles:users"],
"services": ["tljh_repo2docker"],
}
]

app = await hub_app(config=config)
return app

Expand Down
13 changes: 10 additions & 3 deletions tljh_repo2docker/tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ async def test_uppercase_repo(app, minimal_repo_uppercase, generated_image_name)
assert r.status_code == 200
image = await wait_for_image(image_name=generated_image_name)
assert (
image["ContainerConfig"]["Labels"]["tljh_repo2docker.image_name"] == generated_image_name
image["ContainerConfig"]["Labels"]["tljh_repo2docker.image_name"]
== generated_image_name
)


Expand All @@ -55,11 +56,17 @@ async def test_no_repo(app, image_name):

@pytest.mark.asyncio
@pytest.mark.parametrize(
"memory, cpu", [("abcded", ""), ("", "abcde"),],
"memory, cpu",
[
("abcded", ""),
("", "abcde"),
],
)
async def test_wrong_limits(app, minimal_repo, image_name, memory, cpu):
name, ref = image_name.split(":")
r = await add_environment(app, repo=minimal_repo, name=name, ref=ref, memory=memory, cpu=cpu)
r = await add_environment(
app, repo=minimal_repo, name=name, ref=ref, memory=memory, cpu=cpu
)
assert r.status_code == 400
assert "must be a number" in r.text

Expand Down
28 changes: 19 additions & 9 deletions tljh_repo2docker/tests/test_images.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import pytest
from jupyterhub.tests.utils import get_page

from .utils import add_environment, wait_for_image
from .utils import add_environment, get_service_page, wait_for_image


@pytest.mark.asyncio
async def test_images_list_admin(app):
cookies = await app.login_user('admin')
r = await get_page('environments', app, cookies=cookies, allow_redirects=False)
cookies = await app.login_user("admin")
r = await get_service_page(
"environments",
app,
cookies=cookies,
allow_redirects=True,
)
r.raise_for_status()
assert '{"images": [], "default_mem_limit": "None", "default_cpu_limit":"None", "machine_profiles": []}' in r.text
assert (
'{"images": [], "default_mem_limit": "None", "default_cpu_limit":"None", "machine_profiles": []}'
in r.text
)


@pytest.mark.asyncio
async def test_images_list_not_admin(app):
cookies = await app.login_user('wash')
r = await get_page('environments', app, cookies=cookies, allow_redirects=False)
cookies = await app.login_user("wash")
r = await get_service_page(
"environments", app, cookies=cookies, allow_redirects=True
)
assert r.status_code == 403


@pytest.mark.asyncio
async def test_spawn_page(app, minimal_repo, image_name):
cookies = await app.login_user('admin')
cookies = await app.login_user("admin")

# go to the spawn page
r = await get_page('spawn', app, cookies=cookies, allow_redirects=False)
r = await get_page("spawn", app, cookies=cookies, allow_redirects=False)
r.raise_for_status()
assert minimal_repo not in r.text

Expand All @@ -35,7 +45,7 @@ async def test_spawn_page(app, minimal_repo, image_name):
await wait_for_image(image_name=image_name)

# the environment should be on the page
r = await get_page('spawn', app, cookies=cookies, allow_redirects=False)
r = await get_page("spawn", app, cookies=cookies, allow_redirects=False)
r.raise_for_status()
assert r.status_code == 200
assert minimal_repo in r.text
8 changes: 5 additions & 3 deletions tljh_repo2docker/tests/test_logs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import json

import pytest
from jupyterhub.tests.utils import api_request, async_requests
from jupyterhub.tests.utils import async_requests

from .utils import add_environment, wait_for_image
from .utils import add_environment, api_request, wait_for_image


def next_event(it):
Expand Down Expand Up @@ -38,6 +38,8 @@ async def test_stream_simple(app, minimal_repo, image_name):

@pytest.mark.asyncio
async def test_no_build(app, image_name, request):
r = await api_request(app, "environments", "image-not-found:12345", "logs", stream=True)
r = await api_request(
app, "environments", "image-not-found:12345", "logs", stream=True
)
request.addfinalizer(r.close)
assert r.status_code == 404
Loading

0 comments on commit 410f046

Please sign in to comment.