"""Integration tests for full CRUD operations with all mixins.""" from sqlalchemy import select from abacus_models.core.contexts import set_user_context from tests.conftest import TestModelFull class TestCreateOperations: """Integration tests for Create operations.""" def test_create_model_with_all_mixins(self, session, user_context): """Test creating a model with all mixins populates tracking fields.""" model = TestModelFull(name='Integration Test') session.add(model) session.commit() # Verify model was created assert model.id is not None assert model.name == 'Integration Test' # Verify CreateMixin populated fields assert model.created_at is not None assert model.created_by == 'test_user_123' # Verify UpdateMixin fields are also populated on create assert model.last_modified is not None assert model.last_modified_by == 'test_user_123' # Verify SoftDeleteMixin fields are not populated assert model.deleted_at is None assert model.deleted_by is None def test_create_multiple_models_different_users(self, session): """Test creating models with different users tracks correctly.""" with set_user_context('user_alice'): model1 = TestModelFull(name='Alice Model') session.add(model1) session.commit() with set_user_context('user_bob'): model2 = TestModelFull(name='Bob Model') session.add(model2) session.commit() # Verify each model has correct creator assert model1.created_by == 'user_alice' assert model2.created_by == 'user_bob' class TestReadOperations: """Integration tests for Read operations.""" def test_read_model_by_id(self, session, user_context, sample_full_model): """Test reading a model by ID.""" result = TestModelFull.get_by_id(session, sample_full_model.id) assert result is not None assert result.id == sample_full_model.id assert result.name == sample_full_model.name def test_read_all_active_models(self, session, user_context): """Test reading all active models using filter_active.""" model1 = TestModelFull(name='Active 1') model2 = TestModelFull(name='Active 2') model3 = TestModelFull(name='Deleted') session.add_all([model1, model2, model3]) session.commit() model3.soft_delete() session.commit() # Query active models stmt = select(TestModelFull) stmt = TestModelFull.filter_active(stmt) results = session.execute(stmt).scalars().all() assert len(results) == 2 names = {r.name for r in results} assert names == {'Active 1', 'Active 2'} def test_read_model_with_repr(self, session, user_context, sample_full_model): """Test __repr__ provides useful string representation.""" repr_str = repr(sample_full_model) assert 'TestModelFull' in repr_str assert str(sample_full_model.id) in repr_str class TestUpdateOperations: """Integration tests for Update operations.""" def test_update_model_updates_tracking_fields( self, session, user_context, sample_full_model ): """Test updating a model updates last_modified fields.""" original_created_at = sample_full_model.created_at original_created_by = sample_full_model.created_by # Update the model sample_full_model.name = 'Updated Name' session.commit() session.refresh(sample_full_model) # Verify update tracking assert sample_full_model.name == 'Updated Name' assert sample_full_model.last_modified is not None assert sample_full_model.last_modified_by == 'test_user_123' # Verify created fields unchanged assert sample_full_model.created_at == original_created_at assert sample_full_model.created_by == original_created_by def test_update_by_different_user(self, session, sample_full_model): """Test updating by different user tracks correctly.""" with set_user_context('user_creator'): model = TestModelFull(name='Original') session.add(model) session.commit() with set_user_context('user_editor'): model.name = 'Updated' session.commit() session.refresh(model) # Verify creator and editor are different assert model.created_by == 'user_creator' assert model.last_modified_by == 'user_editor' def test_touch_method_updates_without_changes( self, session, user_context, sample_full_model ): """Test touch() updates last_modified without actual changes.""" original_name = sample_full_model.name original_modified = sample_full_model.last_modified sample_full_model.touch() session.commit() session.refresh(sample_full_model) # Name unchanged, but last_modified updated assert sample_full_model.name == original_name assert sample_full_model.last_modified != original_modified class TestDeleteOperations: """Integration tests for Delete operations (soft delete).""" def test_soft_delete_model(self, session, user_context, sample_full_model): """Test soft deleting a model sets deletion fields.""" sample_full_model.soft_delete() session.commit() session.refresh(sample_full_model) # Verify soft deletion assert sample_full_model.is_deleted() is True assert sample_full_model.deleted_at is not None assert sample_full_model.deleted_by == 'test_user_123' # Model still exists in database result = TestModelFull.get_by_id(session, sample_full_model.id) assert result is not None def test_soft_delete_updates_last_modified(self, session, user_context): """Test soft delete also updates last_modified fields.""" model = TestModelFull(name='Test') session.add(model) session.commit() original_modified = model.last_modified model.soft_delete() session.commit() session.refresh(model) # UpdateMixin should trigger on soft_delete assert model.last_modified != original_modified assert model.last_modified_by == 'test_user_123' def test_soft_delete_many_bulk_operation(self, session, user_context): """Test bulk soft delete of multiple models.""" models = [TestModelFull(name=f'Model {i}') for i in range(5)] session.add_all(models) session.commit() # Bulk delete first 3 models ids_to_delete = [models[i].id for i in range(3)] TestModelFull.soft_delete_many(session, ids_to_delete) session.commit() # Refresh models for model in models: session.refresh(model) # Verify first 3 deleted, last 2 active assert models[0].is_deleted() is True assert models[1].is_deleted() is True assert models[2].is_deleted() is True assert models[3].is_deleted() is False assert models[4].is_deleted() is False def test_restore_deleted_model(self, session, user_context, sample_full_model): """Test restoring a soft-deleted model.""" # Soft delete sample_full_model.soft_delete() session.commit() assert sample_full_model.is_deleted() is True # Restore sample_full_model.restore() session.commit() session.refresh(sample_full_model) # Verify restoration assert sample_full_model.is_deleted() is False assert sample_full_model.deleted_at is None assert sample_full_model.deleted_by is None def test_delete_restore_cycle(self, session, user_context, sample_full_model): """Test multiple delete/restore cycles.""" for _ in range(3): sample_full_model.soft_delete() session.commit() assert sample_full_model.is_deleted() is True sample_full_model.restore() session.commit() assert sample_full_model.is_deleted() is False class TestComplexQueries: """Integration tests for complex query operations.""" def test_filter_active_vs_deleted(self, session, user_context): """Test filtering between active and deleted models.""" # Create mix of models active_models = [TestModelFull(name=f'Active {i}') for i in range(3)] deleted_models = [TestModelFull(name=f'Deleted {i}') for i in range(2)] session.add_all(active_models + deleted_models) session.commit() # Delete some models for model in deleted_models: model.soft_delete() session.commit() # Query active stmt = select(TestModelFull) stmt = TestModelFull.filter_active(stmt) active_results = session.execute(stmt).scalars().all() assert len(active_results) == 3 # Query deleted stmt = select(TestModelFull) stmt = TestModelFull.filter_deleted(stmt) deleted_results = session.execute(stmt).scalars().all() assert len(deleted_results) == 2 def test_get_by_id_or_error_with_deleted_model(self, session, user_context): """Test get_by_id_or_error returns deleted models.""" model = TestModelFull(name='Test') session.add(model) session.commit() model_id = model.id model.soft_delete() session.commit() # get_by_id should still return the model (it's not hard-deleted) result = TestModelFull.get_by_id(session, model_id) assert result is not None assert result.is_deleted() is True class TestAuditTrail: """Integration tests for full audit trail.""" def test_full_lifecycle_audit_trail(self, session): """Test complete lifecycle with audit trail.""" # Create by user 1 with set_user_context('user_creator'): model = TestModelFull(name='Original') session.add(model) session.commit() session.refresh(model) created_at = model.created_at created_by = model.created_by # Update by user 2 with set_user_context('user_editor'): model.name = 'Updated' session.commit() session.refresh(model) first_modified_at = model.last_modified first_modified_by = model.last_modified_by # Update again by user 3 with set_user_context('user_editor_2'): model.name = 'Updated Again' session.commit() session.refresh(model) second_modified_at = model.last_modified second_modified_by = model.last_modified_by # Delete by user 4 with set_user_context('user_deleter'): model.soft_delete() session.commit() session.refresh(model) deleted_at = model.deleted_at deleted_by = model.deleted_by final_modified_by = model.last_modified_by # Verify audit trail assert created_by == 'user_creator' assert first_modified_by == 'user_editor' assert second_modified_by == 'user_editor_2' assert deleted_by == 'user_deleter' # Verify timestamps are in order assert created_at <= first_modified_at assert first_modified_at <= second_modified_at assert second_modified_at <= deleted_at # Delete also triggers UpdateMixin assert final_modified_by == 'user_deleter' def test_audit_trail_with_restore(self, session): """Test audit trail includes restore operations.""" with set_user_context('user_creator'): model = TestModelFull(name='Test') session.add(model) session.commit() with set_user_context('user_deleter'): model.soft_delete() session.commit() session.refresh(model) delete_modified_at = model.last_modified with set_user_context('user_restorer'): model.restore() session.commit() session.refresh(model) # Verify restore updated tracking assert model.deleted_at is None assert model.deleted_by is None assert model.last_modified > delete_modified_at assert model.last_modified_by == 'user_restorer' class TestTransactions: """Integration tests for transaction handling.""" def test_rollback_create(self, session, user_context): """Test rollback of create operation.""" model = TestModelFull(name='Test') session.add(model) session.flush() model_id = model.id # Rollback session.rollback() # Model should not exist result = TestModelFull.get_by_id(session, model_id) assert result is None def test_rollback_update(self, session, user_context, sample_full_model): """Test rollback of update operation.""" original_name = sample_full_model.name sample_full_model.name = 'Updated' session.flush() # Rollback session.rollback() session.refresh(sample_full_model) # Name should be unchanged assert sample_full_model.name == original_name def test_rollback_soft_delete(self, session, user_context, sample_full_model): """Test rollback of soft delete operation.""" sample_full_model.soft_delete() session.flush() # Rollback session.rollback() session.refresh(sample_full_model) # Model should not be deleted assert sample_full_model.is_deleted() is False class TestEdgeCases: """Integration tests for edge cases.""" def test_concurrent_updates_last_wins(self, session): """Test concurrent updates (last commit wins).""" with set_user_context('user_1'): model = TestModelFull(name='Original') session.add(model) session.commit() model_id = model.id # Simulate two users updating with set_user_context('user_2'): model_user2 = TestModelFull.get_by_id(session, model_id) model_user2.name = 'Updated by User 2' session.commit() with set_user_context('user_3'): model_user3 = TestModelFull.get_by_id(session, model_id) model_user3.name = 'Updated by User 3' session.commit() # Check final state (last update wins) final_model = TestModelFull.get_by_id(session, model_id) assert final_model.name == 'Updated by User 3' assert final_model.last_modified_by == 'user_3' def test_empty_name_allowed(self, session, user_context): """Test model allows empty string name.""" model = TestModelFull(name='') session.add(model) session.commit() assert model.name == '' assert model.id is not None