feat/retry-mechanism #1
14
app/main.py
14
app/main.py
@ -1,10 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from fastapi import FastAPI, Request, BackgroundTasks
|
from fastapi import FastAPI, Request, BackgroundTasks, Body
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from app.logging import get_logger
|
from app.logging import get_logger
|
||||||
from app.admin import router as admin_router
|
from app.admin import router as admin_router
|
||||||
from app.db import SessionLocal, WebhookEndpoint, RequestLog, DeliveryLog, init_db
|
from app.db import SessionLocal, WebhookEndpoint, RequestLog, DeliveryLog, init_db
|
||||||
from app.services.engine import engine
|
from app.services.engine import engine
|
||||||
|
from app.models import IncomingPayload
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
logger = get_logger("app")
|
logger = get_logger("app")
|
||||||
@ -64,7 +65,7 @@ async def health():
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@app.post("/webhook/{namespace}")
|
@app.post("/webhook/{namespace}")
|
||||||
async def webhook(namespace: str, request: Request, background_tasks: BackgroundTasks):
|
async def webhook(namespace: str, payload: IncomingPayload = Body(...), background_tasks: BackgroundTasks = BackgroundTasks()):
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
endpoint = db.query(WebhookEndpoint).filter(WebhookEndpoint.namespace == namespace).first()
|
endpoint = db.query(WebhookEndpoint).filter(WebhookEndpoint.namespace == namespace).first()
|
||||||
|
|
||||||
@ -82,16 +83,17 @@ async def webhook(namespace: str, request: Request, background_tasks: Background
|
|||||||
endpoint_id = endpoint.id
|
endpoint_id = endpoint.id
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
# payload is validated by Pydantic; convert to plain dict for engine
|
||||||
try:
|
try:
|
||||||
body = await request.json()
|
body_dict = payload.model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
return JSONResponse({"error": "Invalid payload"}, status_code=400)
|
||||||
|
|
||||||
# Use new engine
|
# Use new engine
|
||||||
routed, notified = await engine.process(endpoint_id, body)
|
routed, notified = await engine.process(endpoint_id, body_dict)
|
||||||
|
|
||||||
# Async save logs
|
# Async save logs
|
||||||
background_tasks.add_task(save_logs, namespace, body, routed, notified)
|
background_tasks.add_task(save_logs, namespace, body_dict, routed, notified)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"namespace": namespace,
|
"namespace": namespace,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import asyncio
|
|||||||
import re
|
import re
|
||||||
from app.db import SessionLocal, ProcessingRule, RuleAction, Target, NotificationChannel, MessageTemplate
|
from app.db import SessionLocal, ProcessingRule, RuleAction, Target, NotificationChannel, MessageTemplate
|
||||||
from app.logging import get_logger
|
from app.logging import get_logger
|
||||||
|
from app.templates import safe_render
|
||||||
|
|
||||||
logger = get_logger("engine")
|
logger = get_logger("engine")
|
||||||
|
|
||||||
@ -100,7 +101,8 @@ class RuleEngine:
|
|||||||
render_context = self._flatten_payload(payload)
|
render_context = self._flatten_payload(payload)
|
||||||
render_context.update(current_context["vars"])
|
render_context.update(current_context["vars"])
|
||||||
|
|
||||||
msg = template_content.format(**render_context)
|
# Use safe Jinja2 rendering (supports legacy {var} by conversion)
|
||||||
|
msg = safe_render(template_content, render_context)
|
||||||
|
|
||||||
c_dict = {"channel": action.channel.channel_type, "url": action.channel.webhook_url}
|
c_dict = {"channel": action.channel.channel_type, "url": action.channel.webhook_url}
|
||||||
tasks.append(self._exec_notify(c_dict, msg))
|
tasks.append(self._exec_notify(c_dict, msg))
|
||||||
|
|||||||
23
tests/test_preview_endpoint.py
Normal file
23
tests/test_preview_endpoint.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_preview_endpoint_success():
|
||||||
|
client = TestClient(app)
|
||||||
|
payload = {
|
||||||
|
"template_content": "ok {trans_amt}",
|
||||||
|
"sample_payload": {"trans_amt": 123}
|
||||||
|
}
|
||||||
|
resp = client.post("/admin/templates/preview", json=payload)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json().get("rendered") == "ok 123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_preview_endpoint_strict_failure():
|
||||||
|
client = TestClient(app)
|
||||||
|
# Template refers to undefined variable -> StrictUndefined should cause error -> 400
|
||||||
|
payload = {"template_content": "{{ undefined_var }}", "sample_payload": {}}
|
||||||
|
resp = client.post("/admin/templates/preview", json=payload)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user