"""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())