Files
TimeTracker/tests/test_basic.py
2025-11-14 12:08:50 +01:00

248 lines
7.3 KiB
Python

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:'