""" Comprehensive tests for Favorite Projects functionality. This module tests: - UserFavoriteProject model creation and validation - Relationships between User and Project models - Favorite/unfavorite routes and API endpoints - Filtering projects by favorites - User permissions and access control """ import pytest from datetime import datetime from decimal import Decimal from app import create_app, db from app.models import User, Project, Client, UserFavoriteProject @pytest.fixture def app(): """Create and configure a test application instance.""" app = create_app( { "TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False, "SECRET_KEY": "test-secret-key-do-not-use-in-production", "SERVER_NAME": "localhost:5000", "APPLICATION_ROOT": "/", "PREFERRED_URL_SCHEME": "http", } ) with app.app_context(): db.create_all() yield app db.session.remove() db.drop_all() @pytest.fixture def client_fixture(app): """Create a test Flask client.""" return app.test_client() @pytest.fixture def test_user(app): """Create a test user.""" with app.app_context(): user = User(username="testuser", role="user") db.session.add(user) db.session.commit() return user.id @pytest.fixture def test_admin(app): """Create a test admin user.""" with app.app_context(): admin = User(username="admin", role="admin") db.session.add(admin) db.session.commit() return admin.id @pytest.fixture def test_client(app): """Create a test client.""" with app.app_context(): client = Client(name="Test Client", description="A test client") db.session.add(client) db.session.commit() return client.id @pytest.fixture def test_project(app, test_client): """Create a test project.""" with app.app_context(): project = Project( name="Test Project", client_id=test_client, description="A test project", billable=True, hourly_rate=Decimal("100.00"), ) db.session.add(project) db.session.commit() return project.id @pytest.fixture def test_project_2(app, test_client): """Create a second test project.""" with app.app_context(): project = Project( name="Test Project 2", client_id=test_client, description="Another test project", billable=True, hourly_rate=Decimal("150.00"), ) db.session.add(project) db.session.commit() return project.id # Model Tests class TestUserFavoriteProjectModel: """Test UserFavoriteProject model creation, validation, and basic operations.""" def test_create_favorite(self, app, test_user, test_project): """Test creating a favorite project entry.""" with app.app_context(): favorite = UserFavoriteProject() favorite.user_id = test_user favorite.project_id = test_project db.session.add(favorite) db.session.commit() assert favorite.id is not None assert favorite.user_id == test_user assert favorite.project_id == test_project assert favorite.created_at is not None assert isinstance(favorite.created_at, datetime) def test_favorite_unique_constraint(self, app, test_user, test_project): """Test that a user cannot favorite the same project twice.""" with app.app_context(): # Create first favorite favorite1 = UserFavoriteProject() favorite1.user_id = test_user favorite1.project_id = test_project db.session.add(favorite1) db.session.commit() # Try to create duplicate favorite2 = UserFavoriteProject() favorite2.user_id = test_user favorite2.project_id = test_project db.session.add(favorite2) # Should raise IntegrityError with pytest.raises(Exception): # SQLAlchemy will raise IntegrityError db.session.commit() def test_favorite_to_dict(self, app, test_user, test_project): """Test favorite project to_dict method.""" with app.app_context(): favorite = UserFavoriteProject() favorite.user_id = test_user favorite.project_id = test_project db.session.add(favorite) db.session.commit() data = favorite.to_dict() assert "id" in data assert "user_id" in data assert "project_id" in data assert "created_at" in data assert data["user_id"] == test_user assert data["project_id"] == test_project class TestUserFavoriteProjectMethods: """Test User model methods for managing favorite projects.""" def test_add_favorite_project(self, app, test_user, test_project): """Test adding a project to user's favorites.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add to favorites user.add_favorite_project(project) # Verify it was added assert user.is_project_favorite(project) assert project in user.favorite_projects.all() def test_add_favorite_project_idempotent(self, app, test_user, test_project): """Test that adding a favorite twice doesn't cause errors.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add twice user.add_favorite_project(project) user.add_favorite_project(project) # Should still only have one favorite entry favorites = UserFavoriteProject.query.filter_by(user_id=test_user, project_id=test_project).all() assert len(favorites) == 1 def test_remove_favorite_project(self, app, test_user, test_project): """Test removing a project from user's favorites.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add then remove user.add_favorite_project(project) assert user.is_project_favorite(project) user.remove_favorite_project(project) assert not user.is_project_favorite(project) def test_is_project_favorite_with_id(self, app, test_user, test_project): """Test checking if project is favorite using project ID.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Not a favorite yet assert not user.is_project_favorite(test_project) # Add to favorites user.add_favorite_project(project) # Check with ID assert user.is_project_favorite(test_project) def test_get_favorite_projects(self, app, test_user, test_project, test_project_2): """Test getting user's favorite projects.""" with app.app_context(): user = db.session.get(User, test_user) project1 = db.session.get(Project, test_project) project2 = db.session.get(Project, test_project_2) # Add both to favorites user.add_favorite_project(project1) user.add_favorite_project(project2) # Get favorites favorites = user.get_favorite_projects() assert len(favorites) == 2 assert project1 in favorites assert project2 in favorites def test_get_favorite_projects_filtered_by_status(self, app, test_user, test_project, test_project_2): """Test getting favorite projects filtered by status.""" with app.app_context(): user = db.session.get(User, test_user) project1 = db.session.get(Project, test_project) project2 = db.session.get(Project, test_project_2) # Set different statuses project1.status = "active" project2.status = "archived" db.session.commit() # Add both to favorites user.add_favorite_project(project1) user.add_favorite_project(project2) # Get only active favorites active_favorites = user.get_favorite_projects(status="active") assert len(active_favorites) == 1 assert project1 in active_favorites assert project2 not in active_favorites class TestProjectFavoriteMethods: """Test Project model methods for favorite functionality.""" def test_is_favorited_by_user(self, app, test_user, test_project): """Test checking if project is favorited by a specific user.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Not favorited yet assert not project.is_favorited_by(user) # Add to favorites user.add_favorite_project(project) # Now should be favorited assert project.is_favorited_by(user) def test_is_favorited_by_user_id(self, app, test_user, test_project): """Test checking if project is favorited using user ID.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add to favorites user.add_favorite_project(project) # Check with user ID assert project.is_favorited_by(test_user) def test_project_to_dict_with_favorite_status(self, app, test_user, test_project): """Test project to_dict includes favorite status when user provided.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Without user, no is_favorite field data = project.to_dict() assert "is_favorite" not in data # With user, includes is_favorite data_with_user = project.to_dict(user=user) assert "is_favorite" in data_with_user assert data_with_user["is_favorite"] is False # Add to favorites user.add_favorite_project(project) # Now should be True data_favorited = project.to_dict(user=user) assert data_favorited["is_favorite"] is True # Route Tests class TestFavoriteProjectRoutes: """Test favorite project routes and endpoints.""" def test_favorite_project_route(self, app, client_fixture, test_user, test_project): """Test favoriting a project via POST route.""" with app.app_context(): # Login as test user with client_fixture.session_transaction() as sess: sess["_user_id"] = str(test_user) # Favorite the project response = client_fixture.post( f"/projects/{test_project}/favorite", headers={"X-Requested-With": "XMLHttpRequest"} ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True # Verify in database user = db.session.get(User, test_user) assert user.is_project_favorite(test_project) def test_unfavorite_project_route(self, app, client_fixture, test_user, test_project): """Test unfavoriting a project via POST route.""" with app.app_context(): # Setup: Add to favorites first user = db.session.get(User, test_user) project = db.session.get(Project, test_project) user.add_favorite_project(project) # Login with client_fixture.session_transaction() as sess: sess["_user_id"] = str(test_user) # Unfavorite the project response = client_fixture.post( f"/projects/{test_project}/unfavorite", headers={"X-Requested-With": "XMLHttpRequest"} ) assert response.status_code == 200 data = response.get_json() assert data["success"] is True # Verify in database user = db.session.get(User, test_user) assert not user.is_project_favorite(test_project) def test_favorite_nonexistent_project(self, app, client_fixture, test_user): """Test favoriting a non-existent project returns 404.""" with app.app_context(): with client_fixture.session_transaction() as sess: sess["_user_id"] = str(test_user) response = client_fixture.post("/projects/99999/favorite") assert response.status_code == 404 def test_favorite_project_requires_login(self, app, client_fixture, test_project): """Test that favoriting requires authentication.""" with app.app_context(): response = client_fixture.post(f"/projects/{test_project}/favorite") # Should redirect to login assert response.status_code in [302, 401] class TestFavoriteProjectFiltering: """Test filtering projects by favorites.""" def test_list_projects_with_favorites_filter(self, app, client_fixture, test_user, test_project, test_project_2): """Test listing only favorite projects.""" with app.app_context(): # Setup: Favorite only one project user = db.session.get(User, test_user) project1 = db.session.get(Project, test_project) user.add_favorite_project(project1) # Login with client_fixture.session_transaction() as sess: sess["_user_id"] = str(test_user) # Request favorites only response = client_fixture.get("/projects?favorites=true") assert response.status_code == 200 # Check that the response contains the favorite project assert b"Test Project" in response.data def test_list_all_projects_without_filter(self, app, client_fixture, test_user, test_project, test_project_2): """Test listing all projects without favorites filter.""" with app.app_context(): # Setup: Favorite only one project user = db.session.get(User, test_user) project1 = db.session.get(Project, test_project) user.add_favorite_project(project1) # Login with client_fixture.session_transaction() as sess: sess["_user_id"] = str(test_user) # Request all projects response = client_fixture.get("/projects") assert response.status_code == 200 # Both projects should be in response assert b"Test Project" in response.data # Relationship Tests class TestFavoriteProjectRelationships: """Test database relationships and cascade behavior.""" def test_delete_user_cascades_favorites(self, app, test_user, test_project): """Test that deleting a user removes their favorite entries.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add to favorites user.add_favorite_project(project) # Verify favorite exists favorite_count = UserFavoriteProject.query.filter_by(user_id=test_user).count() assert favorite_count == 1 # Delete user db.session.delete(user) db.session.commit() # Favorite should be deleted favorite_count = UserFavoriteProject.query.filter_by(user_id=test_user).count() assert favorite_count == 0 def test_delete_project_cascades_favorites(self, app, test_user, test_project): """Test that deleting a project removes related favorite entries.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Add to favorites user.add_favorite_project(project) # Verify favorite exists favorite_count = UserFavoriteProject.query.filter_by(project_id=test_project).count() assert favorite_count == 1 # Delete project db.session.delete(project) db.session.commit() # Favorite should be deleted favorite_count = UserFavoriteProject.query.filter_by(project_id=test_project).count() assert favorite_count == 0 def test_multiple_users_favorite_same_project(self, app, test_user, test_admin, test_project): """Test that multiple users can favorite the same project.""" with app.app_context(): user = db.session.get(User, test_user) admin = db.session.get(User, test_admin) project = db.session.get(Project, test_project) # Both favorite the same project user.add_favorite_project(project) admin.add_favorite_project(project) # Verify both have it as favorite assert user.is_project_favorite(project) assert admin.is_project_favorite(project) # Verify database has 2 entries favorite_count = UserFavoriteProject.query.filter_by(project_id=test_project).count() assert favorite_count == 2 # Smoke Tests class TestFavoriteProjectsSmoke: """Smoke tests to verify basic favorite projects functionality.""" def test_complete_favorite_workflow(self, app, test_user, test_project): """Test complete workflow: add favorite, check status, remove favorite.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Initially not favorited assert not user.is_project_favorite(project) # Add to favorites user.add_favorite_project(project) assert user.is_project_favorite(project) # Get favorites list favorites = user.get_favorite_projects() assert len(favorites) == 1 assert project in favorites # Remove from favorites user.remove_favorite_project(project) assert not user.is_project_favorite(project) # Favorites list should be empty favorites = user.get_favorite_projects() assert len(favorites) == 0 def test_favorite_with_archived_projects(self, app, test_user, test_project): """Test that favoriting works with archived projects.""" with app.app_context(): user = db.session.get(User, test_user) project = db.session.get(Project, test_project) # Favorite an active project user.add_favorite_project(project) # Archive the project project.status = "archived" db.session.commit() # Should still be favorited assert user.is_project_favorite(project) # But won't appear in active favorites active_favorites = user.get_favorite_projects(status="active") assert len(active_favorites) == 0 # Will appear in archived favorites archived_favorites = user.get_favorite_projects(status="archived") assert len(archived_favorites) == 1