79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
"""Integration tests for the repository layer. Requires a reachable Postgres."""
|
|
import asyncio
|
|
import sys
|
|
|
|
sys.path.insert(0, "/app")
|
|
|
|
from db.database import init_db, close_db_pool
|
|
from db.repository import Repository
|
|
|
|
TEST_SOURCE_ID = -1009999000001
|
|
TEST_TARGET_ID = -1009999000002
|
|
# Deliberately hostile title: quotes and a backslash must survive the JSONB round-trip.
|
|
TEST_TARGET_TITLE = 'News "Daily" \\ Channel'
|
|
|
|
|
|
async def _cleanup(repo: Repository):
|
|
pool = await repo._get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute("DELETE FROM posts WHERE source_channel_id = $1;", TEST_SOURCE_ID)
|
|
await conn.execute("DELETE FROM sources WHERE channel_id = $1;", TEST_SOURCE_ID)
|
|
await conn.execute("DELETE FROM targets WHERE channel_id = $1;", TEST_TARGET_ID)
|
|
|
|
|
|
async def run_tests():
|
|
await init_db()
|
|
repo = Repository()
|
|
await _cleanup(repo)
|
|
|
|
# 1. Sources are looked up by their Telegram channel_id, not the surrogate row id.
|
|
await repo.add_source(TEST_SOURCE_ID, "Source Tech", "source_tech")
|
|
source = await repo.get_source_by_channel_id(TEST_SOURCE_ID)
|
|
assert source is not None, "get_source_by_channel_id returned None for a registered source"
|
|
assert source.channel_id == TEST_SOURCE_ID
|
|
assert source.title == "Source Tech"
|
|
assert await repo.get_source_by_channel_id(-1000000000000) is None
|
|
|
|
# 2. Deduplication lookup by content hash.
|
|
post_id = await repo.create_raw_post(
|
|
source_channel_id=TEST_SOURCE_ID,
|
|
source_message_id=101,
|
|
raw_text="Breaking news: AI update released!",
|
|
content_hash="hash_12345",
|
|
)
|
|
assert post_id is not None
|
|
dup = await repo.find_duplicate_post("hash_12345")
|
|
assert dup is not None and dup.id == post_id, "find_duplicate_post did not match a stored hash"
|
|
assert await repo.find_duplicate_post("no_such_hash") is None
|
|
|
|
# Re-inserting the same source message is rejected by the unique constraint.
|
|
assert await repo.create_raw_post(TEST_SOURCE_ID, 101, "dupe") is None
|
|
|
|
# 3. Queueing records the target without prematurely marking the post published.
|
|
target_id = await repo.add_target(TEST_TARGET_ID, TEST_TARGET_TITLE, "target_chan", post_interval_min=15)
|
|
await repo.record_post_queued_to_target(post_id, target_id, TEST_TARGET_TITLE)
|
|
post = await repo.get_post_by_id(post_id)
|
|
assert post.status == "pending_review", f"queueing must not publish, got {post.status}"
|
|
assert len(post.published_to) == 1, f"expected 1 queue entry, got {post.published_to}"
|
|
assert post.published_to[0]["target_title"] == TEST_TARGET_TITLE, "title was mangled in JSONB"
|
|
assert post.published_to[0]["published_at"] is None
|
|
|
|
# 4. Publishing flips status and stamps a real timestamp on the existing entry.
|
|
await repo.record_post_published_to_target(post_id, target_id, TEST_TARGET_TITLE)
|
|
post = await repo.get_post_by_id(post_id)
|
|
assert post.status == "published", f"expected published, got {post.status}"
|
|
assert len(post.published_to) == 1, f"publishing must not duplicate the entry, got {post.published_to}"
|
|
stamped = post.published_to[0]["published_at"]
|
|
assert stamped and "class" not in str(stamped), f"published_at is not a timestamp: {stamped!r}"
|
|
|
|
# 5. Counting by status must not require loading every row.
|
|
assert await repo.count_posts_by_status("published") >= 1
|
|
|
|
await _cleanup(repo)
|
|
await close_db_pool()
|
|
print("All repository tests passed successfully!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(run_tests())
|