Skip to content

Rate limiting on boost endpoint and test#87

Open
whisper67265 wants to merge 6 commits into
cppalliance:developfrom
whisper67265:feature/rate-limit-endpoint
Open

Rate limiting on boost endpoint and test#87
whisper67265 wants to merge 6 commits into
cppalliance:developfrom
whisper67265:feature/rate-limit-endpoint

Conversation

@whisper67265
Copy link
Copy Markdown
Collaborator

@whisper67265 whisper67265 commented Jun 1, 2026

Close #82, close #83.

Summary by CodeRabbit

  • New Features

    • Rate limiting applied to Boost endpoints: info (3 requests/min) and add-or-update (3 requests/hour); CI/test defaults expose these via env vars.
  • Tests

    • New and expanded plugin tests covering scoped throttling, 429 behavior, Retry-After, and background-task enqueueing.
  • Chores

    • CI and test infrastructure renamed/realigned from “integration” to “plugin” (workflows, scripts, test groups).
  • Documentation

    • Updated docs and READMEs to reflect plugin-test workflow and usage.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds scoped DRF throttles for Boost endpoints with environment-configurable rates merged into REST_FRAMEWORK, updates endpoint wiring, adds test helpers, unit and live plugin tests validating throttling and headers, and renames test/CI artifacts from integration→plugin.

Changes

Boost Endpoint Rate Limiting Implementation

Layer / File(s) Summary
Throttle rate configuration and environment setup
src/boost_weblate/settings_override.py, tests/django_qbk_format_settings.py
Defines throttle-rate defaults, reads BOOST_ENDPOINT_THROTTLE_* env vars, computes BOOST_ENDPOINT_THROTTLE_RATES, and provides merge_boost_endpoint_throttle_rates() to merge into DRF DEFAULT_THROTTLE_RATES.
DRF throttle classes and endpoint wiring
src/boost_weblate/endpoint/views.py
Adds ScopedRateThrottle subclasses for info and add-or-update, patches allow_request, and sets throttle_scope/throttle_classes on BoostEndpointInfo and AddOrUpdateView.
HTTP response header capture for testing
tests/plugin/lib/http.py
Adds http_json_with_headers and http_get_with_headers helpers returning status, parsed body, and response headers to validate Retry-After and rate-limit headers.
Unit tests for endpoint throttle behavior
tests/endpoint/test_views.py, tests/test_settings_override.py
Adds helpers to override DRF throttle rates and reset cache; includes tests asserting 200/202 for allowed requests, 429 on over-limit with Retry-After, task enqueue assertions, and settings-merge unit test.
Live plugin tests for rate limiting
tests/plugin/test_rate_limit.py
Parses environment throttle strings and performs live-stack tests against running containers for GET /boost-endpoint/info/ and POST /boost-endpoint/add-or-update/, asserting limit transitions and Retry-After.
Test library and fixture migration
tests/plugin/lib/__init__.py, tests/plugin/conftest.py, tests/plugin/lib/weblate_api.py, tests/plugin/lib/gh_repo.py
Migrates imports and docstrings from tests.integration.lib.*tests.plugin.lib.* and updates related docs/commit messages.
Test module markers and imports renaming
tests/plugin/test_auth.py, tests/plugin/test_functional.py, tests/plugin/test_smoke.py
Updates module docstrings and pytestmark from integrationplugin and switches HTTP helper imports to plugin helpers.
Build configuration migration
pyproject.toml
Replaces integration dependency group with plugin, updates pytest addopts to exclude plugin-marked tests by default, and renames pytest marker.
Docker Compose and test script setup
docker/docker-compose.ci.yml, scripts/plugin-auth.sh, scripts/plugin-functional.sh, scripts/plugin-smoke.sh
Updates CI comment to "plugin tests", adds BOOST_ENDPOINT_THROTTLE_INFO/BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE env vars, and switches scripts to install plugin deps and run plugin test targets.
GitHub Actions CI workflow updates
.github/workflows/ci.yml, .github/workflows/ci-plugin-auth.yml, .github/workflows/ci-plugin-functional.yml, .github/workflows/ci-plugin-smoke.yml
Renames combination CI workflows to plugin equivalents, updates job ids/names, invoked scripts, artifact names, and wires main ci.yml to call plugin workflows.
Documentation updates
.github/README.md, docker/README.md, README.md, scripts/README.md, docs/boost-weblate-plugin-refactor-plan.md
Updates docs to reflect plugin test locations, plugin CI workflow names, and new local/CI test instructions.
Code documentation naming
src/boost_weblate/endpoint/apps.py
Minor docstring updates replacing "integration" wording with "plugin".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Rate limiting on boost endpoint #82: Adds per-endpoint DRF throttles, settings/ENV overrides, and tests implementing rate limiting for the boost endpoints, directly addressing the rate-limiting objective.

Possibly related PRs

Suggested reviewers

  • henry0816191
  • wpak-ai

Poem

🐰 I counted hops through headers bright,

Three per minute, then rest at night;
A thumping test, a gentle thud,
Plugin checks each throttle's thud;
The rabbit nods — the stack turns right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Rate limiting on boost endpoint and test' accurately reflects the main changes: implementing rate limiting (throttling) on boost endpoint views and adding comprehensive tests for this functionality.
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

@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: 3

🧹 Nitpick comments (3)
src/boost_weblate/settings_override.py (1)

80-94: ⚖️ Poor tradeoff

Throttle rates are frozen at import time.

BOOST_ENDPOINT_THROTTLE_RATES is evaluated once when this module is first imported, and merge_boost_endpoint_throttle_rates consumes that constant rather than calling boost_endpoint_throttle_rates() fresh. Any process that sets BOOST_ENDPOINT_THROTTLE_* after import (or wants to re-read env) must reload the module. Reading env at merge time would remove that hidden coupling.

♻️ Read env at merge time
 def merge_boost_endpoint_throttle_rates(
     rest_framework: dict[str, Any],
 ) -> dict[str, Any]:
     """Merge Boost endpoint scoped rates into ``REST_FRAMEWORK``."""
     merged = dict(rest_framework)
     existing = dict(merged.get("DEFAULT_THROTTLE_RATES", {}))
-    existing.update(BOOST_ENDPOINT_THROTTLE_RATES)
+    existing.update(boost_endpoint_throttle_rates())
     merged["DEFAULT_THROTTLE_RATES"] = existing
     return merged
🤖 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 `@src/boost_weblate/settings_override.py` around lines 80 - 94, The current
code freezes env reads by evaluating BOOST_ENDPOINT_THROTTLE_RATES at import;
update merge_boost_endpoint_throttle_rates to call
boost_endpoint_throttle_rates() at merge time (or replace the constant with a
function reference) so the environment is re-read when merging. In short, stop
consuming the pre-evaluated BOOST_ENDPOINT_THROTTLE_RATES constant and invoke
boost_endpoint_throttle_rates() inside merge_boost_endpoint_throttle_rates so
changes to BOOST_ENDPOINT_THROTTLE_* take effect without reloading the module.
tests/integration/lib/http.py (2)

25-26: ⚡ Quick win

Header lookups become case-sensitive after this conversion.

resp.headers/e.headers are case-insensitive HTTPMessage objects, but the dict comprehension produces a case-sensitive plain dict. The integration tests then rely on exact casing (headers.get("Retry-After"), "X-RateLimit-Limit" in headers). This works today because DRF emits Retry-After with that exact casing, but it's fragile to any proxy/server that re-cases headers. Consider normalizing keys so lookups stay robust.

♻️ Normalize header keys to lowercase
-def _response_headers(header_obj) -> dict[str, str]:
-    return {k: v for k, v in header_obj.items()}
+def _response_headers(header_obj) -> dict[str, str]:
+    return {k.lower(): v for k, v in header_obj.items()}

Callers would then look up headers.get("retry-after") / "x-ratelimit-limit" in headers.

🤖 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/integration/lib/http.py` around lines 25 - 26, The _response_headers
function converts a case-insensitive HTTPMessage to a plain dict which makes
subsequent header lookups case-sensitive; update _response_headers(header_obj)
to normalize header keys (e.g., to lowercase) when building the dict so callers
can reliably use lowercase lookups like headers.get("retry-after") or
"x-ratelimit-limit" in headers; ensure you only change key normalization and
preserve header values and return type dict[str, str], and update any tests if
they expect original casing.

29-65: ⚡ Quick win

Duplicated request logic with http_json.

http_json_with_headers is a near-verbatim copy of http_json (URL build, headers, body encode, request, error handling, decode). Consider implementing http_json in terms of the new function to avoid drift.

♻️ Delegate to the headers variant
 def http_json(
     method: str,
     path: str,
     *,
     token: str | None = None,
     body: dict[str, Any] | None = None,
     timeout: float = 30.0,
 ) -> tuple[int, Any]:
     """Perform an HTTP request and return ``(status_code, parsed_json_or_text)``."""
-    url = f"{base_url()}{path}"
-    headers: dict[str, str] = {"Accept": "application/json"}
-    if token is not None:
-        headers["Authorization"] = auth_header(token)
-
-    data: bytes | None = None
-    if body is not None:
-        data = json.dumps(body).encode()
-        headers["Content-Type"] = "application/json"
-
-    req = urllib.request.Request(url, data=data, headers=headers, method=method)
-    try:
-        with urllib.request.urlopen(req, timeout=timeout) as resp:
-            raw = resp.read()
-            code: int = resp.getcode()
-    except urllib.error.HTTPError as e:
-        raw = e.read()
-        code = e.code
-
-    if not raw:
-        return code, None
-    try:
-        return code, json.loads(raw.decode())
-    except (json.JSONDecodeError, UnicodeDecodeError):
-        return code, raw.decode(errors="replace")
+    code, parsed, _ = http_json_with_headers(
+        method, path, token=token, body=body, timeout=timeout
+    )
+    return code, 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 `@tests/integration/lib/http.py` around lines 29 - 65, The http_json function
is duplicate of http_json_with_headers; refactor http_json to delegate to
http_json_with_headers to avoid duplication: have http_json call
http_json_with_headers(method, path, token=token, body=body, timeout=timeout),
receive (status, body, headers) and return only (status, body), leaving all
header-building, request/exception handling, and decoding in
http_json_with_headers; update the http_json signature to match parameters used
and ensure callers still get the (status, body) tuple.
🤖 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 `@tests/endpoint/test_views.py`:
- Around line 37-47: The helper _reload_throttle_rates currently overwrites
SimpleRateThrottle.THROTTLE_RATES and doesn't restore the previous state or
clear throttle counters, causing cross-test leakage; modify tests using
_reload_throttle_rates (or wrap it in a fixture) to save the original rates
(orig = SimpleRateThrottle.THROTTLE_RATES.copy()), run the override, and in a
finally block restore SimpleRateThrottle.THROTTLE_RATES = orig and clear the
throttle cache (e.g., call cache.clear() or the project's throttle cache clear
helper) so overridden rates and cached counters are always reset between tests;
if using a fixture, implement it as a yield fixture that performs the same
save/restore and cache clear around the test execution.

In `@tests/integration/test_rate_limit.py`:
- Around line 61-83: The test test_add_or_update_returns_429_when_rate_limited
is order-dependent because prior /boost-endpoint/add-or-update/ calls consume
the UserRateThrottle/AddOrUpdateThrottle quota for throttle_scope =
"add-or-update"; before the loop in
test_add_or_update_returns_429_when_rate_limited, either clear the throttle
state for the current user/token or obtain a fresh token so the quota is
isolated: locate the test function and add a step that deletes the throttle
cache keys for scope "add-or-update" (or calls the app's token factory to create
a new WEBLATE_API_TOKEN) so subsequent calls via http_json_with_headers hit an
empty quota and the loop can expect 202 until the limit is reached.

In `@tests/test_settings_override.py`:
- Around line 96-106: The test currently asserts hardcoded defaults for "info"
and "add-or-update" but those values are resolved from environment variables at
import; update the assertions to compare against the resolved environment-backed
values instead of literals: in
test_merge_boost_endpoint_throttle_rates_preserves_upstream(), import or read
the env vars used by boost_weblate.settings_override (e.g. check
os.environ.get("BOOST_ENDPOINT_THROTTLE_INFO", "60/minute") and
os.environ.get("BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE", "10/hour")) and assert
that rates["info"] and rates["add-or-update"] equal those resolved values,
keeping the existing assertions that upstream keys ("user","anon") are preserved
and referencing merge_boost_endpoint_throttle_rates to locate the logic.

---

Nitpick comments:
In `@src/boost_weblate/settings_override.py`:
- Around line 80-94: The current code freezes env reads by evaluating
BOOST_ENDPOINT_THROTTLE_RATES at import; update
merge_boost_endpoint_throttle_rates to call boost_endpoint_throttle_rates() at
merge time (or replace the constant with a function reference) so the
environment is re-read when merging. In short, stop consuming the pre-evaluated
BOOST_ENDPOINT_THROTTLE_RATES constant and invoke
boost_endpoint_throttle_rates() inside merge_boost_endpoint_throttle_rates so
changes to BOOST_ENDPOINT_THROTTLE_* take effect without reloading the module.

In `@tests/integration/lib/http.py`:
- Around line 25-26: The _response_headers function converts a case-insensitive
HTTPMessage to a plain dict which makes subsequent header lookups
case-sensitive; update _response_headers(header_obj) to normalize header keys
(e.g., to lowercase) when building the dict so callers can reliably use
lowercase lookups like headers.get("retry-after") or "x-ratelimit-limit" in
headers; ensure you only change key normalization and preserve header values and
return type dict[str, str], and update any tests if they expect original casing.
- Around line 29-65: The http_json function is duplicate of
http_json_with_headers; refactor http_json to delegate to http_json_with_headers
to avoid duplication: have http_json call http_json_with_headers(method, path,
token=token, body=body, timeout=timeout), receive (status, body, headers) and
return only (status, body), leaving all header-building, request/exception
handling, and decoding in http_json_with_headers; update the http_json signature
to match parameters used and ensure callers still get the (status, body) tuple.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d7afcf2-1824-4685-a0a2-93fd44fa7de1

📥 Commits

Reviewing files that changed from the base of the PR and between 80b312f and be704ef.

📒 Files selected for processing (9)
  • docker/docker-compose.ci.yml
  • scripts/integration-auth.sh
  • src/boost_weblate/endpoint/views.py
  • src/boost_weblate/settings_override.py
  • tests/django_qbk_format_settings.py
  • tests/endpoint/test_views.py
  • tests/integration/lib/http.py
  • tests/integration/test_rate_limit.py
  • tests/test_settings_override.py

Comment thread tests/endpoint/test_views.py Outdated
Comment thread tests/plugin/test_rate_limit.py Outdated
Comment thread tests/test_settings_override.py Outdated
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.

Caution

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

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

38-83: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix plugin rate-limit tests to avoid cross-module DRF throttle state leakage

scripts/plugin-auth.sh runs tests/plugin/test_auth.py immediately before tests/plugin/test_rate_limit.py in the same live Weblate stack, and both suites use the same WEBLATE_API_TOKEN. The endpoints under test use ScopedRateThrottle with throttle_scope="info" / "add-or-update" (via BoostEndpointInfoThrottle / AddOrUpdateThrottle), and DRF’s ScopedRateThrottle keys are based on throttle_scope + the authenticated user identifier—so the two successful requests in test_auth.py prime the per-user/per-scope throttle counters. That makes test_rate_limit.py’s “expect limit successes, then 429” expectation order-dependent (e.g., for 3/minute, the 3rd request in the loop can become 429).

🐛 Proposed fix: tolerate prior consumption by driving until the first 429
     def test_info_returns_429_when_rate_limited(self, api_token: str) -> None:
         rate = os.environ.get("BOOST_ENDPOINT_THROTTLE_INFO", "3/minute")
         limit = _parse_rate_limit(rate)
 
         last_headers: dict[str, str] = {}
-        for _ in range(limit):
-            code, _body, headers = http_get_with_headers(
-                "/boost-endpoint/info/", token=api_token
-            )
-            assert code == 200, f"expected 200 before limit: {code}"
-            last_headers = headers
-
-        code, _body, headers = http_get_with_headers(
-            "/boost-endpoint/info/", token=api_token
-        )
-        assert code == 429, f"expected 429 after {limit} requests: {code}"
+        saw_success = False
+        code = None
+        headers = {}
+        for _ in range(limit + 1):
+            code, _body, headers = http_get_with_headers(
+                "/boost-endpoint/info/", token=api_token
+            )
+            if code == 429:
+                break
+            assert code == 200, f"expected 200 before limit: {code}"
+            saw_success = True
+            last_headers = headers
+        assert saw_success, "expected at least one 200 before throttling"
+        assert code == 429, f"expected 429 within {limit + 1} requests: {code}"
         retry_after = headers.get("Retry-After")
         assert retry_after is not None
         assert int(retry_after) > 0

Apply the analogous change to test_add_or_update_returns_429_when_rate_limited, expecting 202 before the first 429.

A more deterministic alternative is to clear the Django cache/throttle storage in the Weblate container at the start of each test (e.g., using the existing exec_python fixture with from django.core.cache import cache; cache.clear()), so throttle counters start empty regardless of test ordering.

🤖 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/plugin/test_rate_limit.py` around lines 38 - 83, The tests
test_info_returns_429_when_rate_limited and
test_add_or_update_returns_429_when_rate_limited assume a fresh per-scope
counter; instead make them tolerate prior consumption by driving requests until
the first 429 is observed (loop calling http_get_with_headers in
test_info_returns_429... and http_json_with_headers in test_add_or_update...,
asserting success codes (200/202) only while response != 429 and then asserting
the first 429 provides a positive Retry-After and that X-RateLimit-Limit in the
last success headers equals the parsed limit from _parse_rate_limit);
alternatively, at test start clear DRF throttle storage via exec_python running
from django.core.cache import cache; cache.clear() so counters are
deterministic.
🤖 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.

Outside diff comments:
In `@tests/plugin/test_rate_limit.py`:
- Around line 38-83: The tests test_info_returns_429_when_rate_limited and
test_add_or_update_returns_429_when_rate_limited assume a fresh per-scope
counter; instead make them tolerate prior consumption by driving requests until
the first 429 is observed (loop calling http_get_with_headers in
test_info_returns_429... and http_json_with_headers in test_add_or_update...,
asserting success codes (200/202) only while response != 429 and then asserting
the first 429 provides a positive Retry-After and that X-RateLimit-Limit in the
last success headers equals the parsed limit from _parse_rate_limit);
alternatively, at test start clear DRF throttle storage via exec_python running
from django.core.cache import cache; cache.clear() so counters are
deterministic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69324dbc-e824-432b-8dbc-9bde3c7fd69a

📥 Commits

Reviewing files that changed from the base of the PR and between be704ef and 12496be.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • .github/README.md
  • .github/workflows/ci-plugin-auth.yml
  • .github/workflows/ci-plugin-functional.yml
  • .github/workflows/ci-plugin-smoke.yml
  • .github/workflows/ci.yml
  • README.md
  • docker/README.md
  • docker/docker-compose.ci.yml
  • pyproject.toml
  • scripts/integration-auth.sh
  • scripts/integration-functional.sh
  • scripts/integration-smoke.sh
  • tests/plugin/__init__.py
  • tests/plugin/conftest.py
  • tests/plugin/lib/__init__.py
  • tests/plugin/lib/docker_exec.py
  • tests/plugin/lib/gh_repo.py
  • tests/plugin/lib/http.py
  • tests/plugin/lib/weblate_api.py
  • tests/plugin/test_auth.py
  • tests/plugin/test_functional.py
  • tests/plugin/test_rate_limit.py
  • tests/plugin/test_smoke.py
💤 Files with no reviewable changes (1)
  • tests/plugin/lib/http.py
✅ Files skipped from review due to trivial changes (5)
  • tests/plugin/conftest.py
  • .github/README.md
  • tests/plugin/lib/init.py
  • tests/plugin/test_functional.py
  • docker/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker/docker-compose.ci.yml

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.

🧹 Nitpick comments (2)
scripts/plugin-smoke.sh (1)

45-47: 💤 Low value

Consider adding throttle environment variables for consistency.

Similar to plugin-functional.sh, this smoke test script does not export throttle rate environment variables, while plugin-auth.sh does (lines 41-42 in that file). For consistency across the test suite, consider adding the same throttle configuration before running tests, even if smoke tests are unlikely to trigger rate limits.

🤖 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 `@scripts/plugin-smoke.sh` around lines 45 - 47, Add the same throttle
environment variable exports used in plugin-auth.sh into scripts/plugin-smoke.sh
(place them before the python -m pytest invocation) so the smoke test uses the
same rate-limit configuration; copy the exact export lines from plugin-auth.sh
and insert them right after the uv pip install --quiet --system --group plugin
line and before the pytest command to ensure consistent throttle behavior.
scripts/plugin-functional.sh (1)

61-63: ⚡ Quick win

Consider adding throttle environment variables for consistency.

The plugin-auth.sh script exports BOOST_ENDPOINT_THROTTLE_INFO and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE environment variables (lines 41-42 in that file), but this functional test script does not. If the functional tests make requests to the Boost endpoints, they may encounter default throttle limits that differ from the test expectations.

For consistency and predictability across test suites, consider adding:

export BOOST_ENDPOINT_THROTTLE_INFO="${BOOST_ENDPOINT_THROTTLE_INFO:-3/minute}"
export BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE="${BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE:-3/hour}"

before line 60.

🤖 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 `@scripts/plugin-functional.sh` around lines 61 - 63, Add exported throttle env
vars to the plugin functional test script: in scripts/plugin-functional.sh
export BOOST_ENDPOINT_THROTTLE_INFO and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE
with sensible defaults (e.g., defaulting to "3/minute" and "3/hour") before the
pytest invocation so the tests use the same throttle settings as plugin-auth.sh;
update the script near where the test runner (python -m pytest) is invoked so
BOOST_ENDPOINT_THROTTLE_INFO and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE are set
for the test process.
🤖 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.

Nitpick comments:
In `@scripts/plugin-functional.sh`:
- Around line 61-63: Add exported throttle env vars to the plugin functional
test script: in scripts/plugin-functional.sh export BOOST_ENDPOINT_THROTTLE_INFO
and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE with sensible defaults (e.g.,
defaulting to "3/minute" and "3/hour") before the pytest invocation so the tests
use the same throttle settings as plugin-auth.sh; update the script near where
the test runner (python -m pytest) is invoked so BOOST_ENDPOINT_THROTTLE_INFO
and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE are set for the test process.

In `@scripts/plugin-smoke.sh`:
- Around line 45-47: Add the same throttle environment variable exports used in
plugin-auth.sh into scripts/plugin-smoke.sh (place them before the python -m
pytest invocation) so the smoke test uses the same rate-limit configuration;
copy the exact export lines from plugin-auth.sh and insert them right after the
uv pip install --quiet --system --group plugin line and before the pytest
command to ensure consistent throttle behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 483db73d-9ef1-4892-a03b-d6baa43c31ff

📥 Commits

Reviewing files that changed from the base of the PR and between 12496be and 405f5a1.

📒 Files selected for processing (18)
  • .github/README.md
  • .github/workflows/ci-plugin-auth.yml
  • .github/workflows/ci-plugin-functional.yml
  • .github/workflows/ci-plugin-smoke.yml
  • README.md
  • docs/boost-weblate-plugin-refactor-plan.md
  • scripts/README.md
  • scripts/plugin-auth.sh
  • scripts/plugin-functional.sh
  • scripts/plugin-smoke.sh
  • src/boost_weblate/endpoint/apps.py
  • tests/plugin/lib/gh_repo.py
  • tests/plugin/lib/http.py
  • tests/plugin/lib/weblate_api.py
  • tests/plugin/test_auth.py
  • tests/plugin/test_functional.py
  • tests/plugin/test_rate_limit.py
  • tests/plugin/test_smoke.py
✅ Files skipped from review due to trivial changes (5)
  • tests/plugin/lib/gh_repo.py
  • docs/boost-weblate-plugin-refactor-plan.md
  • .github/README.md
  • scripts/README.md
  • src/boost_weblate/endpoint/apps.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/plugin/lib/weblate_api.py
  • tests/plugin/test_auth.py
  • tests/plugin/test_functional.py
  • .github/workflows/ci-plugin-auth.yml
  • .github/workflows/ci-plugin-smoke.yml
  • tests/plugin/test_smoke.py
  • .github/workflows/ci-plugin-functional.yml
  • tests/plugin/test_rate_limit.py
  • tests/plugin/lib/http.py

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.

🧹 Nitpick comments (1)
tests/plugin/lib/http.py (1)

76-109: ⚡ Quick win

Eliminate code duplication by delegating to http_json_with_headers.

The http_json function duplicates ~33 lines of logic from http_json_with_headers. Refactor http_json to call http_json_with_headers and discard the headers from the returned tuple, establishing a single source of truth for the HTTP request/response handling logic.

♻️ Refactor to eliminate duplication
 def http_json(
     method: str,
     path: str,
     *,
     token: str | None = None,
     body: dict[str, Any] | None = None,
     timeout: float = 30.0,
 ) -> tuple[int, Any]:
     """Perform an HTTP request and return ``(status_code, parsed_json_or_text)``."""
-    url = f"{base_url()}{path}"
-    headers: dict[str, str] = {"Accept": "application/json"}
-    if token is not None:
-        headers["Authorization"] = auth_header(token)
-
-    data: bytes | None = None
-    if body is not None:
-        data = json.dumps(body).encode()
-        headers["Content-Type"] = "application/json"
-
-    req = urllib.request.Request(url, data=data, headers=headers, method=method)
-    try:
-        with urllib.request.urlopen(req, timeout=timeout) as resp:
-            raw = resp.read()
-            code: int = resp.getcode()
-    except urllib.error.HTTPError as e:
-        raw = e.read()
-        code = e.code
-
-    if not raw:
-        return code, None
-    try:
-        return code, json.loads(raw.decode())
-    except (json.JSONDecodeError, UnicodeDecodeError):
-        return code, raw.decode(errors="replace")
+    status_code, parsed_body, _ = http_json_with_headers(
+        method, path, token=token, body=body, timeout=timeout
+    )
+    return status_code, parsed_body
🤖 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/plugin/lib/http.py` around lines 76 - 109, The http_json function
duplicates the request/response logic; refactor it to call
http_json_with_headers(method, path, token=token, body=body, timeout=timeout)
and return only the (status_code, parsed_or_text) part of the tuple, discarding
the headers element; preserve the existing signature and behavior (including
timeout and token handling) so all HTTP logic lives in http_json_with_headers
and http_json becomes a thin wrapper that extracts the first two return values.
🤖 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.

Nitpick comments:
In `@tests/plugin/lib/http.py`:
- Around line 76-109: The http_json function duplicates the request/response
logic; refactor it to call http_json_with_headers(method, path, token=token,
body=body, timeout=timeout) and return only the (status_code, parsed_or_text)
part of the tuple, discarding the headers element; preserve the existing
signature and behavior (including timeout and token handling) so all HTTP logic
lives in http_json_with_headers and http_json becomes a thin wrapper that
extracts the first two return values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0fdbd98d-4be4-49d2-8fc5-6aacd531cd31

📥 Commits

Reviewing files that changed from the base of the PR and between aabac7d and 0b7045a.

📒 Files selected for processing (2)
  • tests/plugin/lib/http.py
  • tests/plugin/test_rate_limit.py

@whisper67265 whisper67265 requested a review from henry0816191 June 1, 2026 22:54
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.

Rate limiting integration test Rate limiting on boost endpoint

1 participant