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

484 lines
16 KiB
Python

"""
Comprehensive tests for news core utilities.
Tests generate_card_id, generate_subscription_id, utc_now, hours_ago, get_local_date_string.
"""
from unittest.mock import Mock, patch
from datetime import datetime, timezone, timedelta
import uuid
import os
class TestGenerateCardId:
"""Tests for generate_card_id function."""
def test_returns_string(self):
"""Test 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(self):
"""Test returns a valid UUID string."""
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
# Should be parseable as UUID
parsed = uuid.UUID(result)
assert str(parsed) == result
def test_returns_unique_ids(self):
"""Test returns unique IDs on multiple calls."""
from local_deep_research.news.core.utils import generate_card_id
ids = [generate_card_id() for _ in range(100)]
assert len(set(ids)) == 100
def test_returns_uuid4_format(self):
"""Test returns UUID version 4."""
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
parsed = uuid.UUID(result)
assert parsed.version == 4
def test_returns_lowercase(self):
"""Test returns lowercase UUID."""
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
assert result == result.lower()
def test_length_is_36(self):
"""Test UUID string length is 36 characters."""
from local_deep_research.news.core.utils import generate_card_id
result = generate_card_id()
assert len(result) == 36
class TestGenerateSubscriptionId:
"""Tests for generate_subscription_id function."""
def test_returns_string(self):
"""Test 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(self):
"""Test returns a valid UUID string."""
from local_deep_research.news.core.utils import generate_subscription_id
result = generate_subscription_id()
parsed = uuid.UUID(result)
assert str(parsed) == result
def test_returns_unique_ids(self):
"""Test returns unique IDs on multiple calls."""
from local_deep_research.news.core.utils import generate_subscription_id
ids = [generate_subscription_id() for _ in range(100)]
assert len(set(ids)) == 100
def test_different_from_card_id(self):
"""Test subscription ID is different from card ID (statistically)."""
from local_deep_research.news.core.utils import (
generate_card_id,
generate_subscription_id,
)
card_id = generate_card_id()
sub_id = generate_subscription_id()
assert card_id != sub_id
class TestUtcNow:
"""Tests for utc_now function."""
def test_returns_datetime(self):
"""Test returns a datetime object."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
assert isinstance(result, datetime)
def test_has_utc_timezone(self):
"""Test returned datetime has UTC timezone."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
assert result.tzinfo is not None
assert result.tzinfo == timezone.utc
def test_is_close_to_current_time(self):
"""Test 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_consistent_on_multiple_calls(self):
"""Test times are consistent and increasing."""
from local_deep_research.news.core.utils import utc_now
times = [utc_now() for _ in range(10)]
# Should be monotonically non-decreasing
for i in range(len(times) - 1):
assert times[i] <= times[i + 1]
def test_not_naive_datetime(self):
"""Test returned datetime is not naive."""
from local_deep_research.news.core.utils import utc_now
result = utc_now()
# Naive datetimes have tzinfo=None
assert result.tzinfo is not None
class TestHoursAgo:
"""Tests for hours_ago function."""
def test_returns_float(self):
"""Test returns a float."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(hours=1)
result = hours_ago(past)
assert isinstance(result, float)
def test_one_hour_ago(self):
"""Test datetime 1 hour ago returns ~1."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(hours=1)
result = hours_ago(past)
assert 0.99 <= result <= 1.01
def test_two_hours_ago(self):
"""Test datetime 2 hours ago returns ~2."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(hours=2)
result = hours_ago(past)
assert 1.99 <= result <= 2.01
def test_future_returns_negative(self):
"""Test future datetime returns negative value."""
from local_deep_research.news.core.utils import hours_ago, utc_now
future = utc_now() + timedelta(hours=1)
result = hours_ago(future)
assert result < 0
assert -1.01 <= result <= -0.99
def test_now_returns_zero(self):
"""Test current datetime returns ~0."""
from local_deep_research.news.core.utils import hours_ago, utc_now
now = utc_now()
result = hours_ago(now)
assert -0.01 <= result <= 0.01
def test_half_hour_ago(self):
"""Test datetime 30 minutes ago returns ~0.5."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(minutes=30)
result = hours_ago(past)
assert 0.49 <= result <= 0.51
def test_handles_naive_datetime(self):
"""Test handles naive datetime by assuming UTC."""
from local_deep_research.news.core.utils import hours_ago
naive = datetime.utcnow() - timedelta(hours=1)
result = hours_ago(naive)
assert 0.99 <= result <= 1.01
def test_handles_aware_datetime(self):
"""Test handles timezone-aware datetime."""
from local_deep_research.news.core.utils import hours_ago
aware = datetime.now(timezone.utc) - timedelta(hours=2)
result = hours_ago(aware)
assert 1.99 <= result <= 2.01
def test_24_hours_ago(self):
"""Test datetime 24 hours ago returns ~24."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(days=1)
result = hours_ago(past)
assert 23.99 <= result <= 24.01
def test_fractional_hours(self):
"""Test fractional hours are accurate."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(hours=1, minutes=30)
result = hours_ago(past)
assert 1.49 <= result <= 1.51
class TestGetLocalDateString:
"""Tests for get_local_date_string function."""
def test_returns_string(self):
"""Test returns a string."""
from local_deep_research.news.core.utils import get_local_date_string
result = get_local_date_string()
assert isinstance(result, str)
def test_returns_iso_format(self):
"""Test returns ISO date format (YYYY-MM-DD)."""
from local_deep_research.news.core.utils import get_local_date_string
import re
result = get_local_date_string()
# Should match YYYY-MM-DD pattern
assert re.match(r"^\d{4}-\d{2}-\d{2}$", result)
def test_uses_utc_as_default(self):
"""Test uses UTC as default timezone."""
from local_deep_research.news.core.utils import get_local_date_string
# Clear TZ env var
with patch.dict(os.environ, {}, clear=True):
result = get_local_date_string()
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_uses_settings_manager_timezone(self):
"""Test uses timezone from settings_manager."""
from local_deep_research.news.core.utils import get_local_date_string
from zoneinfo import ZoneInfo
mock_settings = Mock()
mock_settings.get_setting.return_value = "America/New_York"
result = get_local_date_string(mock_settings)
mock_settings.get_setting.assert_called_once_with("app.timezone")
expected = datetime.now(ZoneInfo("America/New_York")).date().isoformat()
assert result == expected
def test_uses_tz_env_var_as_fallback(self):
"""Test uses TZ environment variable as fallback."""
from local_deep_research.news.core.utils import get_local_date_string
from zoneinfo import ZoneInfo
with patch.dict(os.environ, {"TZ": "Europe/London"}):
result = get_local_date_string(None)
expected = datetime.now(ZoneInfo("Europe/London")).date().isoformat()
assert result == expected
def test_handles_invalid_timezone(self):
"""Test handles invalid timezone by falling back to UTC."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = Mock()
mock_settings.get_setting.return_value = "Invalid/Timezone"
result = get_local_date_string(mock_settings)
# Should fall back to UTC
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_handles_settings_manager_exception(self):
"""Test handles exception from settings_manager."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = Mock()
mock_settings.get_setting.side_effect = Exception("Database error")
with patch.dict(os.environ, {}, clear=True):
result = get_local_date_string(mock_settings)
# Should fall back to UTC
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_handles_none_settings_manager(self):
"""Test handles None settings_manager."""
from local_deep_research.news.core.utils import get_local_date_string
with patch.dict(os.environ, {}, clear=True):
result = get_local_date_string(None)
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_settings_priority_over_env_var(self):
"""Test settings_manager takes priority over TZ env var."""
from local_deep_research.news.core.utils import get_local_date_string
from zoneinfo import ZoneInfo
mock_settings = Mock()
mock_settings.get_setting.return_value = "Asia/Tokyo"
with patch.dict(os.environ, {"TZ": "Europe/Paris"}):
result = get_local_date_string(mock_settings)
expected = datetime.now(ZoneInfo("Asia/Tokyo")).date().isoformat()
assert result == expected
def test_common_timezones(self):
"""Test works with common timezones."""
from local_deep_research.news.core.utils import get_local_date_string
timezones = [
"UTC",
"America/New_York",
"Europe/London",
"Asia/Tokyo",
"Australia/Sydney",
"Pacific/Honolulu",
]
for tz_name in timezones:
mock_settings = Mock()
mock_settings.get_setting.return_value = tz_name
result = get_local_date_string(mock_settings)
# Should return valid date format
assert len(result) == 10
class TestUtilsEdgeCases:
"""Edge case tests for utils module."""
def test_generate_card_id_thread_safe(self):
"""Test generate_card_id is thread-safe."""
from local_deep_research.news.core.utils import generate_card_id
import threading
ids = []
lock = threading.Lock()
def generate_ids():
for _ in range(100):
card_id = generate_card_id()
with lock:
ids.append(card_id)
threads = [threading.Thread(target=generate_ids) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# All IDs should be unique
assert len(ids) == 1000
assert len(set(ids)) == 1000
def test_hours_ago_very_old_datetime(self):
"""Test hours_ago with very old datetime."""
from local_deep_research.news.core.utils import hours_ago
old = datetime(2020, 1, 1, tzinfo=timezone.utc)
result = hours_ago(old)
assert result > 8760 # More than 1 year in hours
def test_hours_ago_very_far_future(self):
"""Test hours_ago with far future datetime."""
from local_deep_research.news.core.utils import hours_ago
future = datetime(2030, 1, 1, tzinfo=timezone.utc)
result = hours_ago(future)
assert result < 0
def test_get_local_date_string_empty_string_timezone(self):
"""Test get_local_date_string with empty string timezone."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = Mock()
mock_settings.get_setting.return_value = ""
with patch.dict(os.environ, {}, clear=True):
result = get_local_date_string(mock_settings)
# Empty string should fall back to UTC
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_get_local_date_string_whitespace_timezone(self):
"""Test get_local_date_string with whitespace timezone."""
from local_deep_research.news.core.utils import get_local_date_string
mock_settings = Mock()
mock_settings.get_setting.return_value = " "
with patch.dict(os.environ, {}, clear=True):
result = get_local_date_string(mock_settings)
# Whitespace should likely be treated as invalid, fall back to UTC
expected = datetime.now(timezone.utc).date().isoformat()
assert result == expected
def test_hours_ago_preserves_precision(self):
"""Test hours_ago preserves sub-hour precision."""
from local_deep_research.news.core.utils import hours_ago, utc_now
past = utc_now() - timedelta(hours=3, minutes=45, seconds=30)
result = hours_ago(past)
# 3 hours + 45.5 minutes = 3.7583...
assert 3.75 <= result <= 3.77
class TestUtilsIntegration:
"""Integration tests for utils module."""
def test_card_id_in_uuid_format_for_database(self):
"""Test card ID can be stored in database UUID field."""
from local_deep_research.news.core.utils import generate_card_id
card_id = generate_card_id()
# Should be valid for PostgreSQL UUID type
assert len(card_id) == 36
assert card_id.count("-") == 4
def test_subscription_id_in_uuid_format_for_database(self):
"""Test subscription ID can be stored in database UUID field."""
from local_deep_research.news.core.utils import generate_subscription_id
sub_id = generate_subscription_id()
assert len(sub_id) == 36
assert sub_id.count("-") == 4
def test_utc_now_compatible_with_database_timestamps(self):
"""Test utc_now produces database-compatible timestamps."""
from local_deep_research.news.core.utils import utc_now
now = utc_now()
# Should have standard datetime attributes
assert hasattr(now, "year")
assert hasattr(now, "month")
assert hasattr(now, "day")
assert hasattr(now, "hour")
assert hasattr(now, "minute")
assert hasattr(now, "second")
assert hasattr(now, "microsecond")
def test_date_string_format_for_news_placeholders(self):
"""Test date string works as YYYY-MM-DD placeholder replacement."""
from local_deep_research.news.core.utils import get_local_date_string
date_str = get_local_date_string()
# Should work for replacing placeholders
template = "News from YYYY-MM-DD"
result = template.replace("YYYY-MM-DD", date_str)
assert "YYYY-MM-DD" not in result
assert date_str in result