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

721 lines
27 KiB
Python

"""
Comprehensive tests for Time Entry Templates feature.
This module tests:
- TimeEntryTemplate model functionality
- Time entry template routes (CRUD operations)
- Template usage tracking
- Integration with time entries
"""
import pytest
from datetime import datetime
from app.models import TimeEntryTemplate, User, Project, Task, TimeEntry
from app import db
# ============================================================================
# Model Tests
# ============================================================================
@pytest.mark.models
class TestTimeEntryTemplateModel:
"""Test TimeEntryTemplate model functionality"""
def test_create_template_with_all_fields(self, app, user, project, task):
"""Test creating a template with all fields populated"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Daily Standup",
description="Template for daily standup meetings",
project_id=project.id,
task_id=task.id,
default_duration_minutes=15,
default_notes="Discussed progress and blockers",
tags="meeting,standup,daily",
billable=True
)
db.session.add(template)
db.session.commit()
# Verify all fields
assert template.id is not None
assert template.name == "Daily Standup"
assert template.description == "Template for daily standup meetings"
assert template.project_id == project.id
assert template.task_id == task.id
assert template.default_duration_minutes == 15
assert template.default_notes == "Discussed progress and blockers"
assert template.tags == "meeting,standup,daily"
assert template.billable is True
assert template.usage_count == 0
assert template.last_used_at is None
assert template.created_at is not None
assert template.updated_at is not None
def test_create_template_minimal_fields(self, app, user):
"""Test creating a template with only required fields"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Quick Task"
)
db.session.add(template)
db.session.commit()
assert template.id is not None
assert template.name == "Quick Task"
assert template.project_id is None
assert template.task_id is None
assert template.default_duration_minutes is None
assert template.default_notes is None
assert template.tags is None
assert template.billable is True # Default value
assert template.usage_count == 0
def test_template_default_duration_property(self, app, user):
"""Test the default_duration property (hours conversion)"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Test Template",
default_duration_minutes=90
)
db.session.add(template)
db.session.commit()
# Test getter
assert template.default_duration == 1.5
# Test setter
template.default_duration = 2.25
assert template.default_duration_minutes == 135
# Test None handling
template.default_duration = None
assert template.default_duration_minutes is None
assert template.default_duration is None
def test_template_record_usage(self, app, user):
"""Test the record_usage method"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Test Template"
)
db.session.add(template)
db.session.commit()
initial_count = template.usage_count
initial_last_used = template.last_used_at
# Record usage
template.record_usage()
db.session.commit()
assert template.usage_count == initial_count + 1
assert template.last_used_at is not None
assert template.last_used_at != initial_last_used
def test_template_increment_usage(self, app, user):
"""Test the increment_usage method"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Test Template"
)
db.session.add(template)
db.session.commit()
# Increment usage multiple times
for i in range(3):
template.increment_usage()
template_id = template.id
# Verify in new query
updated_template = TimeEntryTemplate.query.get(template_id)
assert updated_template.usage_count == 3
assert updated_template.last_used_at is not None
def test_template_to_dict(self, app, user, project, task):
"""Test the to_dict method"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Test Template",
description="Test description",
project_id=project.id,
task_id=task.id,
default_duration_minutes=60,
default_notes="Test notes",
tags="test,template",
billable=True
)
db.session.add(template)
db.session.commit()
template_dict = template.to_dict()
assert template_dict['id'] == template.id
assert template_dict['user_id'] == user.id
assert template_dict['name'] == "Test Template"
assert template_dict['description'] == "Test description"
assert template_dict['project_id'] == project.id
assert template_dict['project_name'] == project.name
assert template_dict['task_id'] == task.id
assert template_dict['task_name'] == task.name
assert template_dict['default_duration'] == 1.0
assert template_dict['default_duration_minutes'] == 60
assert template_dict['default_notes'] == "Test notes"
assert template_dict['tags'] == "test,template"
assert template_dict['billable'] is True
assert template_dict['usage_count'] == 0
assert 'created_at' in template_dict
assert 'updated_at' in template_dict
def test_template_relationships(self, app, user, project, task):
"""Test template relationships with user, project, and task"""
with app.app_context():
# Get IDs before context
user_id = user.id
project_id = project.id
task_id = task.id
template = TimeEntryTemplate(
user_id=user_id,
name="Test Template",
project_id=project_id,
task_id=task_id
)
db.session.add(template)
db.session.commit()
# Test relationships by ID
assert template.user_id == user_id
assert template.project_id == project_id
assert template.task_id == task_id
# Test relationship objects exist
assert template.user is not None
assert template.project is not None
assert template.task is not None
# Test relationship IDs match
assert template.user.id == user_id
assert template.project.id == project_id
assert template.task.id == task_id
def test_template_repr(self, app, user):
"""Test template __repr__ method"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name="Test Template"
)
db.session.add(template)
db.session.commit()
assert repr(template) == '<TimeEntryTemplate Test Template>'
# ============================================================================
# Route Tests
# ============================================================================
@pytest.mark.routes
class TestTimeEntryTemplateRoutes:
"""Test time entry template routes"""
def test_list_templates_authenticated(self, authenticated_client, user):
"""Test accessing templates list page when authenticated"""
response = authenticated_client.get('/templates')
assert response.status_code == 200
assert b'Time Entry Templates' in response.data
def test_list_templates_unauthenticated(self, client):
"""Test accessing templates list page without authentication"""
response = client.get('/templates', follow_redirects=False)
assert response.status_code == 302 # Redirect to login
@pytest.mark.smoke
def test_list_templates_with_usage_data(self, authenticated_client, user, project):
"""Test templates list page renders correctly with templates that have usage data"""
# Create a template with usage data (last_used_at set)
from datetime import datetime, timezone
from app.models import TimeEntryTemplate
from app import db
template = TimeEntryTemplate(
user_id=user.id,
name='Used Template',
project_id=project.id,
default_duration_minutes=60,
usage_count=5,
last_used_at=datetime.now(timezone.utc)
)
db.session.add(template)
db.session.commit()
# Access the list page
response = authenticated_client.get('/templates')
assert response.status_code == 200
assert b'Used Template' in response.data
# Verify that timeago filter is working (should show "just now" or similar)
assert b'ago' in response.data or b'just now' in response.data
def test_create_template_page_get(self, authenticated_client):
"""Test accessing create template page"""
response = authenticated_client.get('/templates/create')
assert response.status_code == 200
assert b'Create Time Entry Template' in response.data
assert b'Template Name' in response.data
def test_create_template_success(self, authenticated_client, user, project):
"""Test creating a new template successfully"""
response = authenticated_client.post('/templates/create', data={
'name': 'New Template',
'project_id': project.id,
'default_duration': '1.5',
'default_notes': 'Test notes',
'tags': 'test,new'
}, follow_redirects=True)
assert response.status_code == 200
assert b'created successfully' in response.data
# Verify template was created
template = TimeEntryTemplate.query.filter_by(
user_id=user.id,
name='New Template'
).first()
assert template is not None
assert template.project_id == project.id
assert template.default_duration == 1.5
assert template.default_notes == 'Test notes'
assert template.tags == 'test,new'
def test_create_template_without_name(self, authenticated_client):
"""Test creating a template without a name fails"""
response = authenticated_client.post('/templates/create', data={
'name': '',
'default_notes': 'Test notes'
}, follow_redirects=True)
assert response.status_code == 200
assert b'required' in response.data or b'error' in response.data
def test_create_template_duplicate_name(self, authenticated_client, user):
"""Test creating a template with duplicate name fails"""
# Create first template
template = TimeEntryTemplate(
user_id=user.id,
name='Duplicate Test'
)
db.session.add(template)
db.session.commit()
# Try to create another with same name
response = authenticated_client.post('/templates/create', data={
'name': 'Duplicate Test',
'default_notes': 'Test notes'
}, follow_redirects=True)
assert response.status_code == 200
assert b'already exists' in response.data
def test_edit_template_page_get(self, authenticated_client, user):
"""Test accessing edit template page"""
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Edit Test'
)
db.session.add(template)
db.session.commit()
response = authenticated_client.get(f'/templates/{template.id}/edit')
assert response.status_code == 200
assert b'Edit Test' in response.data
def test_edit_template_success(self, authenticated_client, user):
"""Test editing a template successfully"""
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Original Name'
)
db.session.add(template)
db.session.commit()
template_id = template.id
# Edit the template
response = authenticated_client.post(f'/templates/{template_id}/edit', data={
'name': 'Updated Name',
'default_notes': 'Updated notes'
}, follow_redirects=True)
assert response.status_code == 200
assert b'updated successfully' in response.data
# Verify update
updated_template = TimeEntryTemplate.query.get(template_id)
assert updated_template.name == 'Updated Name'
assert updated_template.default_notes == 'Updated notes'
def test_delete_template_success(self, authenticated_client, user):
"""Test deleting a template successfully"""
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Delete Test'
)
db.session.add(template)
db.session.commit()
template_id = template.id
# Delete the template
response = authenticated_client.post(f'/templates/{template_id}/delete',
follow_redirects=True)
assert response.status_code == 200
assert b'deleted successfully' in response.data
# Verify deletion
deleted_template = TimeEntryTemplate.query.get(template_id)
assert deleted_template is None
# View template test skipped - view.html template doesn't exist yet
# def test_view_template(self, authenticated_client, user):
# """Test viewing a single template"""
# template = TimeEntryTemplate(
# user_id=user.id,
# name='View Test',
# default_notes='Test notes'
# )
# db.session.add(template)
# db.session.commit()
#
# response = authenticated_client.get(f'/templates/{template.id}')
# assert response.status_code == 200
# assert b'View Test' in response.data
# assert b'Test notes' in response.data
# ============================================================================
# API Tests
# ============================================================================
@pytest.mark.api
class TestTimeEntryTemplateAPI:
"""Test time entry template API endpoints"""
def test_get_templates_api(self, authenticated_client, user):
"""Test getting templates via API"""
# Create some templates
for i in range(3):
template = TimeEntryTemplate(
user_id=user.id,
name=f'Template {i}'
)
db.session.add(template)
db.session.commit()
response = authenticated_client.get('/api/templates')
assert response.status_code == 200
data = response.get_json()
assert 'templates' in data
assert len(data['templates']) >= 3
def test_get_single_template_api(self, authenticated_client, user):
"""Test getting a single template via API"""
template = TimeEntryTemplate(
user_id=user.id,
name='API Test',
default_notes='Test notes'
)
db.session.add(template)
db.session.commit()
response = authenticated_client.get(f'/api/templates/{template.id}')
assert response.status_code == 200
data = response.get_json()
assert data['name'] == 'API Test'
assert data['default_notes'] == 'Test notes'
def test_use_template_api(self, authenticated_client, user):
"""Test marking template as used via API"""
template = TimeEntryTemplate(
user_id=user.id,
name='Use Test'
)
db.session.add(template)
db.session.commit()
template_id = template.id
response = authenticated_client.post(f'/api/templates/{template_id}/use')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
# Verify usage was recorded
updated_template = TimeEntryTemplate.query.get(template_id)
assert updated_template.usage_count == 1
assert updated_template.last_used_at is not None
# ============================================================================
# Smoke Tests
# ============================================================================
@pytest.mark.smoke
class TestTimeEntryTemplatesSmoke:
"""Smoke tests for time entry templates feature"""
def test_templates_page_renders(self, authenticated_client):
"""Smoke test: templates page renders without errors"""
response = authenticated_client.get('/templates')
assert response.status_code == 200
assert b'Time Entry Templates' in response.data
def test_create_template_page_renders(self, authenticated_client):
"""Smoke test: create template page renders without errors"""
response = authenticated_client.get('/templates/create')
assert response.status_code == 200
assert b'Create' in response.data
def test_template_crud_workflow(self, authenticated_client, user, project):
"""Smoke test: complete CRUD workflow for templates"""
# Create
response = authenticated_client.post('/templates/create', data={
'name': 'Smoke Test Template',
'project_id': project.id,
'default_notes': 'Smoke test'
}, follow_redirects=True)
assert response.status_code == 200
# Read
template = TimeEntryTemplate.query.filter_by(
user_id=user.id,
name='Smoke Test Template'
).first()
assert template is not None
# View test skipped - view.html doesn't exist yet
# response = authenticated_client.get(f'/templates/{template.id}')
# assert response.status_code == 200
# Update
response = authenticated_client.post(f'/templates/{template.id}/edit', data={
'name': 'Smoke Test Template Updated',
'default_notes': 'Updated notes'
}, follow_redirects=True)
assert response.status_code == 200
# Delete
response = authenticated_client.post(f'/templates/{template.id}/delete',
follow_redirects=True)
assert response.status_code == 200
# ============================================================================
# Integration Tests
# ============================================================================
@pytest.mark.integration
class TestTimeEntryTemplateIntegration:
"""Integration tests for time entry templates with other features"""
def test_start_timer_from_template(self, authenticated_client, user, project):
"""Test starting a timer directly from a template"""
# Create a template with project
template = TimeEntryTemplate(
user_id=user.id,
name='Timer Test',
project_id=project.id,
default_notes='Test timer notes',
tags='test,timer'
)
db.session.add(template)
db.session.commit()
template_id = template.id
# Start timer from template
response = authenticated_client.get(f'/timer/start/from-template/{template_id}',
follow_redirects=True)
assert response.status_code == 200
assert b'Timer started' in response.data
# Verify timer was created
timer = TimeEntry.query.filter_by(
user_id=user.id,
end_time=None
).first()
assert timer is not None
assert timer.project_id == project.id
assert timer.notes == 'Test timer notes'
assert timer.tags == 'test,timer'
# Verify usage was tracked
updated_template = TimeEntryTemplate.query.get(template_id)
assert updated_template.usage_count == 1
assert updated_template.last_used_at is not None
def test_start_timer_from_template_without_project(self, authenticated_client, user):
"""Test that starting timer from template without project fails"""
# Create template without project
template = TimeEntryTemplate(
user_id=user.id,
name='No Project Template'
)
db.session.add(template)
db.session.commit()
response = authenticated_client.get(f'/timer/start/from-template/{template.id}',
follow_redirects=True)
assert response.status_code == 200
assert b'must have a project' in response.data or b'error' in response.data
def test_start_timer_from_template_with_active_timer(self, authenticated_client, user, project):
"""Test that starting timer from template fails when user has active timer"""
from datetime import datetime
from app.models.time_entry import local_now
# Create an active timer
from factories import TimeEntryFactory
active_timer = TimeEntryFactory(
user_id=user.id,
project_id=project.id,
start_time=local_now(),
end_time=None,
source='auto'
)
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Test Template',
project_id=project.id
)
db.session.add(template)
db.session.commit()
# Try to start timer from template
response = authenticated_client.get(f'/timer/start/from-template/{template.id}',
follow_redirects=True)
assert response.status_code == 200
assert b'already have an active timer' in response.data
def test_manual_entry_with_template_prefill(self, authenticated_client, user, project, task):
"""Test manual entry page pre-fills from template"""
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Manual Entry Test',
project_id=project.id,
task_id=task.id,
default_notes='Prefilled notes',
tags='prefill,test'
)
db.session.add(template)
db.session.commit()
# Access manual entry page with template parameter
response = authenticated_client.get(f'/timer/manual?template={template.id}')
assert response.status_code == 200
# The page should render (full verification would require parsing HTML)
assert b'Manual Entry' in response.data or b'manual' in response.data
def test_start_timer_with_template_id(self, authenticated_client, user, project):
"""Test starting timer with template_id in form data"""
# Create a template
template = TimeEntryTemplate(
user_id=user.id,
name='Timer Form Test',
project_id=project.id,
default_notes='Template notes'
)
db.session.add(template)
db.session.commit()
# Start timer with template_id
response = authenticated_client.post('/timer/start', data={
'template_id': template.id,
'notes': '' # Should use template notes if empty
}, follow_redirects=True)
assert response.status_code == 200
# Verify timer was created (may fail if project validation fails)
# This is a partial test - full integration would require valid form data
def test_template_with_project_and_task(self, app, user, project, task):
"""Test template integration with projects and tasks"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name='Integration Test',
project_id=project.id,
task_id=task.id
)
db.session.add(template)
db.session.commit()
# Verify relationships work
assert template.project.name == project.name
assert template.task.name == task.name
def test_template_usage_tracking_over_time(self, app, user):
"""Test template usage tracking"""
with app.app_context():
template = TimeEntryTemplate(
user_id=user.id,
name='Usage Tracking Test'
)
db.session.add(template)
db.session.commit()
# Use template multiple times
usage_times = []
for _ in range(5):
template.record_usage()
usage_times.append(template.last_used_at)
db.session.commit()
assert template.usage_count == 5
# Last used time should be most recent
assert template.last_used_at == max(usage_times)
def test_multiple_users_separate_templates(self, app):
"""Test that templates are user-specific"""
with app.app_context():
# Create two users
user1 = User(username='template_user1', email='user1@test.com')
user1.is_active = True
user2 = User(username='template_user2', email='user2@test.com')
user2.is_active = True
db.session.add_all([user1, user2])
db.session.commit()
# Create templates for each user
template1 = TimeEntryTemplate(
user_id=user1.id,
name='User1 Template'
)
template2 = TimeEntryTemplate(
user_id=user2.id,
name='User2 Template'
)
db.session.add_all([template1, template2])
db.session.commit()
# Verify isolation
user1_templates = TimeEntryTemplate.query.filter_by(user_id=user1.id).all()
user2_templates = TimeEntryTemplate.query.filter_by(user_id=user2.id).all()
assert len(user1_templates) == 1
assert len(user2_templates) == 1
assert user1_templates[0].name == 'User1 Template'
assert user2_templates[0].name == 'User2 Template'