226 lines
7.0 KiB
Python
226 lines
7.0 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from sqlalchemy import create_engine, select
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_workflow.backend.db.models import (
|
|
WorkflowDefinition,
|
|
WorkflowDefinitionRevision,
|
|
)
|
|
from govoplan_workflow.backend.schemas import (
|
|
WorkflowDefinitionCreateRequest,
|
|
WorkflowDefinitionUpdateRequest,
|
|
WorkflowEdge,
|
|
WorkflowGraph,
|
|
WorkflowNode,
|
|
WorkflowPosition,
|
|
)
|
|
from govoplan_workflow.backend.service import (
|
|
WorkflowConflictError,
|
|
WorkflowNotFoundError,
|
|
activate_definition,
|
|
create_definition,
|
|
delete_definition,
|
|
get_definition,
|
|
list_definition_revisions,
|
|
list_definitions,
|
|
update_definition,
|
|
)
|
|
|
|
|
|
def sample_graph(*, title: str = "Review request") -> WorkflowGraph:
|
|
return WorkflowGraph(
|
|
nodes=[
|
|
WorkflowNode(
|
|
id="start",
|
|
type="workflow.start.manual",
|
|
label="Start",
|
|
position=WorkflowPosition(x=40, y=100),
|
|
config={"input_schema_ref": ""},
|
|
),
|
|
WorkflowNode(
|
|
id="activity",
|
|
type="workflow.activity",
|
|
label="Review",
|
|
position=WorkflowPosition(x=280, y=100),
|
|
config={
|
|
"title": title,
|
|
"instructions": "",
|
|
"assignee": "",
|
|
"due_after": "",
|
|
},
|
|
),
|
|
WorkflowNode(
|
|
id="complete",
|
|
type="workflow.end.completed",
|
|
label="Completed",
|
|
position=WorkflowPosition(x=520, y=100),
|
|
config={"output_mapping": {}},
|
|
),
|
|
],
|
|
edges=[
|
|
WorkflowEdge(id="start-activity", source="start", target="activity"),
|
|
WorkflowEdge(id="activity-complete", source="activity", target="complete"),
|
|
],
|
|
)
|
|
|
|
|
|
class WorkflowServiceTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(
|
|
self.engine,
|
|
tables=[
|
|
WorkflowDefinition.__table__,
|
|
WorkflowDefinitionRevision.__table__,
|
|
],
|
|
)
|
|
self.Session = sessionmaker(bind=self.engine)
|
|
self.session: Session = self.Session()
|
|
|
|
def tearDown(self) -> None:
|
|
self.session.close()
|
|
Base.metadata.drop_all(
|
|
self.engine,
|
|
tables=[
|
|
WorkflowDefinitionRevision.__table__,
|
|
WorkflowDefinition.__table__,
|
|
],
|
|
)
|
|
self.engine.dispose()
|
|
|
|
def _create(self, *, tenant_id: str = "tenant-1") -> WorkflowDefinition:
|
|
definition = create_definition(
|
|
self.session,
|
|
tenant_id=tenant_id,
|
|
actor_id="user-1",
|
|
payload=WorkflowDefinitionCreateRequest(
|
|
name="Monthly case handling",
|
|
graph=sample_graph(),
|
|
),
|
|
)
|
|
self.session.commit()
|
|
return definition
|
|
|
|
def test_create_update_and_activate_pin_immutable_revisions(self) -> None:
|
|
definition = self._create()
|
|
updated = update_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=definition.id,
|
|
actor_id="user-2",
|
|
payload=WorkflowDefinitionUpdateRequest(
|
|
name="Monthly case handling",
|
|
graph=sample_graph(title="Review corrected request"),
|
|
expected_revision=1,
|
|
),
|
|
)
|
|
activate_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=definition.id,
|
|
actor_id="user-2",
|
|
revision=1,
|
|
)
|
|
self.session.commit()
|
|
|
|
revisions = list_definition_revisions(
|
|
self.session,
|
|
definition=updated,
|
|
)
|
|
self.assertEqual(2, updated.current_revision)
|
|
self.assertEqual(1, updated.active_revision)
|
|
self.assertEqual("active", updated.status)
|
|
self.assertEqual([2, 1], [item.revision for item in revisions])
|
|
self.assertNotEqual(revisions[0].content_hash, revisions[1].content_hash)
|
|
historical = next(item for item in revisions if item.revision == 1)
|
|
self.assertEqual(
|
|
"Review request",
|
|
historical.graph["nodes"][1]["config"]["title"],
|
|
)
|
|
|
|
def test_metadata_update_does_not_create_graph_revision(self) -> None:
|
|
definition = self._create()
|
|
updated = update_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=definition.id,
|
|
actor_id="user-2",
|
|
payload=WorkflowDefinitionUpdateRequest(
|
|
name="Renamed workflow",
|
|
description="Updated metadata only",
|
|
graph=sample_graph(),
|
|
metadata={"owner": "finance"},
|
|
expected_revision=1,
|
|
),
|
|
)
|
|
self.session.commit()
|
|
|
|
self.assertEqual(1, updated.current_revision)
|
|
self.assertEqual(
|
|
1,
|
|
len(
|
|
list(
|
|
self.session.scalars(
|
|
select(WorkflowDefinitionRevision).where(
|
|
WorkflowDefinitionRevision.definition_id
|
|
== definition.id
|
|
)
|
|
)
|
|
)
|
|
),
|
|
)
|
|
|
|
def test_stale_update_and_cross_tenant_access_are_rejected(self) -> None:
|
|
definition = self._create()
|
|
with self.assertRaises(WorkflowConflictError):
|
|
update_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=definition.id,
|
|
actor_id="user-2",
|
|
payload=WorkflowDefinitionUpdateRequest(
|
|
name="Stale",
|
|
graph=sample_graph(),
|
|
expected_revision=2,
|
|
),
|
|
)
|
|
with self.assertRaises(WorkflowNotFoundError):
|
|
get_definition(
|
|
self.session,
|
|
tenant_id="tenant-2",
|
|
definition_id=definition.id,
|
|
)
|
|
|
|
def test_soft_delete_preserves_revisions_and_hides_definition(self) -> None:
|
|
definition = self._create()
|
|
delete_definition(
|
|
self.session,
|
|
tenant_id="tenant-1",
|
|
definition_id=definition.id,
|
|
actor_id="user-2",
|
|
)
|
|
self.session.commit()
|
|
|
|
self.assertEqual([], list_definitions(self.session, tenant_id="tenant-1"))
|
|
self.assertEqual(
|
|
1,
|
|
len(
|
|
list(
|
|
self.session.scalars(
|
|
select(WorkflowDefinitionRevision).where(
|
|
WorkflowDefinitionRevision.definition_id
|
|
== definition.id
|
|
)
|
|
)
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|