Skip to content

fix: harden input handling, mock wiring, and config payload (v0.0.36)#45

Merged
Xerolux merged 1 commit into
mainfrom
fix/code-review-hardening-v0.0.36
Jul 18, 2026
Merged

fix: harden input handling, mock wiring, and config payload (v0.0.36)#45
Xerolux merged 1 commit into
mainfrom
fix/code-review-hardening-v0.0.36

Conversation

@Xerolux

@Xerolux Xerolux commented Jul 18, 2026

Copy link
Copy Markdown
Owner

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()sensor was concatenated raw into the request URL. Values with spaces, #, & produced malformed URLs or injected extra query parameters. Now percent-encoded with quote(safe=""), consistent with set_switch_state().
  • get_log()log_type now validated against the documented allow-list (actions/switching/onewire); raises VioletPoolAPIError otherwise.
  • restore_calibration()sensor and timestamp now sanitized (length bounds, character filtering) before posting, matching every key in set_config().

Fixes

  • _sanitize_config_payload()int/bool values were coerced to float via sanitize_numeric(). Integer flags (e.g. DOS_*_use from set_dosage_enabled()) were sent as 1.0/0.0. Now preserved as integers; bool0/1.
  • set_config() — removed undocumented retryable=True override so it behaves like every other state-changing POST (non-retryable on 5xx, matching test_manual_dosing_post_is_not_retried).
  • mock server set_omni_position — handler was defined but never routed. OMNI,OMNI_DC<N> queries fell through to the generic handler, returning OK\nOMNI\n... (wrong) and never updating omni_position. Now dispatched correctly → OK\nOMNITRONIC\n....

Cleanup

  • Removed duplicate InputSanitizer.validate_duration/validate_speed (name collision with the raising validators in _api_model; unused dead code).
  • Corrected duration/last_value type hints: float | Noneint | None (matches actual whole-number validation).
  • Fixed mojibake (ÔåÆ) and dropped redundant except (Exception): parens.

Verification

  • ruff check .All checks passed
  • mypy violet_poolcontroller_apiSuccess, no issues in 15 source files
  • pytest tests/test_api.py tests/test_mock_server.py144 passed (134 baseline + 10 new regression tests)
  • python tests/test_api_smoke.py68/68 passed (now includes OMNI end-to-end coverage)

Test coverage added

Test Covers
test_set_config_preserves_int_values Fix #3 — int preservation
test_set_config_preserves_bool_as_int_flag Fix #3 — bool → 0/1
test_set_config_not_retried_on_server_error Fix #4 — non-retryable POST
test_get_calibration_history_url_encodes_sensor Fix #1 — URL encoding
test_get_log_rejects_unknown_log_type Fix #1 — log_type validation
test_get_log_valid_log_type_reaches_request Fix #1 — happy path
test_restore_calibration_sanitizes_payload Fix #6 — input sanitization
test_input_sanitizer_no_duplicate_validate_duration Fix #5 — dead code removal
+ OMNI smoke test cases Fix #2 — mock wiring

Release notes

See CHANGELOG.md (v0.0.36 section). After merge, creating a GitHub release will trigger the PyPI publish workflow.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +781 to +796
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
        )

Comment thread tests/test_api.py
Comment on lines +1948 to +1949
data = captured["data"]
assert data == {"DOS_1_use": 1, "DOS_2_use": 0}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

@Xerolux
Xerolux merged commit 44df66d into main Jul 18, 2026
6 checks passed
@Xerolux
Xerolux deleted the fix/code-review-hardening-v0.0.36 branch July 18, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant