Files
TimeTracker/tests/test_time_entry_templates.py
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

648 lines
26 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"