dograh/api/tests/test_run_integrations_template.py
Abhishek 55b727a872
Feat/Add API Trigger and Webhooks in Agent Builder (#83)
* feat: add api trigger node for agent runs

* feat: add webhook node

* Execute webhook nodes post workflow run

* Add hint to go to API keys
2025-12-22 14:08:30 +05:30

330 lines
12 KiB
Python

"""Tests for webhook execution in run_integrations.py."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from api.tasks.run_integrations import (
_build_auth_header,
_build_render_context,
_execute_webhook_node,
)
@pytest.fixture(autouse=True)
def mock_logger():
"""Mock the logger for all tests."""
with patch("api.tasks.run_integrations.logger") as mock_log:
mock_log.bind.return_value = mock_log
yield mock_log
class TestBuildAuthHeader:
"""Tests for _build_auth_header function."""
def test_bearer_token(self):
"""Test bearer token auth header."""
credential = MagicMock()
credential.credential_type = "bearer_token"
credential.credential_data = {"token": "my-secret-token"}
result = _build_auth_header(credential)
assert result == {"Authorization": "Bearer my-secret-token"}
def test_api_key(self):
"""Test API key auth header."""
credential = MagicMock()
credential.credential_type = "api_key"
credential.credential_data = {"header_name": "X-API-Key", "api_key": "key123"}
result = _build_auth_header(credential)
assert result == {"X-API-Key": "key123"}
def test_api_key_default_header(self):
"""Test API key with default header name."""
credential = MagicMock()
credential.credential_type = "api_key"
credential.credential_data = {"api_key": "key123"}
result = _build_auth_header(credential)
assert result == {"X-API-Key": "key123"}
def test_basic_auth(self):
"""Test basic auth header."""
credential = MagicMock()
credential.credential_type = "basic_auth"
credential.credential_data = {"username": "user", "password": "pass"}
result = _build_auth_header(credential)
# base64 of "user:pass" is "dXNlcjpwYXNz"
assert result == {"Authorization": "Basic dXNlcjpwYXNz"}
def test_custom_header(self):
"""Test custom header auth."""
credential = MagicMock()
credential.credential_type = "custom_header"
credential.credential_data = {
"header_name": "X-Custom-Auth",
"header_value": "custom-value",
}
result = _build_auth_header(credential)
assert result == {"X-Custom-Auth": "custom-value"}
def test_unknown_type(self):
"""Test unknown credential type returns empty dict."""
credential = MagicMock()
credential.credential_type = "unknown"
credential.credential_data = {}
result = _build_auth_header(credential)
assert result == {}
class TestBuildRenderContext:
"""Tests for _build_render_context function."""
def test_basic_context(self):
"""Test building render context from workflow run."""
workflow_run = MagicMock()
workflow_run.id = 123
workflow_run.name = "WR-TEST-001"
workflow_run.workflow_id = 456
workflow_run.workflow.name = "Test Workflow"
workflow_run.initial_context = {"phone_number": "+1234567890"}
workflow_run.gathered_context = {
"customer_name": "John",
"mapped_call_disposition": "QUALIFIED",
}
workflow_run.usage_info = {"call_duration_seconds": 120}
workflow_run.completed_at = None
result = _build_render_context(workflow_run)
assert result["workflow_run_id"] == 123
assert result["workflow_run_name"] == "WR-TEST-001"
assert result["workflow_id"] == 456
assert result["workflow_name"] == "Test Workflow"
assert result["initial_context"]["phone_number"] == "+1234567890"
assert result["gathered_context"]["customer_name"] == "John"
assert result["cost_info"]["call_duration_seconds"] == 120
assert result["disposition_code"] == "QUALIFIED"
def test_empty_contexts(self):
"""Test with empty/None contexts."""
workflow_run = MagicMock()
workflow_run.id = 1
workflow_run.name = "Test"
workflow_run.workflow_id = 1
workflow_run.workflow.name = "Workflow"
workflow_run.initial_context = None
workflow_run.gathered_context = None
workflow_run.usage_info = None
workflow_run.completed_at = None
result = _build_render_context(workflow_run)
assert result["initial_context"] == {}
assert result["gathered_context"] == {}
assert result["cost_info"] == {}
assert result["disposition_code"] is None
class TestExecuteWebhookNode:
"""Tests for _execute_webhook_node function."""
@pytest.mark.asyncio
async def test_disabled_webhook_skipped(self):
"""Test that disabled webhooks are skipped."""
webhook_data = {"name": "Test Webhook", "enabled": False}
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context={},
organization_id=1,
)
assert result is True # Returns True for skipped webhooks
@pytest.mark.asyncio
async def test_missing_url_returns_false(self):
"""Test that missing endpoint URL returns False."""
webhook_data = {"name": "Test Webhook", "enabled": True, "endpoint_url": None}
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context={},
organization_id=1,
)
assert result is False
@pytest.mark.asyncio
async def test_successful_post_request(self):
"""Test successful POST webhook execution."""
webhook_data = {
"name": "CRM Sync",
"enabled": True,
"http_method": "POST",
"endpoint_url": "https://api.example.com/webhook",
"payload_template": {
"call_id": "{{workflow_run_id}}",
"phone": "{{initial_context.phone_number}}",
},
}
render_context = {
"workflow_run_id": 123,
"initial_context": {"phone_number": "+1234567890"},
}
with patch("api.tasks.run_integrations.db_client") as mock_db:
mock_db.get_credential_by_uuid = AsyncMock(return_value=None)
with patch("api.tasks.run_integrations.httpx.AsyncClient") as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_client_instance = AsyncMock()
mock_client_instance.request = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_client_instance
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context=render_context,
organization_id=1,
)
assert result is True
# Verify the request was made correctly
mock_client_instance.request.assert_called_once()
call_kwargs = mock_client_instance.request.call_args[1]
assert call_kwargs["method"] == "POST"
assert call_kwargs["url"] == "https://api.example.com/webhook"
assert call_kwargs["json"] == {
"call_id": "123",
"phone": "+1234567890",
}
@pytest.mark.asyncio
async def test_webhook_with_credential(self):
"""Test webhook execution with credential auth."""
webhook_data = {
"name": "Authenticated Webhook",
"enabled": True,
"http_method": "POST",
"endpoint_url": "https://api.example.com/webhook",
"credential_uuid": "cred-123",
"payload_template": {},
}
mock_credential = MagicMock()
mock_credential.name = "API Key"
mock_credential.credential_type = "bearer_token"
mock_credential.credential_data = {"token": "secret-token"}
with patch("api.tasks.run_integrations.db_client") as mock_db:
mock_db.get_credential_by_uuid = AsyncMock(return_value=mock_credential)
with patch("api.tasks.run_integrations.httpx.AsyncClient") as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_client_instance = AsyncMock()
mock_client_instance.request = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_client_instance
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context={},
organization_id=1,
)
assert result is True
# Verify auth header was included
call_kwargs = mock_client_instance.request.call_args[1]
assert call_kwargs["headers"]["Authorization"] == "Bearer secret-token"
@pytest.mark.asyncio
async def test_webhook_with_custom_headers(self):
"""Test webhook execution with custom headers."""
webhook_data = {
"name": "Custom Headers Webhook",
"enabled": True,
"http_method": "POST",
"endpoint_url": "https://api.example.com/webhook",
"custom_headers": [
{"key": "X-Source", "value": "dograh"},
{"key": "X-Workflow", "value": "test"},
],
"payload_template": {},
}
with patch("api.tasks.run_integrations.db_client") as mock_db:
mock_db.get_credential_by_uuid = AsyncMock(return_value=None)
with patch("api.tasks.run_integrations.httpx.AsyncClient") as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_client_instance = AsyncMock()
mock_client_instance.request = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_client_instance
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context={},
organization_id=1,
)
assert result is True
# Verify custom headers were included
call_kwargs = mock_client_instance.request.call_args[1]
assert call_kwargs["headers"]["X-Source"] == "dograh"
assert call_kwargs["headers"]["X-Workflow"] == "test"
@pytest.mark.asyncio
async def test_webhook_http_error(self):
"""Test webhook execution with HTTP error."""
import httpx
webhook_data = {
"name": "Failing Webhook",
"enabled": True,
"http_method": "POST",
"endpoint_url": "https://api.example.com/webhook",
"payload_template": {},
}
with patch("api.tasks.run_integrations.db_client") as mock_db:
mock_db.get_credential_by_uuid = AsyncMock(return_value=None)
with patch("api.tasks.run_integrations.httpx.AsyncClient") as mock_client:
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.text = "Internal Server Error"
mock_response.raise_for_status = MagicMock(
side_effect=httpx.HTTPStatusError(
"Server Error",
request=MagicMock(),
response=mock_response,
)
)
mock_client_instance = AsyncMock()
mock_client_instance.request = AsyncMock(return_value=mock_response)
mock_client.return_value.__aenter__.return_value = mock_client_instance
result = await _execute_webhook_node(
webhook_data=webhook_data,
render_context={},
organization_id=1,
)
assert result is False