7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
589 lines
21 KiB
Python
589 lines
21 KiB
Python
"""
|
|
Tests for news/core/utils.py
|
|
|
|
Tests cover:
|
|
- get_local_date_string: timezone priority chain, date boundaries, error handling, DST
|
|
- generate_card_id: UUID generation and uniqueness
|
|
- generate_subscription_id: UUID generation and uniqueness
|
|
- utc_now: timezone-aware UTC datetime
|
|
- hours_ago: time difference calculations
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timezone, timedelta
|
|
from unittest.mock import MagicMock, patch
|
|
from zoneinfo import ZoneInfo
|
|
|
|
|
|
class TestGetLocalDateString:
|
|
"""Tests for get_local_date_string function."""
|
|
|
|
def test_uses_settings_manager_timezone_when_provided(self):
|
|
"""Test that settings manager timezone has highest priority."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
# Mock settings manager returning New York timezone
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "America/New_York"
|
|
|
|
# At 3:00 UTC, it's still 22:00 previous day in NY (-5 hours)
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
# Create a real datetime for comparison
|
|
utc_time = datetime(2024, 1, 16, 3, 0, 0, tzinfo=timezone.utc)
|
|
ny_tz = ZoneInfo("America/New_York")
|
|
|
|
# Make datetime.now() return our controlled time
|
|
mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
mock_settings.get_setting.assert_called_once_with("app.timezone")
|
|
# In NY at 3:00 UTC (22:00 prev day EST), date is 2024-01-15
|
|
assert result == "2024-01-15"
|
|
|
|
def test_uses_tz_env_when_settings_manager_returns_none(self, monkeypatch):
|
|
"""Test TZ environment variable fallback when settings returns None."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = None
|
|
|
|
monkeypatch.setenv("TZ", "Asia/Tokyo")
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 1, 15, 20, 0, 0, tzinfo=timezone.utc)
|
|
tokyo_tz = ZoneInfo("Asia/Tokyo")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(tokyo_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# 20:00 UTC = 05:00 next day in Tokyo (+9 hours)
|
|
assert result == "2024-01-16"
|
|
|
|
def test_uses_tz_env_when_settings_manager_returns_empty_string(
|
|
self, monkeypatch
|
|
):
|
|
"""Test TZ env fallback when settings returns empty string."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = ""
|
|
|
|
monkeypatch.setenv("TZ", "Europe/London")
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
london_tz = ZoneInfo("Europe/London")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(london_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# London in summer is UTC+1, so 12:00 UTC = 13:00 BST, same date
|
|
assert result == "2024-06-15"
|
|
|
|
def test_uses_tz_env_when_no_settings_manager(self, monkeypatch):
|
|
"""Test TZ env fallback when settings_manager is None."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
monkeypatch.setenv("TZ", "Australia/Sydney")
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 1, 15, 15, 0, 0, tzinfo=timezone.utc)
|
|
sydney_tz = ZoneInfo("Australia/Sydney")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(sydney_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=None)
|
|
|
|
# 15:00 UTC = 02:00 next day in Sydney (+11 hours in summer)
|
|
assert result == "2024-01-16"
|
|
|
|
def test_defaults_to_utc_when_no_configuration(self, monkeypatch):
|
|
"""Test UTC fallback when no timezone configured."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
# Remove TZ from environment
|
|
monkeypatch.delenv("TZ", raising=False)
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
mock_datetime.now.return_value = utc_time
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=None)
|
|
|
|
assert result == "2024-01-15"
|
|
|
|
def test_date_boundary_late_night_positive_offset(self):
|
|
"""Test date boundary: 23:30 UTC becomes next day in UTC+9."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "Asia/Tokyo"
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 1, 15, 23, 30, 0, tzinfo=timezone.utc)
|
|
tokyo_tz = ZoneInfo("Asia/Tokyo")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(tokyo_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# 23:30 UTC = 08:30 next day in Tokyo
|
|
assert result == "2024-01-16"
|
|
|
|
def test_date_boundary_early_morning_negative_offset(self):
|
|
"""Test date boundary: 02:00 UTC becomes previous day in UTC-8."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "America/Los_Angeles"
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
utc_time = datetime(2024, 1, 16, 2, 0, 0, tzinfo=timezone.utc)
|
|
la_tz = ZoneInfo("America/Los_Angeles")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(la_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# 02:00 UTC = 18:00 previous day in LA (-8 hours)
|
|
assert result == "2024-01-15"
|
|
|
|
def test_returns_iso_format_date(self, monkeypatch):
|
|
"""Test that result matches YYYY-MM-DD format."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
import re
|
|
|
|
monkeypatch.delenv("TZ", raising=False)
|
|
|
|
result = get_local_date_string(settings_manager=None)
|
|
|
|
# Validate ISO date format
|
|
assert re.match(r"^\d{4}-\d{2}-\d{2}$", result) is not None
|
|
|
|
def test_invalid_timezone_from_settings_falls_back_to_utc(
|
|
self, monkeypatch
|
|
):
|
|
"""Test fallback to UTC when settings has invalid timezone."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "Invalid/Timezone"
|
|
|
|
monkeypatch.delenv("TZ", raising=False)
|
|
|
|
# Should not raise, should return UTC date
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# Should return a valid date (UTC)
|
|
assert len(result) == 10
|
|
assert result.count("-") == 2
|
|
|
|
def test_invalid_timezone_from_env_falls_back_to_utc(self, monkeypatch):
|
|
"""Test fallback to UTC when TZ env has invalid timezone."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
monkeypatch.setenv("TZ", "NotAReal/Timezone")
|
|
|
|
result = get_local_date_string(settings_manager=None)
|
|
|
|
# Should return a valid date (UTC)
|
|
assert len(result) == 10
|
|
|
|
def test_settings_manager_exception_falls_back_gracefully(
|
|
self, monkeypatch
|
|
):
|
|
"""Test graceful fallback when settings_manager raises exception."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.side_effect = RuntimeError(
|
|
"DB connection failed"
|
|
)
|
|
|
|
monkeypatch.setenv("TZ", "UTC")
|
|
|
|
# Should not crash, should fall back
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
assert len(result) == 10
|
|
|
|
def test_logs_warning_on_invalid_timezone(self, monkeypatch):
|
|
"""Test that warning is logged when timezone is invalid."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "Invalid/TZ"
|
|
|
|
monkeypatch.delenv("TZ", raising=False)
|
|
|
|
with patch("local_deep_research.news.core.utils.logger") as mock_logger:
|
|
get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# Should log a warning about invalid timezone
|
|
mock_logger.warning.assert_called_once()
|
|
call_args = mock_logger.warning.call_args[0][0]
|
|
assert "Invalid/TZ" in call_args
|
|
|
|
def test_handles_dst_spring_forward(self):
|
|
"""Test handling of DST spring forward edge case."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "America/New_York"
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
# March 10, 2024 at 2:30 AM - during spring forward
|
|
utc_time = datetime(2024, 3, 10, 7, 30, 0, tzinfo=timezone.utc)
|
|
ny_tz = ZoneInfo("America/New_York")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# Should handle gracefully - 7:30 UTC = 3:30 AM EDT
|
|
assert result == "2024-03-10"
|
|
|
|
def test_handles_dst_fall_back(self):
|
|
"""Test handling of DST fall back edge case."""
|
|
from local_deep_research.news.core.utils import get_local_date_string
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.get_setting.return_value = "America/New_York"
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.datetime"
|
|
) as mock_datetime:
|
|
# November 3, 2024 at 1:30 AM - during fall back
|
|
utc_time = datetime(2024, 11, 3, 6, 30, 0, tzinfo=timezone.utc)
|
|
ny_tz = ZoneInfo("America/New_York")
|
|
|
|
mock_datetime.now.return_value = utc_time.astimezone(ny_tz)
|
|
mock_datetime.side_effect = lambda *args, **kw: datetime(
|
|
*args, **kw
|
|
)
|
|
|
|
result = get_local_date_string(settings_manager=mock_settings)
|
|
|
|
# Should handle gracefully - 6:30 UTC = 1:30 AM EST
|
|
assert result == "2024-11-03"
|
|
|
|
|
|
class TestGenerateCardId:
|
|
"""Tests for generate_card_id function."""
|
|
|
|
def test_returns_string(self):
|
|
"""Test that generate_card_id returns a string."""
|
|
from local_deep_research.news.core.utils import generate_card_id
|
|
|
|
result = generate_card_id()
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_returns_valid_uuid_format(self):
|
|
"""Test that result is a valid UUID4 format."""
|
|
from local_deep_research.news.core.utils import generate_card_id
|
|
import uuid
|
|
|
|
result = generate_card_id()
|
|
|
|
# Should be 36 characters (8-4-4-4-12 with hyphens)
|
|
assert len(result) == 36
|
|
# Should be parseable as UUID
|
|
parsed = uuid.UUID(result)
|
|
# Should be version 4
|
|
assert parsed.version == 4
|
|
|
|
def test_returns_unique_values(self):
|
|
"""Test that 1000 calls return 1000 unique IDs."""
|
|
from local_deep_research.news.core.utils import generate_card_id
|
|
|
|
ids = [generate_card_id() for _ in range(1000)]
|
|
|
|
assert len(set(ids)) == 1000
|
|
|
|
def test_is_lowercase(self):
|
|
"""Test that result is lowercase."""
|
|
from local_deep_research.news.core.utils import generate_card_id
|
|
|
|
result = generate_card_id()
|
|
|
|
assert result == result.lower()
|
|
|
|
|
|
class TestGenerateSubscriptionId:
|
|
"""Tests for generate_subscription_id function."""
|
|
|
|
def test_returns_string(self):
|
|
"""Test that generate_subscription_id returns a string."""
|
|
from local_deep_research.news.core.utils import generate_subscription_id
|
|
|
|
result = generate_subscription_id()
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_returns_valid_uuid_format(self):
|
|
"""Test that result is a valid UUID4 format."""
|
|
from local_deep_research.news.core.utils import generate_subscription_id
|
|
import uuid
|
|
|
|
result = generate_subscription_id()
|
|
|
|
# Should be 36 characters
|
|
assert len(result) == 36
|
|
# Should be parseable as UUID
|
|
parsed = uuid.UUID(result)
|
|
# Should be version 4
|
|
assert parsed.version == 4
|
|
|
|
def test_returns_unique_values(self):
|
|
"""Test that subscription IDs are unique."""
|
|
from local_deep_research.news.core.utils import generate_subscription_id
|
|
|
|
ids = [generate_subscription_id() for _ in range(1000)]
|
|
|
|
assert len(set(ids)) == 1000
|
|
|
|
def test_different_from_card_ids(self):
|
|
"""Test that subscription and card IDs are independent."""
|
|
from local_deep_research.news.core.utils import (
|
|
generate_card_id,
|
|
generate_subscription_id,
|
|
)
|
|
|
|
card_ids = set(generate_card_id() for _ in range(100))
|
|
sub_ids = set(generate_subscription_id() for _ in range(100))
|
|
|
|
# Should be no overlap (extremely unlikely with UUIDs)
|
|
assert len(card_ids & sub_ids) == 0
|
|
|
|
|
|
class TestUtcNow:
|
|
"""Tests for utc_now function."""
|
|
|
|
def test_returns_datetime(self):
|
|
"""Test that utc_now returns a datetime object."""
|
|
from local_deep_research.news.core.utils import utc_now
|
|
|
|
result = utc_now()
|
|
|
|
assert isinstance(result, datetime)
|
|
|
|
def test_is_timezone_aware(self):
|
|
"""Test that returned datetime is timezone-aware."""
|
|
from local_deep_research.news.core.utils import utc_now
|
|
|
|
result = utc_now()
|
|
|
|
assert result.tzinfo is not None
|
|
|
|
def test_timezone_is_utc(self):
|
|
"""Test that timezone is UTC."""
|
|
from local_deep_research.news.core.utils import utc_now
|
|
|
|
result = utc_now()
|
|
|
|
assert result.tzinfo == timezone.utc
|
|
|
|
def test_returns_current_time(self):
|
|
"""Test that returned time is current (within reasonable tolerance)."""
|
|
from local_deep_research.news.core.utils import utc_now
|
|
|
|
before = datetime.now(timezone.utc)
|
|
result = utc_now()
|
|
after = datetime.now(timezone.utc)
|
|
|
|
assert before <= result <= after
|
|
|
|
def test_successive_calls_increase_monotonically(self):
|
|
"""Test that successive calls return increasing times."""
|
|
from local_deep_research.news.core.utils import utc_now
|
|
import time
|
|
|
|
t1 = utc_now()
|
|
time.sleep(0.001) # Small delay
|
|
t2 = utc_now()
|
|
|
|
assert t2 >= t1
|
|
|
|
|
|
class TestHoursAgo:
|
|
"""Tests for hours_ago function."""
|
|
|
|
def test_returns_float(self):
|
|
"""Test that hours_ago returns a float."""
|
|
from local_deep_research.news.core.utils import hours_ago
|
|
|
|
dt = datetime.now(timezone.utc)
|
|
result = hours_ago(dt)
|
|
|
|
assert isinstance(result, float)
|
|
|
|
def test_returns_zero_for_current_time(self):
|
|
"""Test that current time returns approximately zero."""
|
|
from local_deep_research.news.core.utils import hours_ago
|
|
|
|
now = datetime.now(timezone.utc)
|
|
result = hours_ago(now)
|
|
|
|
# Should be very close to zero
|
|
assert abs(result) < 0.01
|
|
|
|
def test_returns_positive_for_past(self):
|
|
"""Test that past datetime returns positive hours."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
two_hours_ago = utc_now() - timedelta(hours=2)
|
|
result = hours_ago(two_hours_ago)
|
|
|
|
assert pytest.approx(result, abs=0.01) == 2.0
|
|
|
|
def test_returns_negative_for_future(self):
|
|
"""Test that future datetime returns negative hours."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
three_hours_ahead = utc_now() + timedelta(hours=3)
|
|
result = hours_ago(three_hours_ahead)
|
|
|
|
assert pytest.approx(result, abs=0.01) == -3.0
|
|
|
|
def test_handles_naive_datetime_as_utc(self):
|
|
"""Test that naive datetime is treated as UTC."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
# Create a naive datetime 1 hour ago in UTC
|
|
one_hour_ago = utc_now() - timedelta(hours=1)
|
|
naive_dt = one_hour_ago.replace(tzinfo=None)
|
|
|
|
result = hours_ago(naive_dt)
|
|
|
|
assert pytest.approx(result, abs=0.01) == 1.0
|
|
|
|
def test_naive_datetime_does_not_modify_original(self):
|
|
"""Test that passing naive datetime doesn't modify the original."""
|
|
from local_deep_research.news.core.utils import hours_ago
|
|
|
|
naive_dt = datetime(2024, 1, 15, 12, 0, 0)
|
|
original_tzinfo = naive_dt.tzinfo
|
|
|
|
hours_ago(naive_dt)
|
|
|
|
# Original should still be naive
|
|
assert naive_dt.tzinfo == original_tzinfo
|
|
|
|
def test_aware_datetime_preserves_timezone_info(self):
|
|
"""Test calculation with non-UTC timezone datetime."""
|
|
from local_deep_research.news.core.utils import hours_ago
|
|
|
|
# Create a Tokyo time that's equivalent to a known UTC time
|
|
tokyo_tz = ZoneInfo("Asia/Tokyo")
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.utc_now"
|
|
) as mock_utc_now:
|
|
# Set "now" to 2024-01-15 12:00:00 UTC
|
|
mock_utc_now.return_value = datetime(
|
|
2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc
|
|
)
|
|
|
|
# Tokyo is UTC+9, so 21:00 Tokyo = 12:00 UTC
|
|
tokyo_time = datetime(2024, 1, 15, 21, 0, 0, tzinfo=tokyo_tz)
|
|
|
|
result = hours_ago(tokyo_time)
|
|
|
|
# Should be 0 hours ago since times are equivalent
|
|
assert pytest.approx(result, abs=0.01) == 0.0
|
|
|
|
def test_fractional_hours(self):
|
|
"""Test that 30 minutes returns 0.5 hours."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
thirty_min_ago = utc_now() - timedelta(minutes=30)
|
|
result = hours_ago(thirty_min_ago)
|
|
|
|
assert pytest.approx(result, abs=0.01) == 0.5
|
|
|
|
def test_large_time_difference(self):
|
|
"""Test calculation with 5 days (120 hours) difference."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
five_days_ago = utc_now() - timedelta(days=5)
|
|
result = hours_ago(five_days_ago)
|
|
|
|
assert pytest.approx(result, abs=0.1) == 120.0
|
|
|
|
def test_very_small_time_difference(self):
|
|
"""Test calculation with 10 seconds difference."""
|
|
from local_deep_research.news.core.utils import hours_ago, utc_now
|
|
|
|
ten_seconds_ago = utc_now() - timedelta(seconds=10)
|
|
result = hours_ago(ten_seconds_ago)
|
|
|
|
expected = 10 / 3600 # 10 seconds in hours
|
|
assert pytest.approx(result, abs=0.001) == expected
|
|
|
|
def test_handles_different_timezone_datetime(self):
|
|
"""Test correct conversion from different timezone."""
|
|
from local_deep_research.news.core.utils import hours_ago
|
|
|
|
ny_tz = ZoneInfo("America/New_York")
|
|
|
|
with patch(
|
|
"local_deep_research.news.core.utils.utc_now"
|
|
) as mock_utc_now:
|
|
# Set "now" to 2024-01-15 17:00:00 UTC
|
|
mock_utc_now.return_value = datetime(
|
|
2024, 1, 15, 17, 0, 0, tzinfo=timezone.utc
|
|
)
|
|
|
|
# NY is UTC-5, so 10:00 NY = 15:00 UTC (2 hours before our "now")
|
|
ny_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=ny_tz)
|
|
|
|
result = hours_ago(ny_time)
|
|
|
|
# Should be 2 hours ago
|
|
assert pytest.approx(result, abs=0.01) == 2.0
|