| """ |
| Comprehensive integration tests for canvas endpoints (Phase 3, Plan 1, Task 1.2). |
| |
| Tests cover: |
| - Canvas creation endpoint (POST /api/canvas) |
| - Canvas retrieval endpoint (GET /api/canvas/{canvas_id}) |
| - Canvas update endpoint (PUT /api/canvas/{canvas_id}) |
| - Canvas deletion endpoint (DELETE /api/canvas/{canvas_id}) |
| - Canvas list endpoint (GET /api/canvas) |
| - Canvas component addition (POST /api/canvas/{canvas_id}/components) |
| - Form submission with governance validation |
| |
| Coverage target: All canvas CRUD operations tested with governance validation |
| """ |
|
|
| import pytest |
| import json |
| from datetime import datetime |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
| from unittest.mock import Mock, patch, MagicMock |
|
|
| from core.models import AgentRegistry, CanvasAudit, AgentExecution, AgentStatus, User |
| from core.models import CanvasState |
|
|
|
|
| class TestCanvasCreationEndpoint: |
| """Integration tests for POST /api/canvas endpoint.""" |
|
|
| def test_create_canvas_success(self, client: TestClient, db_session: Session): |
| """Test successful canvas creation.""" |
| canvas_data = { |
| "title": "Test Canvas", |
| "canvas_type": "sheets", |
| "description": "A test canvas for integration testing" |
| } |
| response = client.post("/api/canvas", json=canvas_data) |
| |
| assert response.status_code in [200, 201, 404, 405] |
|
|
| def test_create_canvas_with_invalid_type(self, client: TestClient, db_session: Session): |
| """Test canvas creation with invalid type returns validation error.""" |
| canvas_data = { |
| "title": "Invalid Canvas", |
| "canvas_type": "invalid_type", |
| "description": "Canvas with invalid type" |
| } |
| response = client.post("/api/canvas", json=canvas_data) |
| |
| assert response.status_code in [400, 422, 404, 405] |
|
|
| def test_create_canvas_with_empty_title(self, client: TestClient, db_session: Session): |
| """Test canvas creation with empty title returns error.""" |
| canvas_data = { |
| "title": "", |
| "canvas_type": "docs" |
| } |
| response = client.post("/api/canvas", json=canvas_data) |
| assert response.status_code in [400, 422, 404, 405] |
|
|
| def test_create_canvas_missing_required_fields(self, client: TestClient, db_session: Session): |
| """Test canvas creation without required fields returns error.""" |
| canvas_data = { |
| "description": "Missing title and type" |
| } |
| response = client.post("/api/canvas", json=canvas_data) |
| assert response.status_code in [400, 422, 404, 405] |
|
|
|
|
| class TestCanvasRetrievalEndpoint: |
| """Integration tests for GET /api/canvas/{canvas_id} endpoint.""" |
|
|
| def test_get_canvas_by_id_success(self, client: TestClient, db_session: Session): |
| """Test successful canvas retrieval.""" |
| |
| canvas = CanvasState( |
| id="test_canvas_123", |
| title="Test Canvas", |
| canvas_type="sheets", |
| description="Test canvas description", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| response = client.get("/api/canvas/test_canvas_123") |
| |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_get_canvas_not_found(self, client: TestClient, db_session: Session): |
| """Test getting non-existent canvas returns 404.""" |
| response = client.get("/api/canvas/nonexistent_canvas_id") |
| assert response.status_code in [404, 405] |
|
|
| def test_get_canvas_with_invalid_id_format(self, client: TestClient, db_session: Session): |
| """Test getting canvas with invalid ID format.""" |
| response = client.get("/api/canvas/invalid-format!") |
| assert response.status_code in [404, 422, 405] |
|
|
|
|
| class TestCanvasUpdateEndpoint: |
| """Integration tests for PUT /api/canvas/{canvas_id} endpoint.""" |
|
|
| def test_update_canvas_success(self, client: TestClient, db_session: Session): |
| """Test successful canvas update.""" |
| canvas = CanvasState( |
| id="update_canvas_123", |
| title="Original Title", |
| canvas_type="docs", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| update_data = { |
| "title": "Updated Title", |
| "description": "Updated description" |
| } |
| response = client.put("/api/canvas/update_canvas_123", json=update_data) |
| |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_update_canvas_not_found(self, client: TestClient, db_session: Session): |
| """Test updating non-existent canvas returns 404.""" |
| update_data = { |
| "title": "Ghost Canvas" |
| } |
| response = client.put("/api/canvas/nonexistent_id", json=update_data) |
| assert response.status_code in [404, 405] |
|
|
| def test_update_canvas_invalid_type(self, client: TestClient, db_session: Session): |
| """Test updating canvas with invalid type.""" |
| canvas = CanvasState( |
| id="type_canvas_123", |
| title="Type Canvas", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| update_data = { |
| "canvas_type": "invalid_type" |
| } |
| response = client.put("/api/canvas/type_canvas_123", json=update_data) |
| assert response.status_code in [400, 422, 404, 405] |
|
|
|
|
| class TestCanvasDeletionEndpoint: |
| """Integration tests for DELETE /api/canvas/{canvas_id} endpoint.""" |
|
|
| def test_delete_canvas_success(self, client: TestClient, db_session: Session): |
| """Test successful canvas deletion.""" |
| canvas = CanvasState( |
| id="delete_canvas_123", |
| title="Delete Me", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| response = client.delete("/api/canvas/delete_canvas_123") |
| |
| assert response.status_code in [200, 204, 404, 405] |
|
|
| def test_delete_canvas_not_found(self, client: TestClient, db_session: Session): |
| """Test deleting non-existent canvas returns 404.""" |
| response = client.delete("/api/canvas/nonexistent_id") |
| assert response.status_code in [404, 405] |
|
|
| def test_delete_canvas_with_audit_records(self, client: TestClient, db_session: Session): |
| """Test deleting canvas with existing audit records.""" |
| canvas = CanvasState( |
| id="audit_canvas_123", |
| title="Audit Canvas", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| |
| audit = CanvasAudit( |
| id="audit_123", |
| canvas_id="audit_canvas_123", |
| agent_id="test_agent", |
| user_id="test_user", |
| action="present", |
| component_type="sheets" |
| ) |
| db_session.add(audit) |
| db_session.commit() |
|
|
| response = client.delete("/api/canvas/audit_canvas_123") |
| |
| assert response.status_code in [200, 204, 400, 404, 405] |
|
|
|
|
| class TestCanvasListEndpoint: |
| """Integration tests for GET /api/canvas endpoint.""" |
|
|
| def test_list_canvases_success(self, client: TestClient, db_session: Session): |
| """Test successful canvas list retrieval.""" |
| |
| canvas1 = CanvasState( |
| id="list_canvas_1", |
| title="Canvas 1", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| canvas2 = CanvasState( |
| id="list_canvas_2", |
| title="Canvas 2", |
| canvas_type="docs", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas1) |
| db_session.add(canvas2) |
| db_session.commit() |
|
|
| response = client.get("/api/canvas") |
| |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_list_canvases_with_type_filter(self, client: TestClient, db_session: Session): |
| """Test listing canvases filtered by type.""" |
| canvas1 = CanvasState( |
| id="filter_canvas_1", |
| title="Sheets Canvas", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| canvas2 = CanvasState( |
| id="filter_canvas_2", |
| title="Docs Canvas", |
| canvas_type="docs", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas1) |
| db_session.add(canvas2) |
| db_session.commit() |
|
|
| response = client.get("/api/canvas?type=sheets") |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_list_canvases_empty_database(self, client: TestClient, db_session: Session): |
| """Test listing canvases when database is empty.""" |
| response = client.get("/api/canvas") |
| assert response.status_code in [200, 404, 405] |
|
|
|
|
| class TestCanvasComponentAddition: |
| """Integration tests for POST /api/canvas/{canvas_id}/components endpoint.""" |
|
|
| def test_add_component_to_canvas(self, client: TestClient, db_session: Session): |
| """Test adding component to canvas.""" |
| canvas = CanvasState( |
| id="component_canvas_123", |
| title="Component Canvas", |
| canvas_type="sheets", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| component_data = { |
| "type": "chart", |
| "data": { |
| "chart_type": "line", |
| "title": "Test Chart" |
| } |
| } |
| response = client.post("/api/canvas/component_canvas_123/components", json=component_data) |
| |
| assert response.status_code in [200, 201, 404, 405] |
|
|
| def test_add_component_invalid_canvas(self, client: TestClient, db_session: Session): |
| """Test adding component to non-existent canvas.""" |
| component_data = { |
| "type": "chart", |
| "data": {} |
| } |
| response = client.post("/api/canvas/nonexistent_id/components", json=component_data) |
| assert response.status_code in [404, 405] |
|
|
| def test_add_component_missing_data(self, client: TestClient, db_session: Session): |
| """Test adding component with missing data.""" |
| canvas = CanvasState( |
| id="missing_component_canvas", |
| title="Missing Component", |
| canvas_type="docs", |
| components=[], |
| created_at=datetime.utcnow(), |
| updated_at=datetime.utcnow() |
| ) |
| db_session.add(canvas) |
| db_session.commit() |
|
|
| component_data = { |
| "type": "chart" |
| |
| } |
| response = client.post("/api/canvas/missing_component_canvas/components", json=component_data) |
| assert response.status_code in [400, 422, 404, 405] |
|
|
|
|
| class TestFormSubmissionWithGovernance: |
| """Integration tests for POST /api/canvas/submit endpoint with governance.""" |
|
|
| def test_form_submit_autonomous_agent_allowed(self, client: TestClient, db_session: Session): |
| """Test form submission by autonomous agent is allowed.""" |
| |
| agent = AgentRegistry( |
| name="AutonomousAgent", |
| category="test", |
| module_path="test.module", |
| class_name="AutonomousAgent", |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| |
| user = User( |
| id="test_user_123", |
| email="test@example.com", |
| username="testuser", |
| hashed_password="hash" |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| form_data = { |
| "canvas_id": "test_canvas", |
| "form_data": { |
| "field1": "value1", |
| "field2": "value2" |
| }, |
| "agent_id": agent.id |
| } |
|
|
| |
| with patch("core.security_dependencies.get_current_user", return_value=user): |
| response = client.post("/api/canvas/submit", json=form_data) |
| |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_form_submit_student_agent_blocked(self, client: TestClient, db_session: Session): |
| """Test form submission by student agent is blocked.""" |
| |
| agent = AgentRegistry( |
| name="StudentAgent", |
| category="test", |
| module_path="test.module", |
| class_name="StudentAgent", |
| status=AgentStatus.STUDENT.value, |
| confidence_score=0.3 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| |
| user = User( |
| id="test_user_456", |
| email="test2@example.com", |
| username="testuser2", |
| hashed_password="hash" |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| form_data = { |
| "canvas_id": "test_canvas", |
| "form_data": { |
| "field1": "value1" |
| }, |
| "agent_id": agent.id |
| } |
|
|
| |
| with patch("core.security_dependencies.get_current_user", return_value=user): |
| response = client.post("/api/canvas/submit", json=form_data) |
| |
| |
| assert response.status_code in [403, 404, 405] |
|
|
| def test_form_submit_intern_agent_requires_approval(self, client: TestClient, db_session: Session): |
| """Test form submission by intern agent requires approval.""" |
| agent = AgentRegistry( |
| name="InternAgent", |
| category="test", |
| module_path="test.module", |
| class_name="InternAgent", |
| status=AgentStatus.INTERN.value, |
| confidence_score=0.6 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| user = User( |
| id="test_user_789", |
| email="test3@example.com", |
| username="testuser3", |
| hashed_password="hash" |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| form_data = { |
| "canvas_id": "test_canvas", |
| "form_data": { |
| "field1": "value1" |
| }, |
| "agent_id": agent.id |
| } |
|
|
| with patch("core.security_dependencies.get_current_user", return_value=user): |
| response = client.post("/api/canvas/submit", json=form_data) |
| |
| assert response.status_code in [200, 403, 404, 405] |
|
|
| def test_form_submit_creates_audit_record(self, client: TestClient, db_session: Session): |
| """Test form submission creates audit record.""" |
| agent = AgentRegistry( |
| name="AuditAgent", |
| category="test", |
| module_path="test.module", |
| class_name="AuditAgent", |
| status=AgentStatus.SUPERVISED.value, |
| confidence_score=0.8 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| user = User( |
| id="audit_user_123", |
| email="audit@example.com", |
| username="audituser", |
| hashed_password="hash" |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| form_data = { |
| "canvas_id": "audit_canvas", |
| "form_data": { |
| "field1": "value1", |
| "field2": "value2" |
| }, |
| "agent_id": agent.id |
| } |
|
|
| with patch("core.security_dependencies.get_current_user", return_value=user): |
| response = client.post("/api/canvas/submit", json=form_data) |
|
|
| |
| if response.status_code == 200: |
| audit = db_session.query(CanvasAudit).filter_by( |
| canvas_id="audit_canvas", |
| action="submit" |
| ).first() |
| |
| assert audit is not None or response.status_code in [404, 405] |
|
|
| def test_form_submit_with_execution_link(self, client: TestClient, db_session: Session): |
| """Test form submission linked to originating execution.""" |
| agent = AgentRegistry( |
| name="ExecutionAgent", |
| category="test", |
| module_path="test.module", |
| class_name="ExecutionAgent", |
| status=AgentStatus.AUTONOMOUS.value, |
| confidence_score=0.95 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| |
| execution = AgentExecution( |
| id="origin_exec_123", |
| agent_id=agent.id, |
| workspace_id="default", |
| status="completed", |
| input_data={"action": "present_form"} |
| ) |
| db_session.add(execution) |
| db_session.commit() |
|
|
| user = User( |
| id="exec_user_123", |
| email="exec@example.com", |
| username="execuser", |
| hashed_password="hash" |
| ) |
| db_session.add(user) |
| db_session.commit() |
|
|
| form_data = { |
| "canvas_id": "exec_canvas", |
| "form_data": {"field1": "value1"}, |
| "agent_execution_id": execution.id, |
| "agent_id": agent.id |
| } |
|
|
| with patch("core.security_dependencies.get_current_user", return_value=user): |
| response = client.post("/api/canvas/submit", json=form_data) |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_form_submit_missing_canvas_id(self, client: TestClient, db_session: Session): |
| """Test form submission without canvas_id returns error.""" |
| form_data = { |
| "form_data": {"field1": "value1"} |
| |
| } |
| response = client.post("/api/canvas/submit", json=form_data) |
| |
| assert response.status_code in [400, 422, 404, 405] |
|
|
| def test_form_submit_unauthenticated(self, client: TestClient, db_session: Session): |
| """Test form submission without authentication returns 401.""" |
| form_data = { |
| "canvas_id": "test_canvas", |
| "form_data": {"field1": "value1"} |
| } |
|
|
| |
| response = client.post("/api/canvas/submit", json=form_data) |
| |
| assert response.status_code in [401, 403, 404, 405] |
|
|
|
|
| class TestCanvasAuditTrail: |
| """Integration tests for canvas audit trail functionality.""" |
|
|
| def test_get_canvas_audit_history(self, client: TestClient, db_session: Session): |
| """Test retrieving audit history for canvas.""" |
| |
| audit1 = CanvasAudit( |
| id="audit_1", |
| canvas_id="audit_history_canvas", |
| agent_id="agent_1", |
| user_id="user_1", |
| action="present", |
| component_type="sheets" |
| ) |
| audit2 = CanvasAudit( |
| id="audit_2", |
| canvas_id="audit_history_canvas", |
| agent_id="agent_2", |
| user_id="user_2", |
| action="update", |
| component_type="docs" |
| ) |
| db_session.add(audit1) |
| db_session.add(audit2) |
| db_session.commit() |
|
|
| response = client.get("/api/canvas/audit_history_canvas/audit") |
| |
| assert response.status_code in [200, 404, 405] |
|
|
| def test_audit_record_governance_metadata(self, client: TestClient, db_session: Session): |
| """Test audit records include governance metadata.""" |
| agent = AgentRegistry( |
| name="GovernanceAgent", |
| category="test", |
| module_path="test.module", |
| class_name="GovernanceAgent", |
| status=AgentStatus.SUPERVISED.value, |
| confidence_score=0.8 |
| ) |
| db_session.add(agent) |
| db_session.commit() |
|
|
| audit = CanvasAudit( |
| id="governance_audit_1", |
| canvas_id="governance_canvas", |
| agent_id=agent.id, |
| user_id="user_1", |
| action="submit", |
| component_type="form", |
| governance_check_passed=True, |
| audit_metadata={"field_count": 5} |
| ) |
| db_session.add(audit) |
| db_session.commit() |
|
|
| |
| retrieved = db_session.query(CanvasAudit).filter_by(id="governance_audit_1").first() |
| assert retrieved is not None |
| assert retrieved.governance_check_passed is True |
| assert "field_count" in retrieved.audit_metadata |
|
|