import pytest from infrasynth.features.models import FeatureFlag from infrasynth.workflows.models import ( Transition, Workflow, WorkflowInstance, WorkflowNode, WorkflowObserver, ) pytestmark = pytest.mark.django_db DEFINITIONS_URL = "/api/workflows/definitions/" INSTANCES_URL = "/api/workflows/instances/" NODES_URL = "/api/workflows/nodes/" TRANSITIONS_URL = "/api/workflows/transitions/" ASSIGNMENTS_URL = "/api/workflows/assignments/" OBSERVERS_URL = "/api/workflows/observers/" @pytest.fixture def workflow(user): return Workflow.objects.create(slug="approval", name="Approval", created_by=user) @pytest.fixture def workflow_graph(workflow): start = WorkflowNode.objects.create(workflow=workflow, name="Start", node_type=WorkflowNode.NodeType.START) end = WorkflowNode.objects.create(workflow=workflow, name="End", node_type=WorkflowNode.NodeType.END) Transition.objects.create(from_node=start, to_node=end, condition_slug="approved") return {"start": start, "end": end} class TestWorkflowViewSet: def test_list_workflows(self, authenticated_client, workflow): response = authenticated_client.get(DEFINITIONS_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["slug"] == "approval" def test_create_workflow(self, authenticated_client): response = authenticated_client.post( DEFINITIONS_URL, {"slug": "leave-request", "name": "Leave Request"}, format="json", ) assert response.status_code == 201 assert Workflow.objects.filter(slug="leave-request").exists() def test_requires_auth(self, api_client, workflow): assert api_client.get(DEFINITIONS_URL).status_code == 401 def test_disabled_feature_returns_404(self, authenticated_client): FeatureFlag.objects.create(slug="workflows", name="Workflows", is_active=False) assert authenticated_client.get(DEFINITIONS_URL).status_code == 404 def test_retrieve_delete(self, authenticated_client, workflow): response = authenticated_client.get(f"{DEFINITIONS_URL}{workflow.id}/") assert response.status_code == 200 response = authenticated_client.delete(f"{DEFINITIONS_URL}{workflow.id}/") assert response.status_code == 204 assert not Workflow.objects.filter(pk=workflow.pk).exists() class TestWorkflowInstanceViewSet: def test_create_starts_instance(self, authenticated_client, workflow_graph, user): response = authenticated_client.post( INSTANCES_URL, {"workflow": workflow_graph["start"].workflow_id}, format="json", ) assert response.status_code == 201 instance = WorkflowInstance.objects.get() assert instance.status == WorkflowInstance.Status.IN_PROGRESS assert instance.current_node_id == workflow_graph["start"].id assert instance.owner_id == user.id def test_list_instances(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine WorkflowEngine().start("approval", owner=user) response = authenticated_client.get(INSTANCES_URL) assert response.status_code == 200 assert response.data["count"] == 1 assert response.data["results"][0]["status"] == "in_progress" def test_requires_auth(self, api_client, workflow_graph): assert api_client.get(INSTANCES_URL).status_code == 401 def test_route_action(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=user) response = authenticated_client.get(f"{INSTANCES_URL}{instance.pk}/route/") assert response.status_code == 200 assert "route" in response.data def test_state_action(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=user) response = authenticated_client.get(f"{INSTANCES_URL}{instance.pk}/state/") assert response.status_code == 200 assert "state" in response.data states = response.data["state"] assert "ACTIVE" in states.values() assert all(state in ("ACTIVE", "PENDING", "COMPLETED", "REJECTED") for state in states.values()) def test_submit_action(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=user) assignment = WorkflowEngine().assign_users(instance.pk, instance.current_node_id, [user])[0] response = authenticated_client.post( f"{INSTANCES_URL}{instance.pk}/submit/", {"assignment_id": assignment.id, "decision": "approved"}, format="json", ) assert response.status_code == 200 assert response.data["status"] == "completed" assert response.data["current_node"]["name"] == "End" def test_submit_requires_fields(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=user) response = authenticated_client.post(f"{INSTANCES_URL}{instance.pk}/submit/", {}, format="json") assert response.status_code == 400 def test_assign_action(self, authenticated_client, workflow_graph, user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=user) response = authenticated_client.post( f"{INSTANCES_URL}{instance.pk}/assign/", { "node_id": instance.current_node_id, "users": [user.id], }, format="json", ) assert response.status_code == 201 assert len(response.data) == 1 def test_observers_add_and_delete(self, authenticated_client, workflow_graph, user, admin_user): from infrasynth.workflows.engine import WorkflowEngine instance = WorkflowEngine().start("approval", owner=admin_user) response = authenticated_client.post( f"{INSTANCES_URL}{instance.pk}/observers/", {"user_id": user.id}, format="json", ) assert response.status_code == 201 assert WorkflowObserver.objects.filter(instance=instance, user=user).exists() response = authenticated_client.delete( f"{INSTANCES_URL}{instance.pk}/observers/", {"user_id": user.id}, format="json", ) assert response.status_code == 204 assert not WorkflowObserver.objects.filter(instance=instance, user=user).exists() class TestWorkflowNodeViewSet: def test_list_nodes(self, authenticated_client, workflow_graph): response = authenticated_client.get(NODES_URL) assert response.status_code == 200 assert response.data["count"] == 2 def test_create_node(self, authenticated_client, workflow): response = authenticated_client.post( NODES_URL, { "workflow": workflow.id, "name": "QA", "node_type": "intermediate", "approval_strategy": "any", }, format="json", ) assert response.status_code == 201 assert WorkflowNode.objects.filter(name="QA").exists() def test_requires_auth(self, api_client, workflow_graph): assert api_client.get(NODES_URL).status_code == 401 class TestTransitionViewSet: def test_list_transitions(self, authenticated_client, workflow_graph): response = authenticated_client.get(TRANSITIONS_URL) assert response.status_code == 200 assert response.data["count"] == 1 def test_create_transition(self, authenticated_client, workflow_graph): start, end = workflow_graph["start"], workflow_graph["end"] response = authenticated_client.post( TRANSITIONS_URL, { "from_node": start.id, "to_node": end.id, "condition_slug": "rejected", }, format="json", ) assert response.status_code == 201 assert Transition.objects.filter(condition_slug="rejected").exists() def test_requires_auth(self, api_client, workflow_graph): assert api_client.get(TRANSITIONS_URL).status_code == 401