From 8e318586c3f7fde5e9c93935b7ccf4669118ad0d Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:12:09 -0400 Subject: [PATCH 1/7] Make `cert` and `key` `required` for `TLS` --- tljh/config_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tljh/config_schema.py b/tljh/config_schema.py index 4a17f13..3e6d8f6 100644 --- a/tljh/config_schema.py +++ b/tljh/config_schema.py @@ -74,6 +74,7 @@ config_schema = { "type": "object", "additionalProperties": False, "properties": {"key": {"type": "string"}, "cert": {"type": "string"}}, + "required": ["key", "cert"], }, "Limits": { "description": "User CPU and memory limits.", From a1f1c5e046a664502cb787fb0a01e9301c4d9809 Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:12:40 -0400 Subject: [PATCH 2/7] Remove `FIXME` --- tljh/configurer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tljh/configurer.py b/tljh/configurer.py index 7a58bbe..2c5a32c 100644 --- a/tljh/configurer.py +++ b/tljh/configurer.py @@ -5,7 +5,6 @@ Config should never append or mutate, only set. Functions here could be called many times per lifetime of a jupyterhub. Traitlets that modify the startup of JupyterHub should not be here. -FIXME: A strong feeling that JSON Schema should be involved somehow. """ import os From 3ee67e15814a565b3e424ef53a6290cb1222bb4e Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:13:28 -0400 Subject: [PATCH 3/7] Add test file for config validation --- tests/test_config_schema.py | 166 ++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/test_config_schema.py diff --git a/tests/test_config_schema.py b/tests/test_config_schema.py new file mode 100644 index 0000000..7fb1c30 --- /dev/null +++ b/tests/test_config_schema.py @@ -0,0 +1,166 @@ +""" +Unit test functions to test JSON Schema validation +""" + +import pytest + +from tljh import config + + +@pytest.mark.parametrize( + "valid_config", + [ + # Valid configuration with JupyterLab as default app and HTTPS enabled with Let's Encrypt + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "letsencrypt": { + "email": "admin@example.com", + "domains": ["example.com"], + }, + }, + }, + # Valid configuration with classic notebook UI + {"user_environment": {"default_app": "classic"}, "https": {"enabled": False}}, + # Valid configuration with culling service enabled + { + "user_environment": {"default_app": "jupyterlab"}, + "https": {"enabled": False}, + "services": { + "cull": { + "enabled": True, + "timeout": 3600, + "every": 600, + "concurrency": 5, + "users": True, + "max_age": 86400, + "remove_named_servers": False, + } + }, + }, + # Valid configuration of resource limits + { + "user_environment": {"default_app": "jupyterlab"}, + "https": {"enabled": False}, + "limits": {"memory": "2G", "cpu": 1.5}, + }, + # Valid configuration with TLS certificates instead of Let's Encrypt + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "tls": { + "key": "/etc/tljh/tls/private.key", + "cert": "/etc/tljh/tls/certificate.crt", + }, + }, + }, + # Valid configuration with TLS and custom HTTPS address/port + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "address": "192.168.1.1", + "port": 443, + "tls": { + "key": "/etc/tljh/tls/private.key", + "cert": "/etc/tljh/tls/certificate.crt", + }, + }, + }, + ], +) +def test_valid_configs(valid_config): + """Test that known good configs pass validation without errors.""" + try: + config.validate_config(valid_config, validate=True) + except Exception as e: + pytest.fail(f"Valid config failed validation: {e}") + + +@pytest.mark.parametrize( + "invalid_config", + [ + # Invalid default_app value + { + "user_environment": {"default_app": "saturnlab"}, + "https": { + "enabled": True, + "letsencrypt": { + "email": "admin@example.com", + "domains": ["sub.example.com"], + }, + }, + }, + # Invalid domains type (should be array) + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "letsencrypt": {"email": "not-an-email", "domains": "example.com"}, + }, + }, + # Extra unexpected property + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "letsencrypt": { + "email": "admin@example.com", + "domains": ["example.com"], + "extra_property": "invalid", + }, + }, + }, + # Invalid culling service config (timeout should be an integer) + { + "user_environment": {"default_app": "jupyterlab"}, + "https": {"enabled": False}, + "services": { + "cull": { + "enabled": True, + "timeout": "one hour", + "every": 600, + "concurrency": 5, + "users": True, + "max_age": 86400, + "remove_named_servers": False, + } + }, + }, + # Invalid resource limits (negative CPU value) + { + "user_environment": {"default_app": "jupyterlab"}, + "https": {"enabled": False}, + "limits": {"memory": "2G", "cpu": -1}, # Negative CPU not allowed + }, + # TLS enabled but missing required key + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "tls": { + "cert": "/etc/tljh/tls/certificate.crt" + # Missing 'key' field + }, + }, + }, + { + "user_environment": {"default_app": "jupyterlab"}, + "https": { + "enabled": True, + "tls": { + "key": "/etc/tljh/tls/private.key" + # Missing 'cert' field + }, + }, + }, + ], +) +def test_invalid_configs(invalid_config): + """Test that known bad configs raise validation errors.""" + with pytest.raises(SystemExit) as exc_info: + config.validate_config(invalid_config, validate=True) + + assert exc_info.value.code == 1 From fa83068cdbadccd4ef055af58ab957fb6b438cd7 Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:38:26 -0400 Subject: [PATCH 4/7] Remove `required` `properties` for `TLS` --- tljh/config_schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tljh/config_schema.py b/tljh/config_schema.py index 3e6d8f6..4a17f13 100644 --- a/tljh/config_schema.py +++ b/tljh/config_schema.py @@ -74,7 +74,6 @@ config_schema = { "type": "object", "additionalProperties": False, "properties": {"key": {"type": "string"}, "cert": {"type": "string"}}, - "required": ["key", "cert"], }, "Limits": { "description": "User CPU and memory limits.", From 251099caac2a798e24c3a03cf5de6f095c045b11 Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:42:22 -0400 Subject: [PATCH 5/7] Remove `tls` tests --- tests/test_config_schema.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/tests/test_config_schema.py b/tests/test_config_schema.py index 7fb1c30..7654d37 100644 --- a/tests/test_config_schema.py +++ b/tests/test_config_schema.py @@ -133,28 +133,7 @@ def test_valid_configs(valid_config): { "user_environment": {"default_app": "jupyterlab"}, "https": {"enabled": False}, - "limits": {"memory": "2G", "cpu": -1}, # Negative CPU not allowed - }, - # TLS enabled but missing required key - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "tls": { - "cert": "/etc/tljh/tls/certificate.crt" - # Missing 'key' field - }, - }, - }, - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "tls": { - "key": "/etc/tljh/tls/private.key" - # Missing 'cert' field - }, - }, + "limits": {"memory": "2G", "cpu": -1}, }, ], ) From 5ff2c8aa6f24054d7a99e24c5c5c63b144770efc Mon Sep 17 00:00:00 2001 From: Jordan Bradford <36420801+jrdnbradford@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:30:14 -0400 Subject: [PATCH 6/7] Simplify testing of schema --- tests/test_config_schema.py | 146 ++---------------------------------- 1 file changed, 6 insertions(+), 140 deletions(-) diff --git a/tests/test_config_schema.py b/tests/test_config_schema.py index 7654d37..8863fbf 100644 --- a/tests/test_config_schema.py +++ b/tests/test_config_schema.py @@ -2,144 +2,10 @@ Unit test functions to test JSON Schema validation """ -import pytest +import jsonschema +from tljh.config_schema import config_schema -from tljh import config - - -@pytest.mark.parametrize( - "valid_config", - [ - # Valid configuration with JupyterLab as default app and HTTPS enabled with Let's Encrypt - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "letsencrypt": { - "email": "admin@example.com", - "domains": ["example.com"], - }, - }, - }, - # Valid configuration with classic notebook UI - {"user_environment": {"default_app": "classic"}, "https": {"enabled": False}}, - # Valid configuration with culling service enabled - { - "user_environment": {"default_app": "jupyterlab"}, - "https": {"enabled": False}, - "services": { - "cull": { - "enabled": True, - "timeout": 3600, - "every": 600, - "concurrency": 5, - "users": True, - "max_age": 86400, - "remove_named_servers": False, - } - }, - }, - # Valid configuration of resource limits - { - "user_environment": {"default_app": "jupyterlab"}, - "https": {"enabled": False}, - "limits": {"memory": "2G", "cpu": 1.5}, - }, - # Valid configuration with TLS certificates instead of Let's Encrypt - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "tls": { - "key": "/etc/tljh/tls/private.key", - "cert": "/etc/tljh/tls/certificate.crt", - }, - }, - }, - # Valid configuration with TLS and custom HTTPS address/port - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "address": "192.168.1.1", - "port": 443, - "tls": { - "key": "/etc/tljh/tls/private.key", - "cert": "/etc/tljh/tls/certificate.crt", - }, - }, - }, - ], -) -def test_valid_configs(valid_config): - """Test that known good configs pass validation without errors.""" - try: - config.validate_config(valid_config, validate=True) - except Exception as e: - pytest.fail(f"Valid config failed validation: {e}") - - -@pytest.mark.parametrize( - "invalid_config", - [ - # Invalid default_app value - { - "user_environment": {"default_app": "saturnlab"}, - "https": { - "enabled": True, - "letsencrypt": { - "email": "admin@example.com", - "domains": ["sub.example.com"], - }, - }, - }, - # Invalid domains type (should be array) - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "letsencrypt": {"email": "not-an-email", "domains": "example.com"}, - }, - }, - # Extra unexpected property - { - "user_environment": {"default_app": "jupyterlab"}, - "https": { - "enabled": True, - "letsencrypt": { - "email": "admin@example.com", - "domains": ["example.com"], - "extra_property": "invalid", - }, - }, - }, - # Invalid culling service config (timeout should be an integer) - { - "user_environment": {"default_app": "jupyterlab"}, - "https": {"enabled": False}, - "services": { - "cull": { - "enabled": True, - "timeout": "one hour", - "every": 600, - "concurrency": 5, - "users": True, - "max_age": 86400, - "remove_named_servers": False, - } - }, - }, - # Invalid resource limits (negative CPU value) - { - "user_environment": {"default_app": "jupyterlab"}, - "https": {"enabled": False}, - "limits": {"memory": "2G", "cpu": -1}, - }, - ], -) -def test_invalid_configs(invalid_config): - """Test that known bad configs raise validation errors.""" - with pytest.raises(SystemExit) as exc_info: - config.validate_config(invalid_config, validate=True) - - assert exc_info.value.code == 1 +def test_valid_config_json_schema(): + """Validate that the JSON schema fits its $schema specification""" + validator_class = jsonschema.validators.validator_for(config_schema) + validator_class.check_schema(config_schema) From 142d22616cbf930241165cb4fbc10dc552979901 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:30:33 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_config_schema.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_config_schema.py b/tests/test_config_schema.py index 8863fbf..e621aea 100644 --- a/tests/test_config_schema.py +++ b/tests/test_config_schema.py @@ -3,9 +3,11 @@ Unit test functions to test JSON Schema validation """ import jsonschema + from tljh.config_schema import config_schema + def test_valid_config_json_schema(): - """Validate that the JSON schema fits its $schema specification""" - validator_class = jsonschema.validators.validator_for(config_schema) - validator_class.check_schema(config_schema) + """Validate that the JSON schema fits its $schema specification""" + validator_class = jsonschema.validators.validator_for(config_schema) + validator_class.check_schema(config_schema)