From 7dc17120e0e147d0709403553455cb51b6d4a775 Mon Sep 17 00:00:00 2001 From: Scr4tch587 Date: Wed, 3 Jun 2026 21:03:02 -0400 Subject: [PATCH 1/3] extra keys exception, unit test, integration test --- builders/server/core/runtime/validator.py | 8 +++-- .../tests/core/runtime/test_validator.py | 12 +++----- .../server/tests/integration/test_errors.py | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/builders/server/core/runtime/validator.py b/builders/server/core/runtime/validator.py index b59ce5d..23cd303 100644 --- a/builders/server/core/runtime/validator.py +++ b/builders/server/core/runtime/validator.py @@ -8,13 +8,17 @@ class ValidationError(Exception): def validate(data: dict, schema: dict[str, SchemaType]) -> None: """Validate that data matches the declared schema. - Checks that all declared keys are present and values match declared types. + Checks that all declared keys are present, from the schema, and values match declared types. Raises ValidationError on failure. """ + for key in data: + if key not in schema: + raise ValidationError(f"Unexpected key '{key}' in builder output") + for key, schema_type in schema.items(): if key not in data: raise ValidationError(f"Missing key '{key}' in builder output") - + expected = schema_type.to_type() if not isinstance(data[key], expected): raise ValidationError( diff --git a/builders/server/tests/core/runtime/test_validator.py b/builders/server/tests/core/runtime/test_validator.py index d8ee66c..6b772f3 100644 --- a/builders/server/tests/core/runtime/test_validator.py +++ b/builders/server/tests/core/runtime/test_validator.py @@ -11,14 +11,10 @@ def test_valid_data_passes() -> None: ) -def test_empty_schema_passes_anything() -> None: - """Empty schema means no constraints.""" - validate({"anything": 123, "goes": "here"}, {}) - - -def test_extra_keys_allowed() -> None: - """Data with extra keys beyond schema passes.""" - validate({"ticker": "AAPL", "extra": 999}, {"ticker": SchemaType.STR}) +def test_extra_keys_raises() -> None: + """Data with extra keys beyond schema fails.""" + with pytest.raises(ValidationError, match="Unexpected key 'extra'"): + validate({"ticker": "AAPL", "extra": 999}, {"ticker": SchemaType.STR}) def test_missing_key_raises() -> None: diff --git a/builders/server/tests/integration/test_errors.py b/builders/server/tests/integration/test_errors.py index 83d38fd..b70e28e 100644 --- a/builders/server/tests/integration/test_errors.py +++ b/builders/server/tests/integration/test_errors.py @@ -140,3 +140,33 @@ def build(dependencies, timestamp: datetime) -> list[dict]: ) assert resp.status_code == 500 assert _row_count(db_conn, name) == 0 + +def test_schema_unexpected_key(client, db_conn, write_temp_builder): + """builder returns key not in the schema -> 500, 0 rows.""" + name, version = write_temp_builder( + "unexpected-key", + "0.1.0", + """\ +name = "unexpected-key" +version = "0.1.0" +builder = "builder.py" +calendar = "everyday" +granularity = "1d" +start-date = "2020-01-01" + +[schema] +value = "int" +""", + """\ +from datetime import datetime + +def build(dependencies, timestamp: datetime) -> list[dict]: + return [{"value": 1, "unexpected_key": "value"}] +""", + ) + resp = client.post( + f"/api/v1/build/{name}/{version}", + params={"start": "2024-01-02", "end": "2024-01-02"}, + ) + assert resp.status_code == 500 + assert _row_count(db_conn, name) == 0 \ No newline at end of file From cfdc037f9716a7519defdab35901aea0099620d5 Mon Sep 17 00:00:00 2001 From: Scr4tch587 Date: Wed, 3 Jun 2026 21:15:31 -0400 Subject: [PATCH 2/3] updated old tests to match new reqs, forgot to precommit --- builders/server/tests/core/service/test_worker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/builders/server/tests/core/service/test_worker.py b/builders/server/tests/core/service/test_worker.py index 5b986a7..f85e41f 100644 --- a/builders/server/tests/core/service/test_worker.py +++ b/builders/server/tests/core/service/test_worker.py @@ -82,7 +82,7 @@ def test_missing_timestamps_built_and_inserted( @patch("core.service.worker.registry") def test_builder_failure_no_partial_insert(mock_registry, mock_db, mock_runner) -> None: """If builder fails on timestamp 3 of 5, no rows are inserted.""" - mock_registry.get_config.return_value = _cfg(name="ds") + mock_registry.get_config.return_value = _cfg(name="ds", schema={"val": SchemaType.INT}) mock_db.get_existing_timestamps.return_value = [] # all missing call_count = 0 @@ -112,7 +112,7 @@ def fail_on_third(*args, **kwargs): @patch("core.service.worker.registry") def test_cancelled_event_stops_early(mock_registry, mock_db, mock_runner) -> None: """When cancelled is set, worker stops before building remaining timestamps.""" - mock_registry.get_config.return_value = _cfg(name="ds") + mock_registry.get_config.return_value = _cfg(name="ds", schema={"val": SchemaType.INT}) mock_db.get_existing_timestamps.return_value = [] mock_runner.run_builder.return_value = [{"val": 1}] @@ -146,6 +146,7 @@ def test_lookback_dep_uses_get_rows_range(mock_registry, mock_db, mock_runner) - """Dependency with lookback fetches data via get_rows_range.""" mock_registry.get_config.return_value = _cfg( name="ds", + schema={"val": SchemaType.INT}, dependencies={ "dep": DependencyInfo(version=V010, lookback_subtract=timedelta(days=4)), }, @@ -174,6 +175,7 @@ def test_no_lookback_dep_uses_get_rows_timestamps( """Dependency without lookback fetches data via get_rows_timestamps.""" mock_registry.get_config.return_value = _cfg( name="ds", + schema={"val": SchemaType.INT}, dependencies={ "dep": DependencyInfo(version=V010), }, From 54fb508deb17a2020c082b6debda05d9fbeff416 Mon Sep 17 00:00:00 2001 From: Scr4tch587 Date: Wed, 3 Jun 2026 21:24:25 -0400 Subject: [PATCH 3/3] chore: fix lint for ci --- builders/server/core/runtime/validator.py | 6 +++--- builders/server/tests/core/service/test_worker.py | 8 ++++++-- builders/server/tests/integration/test_errors.py | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/builders/server/core/runtime/validator.py b/builders/server/core/runtime/validator.py index 23cd303..cb1fc8f 100644 --- a/builders/server/core/runtime/validator.py +++ b/builders/server/core/runtime/validator.py @@ -8,8 +8,8 @@ class ValidationError(Exception): def validate(data: dict, schema: dict[str, SchemaType]) -> None: """Validate that data matches the declared schema. - Checks that all declared keys are present, from the schema, and values match declared types. - Raises ValidationError on failure. + Checks that all keys in data are in the schema, all schema keys are present in data, + and values match declared types. Raises ValidationError on failure. """ for key in data: if key not in schema: @@ -18,7 +18,7 @@ def validate(data: dict, schema: dict[str, SchemaType]) -> None: for key, schema_type in schema.items(): if key not in data: raise ValidationError(f"Missing key '{key}' in builder output") - + expected = schema_type.to_type() if not isinstance(data[key], expected): raise ValidationError( diff --git a/builders/server/tests/core/service/test_worker.py b/builders/server/tests/core/service/test_worker.py index f85e41f..122e2b2 100644 --- a/builders/server/tests/core/service/test_worker.py +++ b/builders/server/tests/core/service/test_worker.py @@ -82,7 +82,9 @@ def test_missing_timestamps_built_and_inserted( @patch("core.service.worker.registry") def test_builder_failure_no_partial_insert(mock_registry, mock_db, mock_runner) -> None: """If builder fails on timestamp 3 of 5, no rows are inserted.""" - mock_registry.get_config.return_value = _cfg(name="ds", schema={"val": SchemaType.INT}) + mock_registry.get_config.return_value = _cfg( + name="ds", schema={"val": SchemaType.INT} + ) mock_db.get_existing_timestamps.return_value = [] # all missing call_count = 0 @@ -112,7 +114,9 @@ def fail_on_third(*args, **kwargs): @patch("core.service.worker.registry") def test_cancelled_event_stops_early(mock_registry, mock_db, mock_runner) -> None: """When cancelled is set, worker stops before building remaining timestamps.""" - mock_registry.get_config.return_value = _cfg(name="ds", schema={"val": SchemaType.INT}) + mock_registry.get_config.return_value = _cfg( + name="ds", schema={"val": SchemaType.INT} + ) mock_db.get_existing_timestamps.return_value = [] mock_runner.run_builder.return_value = [{"val": 1}] diff --git a/builders/server/tests/integration/test_errors.py b/builders/server/tests/integration/test_errors.py index b70e28e..b0d15d6 100644 --- a/builders/server/tests/integration/test_errors.py +++ b/builders/server/tests/integration/test_errors.py @@ -141,6 +141,7 @@ def build(dependencies, timestamp: datetime) -> list[dict]: assert resp.status_code == 500 assert _row_count(db_conn, name) == 0 + def test_schema_unexpected_key(client, db_conn, write_temp_builder): """builder returns key not in the schema -> 500, 0 rows.""" name, version = write_temp_builder( @@ -169,4 +170,4 @@ def build(dependencies, timestamp: datetime) -> list[dict]: params={"start": "2024-01-02", "end": "2024-01-02"}, ) assert resp.status_code == 500 - assert _row_count(db_conn, name) == 0 \ No newline at end of file + assert _row_count(db_conn, name) == 0