Files
replypier/backend/tests/services/test_workflow_service.py
T
WangShen007 54b949b392 chore(brand): rename the product to ReplyPier
The upstream project name is gone from code, assets, docs and build
output: brand strings, domains (replypier.site and its app/api/docs/help
subdomains), env prefixes, image and network names, the widget's public
JS API (window.ReplyPier, replypierId/BaseUrl/Config/Token), CSS tokens
(--rp-*), file and directory names, and the WordPress plugin package.

- widget loader is now webclient/replypier.min.js; both bundles rebuilt
  from source (backend/assets/widget.{js,css}, frontend/public/webclient)
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and config.dev.js
  rewritten rather than string-swapped; the demo config no longer ships
  the previous vendor's Firebase/Razorpay/Google keys
- new migration replypier_ai_model_001 adds REPLYPIER to the managed
  AIModelType and rewrites pre-rename rows
- local compose project, database, role and volume renamed; LOCAL_DEPLOY
  documents carrying the old local data over
2026-10-26 00:51:03 +08:00

559 lines
23 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pytest
import uuid
from unittest.mock import MagicMock, patch
from app.services.workflow import WorkflowService
from app.models.workflow import Workflow, WorkflowStatus
from app.models.schemas.workflow import WorkflowCreate
class TestWorkflowService:
@pytest.fixture
def mock_db(self):
"""Create a mock database session"""
db = MagicMock()
db.add = MagicMock()
db.commit = MagicMock()
db.rollback = MagicMock()
return db
@pytest.fixture
def mock_workflow_repo(self):
"""Create a mock workflow repository"""
repo = MagicMock()
return repo
@pytest.fixture
def mock_agent_repo(self):
"""Create a mock agent repository"""
repo = MagicMock()
return repo
@pytest.fixture
def workflow_service(self, mock_db, mock_workflow_repo, mock_agent_repo):
"""Create a workflow service with mocked dependencies"""
service = WorkflowService(mock_db)
service.workflow_repo = mock_workflow_repo
service.agent_repo = mock_agent_repo
return service
@pytest.fixture
def sample_agent(self):
"""Create a sample agent"""
agent_id = uuid.uuid4()
organization_id = uuid.uuid4()
return MagicMock(
id=agent_id,
organization_id=organization_id
)
@pytest.fixture
def sample_workflow(self, sample_agent):
"""Create a sample workflow"""
workflow_id = uuid.uuid4()
return MagicMock(
id=workflow_id,
name="Test Workflow",
description="Test workflow description",
status=WorkflowStatus.DRAFT,
version=1,
is_template=False,
default_language="en",
canvas_data={},
settings={},
organization_id=sample_agent.organization_id,
agent_id=sample_agent.id,
created_by=uuid.uuid4(),
created_at=None,
updated_at=None
)
@pytest.fixture
def workflow_create_data(self, sample_agent):
"""Create workflow creation data"""
return WorkflowCreate(
name="New Workflow",
description="New workflow description",
agent_id=sample_agent.id
)
def test_create_workflow(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_agent, workflow_create_data):
"""Test creating a new workflow"""
created_by = uuid.uuid4()
organization_id = sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.get_workflows_by_agent.return_value = []
mock_workflow_repo.get_by_name.return_value = None
# Create a mock workflow to return
mock_workflow = MagicMock(
id=uuid.uuid4(),
name=workflow_create_data.name,
description=workflow_create_data.description,
agent_id=sample_agent.id,
organization_id=organization_id,
created_by=created_by
)
mock_workflow_repo.create_workflow.return_value = mock_workflow
# Call the method
result = workflow_service.create_workflow(workflow_create_data, created_by, organization_id)
# Assertions
mock_agent_repo.get_by_id.assert_called_with(sample_agent.id)
mock_workflow_repo.get_workflows_by_agent.assert_called_with(sample_agent.id)
mock_workflow_repo.get_by_name.assert_called_with(workflow_create_data.name, organization_id)
# Check that workflow_repo.create_workflow was called with correct parameters
create_call_args = mock_workflow_repo.create_workflow.call_args[1]
assert create_call_args["name"] == workflow_create_data.name
assert create_call_args["description"] == workflow_create_data.description
assert create_call_args["agent_id"] == sample_agent.id
assert create_call_args["created_by"] == created_by
assert create_call_args["organization_id"] == organization_id
assert result == mock_workflow
def test_create_workflow_agent_not_found(self, workflow_service, mock_agent_repo, workflow_create_data):
"""Test creating a workflow when agent not found"""
created_by = uuid.uuid4()
organization_id = uuid.uuid4()
# Setup mock returns
mock_agent_repo.get_by_id.return_value = None
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent not found"):
workflow_service.create_workflow(workflow_create_data, created_by, organization_id)
def test_create_workflow_agent_wrong_organization(self, workflow_service, mock_agent_repo, sample_agent, workflow_create_data):
"""Test creating a workflow when agent belongs to different organization"""
created_by = uuid.uuid4()
wrong_organization_id = uuid.uuid4() # Different from sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent does not belong to your organization"):
workflow_service.create_workflow(workflow_create_data, created_by, wrong_organization_id)
def test_create_workflow_existing_workflow(self, workflow_service, mock_agent_repo, mock_workflow_repo, sample_agent, sample_workflow, workflow_create_data):
"""Test creating a workflow when agent already has a workflow"""
created_by = uuid.uuid4()
organization_id = sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.get_workflows_by_agent.return_value = [sample_workflow]
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent already has a workflow"):
workflow_service.create_workflow(workflow_create_data, created_by, organization_id)
def test_create_workflow_name_exists(self, workflow_service, mock_agent_repo, mock_workflow_repo, sample_agent, sample_workflow, workflow_create_data):
"""Test creating a workflow with a name that already exists"""
created_by = uuid.uuid4()
organization_id = sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.get_workflows_by_agent.return_value = []
mock_workflow_repo.get_by_name.return_value = sample_workflow
# Call the method and assert exception
with pytest.raises(ValueError, match=f"Workflow with name '{workflow_create_data.name}' already exists"):
workflow_service.create_workflow(workflow_create_data, created_by, organization_id)
def test_get_workflow_by_agent_id(self, workflow_service, mock_agent_repo, mock_workflow_repo, sample_agent, sample_workflow):
"""Test getting workflow by agent ID"""
organization_id = sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.get_workflows_by_agent.return_value = [sample_workflow]
# Call the method
result = workflow_service.get_workflow_by_agent_id(sample_agent.id, organization_id)
# Assertions
mock_agent_repo.get_by_id.assert_called_with(sample_agent.id)
mock_workflow_repo.get_workflows_by_agent.assert_called_with(sample_agent.id)
assert result == sample_workflow
def test_get_workflow_by_agent_id_agent_not_found(self, workflow_service, mock_agent_repo):
"""Test getting workflow by agent ID when agent not found"""
agent_id = uuid.uuid4()
organization_id = uuid.uuid4()
# Setup mock returns
mock_agent_repo.get_by_id.return_value = None
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent not found"):
workflow_service.get_workflow_by_agent_id(agent_id, organization_id)
def test_get_workflow_by_agent_id_agent_wrong_organization(self, workflow_service, mock_agent_repo, sample_agent):
"""Test getting workflow by agent ID when agent belongs to different organization"""
wrong_organization_id = uuid.uuid4() # Different from sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent does not belong to your organization"):
workflow_service.get_workflow_by_agent_id(sample_agent.id, wrong_organization_id)
def test_get_workflow_by_agent_id_no_workflow(self, workflow_service, mock_agent_repo, mock_workflow_repo, sample_agent):
"""Test getting workflow by agent ID when no workflow exists"""
organization_id = sample_agent.organization_id
# Setup mock returns
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.get_workflows_by_agent.return_value = []
# Call the method
result = workflow_service.get_workflow_by_agent_id(sample_agent.id, organization_id)
# Assertions
assert result is None
def test_update_workflow(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow):
"""Test updating a workflow"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Prepare update data
update_data = {
"name": "Updated Workflow",
"description": "Updated description"
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_workflow_repo.get_by_name.return_value = None
# Create a mock updated workflow to return
updated_workflow = MagicMock(
id=sample_workflow.id,
name=update_data["name"],
description=update_data["description"],
organization_id=sample_workflow.organization_id
)
mock_workflow_repo.update_workflow.return_value = updated_workflow
# Call the method
result = workflow_service.update_workflow(workflow_id, update_data, organization_id)
# Assertions
mock_workflow_repo.get_by_id.assert_called_with(workflow_id)
mock_workflow_repo.update_workflow.assert_called_with(workflow_id, **update_data)
assert result == updated_workflow
def test_update_workflow_not_found(self, workflow_service, mock_workflow_repo):
"""Test updating a workflow that doesn't exist"""
workflow_id = uuid.uuid4()
organization_id = uuid.uuid4()
update_data = {"name": "Updated Workflow"}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = None
# Call the method and assert exception
with pytest.raises(ValueError, match="Workflow not found"):
workflow_service.update_workflow(workflow_id, update_data, organization_id)
def test_update_workflow_wrong_organization(self, workflow_service, mock_workflow_repo, sample_workflow):
"""Test updating a workflow from a different organization"""
workflow_id = sample_workflow.id
wrong_organization_id = uuid.uuid4() # Different from sample_workflow.organization_id
update_data = {"name": "Updated Workflow"}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
# Call the method and assert exception
with pytest.raises(ValueError, match="Workflow does not belong to your organization"):
workflow_service.update_workflow(workflow_id, update_data, wrong_organization_id)
def test_update_workflow_name_exists(self, workflow_service, mock_workflow_repo, sample_workflow):
"""Test updating a workflow with a name that already exists"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
new_name = "Existing Workflow Name"
update_data = {"name": new_name}
# Create another workflow with the same name
existing_workflow = MagicMock(
id=uuid.uuid4(), # Different ID
name=new_name,
organization_id=organization_id
)
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_workflow_repo.get_by_name.return_value = existing_workflow
# Call the method and assert exception
with pytest.raises(ValueError, match=f"Workflow with name '{new_name}' already exists"):
workflow_service.update_workflow(workflow_id, update_data, organization_id)
def test_update_workflow_agent_id(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow, sample_agent):
"""Test updating a workflow's agent_id"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Create a new agent
new_agent = MagicMock(
id=uuid.uuid4(),
organization_id=organization_id
)
# Prepare update data
update_data = {
"agent_id": new_agent.id
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = new_agent
mock_workflow_repo.get_workflows_by_agent.return_value = []
# Create a mock updated workflow to return
updated_workflow = MagicMock(
id=sample_workflow.id,
agent_id=new_agent.id,
organization_id=organization_id
)
mock_workflow_repo.update_workflow.return_value = updated_workflow
# Call the method
result = workflow_service.update_workflow(workflow_id, update_data, organization_id)
# Assertions
mock_workflow_repo.get_by_id.assert_called_with(workflow_id)
mock_agent_repo.get_by_id.assert_called_with(new_agent.id)
mock_workflow_repo.get_workflows_by_agent.assert_called_with(new_agent.id)
mock_workflow_repo.update_workflow.assert_called_with(workflow_id, **update_data)
assert result == updated_workflow
def test_update_workflow_agent_id_agent_not_found(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow):
"""Test updating a workflow's agent_id when agent not found"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
new_agent_id = uuid.uuid4()
# Prepare update data
update_data = {
"agent_id": new_agent_id
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = None
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent not found"):
workflow_service.update_workflow(workflow_id, update_data, organization_id)
def test_update_workflow_agent_id_agent_wrong_organization(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow):
"""Test updating a workflow's agent_id when agent belongs to different organization"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Create a new agent with different organization
new_agent = MagicMock(
id=uuid.uuid4(),
organization_id=uuid.uuid4() # Different from workflow's organization_id
)
# Prepare update data
update_data = {
"agent_id": new_agent.id
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = new_agent
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent does not belong to your organization"):
workflow_service.update_workflow(workflow_id, update_data, organization_id)
def test_update_workflow_agent_id_agent_has_workflow(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow):
"""Test updating a workflow's agent_id when agent already has a workflow"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Create a new agent
new_agent = MagicMock(
id=uuid.uuid4(),
organization_id=organization_id
)
# Create another workflow for the new agent
existing_workflow = MagicMock(
id=uuid.uuid4(), # Different ID
agent_id=new_agent.id,
organization_id=organization_id
)
# Prepare update data
update_data = {
"agent_id": new_agent.id
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = new_agent
mock_workflow_repo.get_workflows_by_agent.return_value = [existing_workflow]
# Call the method and assert exception
with pytest.raises(ValueError, match="Agent already has a workflow"):
workflow_service.update_workflow(workflow_id, update_data, organization_id)
def test_update_workflow_status_to_published(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow, sample_agent):
"""Test updating a workflow's status to published"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Prepare update data
update_data = {
"status": "published"
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = sample_agent
# Create a mock updated workflow to return
updated_workflow = MagicMock(
id=sample_workflow.id,
status=WorkflowStatus.PUBLISHED,
agent_id=sample_agent.id,
organization_id=organization_id
)
mock_workflow_repo.update_workflow.return_value = updated_workflow
# Call the method
result = workflow_service.update_workflow(workflow_id, update_data, organization_id)
# Assertions
mock_workflow_repo.get_by_id.assert_called_with(workflow_id)
mock_agent_repo.get_by_id.assert_called_with(sample_agent.id)
mock_agent_repo.update_agent.assert_called_with(
sample_agent.id,
active_workflow_id=workflow_id,
use_workflow=True
)
mock_workflow_repo.update_workflow.assert_called_with(workflow_id, **update_data)
assert result == updated_workflow
def test_update_workflow_status_to_draft(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow, sample_agent):
"""Test updating a workflow's status to draft"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Set the agent to use this workflow
sample_agent.active_workflow_id = workflow_id
# Prepare update data
update_data = {
"status": "draft"
}
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = sample_agent
# Create a mock updated workflow to return
updated_workflow = MagicMock(
id=sample_workflow.id,
status=WorkflowStatus.DRAFT,
agent_id=sample_agent.id,
organization_id=organization_id
)
mock_workflow_repo.update_workflow.return_value = updated_workflow
# Call the method
result = workflow_service.update_workflow(workflow_id, update_data, organization_id)
# Assertions
mock_workflow_repo.get_by_id.assert_called_with(workflow_id)
mock_agent_repo.get_by_id.assert_called_with(sample_agent.id)
mock_agent_repo.update_agent.assert_called_with(
sample_agent.id,
active_workflow_id=None,
use_workflow=False
)
mock_workflow_repo.update_workflow.assert_called_with(workflow_id, **update_data)
assert result == updated_workflow
def test_delete_workflow(self, workflow_service, mock_workflow_repo, mock_agent_repo, sample_workflow, sample_agent):
"""Test deleting a workflow"""
workflow_id = sample_workflow.id
organization_id = sample_workflow.organization_id
# Set the agent to use this workflow
sample_agent.active_workflow_id = workflow_id
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
mock_agent_repo.get_by_id.return_value = sample_agent
mock_workflow_repo.delete_workflow.return_value = True
# Call the method
result = workflow_service.delete_workflow(workflow_id, organization_id)
# Assertions
mock_workflow_repo.get_by_id.assert_called_with(workflow_id)
mock_agent_repo.get_by_id.assert_called_with(sample_workflow.agent_id)
mock_agent_repo.update_agent.assert_called_with(
sample_agent.id,
active_workflow_id=None,
use_workflow=False
)
mock_workflow_repo.delete_workflow.assert_called_with(workflow_id)
assert result is True
def test_delete_workflow_not_found(self, workflow_service, mock_workflow_repo):
"""Test deleting a workflow that doesn't exist"""
workflow_id = uuid.uuid4()
organization_id = uuid.uuid4()
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = None
# Call the method and assert exception
with pytest.raises(ValueError, match="Workflow not found"):
workflow_service.delete_workflow(workflow_id, organization_id)
def test_delete_workflow_wrong_organization(self, workflow_service, mock_workflow_repo, sample_workflow):
"""Test deleting a workflow from a different organization"""
workflow_id = sample_workflow.id
wrong_organization_id = uuid.uuid4() # Different from sample_workflow.organization_id
# Setup mock returns
mock_workflow_repo.get_by_id.return_value = sample_workflow
# Call the method and assert exception
with pytest.raises(ValueError, match="Workflow does not belong to your organization"):
workflow_service.delete_workflow(workflow_id, wrong_organization_id)