| """ |
| Database integration tests with transaction rollback (INTG-02). |
| |
| Tests cover: |
| - Transaction rollback pattern |
| - Test isolation (no data leakage) |
| - Database constraints |
| - Cascade operations |
| - Multiple operations in single transaction |
| """ |
|
|
| import pytest |
| from sqlalchemy.orm import Session |
| from sqlalchemy.exc import IntegrityError |
|
|
| from tests.factories.agent_factory import AgentFactory, StudentAgentFactory |
| from tests.factories.user_factory import UserFactory, AdminUserFactory |
| from tests.factories.execution_factory import AgentExecutionFactory |
| from tests.factories.episode_factory import EpisodeFactory |
| from core.models import ( |
| AgentRegistry, |
| AgentExecution, |
| User, |
| Episode, |
| UserRole |
| ) |
|
|
|
|
| class TestTransactionRollback: |
| """Test transaction rollback ensures test isolation.""" |
|
|
| def test_agent_not_visible_in_next_test(self, db_session: Session): |
| """Test agents created in one test don't appear in next.""" |
| |
| agent = AgentFactory(name="RollbackTestAgent", _session=db_session) |
| db_session.commit() |
| agent_id = agent.id |
|
|
| |
| retrieved = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() |
| assert retrieved is not None |
| assert retrieved.name == "RollbackTestAgent" |
|
|
| def test_database_clean_after_rollback(self, db_session: Session): |
| """Test database is clean after transaction rollback.""" |
| |
| |
| count = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name == "RollbackTestAgent" |
| ).count() |
| assert count == 0, "Previous test data leaked - transaction rollback failed" |
|
|
| def test_multiple_operations_in_single_transaction(self, db_session: Session): |
| """Test multiple operations in same transaction.""" |
| agent = AgentFactory(name="MultiOpAgent", _session=db_session) |
| execution = AgentExecutionFactory(agent_id=agent.id, _session=db_session) |
|
|
| db_session.commit() |
|
|
| |
| assert db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent.id |
| ).first() is not None |
|
|
| assert db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution.id |
| ).first() is not None |
|
|
| def test_user_not_leaked_between_tests(self, db_session: Session): |
| """Test users created don't leak to next test.""" |
| user = UserFactory(email="leak_test@example.com", _session=db_session) |
| db_session.commit() |
| user_id = user.id |
|
|
| |
| retrieved = db_session.query(User).filter(User.id == user_id).first() |
| assert retrieved is not None |
| assert retrieved.email == "leak_test@example.com" |
|
|
| def test_user_clean_after_rollback(self, db_session: Session): |
| """Test users are cleaned up after rollback.""" |
| |
| count = db_session.query(User).filter( |
| User.email == "leak_test@example.com" |
| ).count() |
| assert count == 0, "User data leaked between tests" |
|
|
| def test_episode_not_leaked_between_tests(self, db_session: Session): |
| """Test episodes created don't leak to next test.""" |
| agent = AgentFactory(name="EpisodeLeakTestAgent", _session=db_session) |
| episode = EpisodeFactory(agent_id=agent.id, title="Leak Test Episode", _session=db_session) |
| db_session.commit() |
| episode_id = episode.id |
|
|
| |
| retrieved = db_session.query(Episode).filter(Episode.id == episode_id).first() |
| assert retrieved is not None |
| assert retrieved.title == "Leak Test Episode" |
|
|
| def test_episode_clean_after_rollback(self, db_session: Session): |
| """Test episodes are cleaned up after rollback.""" |
| |
| count = db_session.query(Episode).filter( |
| Episode.title == "Leak Test Episode" |
| ).count() |
| assert count == 0, "Episode data leaked between tests" |
|
|
|
|
| class TestDatabaseConstraints: |
| """Test database constraints are enforced.""" |
|
|
| def test_unique_constraint_on_email(self, db_session: Session): |
| """Test unique email constraint is enforced.""" |
| |
| user1 = UserFactory(email="user1@test.com", _session=db_session) |
| db_session.commit() |
|
|
| |
| with pytest.raises(IntegrityError): |
| UserFactory(email="user1@test.com", _session=db_session) |
|
|
| |
| db_session.rollback() |
|
|
| def test_unique_constraint_on_agent_name_within_workspace(self, db_session: Session): |
| """Test agent name uniqueness (if constraint exists).""" |
| |
| |
| agent1 = AgentFactory(name="SameNameAgent", _session=db_session) |
| agent2 = AgentFactory(name="SameNameAgent", _session=db_session) |
|
|
| db_session.add(agent1) |
| db_session.add(agent2) |
|
|
| |
| try: |
| db_session.commit() |
| |
| assert agent1.id != agent2.id |
| except IntegrityError: |
| |
| pytest.skip("Agent name has unique constraint") |
|
|
| def test_foreign_key_constraint_on_execution(self, db_session: Session): |
| """Test foreign key constraints prevent orphaned records.""" |
| |
| |
| |
| |
| execution = AgentExecutionFactory(agent_id="nonexistent_agent_id", _session=db_session) |
|
|
| db_session.add(execution) |
|
|
| |
| |
| |
| db_session.commit() |
|
|
| |
| db_session.rollback() |
|
|
| def test_agent_status_enum_constraint(self, db_session: Session): |
| """Test agent status only accepts valid enum values.""" |
| from core.models import AgentStatus |
|
|
| |
| agent = AgentFactory(status=AgentStatus.STUDENT.value, _session=db_session) |
| db_session.commit() |
|
|
| |
| retrieved = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent.id |
| ).first() |
| assert retrieved.status == AgentStatus.STUDENT.value |
|
|
| def test_user_role_enum_constraint(self, db_session: Session): |
| """Test user role only accepts valid enum values.""" |
| |
| user = UserFactory(role=UserRole.MEMBER.value, _session=db_session) |
| db_session.commit() |
|
|
| |
| retrieved = db_session.query(User).filter(User.id == user.id).first() |
| assert retrieved.role == UserRole.MEMBER.value |
|
|
| def test_not_null_constraints(self, db_session: Session): |
| """Test NOT NULL constraints on required fields.""" |
| |
| |
|
|
| |
| from core.models import AgentRegistry |
| agent = AgentRegistry( |
| name=None, |
| category="test", |
| status="student" |
| ) |
|
|
| db_session.add(agent) |
|
|
| |
| |
| try: |
| db_session.commit() |
| |
| agents_with_null = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.is_(None) |
| ).all() |
| |
| for a in agents_with_null: |
| db_session.delete(a) |
| db_session.commit() |
| except (IntegrityError, Exception): |
| |
| db_session.rollback() |
|
|
|
|
| class TestCascadeOperations: |
| """Test cascade delete and update operations.""" |
|
|
| def test_agent_deletion_cascades_to_executions(self, db_session: Session): |
| """Test deleting agent and related executions.""" |
| agent = AgentFactory(name="CascadeTestAgent", _session=db_session) |
| execution = AgentExecutionFactory(agent_id=agent.id, _session=db_session) |
| db_session.commit() |
|
|
| agent_id = agent.id |
| execution_id = execution.id |
|
|
| |
| |
| |
| agent = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() |
| execution = db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() |
|
|
| assert agent is not None |
| assert execution is not None |
| assert execution.agent_id == agent_id |
|
|
| |
| db_session.delete(execution) |
| db_session.delete(agent) |
| db_session.commit() |
|
|
| |
| assert db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() is None |
|
|
| assert db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution_id |
| ).first() is None |
|
|
| def test_user_deletion_cascades_to_episodes(self, db_session: Session): |
| """Test deleting user cascades to related episodes (if configured).""" |
| user = UserFactory(email="cascade_user@test.com", _session=db_session) |
| agent = AgentFactory(name="CascadeTestAgent", _session=db_session) |
| episode = EpisodeFactory(agent_id=agent.id, _session=db_session) |
| db_session.commit() |
|
|
| user_id = user.id |
| episode_id = episode.id |
|
|
| |
| db_session.delete(user) |
| db_session.commit() |
|
|
| |
| assert db_session.query(User).filter(User.id == user_id).first() is None |
|
|
| |
| episode = db_session.query(Episode).filter(Episode.id == episode_id).first() |
| |
|
|
| def test_agent_execution_relationship(self, db_session: Session): |
| """Test agent-execution relationship works correctly.""" |
| agent = AgentFactory(name="RelationTestAgent", _session=db_session) |
| execution = AgentExecutionFactory(agent_id=agent.id, _session=db_session) |
| db_session.commit() |
|
|
| |
| retrieved_execution = db_session.query(AgentExecution).filter( |
| AgentExecution.id == execution.id |
| ).first() |
|
|
| |
| assert retrieved_execution is not None |
| assert retrieved_execution.agent_id == agent.id |
|
|
| def test_episode_agent_relationship(self, db_session: Session): |
| """Test episode-agent relationship works correctly.""" |
| agent = AgentFactory(name="EpisodeRelationAgent", _session=db_session) |
| episode = EpisodeFactory(agent_id=agent.id, _session=db_session) |
| db_session.commit() |
|
|
| |
| retrieved_episode = db_session.query(Episode).filter( |
| Episode.id == episode.id |
| ).first() |
|
|
| assert retrieved_episode is not None |
| assert retrieved_episode.agent_id == agent.id |
|
|
|
|
| class TestTransactionIsolation: |
| """Test transaction isolation between concurrent operations.""" |
|
|
| def test_read_committed_isolation(self, db_session: Session): |
| """Test that committed changes are visible.""" |
| |
| agent = AgentFactory(name="IsolationTestAgent", _session=db_session) |
| db_session.commit() |
|
|
| |
| retrieved = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name == "IsolationTestAgent" |
| ).first() |
| assert retrieved is not None |
|
|
| def test_rollback_undoes_changes(self, db_session: Session): |
| """Test that rollback undoes uncommitted changes.""" |
| agent = AgentFactory(name="RollbackIsolationAgent", _session=db_session) |
| db_session.add(agent) |
| db_session.commit() |
| agent_id = agent.id |
|
|
| |
| agent.name = "ModifiedName" |
| db_session.rollback() |
|
|
| |
| retrieved = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() |
| assert retrieved is not None |
| assert retrieved.name == "RollbackIsolationAgent" |
|
|
| def test_multiple_commits_independently(self, db_session: Session): |
| """Test multiple commits are independent.""" |
| |
| agent1 = AgentFactory(name="FirstAgent", _session=db_session) |
| db_session.commit() |
|
|
| |
| agent2 = AgentFactory(name="SecondAgent", _session=db_session) |
| db_session.commit() |
|
|
| |
| assert db_session.query(AgentRegistry).filter( |
| AgentRegistry.name == "FirstAgent" |
| ).first() is not None |
|
|
| assert db_session.query(AgentRegistry).filter( |
| AgentRegistry.name == "SecondAgent" |
| ).first() is not None |
|
|
|
|
| class TestBatchOperations: |
| """Test batch database operations.""" |
|
|
| def test_batch_insert_agents(self, db_session: Session): |
| """Test inserting multiple agents in single transaction.""" |
| agents = [ |
| AgentFactory(name=f"BatchAgent{i}", _session=db_session) for i in range(10) |
| ] |
|
|
| |
| db_session.commit() |
|
|
| |
| count = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("BatchAgent%") |
| ).count() |
|
|
| assert count == 10 |
|
|
| def test_batch_delete_with_filter(self, db_session: Session): |
| """Test deleting multiple records with filter.""" |
| |
| for i in range(5): |
| agent = AgentFactory(name=f"DeleteAgent{i}", _session=db_session) |
| db_session.commit() |
|
|
| |
| db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("DeleteAgent%") |
| ).delete() |
|
|
| db_session.commit() |
|
|
| |
| count = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("DeleteAgent%") |
| ).count() |
|
|
| assert count == 0 |
|
|
| def test_batch_update(self, db_session: Session): |
| """Test updating multiple records in single operation.""" |
| |
| for i in range(5): |
| agent = StudentAgentFactory(name=f"UpdateAgent{i}", _session=db_session) |
| db_session.commit() |
|
|
| |
| from core.models import AgentStatus |
| db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("UpdateAgent%") |
| ).update({"status": AgentStatus.INTERN.value}) |
|
|
| db_session.commit() |
|
|
| |
| count = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("UpdateAgent%"), |
| AgentRegistry.status == AgentStatus.INTERN.value |
| ).count() |
|
|
| assert count == 5 |
|
|