import pytest from app import db from app.models import User, Project, TimeEntry, Settings, Client from factories import TimeEntryFactory from datetime import datetime, timedelta from decimal import Decimal # Note: All fixtures are now imported from conftest.py # No duplicate fixtures needed here @pytest.mark.smoke @pytest.mark.unit def test_app_creation(app): """Test that the app can be created""" assert app is not None assert app.config["TESTING"] is True @pytest.mark.unit @pytest.mark.database def test_database_creation(app): """Test that database tables can be created""" with app.app_context(): # Check that tables exist using inspect from sqlalchemy import inspect inspector = inspect(db.engine) tables = inspector.get_table_names() assert "users" in tables assert "projects" in tables assert "time_entries" in tables assert "settings" in tables @pytest.mark.unit @pytest.mark.models def test_user_creation(app): """Test user creation""" with app.app_context(): user = User(username="testuser", role="user") db.session.add(user) db.session.commit() assert user.id is not None assert user.username == "testuser" assert user.role == "user" assert user.is_admin is False @pytest.mark.unit @pytest.mark.models def test_admin_user(app): """Test admin user properties""" with app.app_context(): admin = User(username="admin", role="admin") db.session.add(admin) db.session.commit() assert admin.is_admin is True @pytest.mark.unit @pytest.mark.models def test_project_creation(app): """Test project creation""" with app.app_context(): # Create a client first client = Client(name="Test Client", default_hourly_rate=Decimal("50.00")) db.session.add(client) db.session.commit() project = Project( name="Test Project", client_id=client.id, description="Test description", billable=True, hourly_rate=Decimal("50.00"), ) db.session.add(project) db.session.commit() assert project.id is not None assert project.name == "Test Project" assert project.client_id == client.id assert project.billable is True assert float(project.hourly_rate) == 50.00 @pytest.mark.unit @pytest.mark.models def test_time_entry_creation(app, user, project): """Test time entry creation""" start_time = datetime.utcnow() end_time = start_time + timedelta(hours=2) entry = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=start_time, end_time=end_time, notes="Test entry", tags="test,work", source="manual", ) db.session.commit() assert entry.id is not None assert entry.duration_hours == 2.0 assert entry.duration_formatted == "02:00:00" assert entry.tag_list == ["test", "work"] @pytest.mark.unit @pytest.mark.models def test_active_timer(app, user, project): """Test active timer functionality""" # Create active timer timer = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=datetime.utcnow(), source="auto", end_time=None ) db.session.commit() assert timer.is_active is True assert timer.end_time is None # Stop timer timer.stop_timer() db.session.commit() assert timer.is_active is False assert timer.end_time is not None assert timer.duration_seconds > 0 @pytest.mark.unit @pytest.mark.models def test_user_active_timer_property(app, user, project): """Test user active timer property""" # Refresh user to check initial state db.session.refresh(user) # Create active timer timer = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=datetime.utcnow(), source="auto", end_time=None ) db.session.commit() # Refresh user to load relationships db.session.expire(user) db.session.refresh(user) # Check active timer assert user.active_timer is not None assert user.active_timer.id == timer.id @pytest.mark.integration @pytest.mark.models def test_project_totals(app, user, project): """Test project total calculations""" # Create time entries start_time = datetime.utcnow() entry1 = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=start_time, end_time=start_time + timedelta(hours=2), source="manual", billable=True, ) entry2 = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=start_time + timedelta(hours=3), end_time=start_time + timedelta(hours=5), source="manual", billable=True, ) db.session.commit() # Refresh project to load relationships db.session.expire(project) db.session.refresh(project) # Check totals assert project.total_hours == 4.0 assert project.total_billable_hours == 4.0 expected_cost = 4.0 * float(project.hourly_rate) assert float(project.estimated_cost) == expected_cost @pytest.mark.unit @pytest.mark.models def test_settings_singleton(app): """Test settings singleton pattern""" with app.app_context(): # Get settings (should create if not exists) settings1 = Settings.get_settings() settings2 = Settings.get_settings() assert settings1.id == settings2.id assert settings1 is settings2 @pytest.mark.smoke @pytest.mark.routes def test_health_check(client): """Test health check endpoint""" response = client.get("/_health") assert response.status_code == 200 data = response.get_json() assert data["status"] == "healthy" @pytest.mark.smoke @pytest.mark.routes def test_login_page(client): """Test login page accessibility""" response = client.get("/login") assert response.status_code == 200 @pytest.mark.unit @pytest.mark.routes def test_protected_route_redirect(client): """Test that protected routes redirect to login""" response = client.get("/dashboard", follow_redirects=False) assert response.status_code == 302 assert "/login" in response.location @pytest.mark.smoke @pytest.mark.unit def test_testing_config_respects_database_url(): """Test that TestingConfig respects DATABASE_URL environment variable This test verifies the fix for GitHub Actions migration validation where DATABASE_URL is set to PostgreSQL but TestingConfig was hardcoded to SQLite. Note: This test runs with whatever DATABASE_URL is currently set in the environment. In CI/CD with DATABASE_URL set to PostgreSQL, it will use PostgreSQL. Locally without DATABASE_URL, it will use SQLite. """ import os from app.config import TestingConfig config = TestingConfig() # Verify that the config uses the DATABASE_URL if set, otherwise defaults to SQLite if "DATABASE_URL" in os.environ: # In CI/CD or when DATABASE_URL is explicitly set assert config.SQLALCHEMY_DATABASE_URI == os.environ["DATABASE_URL"] else: # Local development/testing without DATABASE_URL assert config.SQLALCHEMY_DATABASE_URI == "sqlite:///:memory:"