mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2025-12-31 00:09:58 -06:00
Implement a complete, production-ready CI/CD pipeline that runs 100% on GitHub Actions with zero external dependencies. This replaces and consolidates existing workflows with an optimized, streamlined pipeline. ## Major Changes - Add 3 new workflows (ci-comprehensive, cd-development, cd-release) - Remove 2 redundant workflows (backed up) - Add 130+ tests across 4 new test files - Add 8 documentation guides (60+ KB) - Add developer tools and scripts
260 lines
7.3 KiB
Python
260 lines
7.3 KiB
Python
import pytest
|
|
from app import create_app, db
|
|
from app.models import User, Project, TimeEntry, Settings
|
|
from datetime import datetime, timedelta
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create application for testing"""
|
|
app = create_app({
|
|
'TESTING': True,
|
|
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
|
|
'WTF_CSRF_ENABLED': False,
|
|
'SECRET_KEY': 'test-secret-key'
|
|
})
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.drop_all()
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create test client"""
|
|
return app.test_client()
|
|
|
|
@pytest.fixture
|
|
def runner(app):
|
|
"""Create test CLI runner"""
|
|
return app.test_cli_runner()
|
|
|
|
@pytest.fixture
|
|
def user(app):
|
|
"""Create a test user"""
|
|
user = User(username='testuser', role='user')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
@pytest.fixture
|
|
def admin_user(app):
|
|
"""Create a test admin user"""
|
|
admin = User(username='admin', role='admin')
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
return admin
|
|
|
|
@pytest.fixture
|
|
def project(app):
|
|
"""Create a test project"""
|
|
project = Project(
|
|
name='Test Project',
|
|
client='Test Client',
|
|
description='Test project description',
|
|
billable=True,
|
|
hourly_rate=50.00
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project
|
|
|
|
@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
|
|
assert db.engine.dialect.has_table(db.engine, 'users')
|
|
assert db.engine.dialect.has_table(db.engine, 'projects')
|
|
assert db.engine.dialect.has_table(db.engine, 'time_entries')
|
|
assert db.engine.dialect.has_table(db.engine, 'settings')
|
|
|
|
@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():
|
|
project = Project(
|
|
name='Test Project',
|
|
client='Test Client',
|
|
description='Test description',
|
|
billable=True,
|
|
hourly_rate=50.00
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
assert project.id is not None
|
|
assert project.name == 'Test Project'
|
|
assert project.client == 'Test Client'
|
|
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"""
|
|
with app.app_context():
|
|
start_time = datetime.utcnow()
|
|
end_time = start_time + timedelta(hours=2)
|
|
|
|
entry = TimeEntry(
|
|
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.add(entry)
|
|
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"""
|
|
with app.app_context():
|
|
# Create active timer
|
|
timer = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=datetime.utcnow(),
|
|
source='auto'
|
|
)
|
|
db.session.add(timer)
|
|
db.session.commit()
|
|
|
|
assert timer.is_active is True
|
|
assert timer.end_time is None
|
|
|
|
# Stop timer
|
|
timer.stop_timer()
|
|
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"""
|
|
with app.app_context():
|
|
# No active timer initially
|
|
assert user.active_timer is None
|
|
|
|
# Create active timer
|
|
timer = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=datetime.utcnow(),
|
|
source='auto'
|
|
)
|
|
db.session.add(timer)
|
|
db.session.commit()
|
|
|
|
# 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"""
|
|
with app.app_context():
|
|
# Create time entries
|
|
start_time = datetime.utcnow()
|
|
entry1 = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start_time,
|
|
end_time=start_time + timedelta(hours=2),
|
|
source='manual'
|
|
)
|
|
entry2 = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start_time + timedelta(hours=3),
|
|
end_time=start_time + timedelta(hours=5),
|
|
source='manual'
|
|
)
|
|
db.session.add_all([entry1, entry2])
|
|
db.session.commit()
|
|
|
|
# Check totals
|
|
assert project.total_hours == 4.0
|
|
assert project.total_billable_hours == 4.0
|
|
assert float(project.estimated_cost) == 200.00 # 4 hours * 50 EUR
|
|
|
|
@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
|