import asyncio import json from collections import OrderedDict from dataclasses import asdict from datetime import UTC, datetime from email.utils import formatdate from typing import Any from unittest.mock import AsyncMock, Mock import pytest from app.library.DataStore import DataStore, StoreType from app.library.downloads import Download from app.library.ItemDTO import ItemDTO from app.library.operations import Operation from app.library.sqlite_store import SqliteStore from app.tests.helpers import make_in_memory_db_path async def reset_sqlite_store() -> None: """Close and reset SqliteStore singleton for testing.""" if SqliteStore in SqliteStore._instances: instance = SqliteStore._instances[SqliteStore] # Only close if there's an active connection to avoid event loop issues if instance._conn is not None: try: await instance.close() except RuntimeError: # Event loop issues - just reset without closing pass SqliteStore._reset_singleton() async def make_db(data: int = 0) -> SqliteStore: """Create a named in-memory database with test data.""" await reset_sqlite_store() ins = SqliteStore.get_instance(db_path=make_in_memory_db_path("test-datastore")) await ins.get_connection() base_time = datetime.now(UTC) for i in range(data): created_at: str = base_time.replace(hour=(i // 4) % 24, minute=(i * 15) % 60, second=i % 60).strftime( "%Y-%m-%d %H:%M:%S" ) item_data = { "url": f"https://example.com/video{i}", "title": f"Test Video {i}", "id": f"video{i}", "folder": "/downloads", "status": "finished", } await ins.execute_raw( 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)', ( f"test-id-{i}", str(StoreType.HISTORY), item_data["url"], json.dumps(item_data), created_at, ), ) return ins class StubDownload(Download): def __init__(self, info: ItemDTO, started: bool = False, cancelled: bool = False) -> None: self.info = info self.id = info._id self._stub_started: bool = started self._stub_cancelled: bool = cancelled def started(self) -> bool: return self._stub_started def is_cancelled(self) -> bool: return self._stub_cancelled async def make_store_async(store_type: StoreType) -> DataStore: return DataStore(store_type, await make_db()) def make_item(id: str, url: str = "http://u", title: str = "t", folder: str = "f") -> ItemDTO: return ItemDTO(id=id, title=title, url=url, folder=folder) class TestStoreType: def test_all_and_from_value_and_str(self) -> None: assert set(StoreType.all()) == {"done", "queue"} assert StoreType.from_value("queue") is StoreType.QUEUE assert StoreType.from_value("done") is StoreType.HISTORY assert str(StoreType.QUEUE) == "queue" with pytest.raises(ValueError, match="Invalid StoreType value"): StoreType.from_value("invalid") class TestDataStore: @pytest.mark.asyncio async def test_saved_items_parses_rows(self) -> None: db = await make_db() store = DataStore(StoreType.QUEUE, db) db = store._connection dto = make_item(id="ignore", url="http://x", title="Title", folder="F") data = asdict(dto) data.pop("_id", None) created = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", ("abc", "queue", "http://x", json.dumps(data), created.strftime("%Y-%m-%d %H:%M:%S")), ) items = await store.saved_items() assert len(items) == 1 key, item = items[0] assert key == "abc" assert item._id == "abc" assert item.url == "http://x" assert isinstance(item.datetime, str) assert item.datetime == formatdate(created.timestamp()) @pytest.mark.asyncio async def test_put_and_delete_persist(self) -> None: store = await make_store_async(StoreType.QUEUE) db = store._connection item = make_item(id="vid1") d = StubDownload(info=item) ret = await store.put(d) await db.flush() assert ret is store._dict[item._id] # Verify row written; JSON should not contain datetime field rows = await db.fetch_raw("SELECT * FROM history WHERE id=?", (item._id,)) assert len(rows) == 1, "Should find one row" row = rows[0] assert row["type"] == "queue", "Type should be queue" assert row["url"] == item.url, "URL should match" assert '"datetime"' not in row["data"], "JSON should not contain datetime field" assert row["id"] == item._id, "ID should match" # Delete and ensure removal await store.delete(item._id) await asyncio.sleep(0) await db.flush() rows2 = await db.fetch_raw("SELECT * FROM history WHERE id=?", (item._id,)) assert len(rows2) == 0, "Row should be deleted" await db.close() @pytest.mark.asyncio async def test_exists_and_get(self) -> None: store = await make_store_async(StoreType.QUEUE) item = make_item(id="a1", url="http://u1") d = StubDownload(info=item) await store.put(d) await store._connection.flush() assert await store.exists(key=item._id) is True assert await store.exists(url=item.url) is True with pytest.raises(KeyError): await store.exists() got = await store.get(key=item._id) assert got.info._id == item._id got2 = await store.get(url=item.url) assert got2.info.url == item.url with pytest.raises(KeyError): await store.get() with pytest.raises(KeyError): await store.get(key="missing") await store._connection.close() @pytest.mark.asyncio async def test_next_and_empty(self) -> None: store = await make_store_async(StoreType.QUEUE) assert store.empty() is True d1 = StubDownload(info=make_item(id="x1")) d2 = StubDownload(info=make_item(id="x2")) await store.put(d1) await store.put(d2) await store._connection.flush() assert store.empty() is False first_key, first_val = store.next() assert first_key == d1.info._id assert first_val.info._id == d1.info._id await store._connection.close() @pytest.mark.asyncio async def test_has_downloads_and_get_next_download(self) -> None: store = await make_store_async(StoreType.QUEUE) # One non-auto-start, one started, one cancelled, and one eligible i1 = make_item(id="n1") i1.auto_start = False i2 = make_item(id="s1") i3 = make_item(id="c1") i4 = make_item(id="ok1") await store.put(StubDownload(info=i1, started=False)) await store.put(StubDownload(info=i2, started=True)) await store.put(StubDownload(info=i3, started=False, cancelled=True)) await store.put(StubDownload(info=i4, started=False)) await store._connection.flush() assert store.has_downloads() is True nxt = store.get_next_download() assert isinstance(nxt, StubDownload) assert nxt.info._id == i4._id await store._connection.close() @pytest.mark.asyncio async def test_test_method_executes_query(self) -> None: store = await make_store_async(StoreType.QUEUE) # Should not raise ok = await store.test() assert ok is True @pytest.mark.asyncio async def test_bulk_delete_status_cache(self) -> None: connection = Mock() connection.bulk_delete_by_status = AsyncMock(return_value=2) store = DataStore(StoreType.HISTORY, connection) finished = make_item(id="finished") finished.status = "finished" skipped = make_item(id="skipped") skipped.status = "skip" pending = make_item(id="pending") pending.status = "pending" store._dict[finished._id] = StubDownload(info=finished) store._dict[skipped._id] = StubDownload(info=skipped) store._dict[pending._id] = StubDownload(info=pending) deleted = await store.bulk_delete_by_status("finished,skip") assert deleted == 2 connection.bulk_delete_by_status.assert_awaited_once_with("done", "finished,skip") assert finished._id not in store._dict assert skipped._id not in store._dict assert pending._id in store._dict @pytest.mark.asyncio async def test_get_item_no_filters(self) -> None: """Test that get_item returns None when no kwargs provided.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) result = await store.get_item() assert result is None await db.close() @pytest.mark.asyncio async def test_get_item_finds_by_single_attribute(self) -> None: """Test that get_item correctly finds item by a single attribute.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Create items with different attributes item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1") item1._id = "id1" # Override auto-generated UUID item2 = make_item(id="vid2", url="http://example.com/2", title="Video 2", folder="folder2") item2._id = "id2" # Override auto-generated UUID d1 = StubDownload(info=item1) d2 = StubDownload(info=item2) await store.put(d1) await store.put(d2) await store._connection.flush() # Test finding by title result = await store.get_item(title="Video 1") assert result is not None assert result.info._id == "id1" assert result.info.title == "Video 1" # Test finding by folder result = await store.get_item(folder="folder2") assert result is not None assert result.info._id == "id2" assert result.info.folder == "folder2" # Test finding by url result = await store.get_item(url="http://example.com/1") assert result is not None assert result.info._id == "id1" await db.close() @pytest.mark.asyncio async def test_get_item_finds_by_multiple_attributes(self) -> None: """Test that get_item finds item when ANY of the provided attributes match.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", url="http://example.com/2", title="Video 2", folder="folder2") item2._id = "id2" d1 = StubDownload(info=item1) d2 = StubDownload(info=item2) await store.put(d1) await store.put(d2) await store._connection.flush() # Test finding by multiple attributes where one matches result = await store.get_item(title="Video 1", folder="wrong_folder") assert result is not None assert result.info._id == "id1" # Test finding where second attribute matches result = await store.get_item(title="Wrong Title", folder="folder2") assert result is not None assert result.info._id == "id2" await db.close() @pytest.mark.asyncio async def test_get_item_miss(self) -> None: """Test that get_item returns None when no attributes match.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1") item._id = "id1" d = StubDownload(info=item) await store.put(d) await store._connection.flush() # Test with non-matching attribute result = await store.get_item(title="Nonexistent Video") assert result is None # Test with non-existent attribute key result = await store.get_item(nonexistent_field="value") assert result is None await db.close() @pytest.mark.asyncio async def test_get_item_skips_items_with_no_info(self) -> None: """Test that get_item skips items that have no info attribute.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Create a valid item item = make_item(id="vid1", url="http://example.com/1", title="Video 1") item._id = "id1" d = StubDownload(info=item) await store.put(d) await store._connection.flush() # Manually add an item with None info class BrokenDownload: def __init__(self): self.info = None broken: Any = BrokenDownload() store._dict["broken"] = broken # Should still find the valid item result = await store.get_item(title="Video 1") assert result is not None assert result.info._id == "id1" await db.close() @pytest.mark.asyncio async def test_get_item_first_match(self) -> None: """Test that get_item returns the first matching item when multiple match.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Create multiple items with same title item1 = make_item(id="vid1", url="http://example.com/1", title="Same Title", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", url="http://example.com/2", title="Same Title", folder="folder2") item2._id = "id2" d1 = StubDownload(info=item1) d2 = StubDownload(info=item2) await store.put(d1) await store.put(d2) await store._connection.flush() # Should return first match (note: OrderedDict maintains insertion order) result = await store.get_item(title="Same Title") assert result is not None assert result.info._id == "id1" await db.close() @pytest.mark.asyncio async def test_init(self) -> None: """Test DataStore initialization.""" store = await make_store_async(StoreType.QUEUE) assert store._type == StoreType.QUEUE assert store._connection is not None assert isinstance(store._dict, OrderedDict) assert len(store._dict) == 0 @pytest.mark.asyncio async def test_load(self) -> None: """Test loading items from database into memory.""" db = await make_db() # Insert items directly into database item1_data = asdict(make_item(id="vid1", url="http://example.com/1", title="Video 1")) item1_data.pop("_id", None) item2_data = asdict(make_item(id="vid2", url="http://example.com/2", title="Video 2")) item2_data.pop("_id", None) created = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", ( "id1", str(StoreType.QUEUE), "http://example.com/1", json.dumps(item1_data), created.strftime("%Y-%m-%d %H:%M:%S"), ), ) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", ( "id2", str(StoreType.QUEUE), "http://example.com/2", json.dumps(item2_data), created.strftime("%Y-%m-%d %H:%M:%S"), ), ) store = DataStore(StoreType.QUEUE, db) assert len(store._dict) == 0 await store.load() assert len(store._dict) == 2 assert "id1" in store._dict assert "id2" in store._dict assert store._dict["id1"].info.url == "http://example.com/1" assert store._dict["id2"].info.url == "http://example.com/2" await db.close() @pytest.mark.asyncio async def test_load_with_different_store_types(self) -> None: """Test that load only loads items matching the store type.""" db = await make_db() # conn = db._conn # No longer needed # Insert items with different types item1_data = asdict(make_item(id="vid1", url="http://example.com/1")) item1_data.pop("_id", None) item2_data = asdict(make_item(id="vid2", url="http://example.com/2")) item2_data.pop("_id", None) created = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", ( "id1", str(StoreType.QUEUE), "http://example.com/1", json.dumps(item1_data), created.strftime("%Y-%m-%d %H:%M:%S"), ), ) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", ( "id2", str(StoreType.HISTORY), "http://example.com/2", json.dumps(item2_data), created.strftime("%Y-%m-%d %H:%M:%S"), ), ) # await conn.commit() # Auto-committed # Load QUEUE store - should only get queue items queue_store = DataStore(StoreType.QUEUE, db) await queue_store.load() assert len(queue_store._dict) == 1 assert "id1" in queue_store._dict # Load HISTORY store - should only get history items history_store = DataStore(StoreType.HISTORY, db) await history_store.load() assert len(history_store._dict) == 1 assert "id2" in history_store._dict await db.close() @pytest.mark.asyncio async def test_get_by_id(self) -> None: """Test getting item by ID.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1") item1._id = "id1" item2 = make_item(id="vid2", url="http://example.com/2", title="Video 2") item2._id = "id2" d1 = StubDownload(info=item1) d2 = StubDownload(info=item2) await store.put(d1) await store.put(d2) await store._connection.flush() # Test getting existing items result = await store.get_by_id("id1") assert result is not None assert result.info._id == "id1" assert result.info.title == "Video 1" result = await store.get_by_id("id2") assert result is not None assert result.info._id == "id2" # Test getting non-existent item result = await store.get_by_id("nonexistent") assert result is None await db.close() @pytest.mark.asyncio async def test_items(self) -> None: """Test getting all items as list of tuples.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Empty store result = store.items() assert len(list(result)) == 0 # Add items item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1") item1._id = "id1" item2 = make_item(id="vid2", url="http://example.com/2", title="Video 2") item2._id = "id2" d1 = StubDownload(info=item1) d2 = StubDownload(info=item2) await store.put(d1) await store.put(d2) await store._connection.flush() # Test getting all items result = list(store.items()) assert len(result) == 2 # Verify structure (list of tuples) ids = [item[0] for item in result] assert "id1" in ids assert "id2" in ids assert result[0][0] == "id1", "Verify order is maintained (OrderedDict)" assert result[1][0] == "id2" await db.close() @pytest.mark.asyncio async def test_get_total_count_with_empty_store(self) -> None: """Test get_total_count with empty datastore.""" store = await make_store_async(StoreType.QUEUE) count = await store.get_total_count() assert count == 0 await store._connection.close() @pytest.mark.asyncio async def test_get_total_count_with_items(self) -> None: """Test get_total_count with items in database.""" store = await make_store_async(StoreType.QUEUE) db = store._connection # conn = db._conn # No longer needed # Add items directly to database created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") for i in range(5): item_data = asdict(make_item(id=f"vid{i}")) item_data.pop("_id", None) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", (f"id{i}", str(StoreType.QUEUE), f"http://example.com/{i}", json.dumps(item_data), created), ) # await conn.commit() # Auto-committed count = await store.get_total_count() assert count == 5 await store._connection.close() @pytest.mark.asyncio async def test_get_total_count_respects_store_type(self) -> None: """Test that get_total_count only counts items of the correct type.""" db = await make_db() # conn = db._conn # No longer needed created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") # Add 3 QUEUE items for i in range(3): item_data = asdict(make_item(id=f"vid{i}")) item_data.pop("_id", None) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", (f"q{i}", str(StoreType.QUEUE), f"http://example.com/queue/{i}", json.dumps(item_data), created), ) # Add 2 HISTORY items for i in range(2): item_data = asdict(make_item(id=f"vid{i}")) item_data.pop("_id", None) await db.execute_raw( "INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)", (f"h{i}", str(StoreType.HISTORY), f"http://example.com/history/{i}", json.dumps(item_data), created), ) # await conn.commit() # Auto-committed queue_store = DataStore(StoreType.QUEUE, db) assert await queue_store.get_total_count() == 3 history_store = DataStore(StoreType.HISTORY, db) assert await history_store.get_total_count() == 2 await db.close() @pytest.mark.asyncio async def test_put_with_error_status_emits_event(self) -> None: """Test that put() emits an event when item has error status.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item = make_item(id="vid1") item.status = "error" item.error = "Test error message" d = StubDownload(info=item) # We can't easily test event emission without mocking EventBus # Just verify it doesn't crash result = await store.put(d) await store._connection.flush() assert result is not None await db.close() @pytest.mark.asyncio async def test_put_with_no_notify_skips_event(self) -> None: """Test that put() with no_notify=True doesn't emit events.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item = make_item(id="vid1") item.status = "error" item.error = "Test error message" d = StubDownload(info=item) # Should not emit event when no_notify=True result = await store.put(d, no_notify=True) await store._connection.flush() assert result is not None assert result.info._id == item._id await db.close() @pytest.mark.asyncio async def test_delete_nonexistent_item(self) -> None: """Test that deleting non-existent item doesn't raise error.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Should not raise error await store.delete("nonexistent_id") # Verify nothing was deleted from database await store._connection.flush() rows = await db.fetch_raw("SELECT * FROM history WHERE id=?", ("nonexistent_id",)) assert len(rows) == 0, "Should find no rows" await db.close() @pytest.mark.asyncio async def test_has_downloads_with_empty_dict(self) -> None: """Test has_downloads returns False when dict is empty.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) assert store.has_downloads() is False await db.close() @pytest.mark.asyncio async def test_has_downloads_ineligible(self) -> None: """Test has_downloads returns False when no downloads are eligible.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Add item that's already started item1 = make_item(id="vid1") await store.put(StubDownload(info=item1, started=True)) # Add item with auto_start=False item2 = make_item(id="vid2") item2.auto_start = False await store.put(StubDownload(info=item2, started=False)) await store._connection.flush() assert store.has_downloads() is False await db.close() @pytest.mark.asyncio async def test_next_download_empty(self) -> None: """Test get_next_download returns None when no eligible downloads.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) result = store.get_next_download() assert result is None await db.close() @pytest.mark.asyncio async def test_get_next_download_skips_cancelled(self) -> None: """Test get_next_download skips cancelled downloads.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) # Add cancelled download item1 = make_item(id="vid1") item1._id = "id1" await store.put(StubDownload(info=item1, started=False, cancelled=True)) # Add eligible download item2 = make_item(id="vid2") item2._id = "id2" await store.put(StubDownload(info=item2, started=False, cancelled=False)) await store._connection.flush() result = store.get_next_download() assert result is not None assert result.info._id == "id2" await db.close() @pytest.mark.asyncio async def test_update_item_drops_dt(self) -> None: """Test that _update_store_item removes datetime field before storage.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item = make_item(id="vid1") item.datetime = "Thu, 01 Jan 2024 12:00:00 GMT" # Add datetime field d = StubDownload(info=item) await store.put(d) await store._connection.flush() # Verify datetime field is not in stored JSON rows = await db.fetch_raw("SELECT data FROM history WHERE id=?", (item._id,)) assert len(rows) == 1, "Should find one row" data = json.loads(rows[0]["data"]) assert "datetime" not in data await db.close() @pytest.mark.asyncio async def test_update_item_drops_live_in(self) -> None: """Test that _update_store_item removes live_in field when status is finished.""" store = await make_store_async(StoreType.QUEUE) conn = store._connection item = make_item(id="vid1") item.status = "finished" item.live_in = "PT5M" # Add live_in field d = StubDownload(info=item) await store.put(d) await store._connection.flush() # Verify live_in field is not in stored JSON when status is finished rows = await conn.fetch_raw("SELECT data FROM history WHERE id=?", (item._id,)) assert len(rows) == 1, "Should find one row" data = json.loads(rows[0]["data"]) assert "live_in" not in data await store._connection.close() @pytest.mark.asyncio async def test_update_item_keeps_live_in(self) -> None: """Test that _update_store_item keeps live_in field when status is not finished.""" store = await make_store_async(StoreType.QUEUE) conn = store._connection item = make_item(id="vid1") item.status = "downloading" item.live_in = "PT5M" # Add live_in field d = StubDownload(info=item) await store.put(d) await store._connection.flush() # Verify live_in field IS in stored JSON when status is not finished rows = await conn.fetch_raw("SELECT data FROM history WHERE id=?", (item._id,)) assert len(rows) == 1, "Should find one row" data = json.loads(rows[0]["data"]) assert "live_in" in data assert data["live_in"] == "PT5M" await store._connection.close() @pytest.mark.asyncio class TestDataStoreOperations: """Test get_item with different comparison operations.""" async def test_operation_equal(self) -> None: """Test EQUAL operation (default behavior).""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Exact Match", folder="folder1") item1._id = "id1" await store.put(StubDownload(info=item1)) # Test with explicit EQUAL operation result = await store.get_item(title=(Operation.EQUAL, "Exact Match")) assert result is not None assert result.info._id == "id1" # Test default behavior (no operation specified) result = await store.get_item(title="Exact Match") assert result is not None assert result.info._id == "id1" # Test no match result = await store.get_item(title=(Operation.EQUAL, "No Match")) assert result is None await db.close() async def test_operation_not_equal(self) -> None: """Test NOT_EQUAL operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", title="Video 2", folder="folder2") item2._id = "id2" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item where title is not "Video 1" result = await store.get_item(title=(Operation.NOT_EQUAL, "Video 1")) assert result is not None assert result.info._id == "id2" await db.close() async def test_operation_contain(self) -> None: """Test CONTAIN operation (substring match).""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Python Tutorial Video", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", title="JavaScript Course", folder="folder2") item2._id = "id2" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item with "Python" in title result = await store.get_item(title=(Operation.CONTAIN, "Python")) assert result is not None assert result.info._id == "id1" # Find item with "Tutorial" in title result = await store.get_item(title=(Operation.CONTAIN, "Tutorial")) assert result is not None assert result.info._id == "id1" # No match result = await store.get_item(title=(Operation.CONTAIN, "Rust")) assert result is None await db.close() async def test_operation_not_contain(self) -> None: """Test NOT_CONTAIN operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Python Tutorial", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", title="JavaScript Course", folder="folder2") item2._id = "id2" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item that doesn't contain "Python" result = await store.get_item(title=(Operation.NOT_CONTAIN, "Python")) assert result is not None assert result.info._id == "id2" await db.close() async def test_operation_starts_with(self) -> None: """Test STARTS_WITH operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Tutorial: Python Basics", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", title="Course: JavaScript", folder="folder2") item2._id = "id2" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item starting with "Tutorial" result = await store.get_item(title=(Operation.STARTS_WITH, "Tutorial")) assert result is not None assert result.info._id == "id1" # Find item starting with "Course" result = await store.get_item(title=(Operation.STARTS_WITH, "Course")) assert result is not None assert result.info._id == "id2" # No match result = await store.get_item(title=(Operation.STARTS_WITH, "Video")) assert result is None await db.close() async def test_operation_ends_with(self) -> None: """Test ENDS_WITH operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Learn Python", folder="folder1") item1._id = "id1" item2 = make_item(id="vid2", title="Learn JavaScript", folder="folder2") item2._id = "id2" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item ending with "Python" result = await store.get_item(title=(Operation.ENDS_WITH, "Python")) assert result is not None assert result.info._id == "id1" # Find item ending with "JavaScript" result = await store.get_item(title=(Operation.ENDS_WITH, "JavaScript")) assert result is not None assert result.info._id == "id2" # No match result = await store.get_item(title=(Operation.ENDS_WITH, "Course")) assert result is None await db.close() async def test_operation_greater_than(self) -> None: """Test GREATER_THAN operation with numeric values.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" setattr(item1, "filesize", 1000) item2 = make_item(id="vid2", title="Video 2") item2._id = "id2" setattr(item2, "filesize", 2000) await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item with filesize > 1500 result = await store.get_item(filesize=(Operation.GREATER_THAN, 1500)) assert result is not None assert result.info._id == "id2" # Find item with filesize > 500 (should return first match) result = await store.get_item(filesize=(Operation.GREATER_THAN, 500)) assert result is not None assert result.info._id == "id1" await db.close() async def test_operation_less_than(self) -> None: """Test LESS_THAN operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" setattr(item1, "filesize", 1000) item2 = make_item(id="vid2", title="Video 2") item2._id = "id2" setattr(item2, "filesize", 2000) await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) # Find item with filesize < 1500 result = await store.get_item(filesize=(Operation.LESS_THAN, 1500)) assert result is not None assert result.info._id == "id1" await db.close() async def test_operation_greater_equal(self) -> None: """Test GREATER_EQUAL operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" setattr(item1, "filesize", 1000) await store.put(StubDownload(info=item1)) # Test >= with exact match result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1000)) assert result is not None assert result.info._id == "id1" # Test >= with less than result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 500)) assert result is not None assert result.info._id == "id1" # Test >= with greater than result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1500)) assert result is None await db.close() async def test_operation_less_equal(self) -> None: """Test LESS_EQUAL operation.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" setattr(item1, "filesize", 1000) await store.put(StubDownload(info=item1)) # Test <= with exact match result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1000)) assert result is not None # Test <= with greater than result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1500)) assert result is not None # Test <= with less than result = await store.get_item(filesize=(Operation.LESS_EQUAL, 500)) assert result is None await db.close() async def test_mixed_operations(self) -> None: """Test using multiple operations in a single query.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Python Tutorial", folder="tutorials") item1._id = "id1" item2 = make_item(id="vid2", title="Python Advanced", folder="courses") item2._id = "id2" item3 = make_item(id="vid3", title="JavaScript Basics", folder="tutorials") item3._id = "id3" await store.put(StubDownload(info=item1)) await store.put(StubDownload(info=item2)) await store.put(StubDownload(info=item3)) # Mix of operation and default (any match returns true) result = await store.get_item(title=(Operation.CONTAIN, "Python"), folder="tutorials") assert result is not None assert result.info._id == "id1" # Mix where first condition matches result = await store.get_item(title=(Operation.CONTAIN, "JavaScript"), folder="nonexistent") assert result is not None assert result.info._id == "id3" await db.close() async def test_operation_with_none_values(self) -> None: """Test operations handle None values gracefully.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" setattr(item1, "description", None) await store.put(StubDownload(info=item1)) # CONTAIN with None field should return False result = await store.get_item(description=(Operation.CONTAIN, "test")) assert result is None # NOT_CONTAIN with None field should return True result = await store.get_item(description=(Operation.NOT_CONTAIN, "test")) assert result is not None # GREATER_THAN with None should return False result = await store.get_item(description=(Operation.GREATER_THAN, 100)) assert result is None await db.close() async def test_operation_with_invalid_comparisons(self) -> None: """Test that invalid comparisons are handled gracefully.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" await store.put(StubDownload(info=item1)) # Try to compare string with number using > (should return False/None) result = await store.get_item(title=(Operation.GREATER_THAN, 100)) assert result is None await db.close() async def test_operation_backward_compatibility(self) -> None: """Test that string operation names work for backward compatibility.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Python Tutorial") item1._id = "id1" await store.put(StubDownload(info=item1)) # Using string operation value result = await store.get_item(title=("in", "Python")) assert result is not None assert result.info._id == "id1" # Using string for EQUAL result = await store.get_item(title=("==", "Python Tutorial")) assert result is not None await db.close() async def test_operation_with_nonexistent_field(self) -> None: """Test operations with fields that don't exist.""" db = await make_db() store = DataStore(StoreType.QUEUE, db) item1 = make_item(id="vid1", title="Video 1") item1._id = "id1" await store.put(StubDownload(info=item1)) # Try to match on non-existent field result = await store.get_item(nonexistent_field=(Operation.EQUAL, "value")) assert result is None result = await store.get_item(nonexistent_field=(Operation.CONTAIN, "value")) assert result is None await db.close()