Files
TimeTracker/tests/test_user_settings.py
Dries Peeters dcbdfcc288 feat: Add client custom fields, link templates, UI feature flags, and client billing support
Add client custom fields (JSON) for flexible data storage

Implement link templates system for dynamic URL generation from custom fields

Add client_id support to time entries for direct client billing (project_id now nullable)

Implement user-level UI feature flags for customizable navigation visibility

Add system-wide UI feature flags in settings for admin control

Fix metadata column naming (user_badges.achievement_metadata, leaderboard_entries.entry_metadata)

Update templates and routes to support new features

Add comprehensive UI feature flag management in admin and user settings

Enhance client views with custom fields and link template integration

Update time entry forms to support client billing

Add tests for system UI flags

Migrations: 075-080 for custom fields, link templates, UI flags, client billing, and metadata fixes
2025-11-29 06:17:07 +01:00

674 lines
25 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 "UI Customization" 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_update_ui_feature_flags(self, client, user):
"""Test updating UI feature flags"""
with client.session_transaction() as sess:
sess["_user_id"] = str(user.id)
response = client.post(
"/settings",
data={
"ui_show_inventory": "on",
"ui_show_mileage": "on",
# ui_show_per_diem and ui_show_kanban_board not checked (should be False)
},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Settings saved successfully" in response.data
# Verify changes
db.session.refresh(user)
assert user.ui_show_inventory is True
assert user.ui_show_mileage is True
assert user.ui_show_per_diem is False
assert user.ui_show_kanban_board is False
def test_ui_feature_flags_default_to_true(self, client, user):
"""Test that UI feature flags default to True for new users"""
# Create a new user
new_user = User(username="newuser", email="newuser@example.com")
new_user.set_password("password")
db.session.add(new_user)
db.session.commit()
# Check defaults
assert new_user.ui_show_inventory is True
assert new_user.ui_show_mileage is True
assert new_user.ui_show_per_diem is True
assert new_user.ui_show_kanban_board is True
def test_settings_page_includes_ui_customization_section(self, client, user):
"""Test that settings page includes UI customization section"""
with client.session_transaction() as sess:
sess["_user_id"] = str(user.id)
response = client.get("/settings")
data = response.data.decode("utf-8")
# Check for UI customization section and key flags
assert "UI Customization" in data
assert "ui_show_inventory" in data
assert "ui_show_mileage" in data
assert "ui_show_per_diem" in data
assert "ui_show_kanban_board" in data
assert "ui_show_calendar" in data
assert "ui_show_quotes" in data
assert "ui_show_reports" in data
assert "ui_show_analytics" in data
assert "ui_show_tools" in data
def test_update_ui_feature_flags(self, client, user):
"""Test updating UI feature flags"""
with client.session_transaction() as sess:
sess["_user_id"] = str(user.id)
response = client.post(
"/settings",
data={
"ui_show_inventory": "on",
"ui_show_mileage": "on",
"ui_show_calendar": "on",
"ui_show_quotes": "on",
# Other flags not checked (should be False)
},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Settings saved successfully" in response.data
# Verify changes
db.session.refresh(user)
assert user.ui_show_inventory is True
assert user.ui_show_mileage is True
assert user.ui_show_calendar is True
assert user.ui_show_quotes is True
assert user.ui_show_per_diem is False
assert user.ui_show_kanban_board is False
assert user.ui_show_reports is False
def test_ui_feature_flags_default_to_true(self, client, user):
"""Test that UI feature flags default to True for new users"""
# Create a new user
new_user = User(username="newuser", email="newuser@example.com")
new_user.set_password("password")
db.session.add(new_user)
db.session.commit()
# Check defaults - all should be True
assert new_user.ui_show_inventory is True
assert new_user.ui_show_mileage is True
assert new_user.ui_show_per_diem is True
assert new_user.ui_show_kanban_board is True
assert new_user.ui_show_calendar is True
assert new_user.ui_show_project_templates is True
assert new_user.ui_show_gantt_chart is True
assert new_user.ui_show_weekly_goals is True
assert new_user.ui_show_quotes is True
assert new_user.ui_show_reports is True
assert new_user.ui_show_analytics is True
assert new_user.ui_show_tools is True
def test_settings_page_includes_ui_customization_section(self, client, user):
"""Test that settings page includes UI customization section"""
with client.session_transaction() as sess:
sess["_user_id"] = str(user.id)
response = client.get("/settings")
data = response.data.decode("utf-8")
# Check for UI customization section and key flags
assert "UI Customization" in data
assert "ui_show_inventory" in data
assert "ui_show_mileage" in data
assert "ui_show_per_diem" in data
assert "ui_show_kanban_board" in data
assert "ui_show_calendar" in data
assert "ui_show_quotes" in data
assert "ui_show_reports" in data
assert "ui_show_analytics" in data
assert "ui_show_tools" 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