mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
* 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
330 lines
12 KiB
Python
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
|