""" 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) == '' # ============================================================================ # 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 active_timer = TimeEntry( user_id=user.id, project_id=project.id, start_time=local_now(), source='auto' ) db.session.add(active_timer) # 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'