from datetime import UTC, datetime, timedelta import pytest from sqlalchemy import text from app.library.ItemDTO import ItemDTO from app.library.operations import Operation from app.library.sqlite_store import SqliteStore async def make_store() -> SqliteStore: """Create an isolated in-memory SqliteStore instance with schema.""" SqliteStore._reset_singleton() store = SqliteStore.get_instance(db_path=":memory:") await store.get_connection() assert store._engine is not None, "Engine should be initialized after _ensure_conn" return store def make_item(idx: int, *, status: str = "finished") -> ItemDTO: return ItemDTO( id=f"vid{idx}", title=f"Video {idx}", url=f"https://example.com/{idx}", folder="/downloads", status=status, ) @pytest.mark.asyncio async def test_sessionmaker_returns_valid_sessionmaker() -> None: """Test that sessionmaker() returns a working async_sessionmaker.""" SqliteStore._reset_singleton() store = SqliteStore.get_instance(db_path=":memory:") # Ensure connection is initialized await store.get_connection() # Get sessionmaker sessionmaker = store.sessionmaker() assert sessionmaker is not None, "Sessionmaker should not be None after connection initialization" # Verify we can create a session async with sessionmaker() as session: assert session is not None, "Session should be created successfully" await store.close() @pytest.mark.asyncio async def test_sessionmaker_raises_before_init() -> None: """Test that sessionmaker() raises error before connection initialization.""" SqliteStore._reset_singleton() store = SqliteStore.get_instance(db_path=":memory:") with pytest.raises(RuntimeError, match="Database connection not initialized"): store.sessionmaker() await store.close() @pytest.mark.asyncio async def test_sqlalchemy_engine_disposed_on_close() -> None: """Test that SQLAlchemy engine is properly disposed on close.""" SqliteStore._reset_singleton() store = SqliteStore.get_instance(db_path=":memory:") await store.get_connection() assert store._engine is not None, "Engine should be created" assert store._sessionmaker is not None, "Sessionmaker should be created" await store.close() assert store._engine is None, "Engine should be None after close" assert store._sessionmaker is None, "Sessionmaker should be None after close" @pytest.mark.asyncio async def test_enqueue_upsert_and_fetch_saved(): store = await make_store() item = make_item(1) await store.enqueue_upsert("queue", item) await store.flush() saved = await store.fetch_saved("queue") assert len(saved) == 1 key, loaded = saved[0] assert key == item._id assert loaded.title == item.title await store.close() @pytest.mark.asyncio async def test_delete_and_bulk_delete(): store = await make_store() items = [make_item(i) for i in range(3)] for itm in items: await store.enqueue_upsert("queue", itm) await store.flush() await store.enqueue_delete("queue", items[0]._id) await store.enqueue_bulk_delete("queue", [items[1]._id]) await store.flush() remaining = await store.fetch_saved("queue") assert len(remaining) == 1 assert remaining[0][0] == items[2]._id await store.close() @pytest.mark.asyncio async def test_count_with_status_filters(): store = await make_store() finished_items = [make_item(i, status="finished") for i in range(2)] pending_items = [make_item(i + 10, status="pending") for i in range(3)] for itm in finished_items + pending_items: await store.enqueue_upsert("history", itm) await store.flush() assert await store.count("history", status_filter="finished") == 2 assert await store.count("history", status_filter="pending") == 3 assert await store.count("history", status_filter="!finished") == 3 await store.close() @pytest.mark.asyncio async def test_paginate_order_and_bounds(): store = await make_store() base = datetime(2024, 1, 1, tzinfo=UTC) _list: list[ItemDTO] = [] for i in range(15): itm = make_item(i) encoded = itm.json() created_at = (base + timedelta(minutes=i)).strftime("%Y-%m-%d %H:%M:%S") _list.append(itm) await store._conn.execute( text( 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (:id, :type, :url, :data, :created_at)' ), {"id": itm._id, "type": "history", "url": itm.url, "data": encoded, "created_at": created_at}, ) await store._conn.commit() items, total, page, total_pages = await store.paginate("history", page=1, per_page=5, order="ASC") assert total == 15 assert page == 1 assert total_pages == 3 assert items[0][0] == _list[0]._id items_desc, *_ = await store.paginate("history", page=1, per_page=3, order="DESC") assert len(items_desc) == 3 assert items_desc[0][0] == _list[14]._id await store.close() @pytest.mark.asyncio async def test_get_by_id(): store = await make_store() item = make_item(99) await store.enqueue_upsert("queue", item) await store.flush() assert item._id == (await store.get_by_id("queue", item._id))._id await store.close() @pytest.mark.asyncio async def test_shutdown_closes_worker_queue(): store = await make_store() await store.enqueue_upsert("queue", make_item(99)) await store.shutdown() assert store._queue is None await store.close() @pytest.mark.asyncio async def test_enqueue_delete_removes_row(): store = await make_store() item = make_item(5) await store.enqueue_upsert("queue", item) await store.flush() await store.enqueue_delete("queue", item._id) await store.flush() rows = await store.fetch_saved("queue") assert rows == [] await store.close() @pytest.mark.asyncio async def test_enqueue_bulk_delete_returns_count_and_bulk_path(): store = await make_store() items = [make_item(i) for i in range(3)] for itm in items: await store.enqueue_upsert("history", itm) await store.flush() await store.enqueue_bulk_delete("history", [items[0]._id, items[1]._id]) await store.flush() remaining = await store.fetch_saved("history") assert len(remaining) == 1 assert remaining[0][0] == items[2]._id # direct bulk_delete should report affected rows count = await store.bulk_delete("history", [items[2]._id]) assert count == 1 await store.close() @pytest.mark.asyncio async def test_paginate_out_of_range_returns_last_page(): store = await make_store() for i in range(7): await store.enqueue_upsert("history", make_item(i)) await store.flush() items, total, page, total_pages = await store.paginate("history", page=10, per_page=3, order="DESC") assert total == 7 assert total_pages == 3 assert page == 3 assert len(items) == 1 await store.close() @pytest.mark.asyncio async def test_get_item_returns_none_without_kwargs(): store = await make_store() await store.enqueue_upsert("queue", make_item(1)) await store.flush() result = await store.get_item("queue") assert result is None await store.close() @pytest.mark.asyncio async def test_get_item_matches_conditions(): store = await make_store() first = make_item(1, status="finished") second = make_item(2, status="pending") await store.enqueue_upsert("queue", first) await store.enqueue_upsert("queue", second) await store.flush() # equality match match_equal = await store.get_item("queue", title=first.title) assert match_equal is not None assert match_equal._id == first._id # NOT_EQUAL should find the second item match_not_equal = await store.get_item("queue", status=(Operation.NOT_EQUAL, "finished")) assert match_not_equal is not None assert match_not_equal._id == second._id # CONTAIN should find first created (first) match_contain = await store.get_item("queue", title=(Operation.CONTAIN, "Video")) assert match_contain is not None assert match_contain._id == first._id # No match returns None no_match = await store.get_item("queue", title=(Operation.CONTAIN, "does-not-exist")) assert no_match is None await store.close() @pytest.mark.asyncio async def test_on_shutdown_closes_connection(): store = await make_store() await store.enqueue_upsert("queue", make_item(1)) await store.flush() await store.on_shutdown(None) assert store._conn is None # Note: on_shutdown already closes the connection, so no need to call close() again @pytest.mark.asyncio async def test_exists_and_get_by_key_and_url(): store = await make_store() item = make_item(42) await store.enqueue_upsert("queue", item) await store.flush() assert await store.exists("queue", key=item._id) is True assert await store.exists("queue", url=item.url) is True fetched_by_key = await store.get("queue", key=item._id) assert fetched_by_key is not None assert fetched_by_key._id == item._id fetched_by_url = await store.get("queue", url=item.url) assert fetched_by_url is not None assert fetched_by_url._id == item._id assert await store.exists("queue", key="missing") is False assert await store.get("queue", key="missing") is None await store.close() @pytest.mark.asyncio async def test_exists_and_get_raise_without_key_or_url(): store = await make_store() with pytest.raises(KeyError): await store.exists("queue") with pytest.raises(KeyError): await store.get("queue") await store.close()