Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/database/repositories/dialog_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING

from src.database.pool import ReadConnection
from src.utils.datetime import parse_datetime
from src.utils.datetime import parse_utc_datetime

if TYPE_CHECKING:
from src.database.facade import Database
Expand Down Expand Up @@ -129,15 +129,20 @@ async def has_dialogs(self, phone: str) -> bool:
return bool(await cur.fetchone())

async def get_cached_at(self, phone: str) -> datetime | None:
"""Время самого свежего кэшированного диалога аккаунта (для оценки устаревания)."""
"""Время самого свежего кэшированного диалога аккаунта (для оценки устаревания).

Всегда UTC-aware: строки, вставленные без явного `cached_at`, получают его
из схемного DEFAULT `datetime('now')` — naive-значение без офсета, вычитание
которого из aware-времени падало бы с TypeError.
"""
cur = await self._db.execute(
"SELECT MAX(cached_at) AS cached_at FROM dialog_cache WHERE phone = ?",
(phone,),
)
row = await cur.fetchone()
if not row:
return None
return parse_datetime(row["cached_at"])
return parse_utc_datetime(row["cached_at"])

async def get_all_phones(self) -> list[str]:
"""Return all distinct phone numbers that have entries in dialog_cache."""
Expand Down
60 changes: 60 additions & 0 deletions tests/repositories/test_dialog_cache_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Регресс-гард на naive/aware границу row→model в кэше диалогов (#1291).

`dialog_cache.cached_at` имеет схемный DEFAULT `datetime('now')`, дающий
naive-строку без офсета. `get_cached_at` раньше отдавал её как есть, и
`pool_dialogs._get_db_cached_dialogs` падал на
`datetime.now(timezone.utc) - cached_at` с TypeError.
"""

from __future__ import annotations

from datetime import datetime, timezone

import pytest

from src.database import Database


@pytest.mark.anyio
async def test_get_cached_at_is_utc_aware_for_schema_default_row(tmp_path):
"""Строка, вставленная без явного cached_at, читается как UTC-aware (#1291)."""
db = Database(str(tmp_path / "test.db"))
await db.initialize()
try:
await db.execute_write(
"""
INSERT INTO dialog_cache (phone, dialog_id, title, channel_type)
VALUES (?, ?, ?, ?)
""",
("+70001", 1001, "Default cached_at", "channel"),
)

cached_at = await db.repos.dialog_cache.get_cached_at("+70001")

assert cached_at is not None
assert cached_at.tzinfo is not None
# Арифметика из pool_dialogs._get_db_cached_dialogs не должна падать.
age_sec = (datetime.now(timezone.utc) - cached_at).total_seconds()
assert age_sec >= 0
finally:
await db.close()


@pytest.mark.anyio
async def test_get_cached_at_preserves_aware_value_from_normal_write_path(tmp_path):
"""Обычный путь записи (aware isoformat) читается без сдвига времени (#1291)."""
db = Database(str(tmp_path / "test.db"))
await db.initialize()
try:
await db.repos.dialog_cache.replace_dialogs(
"+70002",
[{"channel_id": 2002, "title": "Aware", "channel_type": "channel"}],
)

cached_at = await db.repos.dialog_cache.get_cached_at("+70002")

assert cached_at is not None
assert cached_at.tzinfo is not None
assert abs((datetime.now(timezone.utc) - cached_at).total_seconds()) < 60
finally:
await db.close()
31 changes: 31 additions & 0 deletions tests/test_main_client_pool_collector_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,37 @@ async def test_get_db_cached_dialogs_channels_only(self):
assert result is not None
assert len(result) == 1 # dm filtered out

@pytest.mark.anyio
async def test_get_db_cached_dialogs_survives_naive_cached_at(self, tmp_path):
"""Regression #1291: schema DEFAULT datetime('now') gives naive cached_at;
_get_db_cached_dialogs must not raise TypeError on age subtraction."""
from src.database import Database
from src.telegram.client_pool import ClientPool

db = Database(str(tmp_path / "test.db"))
await db.initialize()
try:
# Insert a row that relies on the schema DEFAULT for cached_at → naive.
await db.execute_write(
"""
INSERT INTO dialog_cache (phone, dialog_id, title, channel_type)
VALUES (?, ?, ?, ?)
""",
("+1", 1, "Default cached_at", "channel"),
)

pool = ClientPool.__new__(ClientPool)
pool._dialogs_cache = {}
pool._dialogs_cache_ttl_sec = 300
pool._dialogs_db_cache_ttl_sec = 3600.0
pool._db = db

result = await pool._get_db_cached_dialogs("+1", "full")
assert result is not None
assert len(result) == 1
finally:
await db.close()

@pytest.mark.anyio
async def test_get_cached_dialog_found(self):
from src.telegram.client_pool import ClientPool
Expand Down
Loading