Skip to content

refactor: extract path parameter validators#375

Merged
ramimbo merged 2 commits into
ramimbo:mainfrom
MolhamHamwi:refactor/path-param-validation
May 26, 2026
Merged

refactor: extract path parameter validators#375
ramimbo merged 2 commits into
ramimbo:mainfrom
MolhamHamwi:refactor/path-param-validation

Conversation

@MolhamHamwi
Copy link
Copy Markdown
Contributor

@MolhamHamwi MolhamHamwi commented May 26, 2026

Refs #320
Bounty #320

Summary

  • Extract bounded path/search parameter validation out of app.main into app.path_params.
  • Reuse the shared issue-number parser in both page search and MCP bounty lookup.
  • Add focused tests for bounds, hash normalization, and bad-request behavior.

Test Plan

  • ./.venv/bin/python -m pytest -q tests/test_path_params.py tests/test_bounty_pages.py tests/test_api_mcp.py
  • ./.venv/bin/python -m ruff check app/main.py app/path_params.py tests/test_path_params.py
  • ./.venv/bin/python -m ruff format --check app/main.py app/path_params.py tests/test_path_params.py

Summary by CodeRabbit

  • Refactor

    • Centralized path-parameter parsing and validation to ensure consistent handling of IDs, ledger sequences, issue-number queries, and proof hashes across the app.
  • Tests

    • Added tests covering parsing/validation behavior, edge cases, boundary limits, and rejection of invalid inputs.

Review Change Stack

Copilot AI review requested due to automatic review settings May 26, 2026 04:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 323fad35-dfac-4a91-8291-a8b386e9532b

📥 Commits

Reviewing files that changed from the base of the PR and between 75ae35e and b1cdf7b.

📒 Files selected for processing (1)
  • app/path_params.py
💤 Files with no reviewable changes (1)
  • app/path_params.py

📝 Walkthrough

Walkthrough

A new app/path_params module centralizes URL path-parameter parsing and validation. app/main.py is refactored to import these validators and remove local equivalents, rewiring HTTP endpoints and the MCP tool to use shared validation logic with consistent error responses.

Changes

Path Validation Extraction

Layer / File(s) Summary
Path params module: validators and constants
app/path_params.py
Introduces SQLITE_INTEGER_MAX, HEX_HASH_RE, and validator functions (issue_number_search_value, positive_bounty_id, positive_ledger_sequence, proof_hash_from_path) that parse/validate issue numbers, bounty IDs, ledger sequences, and proof hashes, raising HTTPException(400) on invalid input.
Path params test coverage
tests/test_path_params.py
Adds tests asserting acceptance cases (bounded numeric queries, normalized 64-hex hashes) and rejection cases (overflow, non-numeric input, whitespace, wrong-length, non-hex), verifying HTTP 400 on errors.
Main module refactoring: imports and endpoint updates
app/main.py
Removes local constants/helpers and imports shared validators from app.path_params; updates endpoints and the MCP tool to call issue_number_search_value, positive_bounty_id, positive_ledger_sequence, and proof_hash_from_path for input handling and DB lookups.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ramimbo/mergework#313: Related work hardening issue-number query parsing and overflow handling similar to issue_number_search_value.

Suggested labels

mrwk:accepted, mrwk:paid

Suggested reviewers

  • weilixiong
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: extract path parameter validators' clearly summarizes the main change: moving path/search parameter validation from app.main to a new app.path_params module.
Description check ✅ Passed The description includes a summary of changes, references an issue/bounty, and provides a specific test plan, but lacks details on the confusion or problem addressed (the 'Evidence' section is empty).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR centralizes validation/parsing logic for several API path/query parameters into a dedicated module and adds unit tests for the new utilities.

Changes:

  • Added app/path_params.py with shared helpers for bounded SQLite ints, positive IDs/sequences, and proof hash normalization.
  • Updated app/main.py to use the shared path param helpers instead of local/private duplicates.
  • Added tests/test_path_params.py to cover the new path/query parsing behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/test_path_params.py Adds unit tests for issue number parsing, positive ID/sequence validation, and proof-hash normalization.
app/path_params.py Introduces shared validation/parsing helpers (SQLite int bounds, proof hash, positive IDs).
app/main.py Switches endpoints/CLI code paths to use the shared helpers and removes duplicated private implementations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/path_params.py Outdated
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{field} must be an integer") from exc
if parsed < -SQLITE_INTEGER_MAX - 1 or parsed > SQLITE_INTEGER_MAX:
raise HTTPException(status_code=400, detail=f"{field} is too large")
Comment thread tests/test_path_params.py
Comment on lines +1 to +9
from fastapi import HTTPException

from app.path_params import (
SQLITE_INTEGER_MAX,
issue_number_search_value,
positive_bounty_id,
positive_ledger_sequence,
proof_hash_from_path,
)
Comment thread app/path_params.py Outdated
Comment on lines +49 to +52
try:
parsed = int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{field} must be an integer") from exc
Copy link
Copy Markdown
Contributor

@CharlieLZ CharlieLZ left a comment

Choose a reason for hiding this comment

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

Thanks for the focused extraction. I found one behavior-preservation issue to fix before this lands.

app/path_params.py introduces parse_sqlite_int(), but the PR does not use it anywhere (rg parse_sqlite_int only finds its definition) and it does not match the existing integer-parser semantics in app.main: parse_sqlite_int(True, field="bounty_id") returns 1, while the existing _parse_int() / MCP integer parser explicitly reject booleans as invalid integers. Since this PR is a boundary-preserving extraction, adding an unused public helper with looser validation creates a future footgun and is not covered by the new tests.

Please either remove parse_sqlite_int() from this extraction, or make it reject booleans and add regression coverage before introducing it as a shared helper.

Validation I ran:

  • uv run --extra dev python -m pytest tests/test_path_params.py tests/test_bounty_pages.py tests/test_api_mcp.py -q -> 87 passed
  • uv run --extra dev ruff check app/main.py app/path_params.py tests/test_path_params.py -> passed
  • uv run --extra dev ruff format --check app/main.py app/path_params.py tests/test_path_params.py -> passed
  • uv run --extra dev python -m mypy app/main.py app/path_params.py -> passed
  • git diff --check origin/main...HEAD -> clean

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_path_params.py (1)

21-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add tests for parse_sqlite_int changed behavior.

The new validator module introduces parse_sqlite_int, but this suite does not cover its success/bounds/error cases. Please add tests for valid ints, overflow, and non-integer inputs.

As per coding guidelines, tests/**/*.py: Add or update tests for changed behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_path_params.py` around lines 21 - 52, Add unit tests covering the
new parse_sqlite_int behavior: create tests that assert parse_sqlite_int("340")
returns 340 and parse_sqlite_int(str(SQLITE_INTEGER_MAX)) returns
SQLITE_INTEGER_MAX, assert parse_sqlite_int("")/parse_sqlite_int("
340")/parse_sqlite_int("340a")/parse_sqlite_int(str(SQLITE_INTEGER_MAX + 1))
return None (or use assert_bad_request where appropriate), and also add explicit
tests for positive_bounty_id/positive_ledger_sequence to ensure they still
enforce bounds; reference the function name parse_sqlite_int and the constant
SQLITE_INTEGER_MAX when adding the test cases so they mirror existing patterns
in test_issue_number_search_value_rejects_non_numeric_or_overflow_query and
test_positive_bounty_id_and_ledger_sequence_validate_bounds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/path_params.py`:
- Around line 48-55: parse_sqlite_int currently calls int(value) which accepts
booleans and truncates floats; update parse_sqlite_int to explicitly reject bool
and float inputs and only accept either an actual int (but not bool) or a
numeric string that matches an integer pattern (e.g. ^-?\d+$), then convert the
validated string with int(...) and keep the existing bounds check against
SQLITE_INTEGER_MAX; reference parse_sqlite_int and SQLITE_INTEGER_MAX when
making the change.

---

Outside diff comments:
In `@tests/test_path_params.py`:
- Around line 21-52: Add unit tests covering the new parse_sqlite_int behavior:
create tests that assert parse_sqlite_int("340") returns 340 and
parse_sqlite_int(str(SQLITE_INTEGER_MAX)) returns SQLITE_INTEGER_MAX, assert
parse_sqlite_int("")/parse_sqlite_int("
340")/parse_sqlite_int("340a")/parse_sqlite_int(str(SQLITE_INTEGER_MAX + 1))
return None (or use assert_bad_request where appropriate), and also add explicit
tests for positive_bounty_id/positive_ledger_sequence to ensure they still
enforce bounds; reference the function name parse_sqlite_int and the constant
SQLITE_INTEGER_MAX when adding the test cases so they mirror existing patterns
in test_issue_number_search_value_rejects_non_numeric_or_overflow_query and
test_positive_bounty_id_and_ledger_sequence_validate_bounds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4055f8e6-7264-4c2a-894e-acdf8db14faa

📥 Commits

Reviewing files that changed from the base of the PR and between ac99d79 and 75ae35e.

📒 Files selected for processing (3)
  • app/main.py
  • app/path_params.py
  • tests/test_path_params.py

Comment thread app/path_params.py Outdated
Comment on lines +48 to +55
def parse_sqlite_int(value: Any, *, field: str) -> int:
try:
parsed = int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{field} must be an integer") from exc
if parsed < -SQLITE_INTEGER_MAX - 1 or parsed > SQLITE_INTEGER_MAX:
raise HTTPException(status_code=400, detail=f"{field} is too large")
return parsed
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Python coercion behavior and repository usage of parse_sqlite_int (read-only).
python - <<'PY'
print("int(True) ->", int(True))
print("int(False) ->", int(False))
print("int(1.9) ->", int(1.9))
PY

rg -n -C2 '\bparse_sqlite_int\s*\('

Repository: ramimbo/mergework

Length of output: 311


Reject bool/float coercion in parse_sqlite_int (Line 50).

int(value) coerces True/False to 1/0 and truncates floats (e.g., 1.9 → 1), so non-integer inputs can pass parsing and only be validated by the SQLite bounds check.

💡 Proposed fix
 def parse_sqlite_int(value: Any, *, field: str) -> int:
-    try:
-        parsed = int(value)
-    except (TypeError, ValueError) as exc:
-        raise HTTPException(status_code=400, detail=f"{field} must be an integer") from exc
+    if isinstance(value, bool):
+        raise HTTPException(status_code=400, detail=f"{field} must be an integer")
+    if isinstance(value, int):
+        parsed = value
+    elif isinstance(value, str):
+        clean = value.strip()
+        if not clean or not clean.lstrip("+-").isdigit():
+            raise HTTPException(status_code=400, detail=f"{field} must be an integer")
+        try:
+            parsed = int(clean)
+        except ValueError as exc:
+            raise HTTPException(status_code=400, detail=f"{field} must be an integer") from exc
+    else:
+        raise HTTPException(status_code=400, detail=f"{field} must be an integer")
     if parsed < -SQLITE_INTEGER_MAX - 1 or parsed > SQLITE_INTEGER_MAX:
         raise HTTPException(status_code=400, detail=f"{field} is too large")
     return parsed
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/path_params.py` around lines 48 - 55, parse_sqlite_int currently calls
int(value) which accepts booleans and truncates floats; update parse_sqlite_int
to explicitly reject bool and float inputs and only accept either an actual int
(but not bool) or a numeric string that matches an integer pattern (e.g.
^-?\d+$), then convert the validated string with int(...) and keep the existing
bounds check against SQLITE_INTEGER_MAX; reference parse_sqlite_int and
SQLITE_INTEGER_MAX when making the change.

@MolhamHamwi
Copy link
Copy Markdown
Contributor Author

Addressed the behavior-preservation review in b1cdf7b: removed the unused parse_sqlite_int() helper instead of introducing looser boolean/int parsing semantics. Re-ran targeted pytest and ruff checks; all pass.

Copy link
Copy Markdown
Contributor

@newmattock newmattock left a comment

Choose a reason for hiding this comment

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

Thanks for the focused extraction. I found one behavior-preservation problem before this helper becomes shared.

parse_sqlite_int() currently accepts values the existing request/MCP integer parsers reject. In a local checkout of this PR, this probe:

from app.path_params import parse_sqlite_int
for value in [True, False, 1.9, b"7"]:
    print(repr(value), "->", parse_sqlite_int(value, field="bounty_id"))

returns:

True -> 1
False -> 0
1.9 -> 1
b'7' -> 7

The current integer parsing logic rejects booleans explicitly and only accepts actual ints or signed numeric strings. Since this PR is presented as a behavior-preserving path-parameter extraction, introducing an unused helper with looser coercion creates a future route/MCP footgun. Please either remove parse_sqlite_int() from this extraction, or align it with the existing strict integer semantics and add regression coverage for bool/float/bytes inputs.

Validation I ran on current head 75ae35e:

  • uv run --extra dev python -m pytest tests/test_path_params.py tests/test_bounty_pages.py tests/test_api_mcp.py -q -> 87 passed
  • uv run --extra dev python -m ruff check app/main.py app/path_params.py tests/test_path_params.py -> passed
  • uv run --extra dev python -m ruff format --check app/main.py app/path_params.py tests/test_path_params.py -> 3 files already formatted

Copy link
Copy Markdown
Contributor

@CharlieLZ CharlieLZ left a comment

Choose a reason for hiding this comment

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

Follow-up on b1cdf7b: the behavior-preservation issue I requested changes for is resolved. The unused parse_sqlite_int() helper has been removed, so the extraction no longer introduces a looser boolean/int parsing path than the existing validators.\n\nValidation on current head b1cdf7b:\n- uv run --extra dev python -m pytest tests/test_path_params.py tests/test_bounty_pages.py tests/test_api_mcp.py -q -> 87 passed\n- uv run --extra dev ruff check app/main.py app/path_params.py tests/test_path_params.py -> passed\n- uv run --extra dev ruff format --check app/main.py app/path_params.py tests/test_path_params.py -> passed\n- uv run --extra dev python -m mypy app/main.py app/path_params.py -> success\n- git diff --check origin/main...HEAD -> clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mrwk:accepted Maintainer accepted for payout mrwk:paid Ledger payment recorded

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants