Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from httpx import AsyncClient, HTTPStatusError
from httpx import AsyncClient, HTTPStatusError, RequestError
from jwt import DecodeError, ExpiredSignatureError, InvalidSignatureError
from jwt import decode as jwt_decode
from pydantic import BaseModel, ValidationError
Expand Down Expand Up @@ -135,8 +135,10 @@ async def get_current_user(
async def dispatch_webhook(url: str, payload: BaseModel) -> None:
async with AsyncClient(timeout=5) as client:
try:
response = await client.post(url, json=payload.model_dump_json())
response = await client.post(url, json=payload.model_dump(mode="json"))
response.raise_for_status()
logger.info(f"Successfully dispatched to {url}")
except HTTPStatusError as e:
logger.error(f"Error dispatching webhook to {url}: {e.response.status_code} - {e.response.text}")
except RequestError as e:
logger.error(f"Error dispatching webhook to {url}: {e}")
29 changes: 28 additions & 1 deletion src/tests/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from datetime import datetime

import pytest
from fastapi import HTTPException
from fastapi.security import SecurityScopes
from pydantic import BaseModel

from app.api.dependencies import get_jwt
from app.api.dependencies import dispatch_webhook, get_jwt
from app.core.security import create_access_token


Expand Down Expand Up @@ -54,3 +57,27 @@ def test_get_jwt(scopes, token, expires_minutes, error_code, expected_payload):
payload = get_jwt(SecurityScopes(scopes), token_)
if expected_payload is not None:
assert payload.model_dump() == expected_payload


@pytest.mark.asyncio
async def test_dispatch_webhook_sends_json_object(monkeypatch):
captured = {}

class _Response:
def raise_for_status(self):
return None

async def _post(self, url, json=None): # noqa: RUF029 - must match AsyncClient.post's async signature
captured["json"] = json
return _Response()

monkeypatch.setattr("app.api.dependencies.AsyncClient.post", _post)

class _Payload(BaseModel):
id: int
created_at: datetime

await dispatch_webhook("https://example.com/hook", _Payload(id=1, created_at=datetime(2026, 6, 11, 15, 38, 6)))

# The body must be a JSON object, not a double-encoded JSON string
assert captured["json"] == {"id": 1, "created_at": "2026-06-11T15:38:06"}
Loading