ytptube/app/tests/test_datastore.py
2026-01-17 23:32:39 +03:00

1129 lines
40 KiB
Python

import asyncio
import json
from collections import OrderedDict
from dataclasses import asdict
from datetime import UTC, datetime
from email.utils import formatdate
import pytest
from app.library.DataStore import DataStore, StoreType
from app.library.ItemDTO import ItemDTO
from app.library.operations import Operation
from app.library.sqlite_store import SqliteStore
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 temporary database with test data."""
await reset_sqlite_store()
ins = SqliteStore.get_instance(db_path=":memory:")
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:
def __init__(self, info: ItemDTO, started: bool = False, cancelled: bool = False):
self.info = info
self._started: bool = started
self._cancelled: bool = cancelled
def started(self) -> bool:
return self._started
def is_cancelled(self) -> bool:
return self._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_get_item_returns_none_when_no_kwargs(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_returns_none_when_no_match(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
store._dict["broken"] = BrokenDownload()
# 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_returns_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_with_no_eligible_downloads(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_get_next_download_returns_none_when_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_store_item_removes_datetime_field(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_store_item_removes_live_in_when_finished(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_store_item_keeps_live_in_when_not_finished(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"
item1.filesize = 1000
item2 = make_item(id="vid2", title="Video 2")
item2._id = "id2"
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"
item1.filesize = 1000
item2 = make_item(id="vid2", title="Video 2")
item2._id = "id2"
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"
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"
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"
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()