diff --git a/app/main.py b/app/main.py index 2bca5dc..f94f105 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,11 @@ import asyncio -from fastapi import FastAPI, Request, BackgroundTasks +from fastapi import FastAPI, Request, BackgroundTasks, Body from fastapi.responses import JSONResponse from app.logging import get_logger from app.admin import router as admin_router from app.db import SessionLocal, WebhookEndpoint, RequestLog, DeliveryLog, init_db from app.services.engine import engine +from app.models import IncomingPayload from contextlib import asynccontextmanager logger = get_logger("app") @@ -64,7 +65,7 @@ async def health(): return {"status": "ok"} @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() 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 db.close() + # payload is validated by Pydantic; convert to plain dict for engine try: - body = await request.json() + body_dict = payload.model_dump() except Exception: - return JSONResponse({"error": "Invalid JSON"}, status_code=400) + return JSONResponse({"error": "Invalid payload"}, status_code=400) # Use new engine - routed, notified = await engine.process(endpoint_id, body) + routed, notified = await engine.process(endpoint_id, body_dict) # 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 = { "namespace": namespace, diff --git a/app/services/engine.py b/app/services/engine.py index 32d7669..41770b3 100644 --- a/app/services/engine.py +++ b/app/services/engine.py @@ -3,6 +3,7 @@ import asyncio import re from app.db import SessionLocal, ProcessingRule, RuleAction, Target, NotificationChannel, MessageTemplate from app.logging import get_logger +from app.templates import safe_render logger = get_logger("engine") @@ -100,7 +101,8 @@ class RuleEngine: render_context = self._flatten_payload(payload) 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} tasks.append(self._exec_notify(c_dict, msg)) diff --git a/tests/test_preview_endpoint.py b/tests/test_preview_endpoint.py new file mode 100644 index 0000000..c9501d5 --- /dev/null +++ b/tests/test_preview_endpoint.py @@ -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 +