fix: harden input handling, mock wiring, and config payload (v0.0.36)#45
Conversation
Code review found several concrete defects. This release addresses them. Security / hardening: - get_calibration_history(): percent-encode the `sensor` argument before concatenating it into the request URL. A value with spaces, `#`, `&` or other reserved chars previously produced a malformed URL or injected an extra query parameter. - get_log(): validate `log_type` against the documented allow-list (actions/switching/onewire) and raise VioletPoolAPIError otherwise. - restore_calibration(): sanitize `sensor` and `timestamp` (length bounds, character filtering) before posting, matching the treatment of every key in set_config(). Fixes: - _sanitize_config_payload(): preserve `int` and `bool` values instead of routing them through sanitize_numeric(), which always returns float. Integer flags (e.g. DOS_*_use from set_dosage_enabled()) were being sent as `1.0`/`0.0`. bool is now serialized as 0/1. - set_config(): drop the undocumented retryable=True override so it behaves like every other state-changing POST (non-retryable on 5xx). - mock server: dispatch OMNI queries from handle_set_function_manually to handle_set_omni_position. The handler existed but was never routed, so set_omni_position() returned `OK\nOMNI\n...` (wrong) and never updated omni_position in /mock/state. Now returns `OK\nOMNITRONIC\n...`. Cleanup: - Remove duplicate InputSanitizer.validate_duration/validate_speed that collided by name with the raising validators in _api_model but silently clamped values. Unused dead code. - Type `duration`/`last_value` as int | None (was float | None) to match the actual whole-number validation/truncation behaviour. - Fix mojibake in docstrings/comments and drop redundant except (Exception): parentheses. Tests: +10 regression tests covering each fix; smoke test gains OMNI end-to-end coverage. 144 unit + 68 smoke tests pass; ruff & mypy clean. Bumps version to 0.0.36.
There was a problem hiding this comment.
Code Review
This pull request updates the library to version 0.0.36, introducing several security hardening fixes, bug fixes, and cleanups. Key changes include URL-encoding sensor inputs, validating log types, sanitizing calibration restore inputs, preserving integer/boolean types in configuration payloads, and removing unused sanitizer methods. Feedback on these changes highlights that the try...except ValueError block in restore_calibration is dead code because the sanitizer does not raise ValueError, and recommends explicitly asserting the types in test_set_config_preserves_bool_as_int_flag to ensure booleans are correctly converted to integers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| try: | ||
| safe_sensor = InputSanitizer.sanitize_string( | ||
| sensor, | ||
| max_length=64, | ||
| allow_special_chars=True, | ||
| escape_html=False, | ||
| ) | ||
| safe_timestamp = InputSanitizer.sanitize_string( | ||
| timestamp, | ||
| max_length=64, | ||
| allow_special_chars=True, | ||
| escape_html=False, | ||
| ) | ||
| except ValueError as err: | ||
| msg = "Invalid sensor or timestamp for calibration restore" | ||
| raise VioletPoolAPIError(msg) from err |
There was a problem hiding this comment.
The try...except ValueError block is dead code because InputSanitizer.sanitize_string does not raise a ValueError (it silently truncates strings exceeding max_length and returns them). If the intention is to reject invalid or overly long inputs with a VioletPoolAPIError, you should explicitly validate the lengths of sensor and timestamp before sanitizing them. Otherwise, the try-except block should be removed to avoid misleading exception handling.
safe_sensor = InputSanitizer.sanitize_string(
sensor,
max_length=64,
allow_special_chars=True,
escape_html=False,
)
safe_timestamp = InputSanitizer.sanitize_string(
timestamp,
max_length=64,
allow_special_chars=True,
escape_html=False,
)| data = captured["data"] | ||
| assert data == {"DOS_1_use": 1, "DOS_2_use": 0} |
There was a problem hiding this comment.
In Python, bool is a subclass of int, meaning True == 1 and False == 0 evaluate to True. Consequently, the assertion assert data == {"DOS_1_use": 1, "DOS_2_use": 0} will pass even if the values remain booleans (True/False) and the conversion to integer flags fails. To ensure the values are strictly integers, you should explicitly assert their types.
| data = captured["data"] | |
| assert data == {"DOS_1_use": 1, "DOS_2_use": 0} | |
| data = captured["data"] | |
| assert data == {"DOS_1_use": 1, "DOS_2_use": 0} | |
| assert type(data["DOS_1_use"]) is int | |
| assert type(data["DOS_2_use"]) is int |
Summary
Comprehensive code review identified several concrete defects. This PR fixes them and bumps the version to v0.0.36.
Security / Hardening
get_calibration_history()—sensorwas concatenated raw into the request URL. Values with spaces,#,&produced malformed URLs or injected extra query parameters. Now percent-encoded withquote(safe=""), consistent withset_switch_state().get_log()—log_typenow validated against the documented allow-list (actions/switching/onewire); raisesVioletPoolAPIErrorotherwise.restore_calibration()—sensorandtimestampnow sanitized (length bounds, character filtering) before posting, matching every key inset_config().Fixes
_sanitize_config_payload()—int/boolvalues were coerced to float viasanitize_numeric(). Integer flags (e.g.DOS_*_usefromset_dosage_enabled()) were sent as1.0/0.0. Now preserved as integers;bool→0/1.set_config()— removed undocumentedretryable=Trueoverride so it behaves like every other state-changing POST (non-retryable on 5xx, matchingtest_manual_dosing_post_is_not_retried).set_omni_position— handler was defined but never routed.OMNI,OMNI_DC<N>queries fell through to the generic handler, returningOK\nOMNI\n...(wrong) and never updatingomni_position. Now dispatched correctly →OK\nOMNITRONIC\n....Cleanup
InputSanitizer.validate_duration/validate_speed(name collision with the raising validators in_api_model; unused dead code).duration/last_valuetype hints:float | None→int | None(matches actual whole-number validation).ÔåÆ→→) and dropped redundantexcept (Exception):parens.Verification
ruff check .— All checks passedmypy violet_poolcontroller_api— Success, no issues in 15 source filespytest tests/test_api.py tests/test_mock_server.py— 144 passed (134 baseline + 10 new regression tests)python tests/test_api_smoke.py— 68/68 passed (now includes OMNI end-to-end coverage)Test coverage added
test_set_config_preserves_int_valuestest_set_config_preserves_bool_as_int_flagtest_set_config_not_retried_on_server_errortest_get_calibration_history_url_encodes_sensortest_get_log_rejects_unknown_log_typetest_get_log_valid_log_type_reaches_requesttest_restore_calibration_sanitizes_payloadtest_input_sanitizer_no_duplicate_validate_durationRelease notes
See
CHANGELOG.md(v0.0.36 section). After merge, creating a GitHub release will trigger the PyPI publish workflow.