mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-03 18:19:46 -06:00
- 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.
324 lines
10 KiB
Python
324 lines
10 KiB
Python
import pytest
|
|
import pytz
|
|
from datetime import datetime, timedelta, timezone
|
|
from app import create_app, db
|
|
from app.models import Settings, TimeEntry, User, Project
|
|
from app.utils.timezone import (
|
|
get_app_timezone,
|
|
utc_to_local,
|
|
local_to_utc,
|
|
now_in_app_timezone,
|
|
convert_app_datetime_to_user,
|
|
format_user_datetime,
|
|
get_available_timezones,
|
|
)
|
|
from app.routes.user import update_preferences
|
|
from flask_login import login_user
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create application for testing"""
|
|
app = create_app(
|
|
{
|
|
"TESTING": True,
|
|
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
|
|
"WTF_CSRF_ENABLED": False,
|
|
"SECRET_KEY": "test-secret-key-for-testing-at-least-32-chars",
|
|
"FLASK_ENV": "testing",
|
|
}
|
|
)
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create test client"""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def user(app):
|
|
"""Create test user"""
|
|
with app.app_context():
|
|
user = User(username="testuser", role="user")
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def project(app):
|
|
"""Create test project"""
|
|
with app.app_context():
|
|
project = Project(name="Test Project", client="Test Client", billable=True, hourly_rate=50.0)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
def test_timezone_default_from_environment(app):
|
|
"""Test that timezone defaults to environment variable when no database settings exist"""
|
|
with app.app_context():
|
|
# Clear any existing settings
|
|
Settings.query.delete()
|
|
db.session.commit()
|
|
|
|
# Test default timezone
|
|
timezone = get_app_timezone()
|
|
assert timezone == "Europe/Rome" # Default from config
|
|
|
|
|
|
def test_timezone_from_database_settings(app):
|
|
"""Test that timezone is read from database settings"""
|
|
with app.app_context():
|
|
# Create settings with custom timezone
|
|
settings = Settings(timezone="America/New_York")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Test that timezone is read from database
|
|
timezone = get_app_timezone()
|
|
assert timezone == "America/New_York"
|
|
|
|
|
|
@pytest.mark.xfail(reason="Timezone display test needs adjustment - comparing timezone-aware datetimes")
|
|
def test_timezone_change_affects_display(app, user, project):
|
|
"""Test that changing timezone affects how times are displayed"""
|
|
with app.app_context():
|
|
# Create settings with Europe/Rome timezone
|
|
settings = Settings(timezone="Europe/Rome")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Create a time entry at a specific UTC time
|
|
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
|
|
|
|
# Refresh the user and project objects to ensure they're attached to the session
|
|
user = db.session.merge(user)
|
|
project = db.session.merge(project)
|
|
|
|
from factories import TimeEntryFactory
|
|
|
|
entry = TimeEntryFactory(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=utc_time,
|
|
end_time=utc_time + timedelta(hours=2),
|
|
source="manual",
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
# Get time in Rome timezone (UTC+1 or UTC+2 depending on DST)
|
|
rome_time = utc_to_local(entry.start_time)
|
|
|
|
# Change timezone to America/New_York
|
|
settings.timezone = "America/New_York"
|
|
db.session.commit()
|
|
|
|
# Get time in New York timezone (UTC-5 or UTC-4 depending on DST)
|
|
ny_time = utc_to_local(entry.start_time)
|
|
|
|
# Times should be different
|
|
assert rome_time != ny_time
|
|
|
|
# New York time should be earlier than Rome time (behind UTC)
|
|
# This is a basic check - actual difference depends on DST
|
|
assert ny_time.hour != rome_time.hour or abs(ny_time.hour - rome_time.hour) > 1
|
|
|
|
|
|
def test_timezone_aware_current_time(app):
|
|
"""Test that current time is returned in the configured timezone"""
|
|
with app.app_context():
|
|
# Set timezone to Europe/Rome
|
|
settings = Settings(timezone="Europe/Rome")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Get current time in app timezone
|
|
app_time = now_in_app_timezone()
|
|
utc_now = datetime.now(timezone.utc)
|
|
|
|
# App time should be in Rome timezone
|
|
assert app_time.tzinfo is not None
|
|
assert "Europe/Rome" in str(app_time.tzinfo)
|
|
|
|
# Convert app_time to UTC for comparison
|
|
app_time_utc = app_time.astimezone(timezone.utc)
|
|
|
|
# Times should be close (within a few seconds)
|
|
time_diff = abs((app_time_utc - utc_now).total_seconds())
|
|
assert time_diff < 10
|
|
|
|
|
|
def test_timezone_conversion_utc_to_local(app):
|
|
"""Test UTC to local timezone conversion"""
|
|
with app.app_context():
|
|
# Set timezone to Asia/Tokyo
|
|
settings = Settings(timezone="Asia/Tokyo")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Create a UTC time
|
|
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
|
|
|
|
# Convert to Tokyo time
|
|
tokyo_time = utc_to_local(utc_time)
|
|
|
|
# Tokyo time should be ahead of UTC (UTC+9)
|
|
assert tokyo_time.tzinfo is not None
|
|
assert "Asia/Tokyo" in str(tokyo_time.tzinfo)
|
|
|
|
# Tokyo time should be ahead of UTC time
|
|
# Tokyo is UTC+9, so 12:00 UTC should be 21:00 in Tokyo
|
|
assert tokyo_time.hour == 21
|
|
|
|
|
|
def test_timezone_conversion_local_to_utc(app):
|
|
"""Test local timezone to UTC conversion"""
|
|
with app.app_context():
|
|
# Set timezone to Europe/London
|
|
settings = Settings(timezone="Europe/London")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Create a local time (assumed to be in London timezone)
|
|
local_time = datetime.now().replace(hour=15, minute=30, second=0, microsecond=0)
|
|
|
|
# Convert to UTC
|
|
utc_time = local_to_utc(local_time)
|
|
|
|
# UTC time should have timezone info
|
|
assert utc_time.tzinfo is not None
|
|
|
|
# Convert back to local to verify
|
|
back_to_local = utc_to_local(utc_time)
|
|
assert back_to_local.hour == 15
|
|
assert back_to_local.minute == 30
|
|
|
|
|
|
def test_invalid_timezone_fallback(app):
|
|
"""Test that invalid timezone falls back to UTC"""
|
|
with app.app_context():
|
|
# Set invalid timezone
|
|
settings = Settings(timezone="Invalid/Timezone")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Should fall back to UTC
|
|
timezone = get_app_timezone()
|
|
assert timezone == "Invalid/Timezone" # Still stored in database
|
|
|
|
# But timezone object should fall back to UTC
|
|
from app.utils.timezone import get_timezone_obj
|
|
|
|
tz_obj = get_timezone_obj()
|
|
assert "UTC" in str(tz_obj)
|
|
|
|
|
|
def test_timezone_settings_update(app):
|
|
"""Test that updating timezone settings takes effect immediately"""
|
|
with app.app_context():
|
|
# Create initial settings
|
|
settings = Settings(timezone="Europe/Rome")
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
# Verify initial timezone
|
|
assert get_app_timezone() == "Europe/Rome"
|
|
|
|
# Update timezone
|
|
settings.timezone = "America/Los_Angeles"
|
|
db.session.commit()
|
|
|
|
# Verify timezone change takes effect
|
|
assert get_app_timezone() == "America/Los_Angeles"
|
|
|
|
# Test that time conversion uses new timezone
|
|
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
|
|
la_time = utc_to_local(utc_time)
|
|
|
|
# LA time should be in Pacific timezone
|
|
assert la_time.tzinfo is not None
|
|
assert "America/Los_Angeles" in str(la_time.tzinfo)
|
|
|
|
|
|
def test_get_available_timezones_matches_pytz_common():
|
|
"""Ensure available timezone helper mirrors pytz common timezones."""
|
|
timezones = get_available_timezones()
|
|
assert isinstance(timezones, tuple)
|
|
expected = tuple(sorted(pytz.common_timezones))
|
|
assert timezones == expected
|
|
|
|
|
|
def test_convert_app_datetime_to_user_respects_user_timezone(app, user):
|
|
"""Ensure user-specific timezone overrides application timezone for formatting."""
|
|
with app.app_context():
|
|
settings = Settings.get_settings()
|
|
settings.timezone = "America/Denver"
|
|
db.session.commit()
|
|
|
|
user = db.session.merge(user)
|
|
user.timezone = "Asia/Kolkata"
|
|
db.session.commit()
|
|
|
|
naive_app_time = datetime(2025, 1, 15, 9, 0, 0)
|
|
user_local = convert_app_datetime_to_user(naive_app_time, user=user)
|
|
|
|
assert user_local.tzinfo is not None
|
|
assert "Asia/Kolkata" in str(user_local.tzinfo)
|
|
assert user_local.hour == 21
|
|
assert user_local.minute == 30
|
|
|
|
formatted = format_user_datetime(naive_app_time, "%Y-%m-%d %H:%M", user=user)
|
|
assert formatted.endswith("21:30")
|
|
|
|
|
|
def test_convert_app_datetime_to_user_falls_back_to_app_timezone(app, user):
|
|
"""Ensure app timezone is used when user has no preference."""
|
|
with app.app_context():
|
|
settings = Settings.get_settings()
|
|
settings.timezone = "America/Denver"
|
|
db.session.commit()
|
|
|
|
user = db.session.merge(user)
|
|
user.timezone = None
|
|
db.session.commit()
|
|
|
|
naive_app_time = datetime(2025, 1, 15, 9, 0, 0)
|
|
local_time = convert_app_datetime_to_user(naive_app_time, user=user)
|
|
|
|
assert local_time.tzinfo is not None
|
|
assert "America/Denver" in str(local_time.tzinfo)
|
|
assert local_time.hour == 9
|
|
assert local_time.minute == 0
|
|
|
|
|
|
def test_update_preferences_allows_clearing_user_timezone(app, user):
|
|
"""Ensure API allows resetting to system default by clearing timezone."""
|
|
with app.app_context():
|
|
user = db.session.merge(user)
|
|
user.timezone = "Asia/Tokyo"
|
|
db.session.commit()
|
|
|
|
# Clear with empty string
|
|
with app.test_request_context("/user/api/preferences", method="PATCH", json={"timezone": ""}):
|
|
login_user(user)
|
|
response = update_preferences()
|
|
assert response.status_code == 200
|
|
assert user.timezone is None
|
|
|
|
# Reapply and clear with 'system'
|
|
user.timezone = "Asia/Tokyo"
|
|
db.session.commit()
|
|
with app.test_request_context("/user/api/preferences", method="PATCH", json={"timezone": "system"}):
|
|
login_user(user)
|
|
response = update_preferences()
|
|
assert response.status_code == 200
|
|
assert user.timezone is None
|