"""Tests for dashboard/db.py""" import asyncio import os import tempfile import time # Must set DB_PATH before importing db _tmpdir = tempfile.mkdtemp() os.environ["DB_PATH"] = os.path.join(_tmpdir, "test_reclip.db") import db # noqa: E402 _loop = asyncio.new_event_loop() def run(coro): return _loop.run_until_complete(coro) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _job(suffix=""): return f"job-{suffix or int(time.time() * 1000)}" async def _seed_download(job_id, platform="youtube", status="done", user_id=1, username="alice", chat_id=100, url="https://example.com/v"): await db.insert_download_start( job_id=job_id, user_id=user_id, username=username, chat_id=chat_id, url=url, platform=platform, ) if status == "done": await db.update_download_done( job_id=job_id, title="Test Video", format="mp4", quality="720p", file_size_bytes=1_000_000, download_duration_sec=3.5, ) elif status == "error": await db.update_download_error(job_id=job_id, error_message="network timeout") # --------------------------------------------------------------------------- # Task 1 tests: CRUD # --------------------------------------------------------------------------- def test_insert_download_start(): job_id = _job("start1") run(db.insert_download_start( job_id=job_id, user_id=42, username="bob", chat_id=999, url="https://youtube.com/watch?v=abc", platform="youtube", )) row = run(db.get_download_by_job_id(job_id)) assert row is not None assert row["job_id"] == job_id assert row["status"] == "started" assert row["user_id"] == 42 assert row["username"] == "bob" assert row["platform"] == "youtube" assert row["url"] == "https://youtube.com/watch?v=abc" def test_update_download_done(): job_id = _job("done1") run(db.insert_download_start( job_id=job_id, user_id=1, username="alice", chat_id=10, url="https://example.com", platform="tiktok", )) run(db.update_download_done( job_id=job_id, title="Cool Clip", format="mp4", quality="1080p", file_size_bytes=5_000_000, download_duration_sec=10.2, )) row = run(db.get_download_by_job_id(job_id)) assert row["status"] == "done" assert row["title"] == "Cool Clip" assert row["format"] == "mp4" assert row["quality"] == "1080p" assert row["file_size_bytes"] == 5_000_000 assert abs(row["download_duration_sec"] - 10.2) < 0.01 assert row["completed_at"] is not None def test_update_download_error(): job_id = _job("err1") run(db.insert_download_start( job_id=job_id, user_id=7, username="charlie", chat_id=77, url="https://example.com", platform="instagram", )) run(db.update_download_error(job_id=job_id, error_message="403 Forbidden")) row = run(db.get_download_by_job_id(job_id)) assert row["status"] == "error" assert row["error_message"] == "403 Forbidden" assert row["completed_at"] is not None def test_get_download_by_job_id_missing(): row = run(db.get_download_by_job_id("nonexistent-job-id")) assert row is None # --------------------------------------------------------------------------- # Task 2 tests: stats queries # --------------------------------------------------------------------------- def test_get_dashboard_stats_empty(): """Stats on a fresh DB (after inserts above, but we want structure check).""" stats = run(db.get_dashboard_stats()) assert "downloads_today" in stats assert "downloads_yesterday" in stats assert "active_users_24h" in stats assert "error_rate" in stats assert "error_rate_yesterday" in stats assert "errors_today" in stats assert "total_today" in stats def test_get_dashboard_stats_with_data(): job_done = _job("stats-done") job_err = _job("stats-err") run(_seed_download(job_done, platform="youtube", status="done")) run(_seed_download(job_err, platform="youtube", status="error")) stats = run(db.get_dashboard_stats()) # At least the downloads we inserted are counted assert stats["downloads_today"] >= 1 assert stats["errors_today"] >= 1 assert stats["total_today"] >= 2 def test_get_chart_data_ranges(): for rng in ("1D", "7D", "1M", "1Y"): data = run(db.get_chart_data(rng)) assert "labels" in data assert "values" in data assert "platforms" in data assert "top_users" in data assert isinstance(data["labels"], list) assert isinstance(data["values"], list) assert isinstance(data["platforms"], list) assert isinstance(data["top_users"], list) assert len(data["labels"]) == len(data["values"]) def test_get_chart_data_platform_top5(): data = run(db.get_chart_data("7D")) assert len(data["platforms"]) <= 5 for p in data["platforms"]: assert "platform" in p assert "count" in p def test_get_chart_data_top_users(): data = run(db.get_chart_data("7D")) assert len(data["top_users"]) <= 5 for u in data["top_users"]: assert "username" in u assert "count" in u def test_get_downloads_page_basic(): result = run(db.get_downloads_page(page=1, per_page=10)) assert "rows" in result assert "total" in result assert "pages" in result assert "page" in result assert result["page"] == 1 assert isinstance(result["rows"], list) def test_get_downloads_page_pagination(): # Seed 5 more downloads for i in range(5): run(_seed_download(_job(f"page-{i}"), platform="youtube", status="done", user_id=10 + i, username=f"user{i}")) result_p1 = run(db.get_downloads_page(page=1, per_page=3)) assert len(result_p1["rows"]) == 3 assert result_p1["total"] >= 5 result_p2 = run(db.get_downloads_page(page=2, per_page=3)) assert len(result_p2["rows"]) >= 1 # No overlap between pages ids_p1 = {r["id"] for r in result_p1["rows"]} ids_p2 = {r["id"] for r in result_p2["rows"]} assert ids_p1.isdisjoint(ids_p2) def test_get_downloads_page_filters(): run(_seed_download(_job("filter-tw"), platform="twitter", status="done", user_id=999, username="twitteruser")) result = run(db.get_downloads_page(page=1, per_page=50, platform="twitter")) assert all(r["platform"] == "twitter" for r in result["rows"]) result_err = run(db.get_downloads_page(page=1, per_page=50, status="error")) assert all(r["status"] == "error" for r in result_err["rows"]) result_user = run(db.get_downloads_page(page=1, per_page=50, user="twitteruser")) assert any(r["username"] == "twitteruser" for r in result_user["rows"]) def test_get_error_downloads(): errors = run(db.get_error_downloads()) assert isinstance(errors, list) for e in errors: assert e["status"] == "error" def test_get_error_downloads_date_filter(): from datetime import datetime, timezone, timedelta yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d") tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime("%Y-%m-%d") errors = run(db.get_error_downloads(date_from=yesterday, date_to=tomorrow)) assert isinstance(errors, list) # All errors we seeded today should appear for e in errors: assert e["status"] == "error" def test_insert_and_get_disk_snapshot(): run(db.insert_disk_snapshot(total_bytes=1_000_000_000, file_count=42)) snap = run(db.get_latest_disk_snapshot()) assert snap is not None assert snap["total_bytes"] == 1_000_000_000 assert snap["file_count"] == 42 assert snap["snapshot_at"] is not None def test_get_latest_disk_snapshot_empty(): # Use a separate temp DB to test empty state with tempfile.TemporaryDirectory() as d: old_path = os.environ["DB_PATH"] empty_path = os.path.join(d, "empty.db") os.environ["DB_PATH"] = empty_path # init_db will create schema on empty_path run(db.init_db()) snap = run(db.get_latest_disk_snapshot()) assert snap is None os.environ["DB_PATH"] = old_path def test_disk_snapshot_latest_is_newest(): run(db.insert_disk_snapshot(total_bytes=100, file_count=1)) run(db.insert_disk_snapshot(total_bytes=200, file_count=2)) run(db.insert_disk_snapshot(total_bytes=300, file_count=3)) snap = run(db.get_latest_disk_snapshot()) assert snap["total_bytes"] == 300 assert snap["file_count"] == 3