Files
TimeTracker/tests/test_user_settings.py
Dries Peeters fc81cc3d8c feat: Add tests and docs for User Settings page
Add extensive test coverage and documentation for the existing User Settings
page, completing the feature implementation to production-ready status.

## Changes

### Testing (44 tests, 100% passing)
- Add 30 unit tests in tests/test_user_settings.py
  * Page rendering and authentication tests
  * Form validation and preference update tests
  * API endpoint tests (PATCH /api/preferences, POST /api/theme)
  * Integration and CSRF protection tests
- Add 14 smoke tests in tests/smoke_test_user_settings.py
  * Basic functionality validation
  * Critical user path verification
  * Error handling checks

### Documentation
- Add docs/USER_SETTINGS_GUIDE.md
  * Comprehensive user guide for all settings
  * API documentation with examples
  * Database schema reference
  * Troubleshooting guide
  * Best practices for developers
- Add USER_SETTINGS_IMPLEMENTATION_SUMMARY.md
  * Complete implementation overview
  * Feature checklist and verification
  * Test results and metrics

## Features Tested

-  Profile information management (name, email)
-  Notification preferences (5 toggles)
-  Theme selection (light/dark/system) with live preview
-  Regional settings (timezone, date/time formats, week start)
-  Time rounding preferences (intervals, methods)
-  Overtime settings (standard hours per day)
-  API endpoints for AJAX updates
-  Input validation and error handling

## Test Coverage

- Settings page rendering: 4 tests
- Preference updates: 16 tests
- API endpoints: 7 tests
- Integration: 3 tests
- Smoke tests: 14 tests
- Total: 44 tests, 100% passing

## Notes

The User Settings feature backend and frontend were already fully implemented
in app/routes/user.py and app/templates/user/settings.html. This commit adds
the missing test co
2025-10-30 07:47:58 +01:00

570 lines
21 KiB
Python

"""
Comprehensive tests for user settings routes and functionality.
Tests settings page rendering, form validation, preference updates, and API endpoints.
"""
import pytest
from flask import url_for
from app.models import User
from app import db
import pytz
class TestUserSettingsPage:
"""Tests for the user settings page GET endpoint"""
def test_settings_page_requires_login(self, client):
"""Test that settings page redirects to login if not authenticated"""
response = client.get('/settings', follow_redirects=False)
assert response.status_code == 302
assert '/login' in response.location
def test_settings_page_loads_for_authenticated_user(self, client, user):
"""Test that settings page loads successfully for authenticated users"""
# Log in the user
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.get('/settings')
assert response.status_code == 200
assert b'Settings' in response.data
assert b'Notification Preferences' in response.data
assert b'Display Preferences' in response.data
assert b'Regional Settings' in response.data
def test_settings_page_displays_current_values(self, client, user):
"""Test that settings page displays user's current settings"""
# Set some specific settings
user.theme_preference = 'dark'
user.timezone = 'America/New_York'
user.date_format = 'MM/DD/YYYY'
user.email_notifications = True
db.session.commit()
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.get('/settings')
assert response.status_code == 200
data = response.data.decode('utf-8')
# Check that current values are selected
assert 'value="dark" selected' in data or 'value="dark"' in data
assert 'America/New_York' in data
assert 'email_notifications' in data
def test_settings_page_includes_all_sections(self, client, user):
"""Test that all settings sections are present on the page"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.get('/settings')
data = response.data.decode('utf-8')
# Check for all major sections
assert 'Profile Information' in data
assert 'Notification Preferences' in data
assert 'Display Preferences' in data
assert 'Time Rounding Preferences' in data
assert 'Overtime Settings' in data
assert 'Regional Settings' in data
class TestUserSettingsUpdate:
"""Tests for updating user settings via POST"""
def test_update_profile_information(self, client, user):
"""Test updating profile information (full name and email)"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'full_name': 'John Doe',
'email': 'john.doe@example.com'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Settings saved successfully' in response.data
# Verify changes in database
db.session.refresh(user)
assert user.full_name == 'John Doe'
assert user.email == 'john.doe@example.com'
def test_update_notification_preferences(self, client, user):
"""Test updating email notification preferences"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'email_notifications': 'on',
'notification_overdue_invoices': 'on',
'notification_task_assigned': 'on',
'notification_task_comments': 'on',
'notification_weekly_summary': 'on'
}, follow_redirects=True)
assert response.status_code == 200
# Verify changes
db.session.refresh(user)
assert user.email_notifications is True
assert user.notification_overdue_invoices is True
assert user.notification_task_assigned is True
assert user.notification_task_comments is True
assert user.notification_weekly_summary is True
def test_update_notification_preferences_all_disabled(self, client, user):
"""Test disabling all notification preferences"""
# First enable them
user.email_notifications = True
user.notification_overdue_invoices = True
db.session.commit()
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# POST without checkbox values (unchecked checkboxes don't send values)
response = client.post('/settings', data={
'full_name': user.full_name or ''
}, follow_redirects=True)
assert response.status_code == 200
# Verify all notifications are disabled
db.session.refresh(user)
assert user.email_notifications is False
assert user.notification_overdue_invoices is False
assert user.notification_task_assigned is False
assert user.notification_task_comments is False
assert user.notification_weekly_summary is False
def test_update_theme_preference(self, client, user):
"""Test updating theme preference"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Test setting dark theme
response = client.post('/settings', data={
'theme_preference': 'dark'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.theme_preference == 'dark'
# Test setting light theme
response = client.post('/settings', data={
'theme_preference': 'light'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.theme_preference == 'light'
# Test setting system default (empty string)
response = client.post('/settings', data={
'theme_preference': ''
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.theme_preference is None
def test_update_timezone(self, client, user):
"""Test updating timezone preference"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'timezone': 'America/New_York'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.timezone == 'America/New_York'
def test_update_timezone_with_invalid_value(self, client, user):
"""Test that invalid timezone is rejected"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'timezone': 'Invalid/Timezone'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Invalid timezone selected' in response.data
def test_update_date_format(self, client, user):
"""Test updating date format preference"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
for date_format in ['YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY', 'DD.MM.YYYY']:
response = client.post('/settings', data={
'date_format': date_format
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.date_format == date_format
def test_update_time_format(self, client, user):
"""Test updating time format preference (12h/24h)"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Test 24-hour format
response = client.post('/settings', data={
'time_format': '24h'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.time_format == '24h'
# Test 12-hour format
response = client.post('/settings', data={
'time_format': '12h'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.time_format == '12h'
def test_update_week_start_day(self, client, user):
"""Test updating week start day preference"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Test Monday (1)
response = client.post('/settings', data={
'week_start_day': '1'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.week_start_day == 1
# Test Sunday (0)
response = client.post('/settings', data={
'week_start_day': '0'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.week_start_day == 0
def test_update_time_rounding_preferences(self, client, user):
"""Test updating time rounding preferences"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'time_rounding_enabled': 'on',
'time_rounding_minutes': '15',
'time_rounding_method': 'up'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.time_rounding_enabled is True
assert user.time_rounding_minutes == 15
assert user.time_rounding_method == 'up'
def test_update_time_rounding_intervals(self, client, user):
"""Test all valid time rounding intervals"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
valid_intervals = [1, 5, 10, 15, 30, 60]
for interval in valid_intervals:
response = client.post('/settings', data={
'time_rounding_enabled': 'on',
'time_rounding_minutes': str(interval)
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.time_rounding_minutes == interval
def test_update_time_rounding_methods(self, client, user):
"""Test all valid time rounding methods"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
valid_methods = ['nearest', 'up', 'down']
for method in valid_methods:
response = client.post('/settings', data={
'time_rounding_enabled': 'on',
'time_rounding_method': method
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.time_rounding_method == method
def test_update_standard_hours_per_day(self, client, user):
"""Test updating standard hours per day for overtime calculation"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'standard_hours_per_day': '7.5'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.standard_hours_per_day == 7.5
def test_update_standard_hours_validation(self, client, user):
"""Test validation of standard hours per day (must be between 0.5 and 24)"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Test too low
response = client.post('/settings', data={
'standard_hours_per_day': '0.2'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Standard hours per day must be between 0.5 and 24' in response.data
# Test too high
response = client.post('/settings', data={
'standard_hours_per_day': '25'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Standard hours per day must be between 0.5 and 24' in response.data
def test_update_language_preference(self, client, user):
"""Test updating language preference"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'preferred_language': 'de'
}, follow_redirects=True)
assert response.status_code == 200
db.session.refresh(user)
assert user.preferred_language == 'de'
def test_update_multiple_settings_at_once(self, client, user):
"""Test updating multiple settings in a single request"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/settings', data={
'full_name': 'Jane Smith',
'email': 'jane@example.com',
'theme_preference': 'dark',
'timezone': 'Europe/London',
'date_format': 'DD/MM/YYYY',
'time_format': '24h',
'email_notifications': 'on',
'time_rounding_enabled': 'on',
'time_rounding_minutes': '15',
'standard_hours_per_day': '8'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Settings saved successfully' in response.data
# Verify all changes
db.session.refresh(user)
assert user.full_name == 'Jane Smith'
assert user.email == 'jane@example.com'
assert user.theme_preference == 'dark'
assert user.timezone == 'Europe/London'
assert user.date_format == 'DD/MM/YYYY'
assert user.time_format == '24h'
assert user.email_notifications is True
assert user.time_rounding_enabled is True
assert user.time_rounding_minutes == 15
assert user.standard_hours_per_day == 8.0
class TestUserSettingsAPIEndpoints:
"""Tests for API endpoints for updating preferences"""
def test_update_preferences_api_requires_login(self, client):
"""Test that API endpoint requires authentication"""
response = client.patch('/api/preferences',
json={'theme_preference': 'dark'})
assert response.status_code == 302 # Redirect to login
def test_update_theme_via_api(self, client, user):
"""Test updating theme via AJAX API"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.patch('/api/preferences',
json={'theme_preference': 'dark'})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
db.session.refresh(user)
assert user.theme_preference == 'dark'
def test_update_email_notifications_via_api(self, client, user):
"""Test updating email notifications via API"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.patch('/api/preferences',
json={'email_notifications': False})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
db.session.refresh(user)
assert user.email_notifications is False
def test_update_timezone_via_api(self, client, user):
"""Test updating timezone via API"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.patch('/api/preferences',
json={'timezone': 'Asia/Tokyo'})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
db.session.refresh(user)
assert user.timezone == 'Asia/Tokyo'
def test_update_invalid_timezone_via_api(self, client, user):
"""Test that API rejects invalid timezone"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.patch('/api/preferences',
json={'timezone': 'Invalid/Zone'})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
def test_set_theme_api_endpoint(self, client, user):
"""Test the dedicated theme switcher API endpoint"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Test setting dark theme
response = client.post('/api/theme',
json={'theme': 'dark'})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['theme'] == 'dark'
db.session.refresh(user)
assert user.theme_preference == 'dark'
# Test setting system default
response = client.post('/api/theme',
json={'theme': ''})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['theme'] == 'system'
db.session.refresh(user)
assert user.theme_preference is None
def test_set_invalid_theme_via_api(self, client, user):
"""Test that API rejects invalid theme values"""
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.post('/api/theme',
json={'theme': 'invalid'})
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
class TestUserSettingsIntegration:
"""Integration tests for user settings feature"""
def test_settings_persist_across_sessions(self, client, user):
"""Test that settings persist after saving and reloading"""
# Save settings
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
client.post('/settings', data={
'full_name': 'Test User',
'email': 'test@example.com',
'theme_preference': 'dark',
'timezone': 'America/Los_Angeles',
'date_format': 'MM/DD/YYYY',
'email_notifications': 'on',
'time_rounding_enabled': 'on',
'time_rounding_minutes': '15',
'standard_hours_per_day': '8'
}, follow_redirects=True)
# Reload page and verify settings are still there
response = client.get('/settings')
data = response.data.decode('utf-8')
assert 'Test User' in data
assert 'test@example.com' in data
assert 'America/Los_Angeles' in data
def test_default_settings_for_new_user(self, app):
"""Test that new users get appropriate default settings"""
with app.app_context():
new_user = User(username='newuser', role='user')
db.session.add(new_user)
db.session.commit()
# Check defaults
assert new_user.email_notifications is True
assert new_user.notification_overdue_invoices is True
assert new_user.notification_task_assigned is True
assert new_user.notification_task_comments is True
assert new_user.notification_weekly_summary is False
assert new_user.date_format == 'YYYY-MM-DD'
assert new_user.time_format == '24h'
assert new_user.week_start_day == 1
assert new_user.time_rounding_enabled is True
assert new_user.time_rounding_minutes == 1
assert new_user.time_rounding_method == 'nearest'
assert new_user.standard_hours_per_day == 8.0
def test_settings_form_csrf_protection(self, app):
"""Test that settings form is protected with CSRF token"""
# Create app with CSRF enabled
app.config['WTF_CSRF_ENABLED'] = True
client = app.test_client()
# Create a test user
with app.app_context():
user = User(username='testuser', role='user')
db.session.add(user)
db.session.commit()
user_id = user.id
with client.session_transaction() as sess:
sess['_user_id'] = str(user_id)
# Verify CSRF token is present in the form
response = client.get('/settings')
assert b'csrf_token' in response.data or b'CSRF' in response.data