diff --git a/jupyterhub/app.py b/jupyterhub/app.py index b61686ca..acb6ae84 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2381,6 +2381,9 @@ class JupyterHub(Application): if orm_service.oauth_client is not None: service.oauth_redirect_uri = orm_service.oauth_client.redirect_uri + service.oauth_client_id = orm_service.oauth_client.identifier + service.oauth_redirect_uri = orm_service.oauth_client.redirect_uri + self._service_map[name] = service return service diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index e9b2260e..9930fbf6 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -534,3 +534,14 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): queries = db_counts[nodeid] if queries: terminalreporter.line(f"{queries:<6} {nodeid}") + +@fixture +def service_data(): + """Data used to create service at runtime""" + 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'}, + } diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 724ad89b..00ef6112 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -14,7 +14,7 @@ from tornado.httputil import url_concat import jupyterhub -from .. import orm, roles +from .. import orm from ..apihandlers.base import PAGINATION_MEDIA_TYPE from ..objects import Server from ..utils import url_path_join as ujoin @@ -2093,29 +2093,8 @@ async def test_get_service(app, mockservice_url): @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 +def service_admin_user(create_user_with_scopes): + return create_user_with_scopes('admin:services') @mark.services diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index f1967dce..d691f2d7 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -15,10 +15,11 @@ import pytest import traitlets from traitlets.config import Config -from .. import orm +from .. import orm, roles from ..app import COOKIE_SECRET_BYTES, JupyterHub from .mocking import MockHub from .test_api import add_user +from .utils import api_request, auth_header def test_help_all(): @@ -473,3 +474,107 @@ async def test_user_creation(tmpdir, request): "in-group", "in-role", } + +@pytest.fixture(scope='module') +def db_temp_path(tmp_path_factory): + fn = tmp_path_factory.mktemp("db") / "jupyterhub.sqlite" + return fn + + +async def test_add_service_at_runtime(request, db_temp_path, service_data): + if not os.getenv('JUPYTERHUB_TEST_DB_URL'): + p = patch.dict( + os.environ, + {'JUPYTERHUB_TEST_DB_URL': 'sqlite:///%s' % str(db_temp_path)}, + ) + p.start() + request.addfinalizer(p.stop) + + kwargs = {"test_clean_db": False} + ssl_enabled = getattr(request.module, "ssl_enabled", False) + if ssl_enabled: + kwargs['internal_certs_location'] = db_temp_path.parents[0] + app = MockHub(**kwargs) + + def end(): + app.log.handlers = [] + MockHub.clear_instance() + try: + app.stop() + except Exception as e: + print("Error stopping Hub: %s" % e, file=sys.stderr) + + request.addfinalizer(end) + + await app.initialize([]) + await app.start() + db = app.db + + 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']) + + service_name = 'service-from-api' + + r = await api_request( + app, + f'services/{service_name}', + headers=auth_header(db, user_name), + data=json.dumps(service_data), + method='post', + ) + assert r.status_code == 201 + assert r.json()['name'] == service_name + oauth_client = ( + app.db.query(orm.OAuthClient) + .filter_by(identifier=service_data['oauth_client_id']) + .first() + ) + assert oauth_client.redirect_uri == service_data['oauth_redirect_uri'] + + +async def test_recreate_service_from_database(request, db_temp_path, service_data): + if not os.getenv('JUPYTERHUB_TEST_DB_URL'): + p = patch.dict( + os.environ, + {'JUPYTERHUB_TEST_DB_URL': 'sqlite:///%s' % str(db_temp_path)}, + ) + p.start() + request.addfinalizer(p.stop) + kwargs = {"test_clean_db": False} + + ssl_enabled = getattr(request.module, "ssl_enabled", False) + if ssl_enabled: + kwargs['internal_certs_location'] = db_temp_path.parents[0] + app = MockHub(**kwargs) + + def end(): + app.log.handlers = [] + MockHub.clear_instance() + try: + app.stop() + except Exception as e: + print("Error stopping Hub: %s" % e, file=sys.stderr) + + request.addfinalizer(end) + + await app.initialize([]) + await app.start() + + assert 'service-from-api' in app._service_map + assert ( + service_data['oauth_client_id'] in app.tornado_settings['oauth_no_confirm_list'] + ) + oauth_client = ( + app.db.query(orm.OAuthClient) + .filter_by(identifier=service_data['oauth_client_id']) + .first() + ) + assert oauth_client.redirect_uri == service_data['oauth_redirect_uri'] diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 0d00c5ea..afa35999 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -145,13 +145,90 @@ def test_token_expiry(db): assert orm_token not in user.api_tokens -def test_service_check_data_only_column(db): +@pytest.mark.parametrize( + 'column, expected', + [ + ( + 'name', + True, + ), + ( + 'admin', + True, + ), + ( + 'url', + True, + ), + ( + 'oauth_client_allowed_scopes', + True, + ), + ( + 'info', + True, + ), + ( + 'display', + True, + ), + ( + 'oauth_no_confirm', + True, + ), + ( + 'command', + True, + ), + ( + 'cwd', + True, + ), + ( + 'environment', + True, + ), + ( + 'user', + True, + ), + ( + 'from_config', + True, + ), + ( + 'api_tokens', + False, + ), + ( + '_server_id', + False, + ), + ( + 'server', + False, + ), + ( + 'pid', + True, + ), + ( + 'oauth_client_id', + False, + ), + ( + 'oauth_client', + False, + ), + ], +) +def test_service_check_data_only_column(db, column, expected): 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') + assert orm_service._check_data_only_column(column) == expected + db.delete(orm_service) + db.commit() def test_service_get_column(db):