mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 05:53:00 +00:00
Add tests
This commit is contained in:

committed by
Duc Trung LE

parent
bf565ece3b
commit
bdcf697fe9
@@ -21,7 +21,7 @@ fi
|
|||||||
# Configure a set of databases in the database server for upgrade tests
|
# Configure a set of databases in the database server for upgrade tests
|
||||||
# this list must be in sync with versions in test_db.py:test_upgrade
|
# this list must be in sync with versions in test_db.py:test_upgrade
|
||||||
set -x
|
set -x
|
||||||
for SUFFIX in '' _upgrade_110 _upgrade_122 _upgrade_130 _upgrade_150 _upgrade_211; do
|
for SUFFIX in '' _upgrade_110 _upgrade_122 _upgrade_130 _upgrade_150 _upgrade_211 _upgrade_311; do
|
||||||
$SQL_CLIENT "DROP DATABASE jupyterhub${SUFFIX};" 2>/dev/null || true
|
$SQL_CLIENT "DROP DATABASE jupyterhub${SUFFIX};" 2>/dev/null || true
|
||||||
$SQL_CLIENT "CREATE DATABASE jupyterhub${SUFFIX} ${EXTRA_CREATE_DATABASE_ARGS:-};"
|
$SQL_CLIENT "CREATE DATABASE jupyterhub${SUFFIX} ${EXTRA_CREATE_DATABASE_ARGS:-};"
|
||||||
done
|
done
|
||||||
|
@@ -49,10 +49,16 @@ class ServiceAPIHandler(APIHandler):
|
|||||||
|
|
||||||
self._check_service_model(spec)
|
self._check_service_model(spec)
|
||||||
service_name = spec["name"]
|
service_name = spec["name"]
|
||||||
new_service = self.service_from_spec(spec)
|
try:
|
||||||
|
new_service = self.service_from_spec(spec)
|
||||||
|
except Exception:
|
||||||
|
msg = f"Failed to create service {service_name}"
|
||||||
|
self.log.error(msg, exc_info=True)
|
||||||
|
raise web.HTTPError(400, msg)
|
||||||
|
|
||||||
if new_service is None:
|
if new_service is None:
|
||||||
raise web.HTTPError(400, f"Failed to create service {service_name}")
|
raise web.HTTPError(400, f"Failed to create service {service_name}")
|
||||||
|
|
||||||
if new_service.api_token:
|
if new_service.api_token:
|
||||||
# Add api token to database
|
# Add api token to database
|
||||||
await self.app._add_tokens(
|
await self.app._add_tokens(
|
||||||
@@ -150,7 +156,7 @@ class ServiceAPIHandler(APIHandler):
|
|||||||
|
|
||||||
if service.oauth_no_confirm:
|
if service.oauth_no_confirm:
|
||||||
oauth_no_confirm_list = self.settings.get('oauth_no_confirm_list')
|
oauth_no_confirm_list = self.settings.get('oauth_no_confirm_list')
|
||||||
oauth_no_confirm_list.remove(service.oauth_client_id)
|
oauth_no_confirm_list.discard(service.oauth_client_id)
|
||||||
|
|
||||||
self.db.delete(orm_service)
|
self.db.delete(orm_service)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
@@ -458,39 +458,54 @@ class Service(Base):
|
|||||||
"""
|
"""
|
||||||
return db.query(cls).filter(cls.name == name).first()
|
return db.query(cls).filter(cls.name == name).first()
|
||||||
|
|
||||||
def update_column(self, column_name: str, value: any) -> bool:
|
def _check_data_only_column(self, column_name: str) -> bool:
|
||||||
"""_summary_
|
"""Check if a column is not a ForeignKey or in a
|
||||||
|
relationship.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
column_name (str): _description_
|
column_name (str): ame of the column
|
||||||
value (any): _description_
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: _description_
|
bool: returns `True` if the column is data-only,
|
||||||
|
returns `False` otherwise.
|
||||||
"""
|
"""
|
||||||
if (
|
if hasattr(self, column_name):
|
||||||
hasattr(self, column_name)
|
column = getattr(Service, column_name)
|
||||||
and not getattr(Service, column_name).foreign_keys
|
if hasattr(column, 'foreign_keys'):
|
||||||
):
|
return len(column.foreign_keys) == 0
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_column(self, column_name: str, value: any) -> bool:
|
||||||
|
"""Update the value of a non-ForeignKey column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_name (str): Name of the column
|
||||||
|
value (any): New value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: returns `True` if the column is updated,
|
||||||
|
returns `False` otherwise.
|
||||||
|
"""
|
||||||
|
if self._check_data_only_column(column_name):
|
||||||
setattr(self, column_name, value)
|
setattr(self, column_name, value)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_column(self, column_name: str):
|
def get_column(self, column_name: str):
|
||||||
"""_summary_
|
"""Get non-ForeignKey, non-relationship column
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
column_name (str): _description_
|
column_name (str): Name of the column
|
||||||
value (any): _description_
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: _description_
|
The value of requested column, None if it is
|
||||||
|
a foreign key or it does not exist.
|
||||||
"""
|
"""
|
||||||
if (
|
if self._check_data_only_column(column_name):
|
||||||
hasattr(self, column_name)
|
|
||||||
and not getattr(Service, column_name).foreign_keys
|
|
||||||
):
|
|
||||||
return getattr(self, column_name)
|
return getattr(self, column_name)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@@ -8,12 +8,13 @@ from datetime import datetime, timedelta
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import quote, urlparse
|
from urllib.parse import quote, urlparse
|
||||||
|
|
||||||
|
import pytest
|
||||||
from pytest import fixture, mark
|
from pytest import fixture, mark
|
||||||
from tornado.httputil import url_concat
|
from tornado.httputil import url_concat
|
||||||
|
|
||||||
import jupyterhub
|
import jupyterhub
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm, roles
|
||||||
from ..apihandlers.base import PAGINATION_MEDIA_TYPE
|
from ..apihandlers.base import PAGINATION_MEDIA_TYPE
|
||||||
from ..objects import Server
|
from ..objects import Server
|
||||||
from ..utils import url_path_join as ujoin
|
from ..utils import url_path_join as ujoin
|
||||||
@@ -2091,6 +2092,155 @@ async def test_get_service(app, mockservice_url):
|
|||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service_data():
|
||||||
|
return {
|
||||||
|
"oauth_client_id": "service-oauth-client-from-api",
|
||||||
|
"api_token": "api_token-from-api",
|
||||||
|
"oauth_redirect_uri": "http://127.0.0.1:5555/oauth_callback-from-api",
|
||||||
|
"oauth_no_confirm": True,
|
||||||
|
"info": {'foo': 'bar'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service_admin_user(app):
|
||||||
|
user_name = 'admin_services'
|
||||||
|
service_role = {
|
||||||
|
'name': 'admin-services-role',
|
||||||
|
'description': '',
|
||||||
|
'users': [user_name],
|
||||||
|
'scopes': ['admin:services'],
|
||||||
|
}
|
||||||
|
roles.create_role(app.db, service_role)
|
||||||
|
user = add_user(app.db, name=user_name)
|
||||||
|
roles.update_roles(app.db, user, roles=['admin-services-role'])
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_create_service(app, service_admin_user, service_data):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-api'
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, service_admin_user.name),
|
||||||
|
data=json.dumps(service_data),
|
||||||
|
method='post',
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert r.json()['name'] == service_name
|
||||||
|
orm_service = orm.Service.find(db, service_name)
|
||||||
|
assert orm_service is not None
|
||||||
|
|
||||||
|
oath_client = (
|
||||||
|
db.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=service_data['oauth_client_id'])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
assert oath_client.redirect_uri == service_data['oauth_redirect_uri']
|
||||||
|
|
||||||
|
assert service_name in app._service_map
|
||||||
|
assert (
|
||||||
|
app._service_map[service_name].oauth_no_confirm
|
||||||
|
== service_data['oauth_no_confirm']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_create_service_no_role(app, service_data):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-api'
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, 'user'),
|
||||||
|
data=json.dumps(service_data),
|
||||||
|
method='post',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_create_service_conflict(app, service_admin_user, service_data):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-config'
|
||||||
|
app.services = [{'name': service_name}]
|
||||||
|
app.init_services()
|
||||||
|
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, service_admin_user.name),
|
||||||
|
data=json.dumps(service_data),
|
||||||
|
method='post',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_create_service_duplication(app, service_admin_user, service_data):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-api'
|
||||||
|
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, service_admin_user.name),
|
||||||
|
data=json.dumps(service_data),
|
||||||
|
method='post',
|
||||||
|
)
|
||||||
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_remove_service(app, service_admin_user, service_data):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-api'
|
||||||
|
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, service_admin_user.name),
|
||||||
|
method='delete',
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
orm_service = orm.Service.find(db, service_name)
|
||||||
|
assert orm_service is None
|
||||||
|
|
||||||
|
oath_client = (
|
||||||
|
db.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=service_data['oauth_client_id'])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
assert oath_client is None
|
||||||
|
|
||||||
|
assert service_name not in app._service_map
|
||||||
|
|
||||||
|
|
||||||
|
@mark.services
|
||||||
|
async def test_remove_service_from_config(app, service_admin_user):
|
||||||
|
db = app.db
|
||||||
|
service_name = 'service-from-config'
|
||||||
|
r = await api_request(
|
||||||
|
app,
|
||||||
|
f'services/{service_name}',
|
||||||
|
headers=auth_header(db, service_admin_user.name),
|
||||||
|
method='delete',
|
||||||
|
)
|
||||||
|
assert r.status_code == 405
|
||||||
|
assert (
|
||||||
|
r.json()['message']
|
||||||
|
== 'Service service-from-config is not modifiable at runtime'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_root_api(app):
|
async def test_root_api(app):
|
||||||
base_url = app.hub.url
|
base_url = app.hub.url
|
||||||
url = ujoin(base_url, 'api')
|
url = ujoin(base_url, 'api')
|
||||||
|
@@ -44,7 +44,9 @@ def generate_old_db(env_dir, hub_version, db_url):
|
|||||||
|
|
||||||
# changes to this version list must also be reflected
|
# changes to this version list must also be reflected
|
||||||
# in ci/init-db.sh
|
# in ci/init-db.sh
|
||||||
@pytest.mark.parametrize('hub_version', ["1.1.0", "1.2.2", "1.3.0", "1.5.0", "2.1.1"])
|
@pytest.mark.parametrize(
|
||||||
|
'hub_version', ['1.1.0', '1.2.2', '1.3.0', '1.5.0', '2.1.1', '3.1.1']
|
||||||
|
)
|
||||||
async def test_upgrade(tmpdir, hub_version):
|
async def test_upgrade(tmpdir, hub_version):
|
||||||
db_url = os.getenv('JUPYTERHUB_TEST_DB_URL')
|
db_url = os.getenv('JUPYTERHUB_TEST_DB_URL')
|
||||||
if db_url:
|
if db_url:
|
||||||
|
@@ -145,6 +145,33 @@ def test_token_expiry(db):
|
|||||||
assert orm_token not in user.api_tokens
|
assert orm_token not in user.api_tokens
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_check_data_only_column(db):
|
||||||
|
orm_service = orm.Service(name='check_data_only_column', from_config=True)
|
||||||
|
db.add(orm_service)
|
||||||
|
db.commit()
|
||||||
|
assert orm_service._check_data_only_column('from_config')
|
||||||
|
assert not orm_service._check_data_only_column('server')
|
||||||
|
assert not orm_service._check_data_only_column('oauth_client_id')
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_get_column(db):
|
||||||
|
orm_service = orm.Service(name='test_service_get_column', from_config=True)
|
||||||
|
db.add(orm_service)
|
||||||
|
db.commit()
|
||||||
|
orm_service.server = orm.Server()
|
||||||
|
assert orm_service.get_column('from_config')
|
||||||
|
assert orm_service.get_column('server') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_update_column(db):
|
||||||
|
orm_service = orm.Service(name='test_service_set_column', from_config=True)
|
||||||
|
db.add(orm_service)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
assert orm_service.update_column('from_config', False)
|
||||||
|
assert not orm_service.update_column('server', orm.Server())
|
||||||
|
|
||||||
|
|
||||||
def test_service_tokens(db):
|
def test_service_tokens(db):
|
||||||
service = orm.Service(name='secret')
|
service = orm.Service(name='secret')
|
||||||
db.add(service)
|
db.add(service)
|
||||||
|
@@ -236,6 +236,16 @@ def test_orm_roles_delete_cascade(db):
|
|||||||
['tokens!group=hobbits'],
|
['tokens!group=hobbits'],
|
||||||
{'tokens!group=hobbits', 'read:tokens!group=hobbits'},
|
{'tokens!group=hobbits', 'read:tokens!group=hobbits'},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
['admin:services'],
|
||||||
|
{
|
||||||
|
'read:roles:services',
|
||||||
|
'read:services:name',
|
||||||
|
'admin:services',
|
||||||
|
'list:services',
|
||||||
|
'read:services',
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_expanded_scopes(db, scopes, expected_scopes):
|
def test_get_expanded_scopes(db, scopes, expected_scopes):
|
||||||
|
Reference in New Issue
Block a user