Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

466 lines
16 KiB
Python

"""
Extended tests for news/core/utils.py
Tests cover:
- get_local_date_string() - timezone handling and date formatting
- generate_card_id() - UUID generation
- generate_subscription_id() - UUID generation
- utc_now() - UTC time with timezone
- hours_ago() - time difference calculation
- Edge cases and error handling
"""
import pytest
from unittest.mock import MagicMock
from datetime import datetime, timezone, timedelta
import os
class TestGetLocalDateString:
"""Tests for get_local_date_string function."""
def test_returns_iso_date_format(self):
"""Returns date in YYYY-MM-DD format."""
from local_deep_research.news.core.utils import get_local_date_string
result = get_local_date_string()
# Should match ISO date format
assert len(result) == 10
assert result[4] == "-"
assert result[7] == "-"
def test_uses_settings_manager_timezone(self):
"""Uses timezone from settings_manager when provided."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = MagicMock()
mock_settings.get_setting.return_value = "America/New_York"
# Clear TZ env var temporarily
original_tz = os.environ.get("TZ")
if "TZ" in os.environ:
del os.environ["TZ"]
try:
result = get_local_date_string(settings_manager=mock_settings)
mock_settings.get_setting.assert_called_once_with("app.timezone")
assert isinstance(result, str)
finally:
if original_tz is not None:
os.environ["TZ"] = original_tz
def test_uses_tz_env_var_fallback(self):
"""Uses TZ environment variable when no settings_manager."""
from local_deep_research.news.core.utils import get_local_date_string
original_tz = os.environ.get("TZ")
try:
os.environ["TZ"] = "Europe/London"
result = get_local_date_string()
assert isinstance(result, str)
finally:
if original_tz is not None:
os.environ["TZ"] = original_tz
elif "TZ" in os.environ:
del os.environ["TZ"]
def test_defaults_to_utc(self):
"""Defaults to UTC when no timezone configured."""
from local_deep_research.news.core.utils import get_local_date_string
original_tz = os.environ.get("TZ")
try:
if "TZ" in os.environ:
del os.environ["TZ"]
result = get_local_date_string()
assert isinstance(result, str)
finally:
if original_tz is not None:
os.environ["TZ"] = original_tz
def test_handles_settings_manager_exception(self):
"""Handles exception from settings_manager gracefully."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = MagicMock()
mock_settings.get_setting.side_effect = Exception("DB error")
# Should not raise, just fall back
result = get_local_date_string(settings_manager=mock_settings)
assert isinstance(result, str)
def test_handles_invalid_timezone(self):
"""Falls back to UTC for 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"
result = get_local_date_string(settings_manager=mock_settings)
# Should fall back to UTC and still return valid date
assert isinstance(result, str)
assert len(result) == 10
def test_date_changes_with_timezone(self):
"""Date may differ based on timezone."""
from local_deep_research.news.core.utils import get_local_date_string
# This test verifies the function works with different timezones
# without asserting specific dates (which would be time-dependent)
mock_settings = MagicMock()
mock_settings.get_setting.return_value = "Pacific/Auckland"
result = get_local_date_string(settings_manager=mock_settings)
assert isinstance(result, str)
def test_settings_manager_returns_none(self):
"""Handles settings_manager returning None."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = MagicMock()
mock_settings.get_setting.return_value = None
original_tz = os.environ.get("TZ")
try:
if "TZ" in os.environ:
del os.environ["TZ"]
result = get_local_date_string(settings_manager=mock_settings)
assert isinstance(result, str)
finally:
if original_tz is not None:
os.environ["TZ"] = original_tz
def test_settings_manager_returns_empty_string(self):
"""Handles settings_manager returning empty string."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = MagicMock()
mock_settings.get_setting.return_value = ""
original_tz = os.environ.get("TZ")
try:
if "TZ" in os.environ:
del os.environ["TZ"]
result = get_local_date_string(settings_manager=mock_settings)
assert isinstance(result, str)
finally:
if original_tz is not None:
os.environ["TZ"] = original_tz
class TestGenerateCardId:
"""Tests for generate_card_id function."""
def test_returns_string(self):
"""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_uuid_format(self):
"""Returns string in UUID format (36 characters with dashes)."""
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
assert len(result) == 36
assert result.count("-") == 4
def test_generates_unique_ids(self):
"""Generates unique IDs on each call."""
from local_deep_research.news.core.utils import generate_card_id
ids = [generate_card_id() for _ in range(100)]
assert len(ids) == len(set(ids)) # All unique
def test_id_is_valid_uuid(self):
"""Generated ID is a valid UUID."""
import uuid
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
# Should not raise
parsed = uuid.UUID(result)
assert str(parsed) == result
class TestGenerateSubscriptionId:
"""Tests for generate_subscription_id function."""
def test_returns_string(self):
"""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_uuid_format(self):
"""Returns string in UUID format."""
from local_deep_research.news.core.utils import generate_subscription_id
result = generate_subscription_id()
assert len(result) == 36
assert result.count("-") == 4
def test_generates_unique_ids(self):
"""Generates unique IDs on each call."""
from local_deep_research.news.core.utils import generate_subscription_id
ids = [generate_subscription_id() for _ in range(100)]
assert len(ids) == len(set(ids))
def test_id_is_valid_uuid(self):
"""Generated ID is a valid UUID."""
import uuid
from local_deep_research.news.core.utils import generate_subscription_id
result = generate_subscription_id()
parsed = uuid.UUID(result)
assert str(parsed) == result
class TestUtcNow:
"""Tests for utc_now function."""
def test_returns_datetime(self):
"""Returns a datetime object."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
assert isinstance(result, datetime)
def test_has_timezone_info(self):
"""Returned datetime has timezone info."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
assert result.tzinfo is not None
def test_is_utc_timezone(self):
"""Returned datetime is in UTC timezone."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
assert result.tzinfo == timezone.utc
def test_returns_current_time(self):
"""Returned time is close to current time."""
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(self):
"""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_recent_datetime(self):
"""Calculates hours ago for recent datetime."""
from local_deep_research.news.core.utils import hours_ago
# 2 hours ago
dt = datetime.now(timezone.utc) - timedelta(hours=2)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == 2.0
def test_exact_hours(self):
"""Calculates exact hours correctly."""
from local_deep_research.news.core.utils import hours_ago
# Exactly 24 hours ago
dt = datetime.now(timezone.utc) - timedelta(hours=24)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == 24.0
def test_fractional_hours(self):
"""Handles fractional hours."""
from local_deep_research.news.core.utils import hours_ago
# 1.5 hours ago (90 minutes)
dt = datetime.now(timezone.utc) - timedelta(minutes=90)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == 1.5
def test_future_datetime_negative(self):
"""Future datetime returns negative value."""
from local_deep_research.news.core.utils import hours_ago
# 2 hours in the future
dt = datetime.now(timezone.utc) + timedelta(hours=2)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == -2.0
def test_handles_naive_datetime(self):
"""Handles naive datetime by assuming UTC."""
from local_deep_research.news.core.utils import hours_ago
# Naive datetime (no timezone) - the function adds UTC timezone
dt = datetime.utcnow() - timedelta(hours=3)
result = hours_ago(dt)
# Should be approximately 3 hours (with some tolerance for test execution time)
assert pytest.approx(result, abs=0.5) == 3.0
def test_zero_hours(self):
"""Returns approximately zero for current time."""
from local_deep_research.news.core.utils import hours_ago
dt = datetime.now(timezone.utc)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == 0.0
def test_very_old_datetime(self):
"""Handles very old datetime."""
from local_deep_research.news.core.utils import hours_ago
# 30 days ago = 720 hours
dt = datetime.now(timezone.utc) - timedelta(days=30)
result = hours_ago(dt)
assert pytest.approx(result, abs=1) == 720.0
def test_seconds_precision(self):
"""Handles seconds precision."""
from local_deep_research.news.core.utils import hours_ago
# 30 minutes ago = 0.5 hours
dt = datetime.now(timezone.utc) - timedelta(minutes=30)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.01) == 0.5
class TestEdgeCases:
"""Edge cases for utils functions."""
def test_generate_card_id_thread_safety(self):
"""IDs are unique even when generated concurrently."""
from local_deep_research.news.core.utils import generate_card_id
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as executor:
ids = list(executor.map(lambda _: generate_card_id(), range(100)))
assert len(ids) == len(set(ids))
def test_generate_subscription_id_thread_safety(self):
"""Subscription IDs are unique even when generated concurrently."""
from local_deep_research.news.core.utils import generate_subscription_id
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as executor:
ids = list(
executor.map(lambda _: generate_subscription_id(), range(100))
)
assert len(ids) == len(set(ids))
def test_hours_ago_with_different_timezones(self):
"""hours_ago works with different timezone-aware datetimes."""
from local_deep_research.news.core.utils import hours_ago
from zoneinfo import ZoneInfo
# Create datetime in a different timezone
ny_tz = ZoneInfo("America/New_York")
dt = datetime.now(ny_tz) - timedelta(hours=5)
result = hours_ago(dt)
assert pytest.approx(result, abs=0.1) == 5.0
def test_get_local_date_string_all_valid_timezones(self):
"""get_local_date_string works with various valid timezones."""
from local_deep_research.news.core.utils import get_local_date_string
timezones = [
"UTC",
"America/New_York",
"Europe/London",
"Asia/Tokyo",
"Australia/Sydney",
]
for tz_name in timezones:
mock_settings = MagicMock()
mock_settings.get_setting.return_value = tz_name
result = get_local_date_string(settings_manager=mock_settings)
assert isinstance(result, str)
assert len(result) == 10
def test_utc_now_called_rapidly(self):
"""utc_now can be called rapidly without issues."""
from local_deep_research.news.core.utils import utc_now
times = [utc_now() for _ in range(1000)]
# All should be valid datetime objects
assert all(isinstance(t, datetime) for t in times)
# All should have UTC timezone
assert all(t.tzinfo == timezone.utc for t in times)
# Times should be non-decreasing
for i in range(1, len(times)):
assert times[i] >= times[i - 1]
def test_hours_ago_leap_second(self):
"""hours_ago handles times that might span leap seconds."""
from local_deep_research.news.core.utils import hours_ago
# Use a fixed datetime that doesn't depend on current time
dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
# This will give hours from Jan 1, 2024 to now
result = hours_ago(dt)
# Should be positive and reasonable
assert result > 0
def test_card_id_format_consistent(self):
"""Card ID format is consistent."""
from local_deep_research.news.core.utils import generate_card_id
import re
uuid_pattern = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
)
for _ in range(10):
card_id = generate_card_id()
assert uuid_pattern.match(card_id)
def test_subscription_id_format_consistent(self):
"""Subscription ID format is consistent."""
from local_deep_research.news.core.utils import generate_subscription_id
import re
uuid_pattern = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
)
for _ in range(10):
sub_id = generate_subscription_id()
assert uuid_pattern.match(sub_id)