""" API Tests for Bug Fixes Tests all the fixes for the identified bugs """ import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from datetime import datetime, timedelta, timezone import tempfile import os # Setup test database TEST_DB_FILE = tempfile.mktemp(suffix='.db') @pytest.fixture(scope="module") def test_db(): """Create test database""" from src.backend.models import Base from src.backend.database import get_db engine = create_engine(f'sqlite:///{TEST_DB_FILE}', connect_args={"check_same_thread": False}) Base.metadata.create_all(engine) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): try: db = TestingSessionLocal() yield db finally: db.close() yield engine, override_get_db os.unlink(TEST_DB_FILE) @pytest.fixture def client(test_db): """Create test client""" from src.backend.main import app from src.backend.dependencies import get_db engine, override_get_db = test_db app.dependency_overrides[get_db] = override_get_db with TestClient(app) as test_client: yield test_client app.dependency_overrides.clear() @pytest.fixture def session(test_db): """Create database session for setup""" engine, _ = test_db TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = TestingSessionLocal() yield session session.close() class TestBugFix1ElectionsEndpoints: """Test for Bug #1: Missing /api/elections/upcoming and /completed endpoints""" def test_upcoming_elections_endpoint_exists(self, client, session): """Test that GET /api/elections/upcoming endpoint exists and returns list""" # Register a user first register_resp = client.post("/api/auth/register", json={ "email": "test1@example.com", "password": "password123", "first_name": "John", "last_name": "Doe", "citizen_id": "ID123456" }) assert register_resp.status_code == 200 token = register_resp.json()["access_token"] # Test upcoming elections endpoint headers = {"Authorization": f"Bearer {token}"} response = client.get("/api/elections/upcoming", headers=headers) assert response.status_code == 200 data = response.json() assert isinstance(data, list) def test_completed_elections_endpoint_exists(self, client, session): """Test that GET /api/elections/completed endpoint exists and returns list""" # Register a user first register_resp = client.post("/api/auth/register", json={ "email": "test2@example.com", "password": "password123", "first_name": "Jane", "last_name": "Doe", "citizen_id": "ID789012" }) assert register_resp.status_code == 200 token = register_resp.json()["access_token"] # Test completed elections endpoint headers = {"Authorization": f"Bearer {token}"} response = client.get("/api/elections/completed", headers=headers) assert response.status_code == 200 data = response.json() assert isinstance(data, list) def test_upcoming_elections_returns_future_elections(self, client, session): """Test that upcoming endpoint correctly filters future elections""" from src.backend.models import Election, Candidate now = datetime.now(timezone.utc) # Create upcoming election upcoming_election = Election( name="Future Election", description="An election in the future", start_date=now + timedelta(days=10), end_date=now + timedelta(days=15), is_active=True ) session.add(upcoming_election) session.commit() # Register user register_resp = client.post("/api/auth/register", json={ "email": "test3@example.com", "password": "password123", "first_name": "Test", "last_name": "User", "citizen_id": "ID345678" }) token = register_resp.json()["access_token"] # Get upcoming response = client.get("/api/elections/upcoming", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 elections = response.json() assert any(e["name"] == "Future Election" for e in elections) def test_completed_elections_returns_past_elections(self, client, session): """Test that completed endpoint correctly filters past elections""" from src.backend.models import Election now = datetime.now(timezone.utc) # Create completed election completed_election = Election( name="Past Election", description="An election in the past", start_date=now - timedelta(days=10), end_date=now - timedelta(days=5), is_active=True ) session.add(completed_election) session.commit() # Register user register_resp = client.post("/api/auth/register", json={ "email": "test4@example.com", "password": "password123", "first_name": "Test", "last_name": "User", "citizen_id": "ID901234" }) token = register_resp.json()["access_token"] # Get completed response = client.get("/api/elections/completed", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 elections = response.json() assert any(e["name"] == "Past Election" for e in elections) class TestBugFix2AuthContextState: """Test for Bug #2: Auth context has_voted state inconsistency""" def test_login_returns_has_voted_field(self, client): """Test that login response includes has_voted field""" # Register a user client.post("/api/auth/register", json={ "email": "test5@example.com", "password": "password123", "first_name": "John", "last_name": "Voter", "citizen_id": "ID567890" }) # Login and check for has_voted in response response = client.post("/api/auth/login", json={ "email": "test5@example.com", "password": "password123" }) assert response.status_code == 200 data = response.json() assert "has_voted" in data assert isinstance(data["has_voted"], bool) def test_register_returns_has_voted_field(self, client): """Test that register response includes has_voted field""" response = client.post("/api/auth/register", json={ "email": "test6@example.com", "password": "password123", "first_name": "Jane", "last_name": "Voter", "citizen_id": "ID456789" }) assert response.status_code == 200 data = response.json() assert "has_voted" in data assert isinstance(data["has_voted"], bool) assert data["has_voted"] is False # New voter should not have voted def test_has_voted_reflects_actual_state(self, client, session): """Test that has_voted correctly reflects whether user voted""" from src.backend.models import Voter, Election, Candidate, Vote # Create voter voter = Voter( email="test7@example.com", password_hash="hashed_password", first_name="Test", last_name="Voter", citizen_id="ID789456", has_voted=False ) session.add(voter) session.commit() # Login before voting response = client.post("/api/auth/login", json={ "email": "test7@example.com", "password": "password" # Won't match but we need to setup properly }) # For proper test, use token from successful flow register_resp = client.post("/api/auth/register", json={ "email": "test7b@example.com", "password": "password123", "first_name": "Test", "last_name": "Voter", "citizen_id": "ID789457" }) login_resp = client.post("/api/auth/login", json={ "email": "test7b@example.com", "password": "password123" }) assert login_resp.json()["has_voted"] is False def test_profile_endpoint_returns_has_voted(self, client): """Test that profile endpoint returns has_voted field""" # Register and login client.post("/api/auth/register", json={ "email": "test8@example.com", "password": "password123", "first_name": "Test", "last_name": "User", "citizen_id": "ID234567" }) login_resp = client.post("/api/auth/login", json={ "email": "test8@example.com", "password": "password123" }) token = login_resp.json()["access_token"] # Get profile response = client.get("/api/auth/profile", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 data = response.json() assert "has_voted" in data class TestBugFix3TransactionSafety: """Test for Bug #3: Transaction safety in vote submission""" def test_vote_status_endpoint_exists(self, client): """Test that /api/votes/status endpoint exists""" # Register and login client.post("/api/auth/register", json={ "email": "test9@example.com", "password": "password123", "first_name": "Test", "last_name": "Voter", "citizen_id": "ID345902" }) login_resp = client.post("/api/auth/login", json={ "email": "test9@example.com", "password": "password123" }) token = login_resp.json()["access_token"] # Check vote status endpoint exists response = client.get( "/api/votes/status?election_id=999", headers={"Authorization": f"Bearer {token}"} ) # Should return 200 even if election doesn't exist (just returns has_voted: false) assert response.status_code == 200 assert "has_voted" in response.json() def test_vote_response_includes_marked_voted_status(self, client, session): """Test that vote submission response includes voter_marked_voted flag""" from src.backend.models import Election, Candidate now = datetime.now(timezone.utc) # Create election and candidate election = Election( name="Test Election", description="Test", start_date=now - timedelta(hours=1), end_date=now + timedelta(hours=1), is_active=True ) session.add(election) session.flush() election_id = election.id candidate = Candidate( election_id=election_id, name="Test Candidate", order=1 ) session.add(candidate) session.commit() # Register and login register_resp = client.post("/api/auth/register", json={ "email": "test10@example.com", "password": "password123", "first_name": "Test", "last_name": "Voter", "citizen_id": "ID456902" }) token = register_resp.json()["access_token"] # Submit vote response = client.post( "/api/votes", json={ "election_id": election_id, "choix": "Test Candidate" }, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert "voter_marked_voted" in data assert isinstance(data["voter_marked_voted"], bool) class TestBugFix4VoteStatusEndpoint: """Test for Bug #4: Missing /api/votes/status endpoint""" def test_vote_status_returns_has_voted_false_initially(self, client): """Test that vote status returns has_voted: false for new voter""" # Register and login client.post("/api/auth/register", json={ "email": "test11@example.com", "password": "password123", "first_name": "Test", "last_name": "User", "citizen_id": "ID567902" }) login_resp = client.post("/api/auth/login", json={ "email": "test11@example.com", "password": "password123" }) token = login_resp.json()["access_token"] # Check vote status for election response = client.get( "/api/votes/status?election_id=1", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert response.json()["has_voted"] is False def test_vote_status_requires_election_id_param(self, client): """Test that vote status endpoint requires election_id parameter""" # Register and login client.post("/api/auth/register", json={ "email": "test12@example.com", "password": "password123", "first_name": "Test", "last_name": "User", "citizen_id": "ID678902" }) login_resp = client.post("/api/auth/login", json={ "email": "test12@example.com", "password": "password123" }) token = login_resp.json()["access_token"] # Call without election_id should fail response = client.get( "/api/votes/status", headers={"Authorization": f"Bearer {token}"} ) # Should be 422 (validation error) or 400 assert response.status_code in [400, 422] def test_vote_status_requires_authentication(self, client): """Test that vote status endpoint requires authentication""" response = client.get("/api/votes/status?election_id=1") # Should be 401 (unauthorized) or 403 (forbidden) assert response.status_code in [401, 403] class TestIntegrationAllFixes: """Integration tests for all fixes working together""" def test_complete_flow_with_all_fixes(self, client, session): """Test complete flow: register -> login -> check status -> vote -> status updated""" from src.backend.models import Election, Candidate now = datetime.now(timezone.utc) # Create election and candidate election = Election( name="Complete Test Election", description="Test all fixes", start_date=now - timedelta(hours=1), end_date=now + timedelta(hours=1), is_active=True ) session.add(election) session.flush() election_id = election.id candidate = Candidate( election_id=election_id, name="Complete Test Candidate", order=1 ) session.add(candidate) session.commit() # 1. Register register_resp = client.post("/api/auth/register", json={ "email": "complete@example.com", "password": "password123", "first_name": "Complete", "last_name": "Test", "citizen_id": "ID789902" }) assert register_resp.status_code == 200 data = register_resp.json() assert "has_voted" in data assert data["has_voted"] is False token = data["access_token"] # 2. Login and verify has_voted is in response login_resp = client.post("/api/auth/login", json={ "email": "complete@example.com", "password": "password123" }) assert login_resp.status_code == 200 assert "has_voted" in login_resp.json() # 3. Check vote status before voting status_resp = client.get( f"/api/votes/status?election_id={election_id}", headers={"Authorization": f"Bearer {token}"} ) assert status_resp.status_code == 200 assert status_resp.json()["has_voted"] is False # 4. Submit vote vote_resp = client.post( "/api/votes", json={ "election_id": election_id, "choix": "Complete Test Candidate" }, headers={"Authorization": f"Bearer {token}"} ) assert vote_resp.status_code == 200 assert "voter_marked_voted" in vote_resp.json() # 5. Check that you can't vote twice vote_again_resp = client.post( "/api/votes", json={ "election_id": election_id, "choix": "Complete Test Candidate" }, headers={"Authorization": f"Bearer {token}"} ) assert vote_again_resp.status_code == 400 assert "already voted" in vote_again_resp.json()["detail"]