Skip to content

Ensure webhook sync uses independent sessions#90

Merged
shayancoin merged 2 commits intomainfrom
codex/refactor-_process-to-use-own-sqlalchemy-session
Oct 17, 2025
Merged

Ensure webhook sync uses independent sessions#90
shayancoin merged 2 commits intomainfrom
codex/refactor-_process-to-use-own-sqlalchemy-session

Conversation

@shayancoin
Copy link
Owner

@shayancoin shayancoin commented Oct 16, 2025

Summary

  • open and close a dedicated database session inside the Hygraph webhook background task
  • add a helper to resolve the API write token from configuration
  • extend sync route tests with a regression to ensure the background task uses a live session

Testing

  • pytest backend/tests/test_sync_routes_metrics.py

https://chatgpt.com/codex/tasks/task_e_68f12bd677208330b058f2bfd68f31f8

Summary by CodeRabbit

  • Chores
    • Improved backend API token resolution handling.
  • Tests
    • Enhanced webhook test validation with dynamic payload generation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

A helper function resolves API write tokens by checking settings first, then falling back to environment variables. Sync webhook tests now use dynamic UUID-based payloads and secrets sourced from settings instead of hard-coded values.

Changes

Cohort / File(s) Summary
Token Resolution Helper
backend/api/security.py
Added _load_expected_write_token(settings) helper that resolves API write tokens from settings or the API_WRITE_TOKEN environment variable; integrated into require_write_token flow.
Dynamic Webhook Test Payload
backend/tests/test_sync_routes_metrics.py
Replaced static webhook payload and hard-coded secret "whsec" with UUID-based payload and sync_settings.hygraph_webhook_secret; added imports for uuid and sync_settings.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • shayancoin/paform#90: Adds the same _load_expected_write_token helper and updates sync webhook tests with dynamic secrets, with additional session changes in routes_sync.py.
  • shayancoin/paform#88: Modifies backend/api/security.py to add API write token loading helper and caching logic.
  • shayancoin/paform#89: Modifies _load_expected_write_token and related token-loading logic in backend/api/security.py.

Poem

🐰 A token hops from settings to env,
With fallback paths that surely mend,
And webhooks now wear UUID clothes,
No secrets static—dynamically flows! ✨
thump thump 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description does not follow the required template: it is missing the "PR Type" section, "Short Description" heading, and "Tests Added" section. While the summary and testing information is present, the required template fields and headings are not used. Please update the PR description to include a "PR Type" heading with the appropriate type, a "Short Description" section summarizing the changes, and a "Tests Added" section listing new tests to fully adhere to the repository’s PR template.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the primary change—ensuring webhook synchronization uses its own independent database sessions—and is clear and specific enough for a teammate scanning the history to understand the main update without extraneous details.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af747dd and bf54ca7.

📒 Files selected for processing (2)
  • backend/api/security.py (1 hunks)
  • backend/tests/test_sync_routes_metrics.py (2 hunks)

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

Copy link
Contributor

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

Caution

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

⚠️ Outside diff range comments (4)
backend/api/security.py (1)

39-42: Fix line length and return type annotation.

Line 42 exceeds the 88-character limit. Additionally, consider updating the return type of require_write_token to None since it raises exceptions on failure rather than returning a boolean value that callers can check.

Apply this diff to fix the line length:

-    if not expected or not secrets.compare_digest(token, expected):
-        raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Invalid API write token.")
+    if not expected or not secrets.compare_digest(token, expected):
+        raise HTTPException(
+            status_code=HTTP_403_FORBIDDEN, detail="Invalid API write token."
+        )

Consider also changing the return type annotation from bool to None:

 async def require_write_token(
     authorization: str | None = Security(api_key_header),
     settings: Settings = Depends(get_settings),
-) -> bool:
+) -> None:
backend/api/routes_sync.py (3)

111-141: LGTM with a suggestion for commit handling.

The session lifecycle management is correct: creating a new session with SessionLocal(), using it for the background pull operation, and closing it in a finally block. This ensures the background task has an independent session that won't be affected by the outer request session's lifecycle.

Consider adding an explicit commit before closing the session if pull_all makes any database changes that need to be persisted:

         except Exception as e:  # noqa: BLE001
             sync_failure_total.labels("all").inc()
             logger.exception(
                 "hygraph_webhook_failure",
                 extra={
                     "event_id": event_id_local,
                     "dedup": False,
                     "error": str(e),
                 },
             )
         finally:
+            session.commit()  # Persist any changes made during pull_all
             session.close()

However, if pull_all already manages its own transaction commits, this is not necessary.


66-144: Fix return type annotation mismatch.

The function signature declares a return type of Dict[str, Any], but both return statements (lines 104 and 144) return JSONResponse objects. This causes a type checking failure.

Update the return type annotation to match the actual return type:

 async def hygraph_webhook(
     request: Request,
     background: BackgroundTasks,
     db: Session = Depends(get_db),
-) -> Dict[str, Any]:
+) -> JSONResponse:

46-58: Fix line length.

Line 46 exceeds the 88-character limit.

Apply this diff to fix the formatting:

 def _error_envelope(code: str, message: str, details: Optional[dict] = None) -> Dict[str, Any]:
     """
     Builds a standardized error envelope for API responses.
     
     Parameters:
-        code (str): Machine-readable error code identifying the error.
-        message (str): Human-readable error message describing the failure.
-        details (Optional[dict]): Additional contextual information; defaults to an empty dict when not provided.
+        code (str): Machine-readable error code.
+        message (str): Human-readable error message.
+        details (Optional[dict]): Additional context; defaults to empty dict.
     
     Returns:
-        Dict[str, Any]: Dictionary with keys `ok` (False) and `error` containing `code`, `message`, and `details`.
+        Dict[str, Any]: Dictionary with keys `ok` (False) and `error`
+            containing `code`, `message`, and `details`.
     """
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2a35c4 and af747dd.

📒 Files selected for processing (3)
  • backend/api/routes_sync.py (3 hunks)
  • backend/api/security.py (1 hunks)
  • backend/tests/test_sync_routes_metrics.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/api/security.py (1)
backend/api/config.py (1)
  • Settings (10-62)
backend/api/routes_sync.py (2)
backend/api/db.py (1)
  • get_db (53-59)
backend/services/hygraph_service.py (2)
  • HygraphService (11-80)
  • pull_all (62-64)
backend/tests/test_sync_routes_metrics.py (1)
backend/services/hygraph_service.py (1)
  • HygraphService (11-80)
🪛 GitHub Actions: CI
backend/api/security.py

[error] 42-42: Line too long (94 > 88)

backend/api/routes_sync.py

[error] 46-46: Line too long (95 > 88)


[error] 110-110: Incompatible return value type: JSONResponse returned where dict[str, Any] expected

🔇 Additional comments (4)
backend/api/routes_sync.py (1)

23-23: LGTM!

The import of SessionLocal enables the webhook background task to create its own database session, ensuring proper session lifecycle management independent of the request scope.

backend/tests/test_sync_routes_metrics.py (3)

6-6: LGTM!

The new imports support the enhanced test functionality:

  • uuid for generating unique payloads to ensure test isolation
  • text from SQLAlchemy for executing raw SQL in the session verification test
  • sync_settings for accessing the webhook secret configuration

Also applies to: 10-10, 13-13


21-23: LGTM!

Simplifying the client fixture from environment-based setup to a plain TestClient(app) improves test clarity and removes unnecessary complexity.


35-37: LGTM!

Using dynamic payloads with uuid.uuid4() and sourcing the webhook secret from sync_settings.hygraph_webhook_secret improves test isolation and eliminates hardcoded values, making the tests more maintainable and reliable.

Comment on lines 61 to 81
def test_webhook_background_uses_new_session(client, caplog, monkeypatch):
from services import hygraph_service as svc

calls: list[bool] = []

async def fake_pull_all(db, page_size=None):
db.execute(text("SELECT 1"))
calls.append(db.is_active)
return {"materials": 0, "modules": 0, "systems": 0}

monkeypatch.setattr(svc.HygraphService, "pull_all", fake_pull_all)

body = json.dumps({"ping": str(uuid.uuid4())}).encode()
sig = _sig(sync_settings.hygraph_webhook_secret, body)

r = client.post("/api/sync/hygraph", data=body, headers={"x-hygraph-signature": sig})
assert r.status_code == 202
assert r.json()["ok"] is True
assert calls == [True]
assert not any(rec.getMessage() == "hygraph_webhook_failure" for rec in caplog.records)

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM with a clarification on test coverage.

The test successfully verifies that:

  1. The background task receives an active database session
  2. The session can execute SQL queries
  3. No failure logs are emitted during processing

This confirms the webhook background processing uses a live, independent session as intended by the PR objectives.

Consider expanding the test to verify that the background session is truly independent from the request session. For example, you could verify that changes made in the background session are not visible to the request session until committed:

async def fake_pull_all(db, page_size=None):
    # Verify session independence by checking it's a different instance
    db.execute(text("SELECT 1"))
    calls.append({
        "is_active": db.is_active,
        "session_id": id(db)  # Track session instance
    })
    return {"materials": 0, "modules": 0, "systems": 0}

However, the current implementation adequately verifies the core requirement.

🤖 Prompt for AI Agents
In backend/tests/test_sync_routes_metrics.py around lines 61 to 81, update the
fake_pull_all used in the test to record the background session instance id in
addition to is_active, and then update the assertions to expect a dict with both
keys and validate the session_id is an integer (and if you can obtain the
request-session id in the test, assert they are not equal to prove
independence). Specifically, change fake_pull_all to append {"is_active":
db.is_active, "session_id": id(db)} to calls, and replace the existing
assertions to check calls == [{"is_active": True, "session_id": <int>}] (and if
request session id is available, assert calls[0]["session_id"] !=
request_session_id).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #154

coderabbitai bot added a commit that referenced this pull request Oct 17, 2025
…ssion`

Docstrings generation was requested by @shayancoin.

* #90 (comment)

The following files were modified:

* `backend/api/routes_sync.py`
* `backend/api/security.py`
* `backend/tests/test_sync_routes_metrics.py`
@shayancoin shayancoin merged commit 84bcc6e into main Oct 17, 2025
0 of 5 checks passed
shayancoin added a commit that referenced this pull request Oct 18, 2025
…ssion` (#154)

Docstrings generation was requested by @shayancoin.

* #90 (comment)

The following files were modified:

* `backend/api/routes_sync.py`
* `backend/api/security.py`
* `backend/tests/test_sync_routes_metrics.py`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Shayan <shayan@coin.link>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant