mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-10 11:12:13 +02:00
* fix: fix org scoped access for resources * Fix auth and config validation regressions * fix: track org config validation timestamp * fix: backfill org model configuration v2 from legacy user rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: align config tests with org-level v2 resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: helm example values tweaks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
326 lines
12 KiB
Python
326 lines
12 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from api.routes.public_agent import router
|
|
from api.services.call_concurrency import CallConcurrencyLimitError
|
|
|
|
|
|
def _make_test_app() -> FastAPI:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
return app
|
|
|
|
|
|
def _active_workflow(*, trigger_path: str | None = None):
|
|
nodes = []
|
|
if trigger_path is not None:
|
|
nodes.append(
|
|
{
|
|
"type": "trigger",
|
|
"data": {"trigger_path": trigger_path},
|
|
}
|
|
)
|
|
|
|
return SimpleNamespace(
|
|
id=33,
|
|
user_id=99,
|
|
organization_id=11,
|
|
status="active",
|
|
workflow_uuid="workflow-uuid-123",
|
|
released_definition=SimpleNamespace(
|
|
workflow_json={"nodes": nodes, "edges": []}
|
|
),
|
|
)
|
|
|
|
|
|
def _provider():
|
|
return SimpleNamespace(
|
|
PROVIDER_NAME="twilio",
|
|
WEBHOOK_ENDPOINT="outbound",
|
|
validate_config=Mock(return_value=True),
|
|
initiate_call=AsyncMock(),
|
|
)
|
|
|
|
|
|
def test_trigger_route_executes_as_workflow_owner():
|
|
app = _make_test_app()
|
|
client = TestClient(app)
|
|
|
|
workflow = _active_workflow(trigger_path="trigger-uuid-123")
|
|
provider = _provider()
|
|
quota_mock = AsyncMock(
|
|
return_value=SimpleNamespace(has_quota=True, error_message="")
|
|
)
|
|
|
|
with (
|
|
patch("api.routes.public_agent.db_client") as mock_db,
|
|
patch("api.routes.public_agent.call_concurrency") as mock_concurrency,
|
|
patch(
|
|
"api.routes.public_agent.authorize_workflow_run_start",
|
|
new=quota_mock,
|
|
),
|
|
patch(
|
|
"api.routes.public_agent.get_default_telephony_provider",
|
|
new=AsyncMock(return_value=provider),
|
|
),
|
|
patch(
|
|
"api.routes.public_agent.get_backend_endpoints",
|
|
new=AsyncMock(return_value=("https://api.example.com", "wss://ignored")),
|
|
),
|
|
):
|
|
slot = object()
|
|
mock_concurrency.acquire_org_slot = AsyncMock(return_value=slot)
|
|
mock_concurrency.bind_workflow_run = AsyncMock()
|
|
mock_concurrency.release_workflow_run_slot = AsyncMock()
|
|
mock_concurrency.release_slot = AsyncMock()
|
|
|
|
mock_db.validate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(id=7, organization_id=11, created_by=22)
|
|
)
|
|
mock_db.get_agent_trigger_by_path = AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
workflow_id=workflow.id,
|
|
organization_id=11,
|
|
state="active",
|
|
)
|
|
)
|
|
mock_db.get_workflow = AsyncMock(return_value=workflow)
|
|
mock_db.get_default_telephony_configuration = AsyncMock(
|
|
return_value=SimpleNamespace(id=55)
|
|
)
|
|
mock_db.create_workflow_run = AsyncMock(return_value=SimpleNamespace(id=501))
|
|
|
|
response = client.post(
|
|
"/public/agent/trigger-uuid-123",
|
|
headers={"X-API-Key": "test-api-key"},
|
|
json={"phone_number": "+15551234567"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
quota_mock.assert_awaited_once_with(
|
|
workflow_id=workflow.id,
|
|
organization_id=workflow.organization_id,
|
|
workflow_run_id=501,
|
|
)
|
|
mock_concurrency.acquire_org_slot.assert_awaited_once_with(
|
|
workflow.organization_id,
|
|
source="public_agent",
|
|
timeout=0,
|
|
)
|
|
mock_concurrency.bind_workflow_run.assert_awaited_once_with(slot, 501)
|
|
mock_db.get_workflow.assert_awaited_once_with(workflow.id, organization_id=11)
|
|
|
|
create_kwargs = mock_db.create_workflow_run.await_args.kwargs
|
|
assert create_kwargs["workflow_id"] == workflow.id
|
|
assert create_kwargs["user_id"] == workflow.user_id
|
|
assert create_kwargs["organization_id"] == workflow.organization_id
|
|
assert create_kwargs["initial_context"]["agent_uuid"] == "trigger-uuid-123"
|
|
assert create_kwargs["initial_context"]["agent_identifier"] == "trigger-uuid-123"
|
|
assert create_kwargs["initial_context"]["agent_identifier_type"] == "trigger_path"
|
|
assert create_kwargs["initial_context"]["workflow_uuid"] == workflow.workflow_uuid
|
|
assert create_kwargs["initial_context"]["api_key_id"] == 7
|
|
assert create_kwargs["initial_context"]["api_key_created_by"] == 22
|
|
|
|
initiate_kwargs = provider.initiate_call.await_args.kwargs
|
|
assert initiate_kwargs["workflow_id"] == workflow.id
|
|
assert initiate_kwargs["user_id"] == workflow.user_id
|
|
|
|
|
|
def test_workflow_uuid_route_uses_scoped_lookup_and_shared_execution():
|
|
app = _make_test_app()
|
|
client = TestClient(app)
|
|
|
|
workflow = _active_workflow()
|
|
provider = _provider()
|
|
quota_mock = AsyncMock(
|
|
return_value=SimpleNamespace(has_quota=True, error_message="")
|
|
)
|
|
|
|
with (
|
|
patch("api.routes.public_agent.db_client") as mock_db,
|
|
patch("api.routes.public_agent.call_concurrency") as mock_concurrency,
|
|
patch(
|
|
"api.routes.public_agent.authorize_workflow_run_start",
|
|
new=quota_mock,
|
|
),
|
|
patch(
|
|
"api.routes.public_agent.get_default_telephony_provider",
|
|
new=AsyncMock(return_value=provider),
|
|
),
|
|
patch(
|
|
"api.routes.public_agent.get_backend_endpoints",
|
|
new=AsyncMock(return_value=("https://api.example.com", "wss://ignored")),
|
|
),
|
|
):
|
|
slot = object()
|
|
mock_concurrency.acquire_org_slot = AsyncMock(return_value=slot)
|
|
mock_concurrency.bind_workflow_run = AsyncMock()
|
|
mock_concurrency.release_workflow_run_slot = AsyncMock()
|
|
mock_concurrency.release_slot = AsyncMock()
|
|
|
|
mock_db.validate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(id=8, organization_id=11, created_by=22)
|
|
)
|
|
mock_db.get_workflow_by_uuid = AsyncMock(return_value=workflow)
|
|
mock_db.get_default_telephony_configuration = AsyncMock(
|
|
return_value=SimpleNamespace(id=55)
|
|
)
|
|
mock_db.create_workflow_run = AsyncMock(return_value=SimpleNamespace(id=601))
|
|
|
|
response = client.post(
|
|
f"/public/agent/workflow/{workflow.workflow_uuid}",
|
|
headers={"X-API-Key": "test-api-key"},
|
|
json={"phone_number": "+15551234567"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
mock_db.get_workflow_by_uuid.assert_awaited_once_with(
|
|
workflow.workflow_uuid,
|
|
11,
|
|
)
|
|
assert not mock_db.get_agent_trigger_by_path.called
|
|
mock_concurrency.acquire_org_slot.assert_awaited_once_with(
|
|
workflow.organization_id,
|
|
source="public_agent",
|
|
timeout=0,
|
|
)
|
|
mock_concurrency.bind_workflow_run.assert_awaited_once_with(slot, 601)
|
|
|
|
create_kwargs = mock_db.create_workflow_run.await_args.kwargs
|
|
assert create_kwargs["user_id"] == workflow.user_id
|
|
assert (
|
|
create_kwargs["initial_context"]["agent_identifier"] == workflow.workflow_uuid
|
|
)
|
|
assert create_kwargs["initial_context"]["agent_identifier_type"] == "workflow_uuid"
|
|
assert "agent_uuid" not in create_kwargs["initial_context"]
|
|
|
|
|
|
def test_trigger_route_rejects_when_concurrency_limit_reached():
|
|
app = _make_test_app()
|
|
client = TestClient(app)
|
|
|
|
workflow = _active_workflow(trigger_path="trigger-uuid-123")
|
|
provider = _provider()
|
|
|
|
with (
|
|
patch("api.routes.public_agent.db_client") as mock_db,
|
|
patch("api.routes.public_agent.call_concurrency") as mock_concurrency,
|
|
patch(
|
|
"api.routes.public_agent.get_default_telephony_provider",
|
|
new=AsyncMock(return_value=provider),
|
|
),
|
|
):
|
|
mock_concurrency.acquire_org_slot = AsyncMock(
|
|
side_effect=CallConcurrencyLimitError(
|
|
organization_id=11,
|
|
source="public_agent",
|
|
wait_time=0,
|
|
max_concurrent=2,
|
|
)
|
|
)
|
|
mock_db.validate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(id=7, organization_id=11, created_by=22)
|
|
)
|
|
mock_db.get_agent_trigger_by_path = AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
workflow_id=workflow.id,
|
|
organization_id=11,
|
|
state="active",
|
|
)
|
|
)
|
|
mock_db.get_workflow = AsyncMock(return_value=workflow)
|
|
mock_db.get_default_telephony_configuration = AsyncMock(
|
|
return_value=SimpleNamespace(id=55)
|
|
)
|
|
mock_db.create_workflow_run = AsyncMock()
|
|
|
|
response = client.post(
|
|
"/public/agent/trigger-uuid-123",
|
|
headers={"X-API-Key": "test-api-key"},
|
|
json={"phone_number": "+15551234567"},
|
|
)
|
|
|
|
assert response.status_code == 429
|
|
assert response.json()["detail"] == "Concurrent call limit reached"
|
|
mock_db.create_workflow_run.assert_not_called()
|
|
|
|
|
|
def test_trigger_route_releases_concurrency_slot_when_quota_fails():
|
|
app = _make_test_app()
|
|
client = TestClient(app)
|
|
|
|
workflow = _active_workflow(trigger_path="trigger-uuid-123")
|
|
provider = _provider()
|
|
quota_mock = AsyncMock(
|
|
return_value=SimpleNamespace(has_quota=False, error_message="Quota exceeded")
|
|
)
|
|
|
|
with (
|
|
patch("api.routes.public_agent.db_client") as mock_db,
|
|
patch("api.routes.public_agent.call_concurrency") as mock_concurrency,
|
|
patch(
|
|
"api.routes.public_agent.authorize_workflow_run_start",
|
|
new=quota_mock,
|
|
),
|
|
patch(
|
|
"api.routes.public_agent.get_default_telephony_provider",
|
|
new=AsyncMock(return_value=provider),
|
|
),
|
|
):
|
|
mock_concurrency.acquire_org_slot = AsyncMock(return_value=object())
|
|
mock_concurrency.bind_workflow_run = AsyncMock()
|
|
mock_concurrency.release_workflow_run_slot = AsyncMock()
|
|
mock_concurrency.release_slot = AsyncMock()
|
|
|
|
mock_db.validate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(id=7, organization_id=11, created_by=22)
|
|
)
|
|
mock_db.get_agent_trigger_by_path = AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
workflow_id=workflow.id,
|
|
organization_id=11,
|
|
state="active",
|
|
)
|
|
)
|
|
mock_db.get_workflow = AsyncMock(return_value=workflow)
|
|
mock_db.get_default_telephony_configuration = AsyncMock(
|
|
return_value=SimpleNamespace(id=55)
|
|
)
|
|
mock_db.create_workflow_run = AsyncMock(return_value=SimpleNamespace(id=501))
|
|
|
|
response = client.post(
|
|
"/public/agent/trigger-uuid-123",
|
|
headers={"X-API-Key": "test-api-key"},
|
|
json={"phone_number": "+15551234567"},
|
|
)
|
|
|
|
assert response.status_code == 402
|
|
mock_concurrency.release_workflow_run_slot.assert_awaited_once_with(501)
|
|
provider.initiate_call.assert_not_awaited()
|
|
|
|
|
|
def test_workflow_uuid_route_rejects_archived_workflows():
|
|
app = _make_test_app()
|
|
client = TestClient(app)
|
|
|
|
workflow = _active_workflow()
|
|
workflow.status = "archived"
|
|
|
|
with patch("api.routes.public_agent.db_client") as mock_db:
|
|
mock_db.validate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(id=9, organization_id=11, created_by=22)
|
|
)
|
|
mock_db.get_workflow_by_uuid = AsyncMock(return_value=workflow)
|
|
|
|
response = client.post(
|
|
f"/public/agent/workflow/{workflow.workflow_uuid}",
|
|
headers={"X-API-Key": "test-api-key"},
|
|
json={"phone_number": "+15551234567"},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
assert response.json()["detail"] == "Workflow is not active"
|
|
assert not mock_db.create_workflow_run.called
|