ComfyUI/tests-unit/assets_test/queries_crud_test.py
bymyself 1ad4b76b55 Add comprehensive test suite for assets API
- conftest.py: Test fixtures (in-memory SQLite, mock UserManager, test image)
- schemas_test.py: 98 tests for Pydantic input validation
- helpers_test.py: 50 tests for utility functions
- queries_crud_test.py: 27 tests for core CRUD operations
- queries_filter_test.py: 28 tests for filtering/pagination
- queries_tags_test.py: 24 tests for tag operations
- routes_upload_test.py: 18 tests for upload endpoints
- routes_read_update_test.py: 21 tests for read/update endpoints
- routes_tags_delete_test.py: 17 tests for tags/delete endpoints

Total: 283 tests covering all 12 asset API endpoints
Amp-Thread-ID: https://ampcode.com/threads/T-019be932-d48b-76b9-843a-790e9d2a1f58
Co-authored-by: Amp <amp@ampcode.com>
2026-01-22 23:15:19 -08:00

598 lines
18 KiB
Python

"""
Tests for core CRUD database query functions in app.assets.database.queries.
"""
import pytest
import uuid
from datetime import datetime, timedelta, timezone
from app.assets.database.queries import (
asset_exists_by_hash,
get_asset_by_hash,
get_asset_info_by_id,
create_asset_info_for_existing_asset,
ingest_fs_asset,
delete_asset_info_by_id,
touch_asset_info_by_id,
update_asset_info_full,
fetch_asset_info_and_asset,
fetch_asset_info_asset_and_tags,
ensure_tags_exist,
)
from app.assets.database.models import Asset, AssetInfo, AssetCacheState
def make_hash(seed: str = "a") -> str:
return "blake3:" + seed * 64
def make_unique_hash() -> str:
return "blake3:" + uuid.uuid4().hex + uuid.uuid4().hex
class TestAssetExistsByHash:
def test_returns_true_when_exists(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"fake png data")
asset_hash = make_unique_hash()
ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=len(b"fake png data"),
mtime_ns=1000000,
mime_type="image/png",
)
db_session.flush()
assert asset_exists_by_hash(db_session, asset_hash=asset_hash) is True
def test_returns_false_when_missing(self, db_session):
assert asset_exists_by_hash(db_session, asset_hash=make_unique_hash()) is False
class TestGetAssetByHash:
def test_returns_asset_when_exists(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"test data")
asset_hash = make_unique_hash()
ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=9,
mtime_ns=1000000,
mime_type="image/png",
)
db_session.flush()
asset = get_asset_by_hash(db_session, asset_hash=asset_hash)
assert asset is not None
assert asset.hash == asset_hash
assert asset.size_bytes == 9
assert asset.mime_type == "image/png"
def test_returns_none_when_missing(self, db_session):
result = get_asset_by_hash(db_session, asset_hash=make_unique_hash())
assert result is None
class TestGetAssetInfoById:
def test_returns_asset_info_when_exists(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"test data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=9,
mtime_ns=1000000,
info_name="my-asset",
owner_id="user1",
)
db_session.flush()
info = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
assert info is not None
assert info.name == "my-asset"
assert info.owner_id == "user1"
def test_returns_none_when_missing(self, db_session):
fake_id = str(uuid.uuid4())
result = get_asset_info_by_id(db_session, asset_info_id=fake_id)
assert result is None
class TestCreateAssetInfoForExistingAsset:
def test_creates_linked_asset_info(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"test data")
asset_hash = make_unique_hash()
ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=9,
mtime_ns=1000000,
)
db_session.flush()
info = create_asset_info_for_existing_asset(
db_session,
asset_hash=asset_hash,
name="new-info",
owner_id="owner123",
user_metadata={"key": "value"},
)
db_session.flush()
assert info is not None
assert info.name == "new-info"
assert info.owner_id == "owner123"
asset = get_asset_by_hash(db_session, asset_hash=asset_hash)
assert info.asset_id == asset.id
def test_raises_on_unknown_hash(self, db_session):
with pytest.raises(ValueError, match="Unknown asset hash"):
create_asset_info_for_existing_asset(
db_session,
asset_hash=make_unique_hash(),
name="test",
)
def test_returns_existing_on_duplicate(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"test data")
asset_hash = make_unique_hash()
ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=9,
mtime_ns=1000000,
)
db_session.flush()
info1 = create_asset_info_for_existing_asset(
db_session,
asset_hash=asset_hash,
name="same-name",
owner_id="owner1",
)
db_session.flush()
info2 = create_asset_info_for_existing_asset(
db_session,
asset_hash=asset_hash,
name="same-name",
owner_id="owner1",
)
db_session.flush()
assert info1.id == info2.id
class TestIngestFsAsset:
def test_creates_all_records(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"fake png data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=len(b"fake png data"),
mtime_ns=1000000,
mime_type="image/png",
info_name="test-asset",
owner_id="user1",
)
db_session.flush()
assert result["asset_created"] is True
assert result["state_created"] is True
assert result["asset_info_id"] is not None
asset = get_asset_by_hash(db_session, asset_hash=asset_hash)
assert asset is not None
assert asset.size_bytes == len(b"fake png data")
info = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
assert info is not None
assert info.name == "test-asset"
cache_states = db_session.query(AssetCacheState).filter_by(asset_id=asset.id).all()
assert len(cache_states) == 1
assert cache_states[0].file_path == str(test_file)
def test_idempotent_on_same_file(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result1 = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
result2 = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
assert result1["asset_info_id"] == result2["asset_info_id"]
assert result2["asset_created"] is False
def test_creates_with_tags(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
tags=["tag1", "tag2"],
)
db_session.flush()
info, asset, tags = fetch_asset_info_asset_and_tags(
db_session,
asset_info_id=result["asset_info_id"],
)
assert set(tags) == {"tag1", "tag2"}
class TestDeleteAssetInfoById:
def test_deletes_existing_record(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="to-delete",
owner_id="user1",
)
db_session.flush()
deleted = delete_asset_info_by_id(
db_session,
asset_info_id=result["asset_info_id"],
owner_id="user1",
)
db_session.flush()
assert deleted is True
assert get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"]) is None
def test_returns_false_for_nonexistent(self, db_session):
result = delete_asset_info_by_id(
db_session,
asset_info_id=str(uuid.uuid4()),
owner_id="user1",
)
assert result is False
def test_respects_owner_visibility(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="owned-asset",
owner_id="user1",
)
db_session.flush()
deleted = delete_asset_info_by_id(
db_session,
asset_info_id=result["asset_info_id"],
owner_id="different-user",
)
assert deleted is False
assert get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"]) is not None
class TestTouchAssetInfoById:
def test_updates_last_access_time(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
info_before = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
original_time = info_before.last_access_time
new_time = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=1)
touch_asset_info_by_id(
db_session,
asset_info_id=result["asset_info_id"],
ts=new_time,
)
db_session.flush()
db_session.expire_all()
info_after = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
assert info_after.last_access_time == new_time
assert info_after.last_access_time > original_time
def test_only_if_newer_respects_flag(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
info = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
original_time = info.last_access_time
older_time = original_time - timedelta(hours=1)
touch_asset_info_by_id(
db_session,
asset_info_id=result["asset_info_id"],
ts=older_time,
only_if_newer=True,
)
db_session.flush()
db_session.expire_all()
info_after = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
assert info_after.last_access_time == original_time
class TestUpdateAssetInfoFull:
def test_updates_name(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="original-name",
)
db_session.flush()
updated = update_asset_info_full(
db_session,
asset_info_id=result["asset_info_id"],
name="new-name",
)
db_session.flush()
assert updated.name == "new-name"
def test_updates_tags(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
update_asset_info_full(
db_session,
asset_info_id=result["asset_info_id"],
tags=["newtag1", "newtag2"],
)
db_session.flush()
_, _, tags = fetch_asset_info_asset_and_tags(
db_session,
asset_info_id=result["asset_info_id"],
)
assert set(tags) == {"newtag1", "newtag2"}
def test_updates_metadata(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
update_asset_info_full(
db_session,
asset_info_id=result["asset_info_id"],
user_metadata={"custom_key": "custom_value"},
)
db_session.flush()
db_session.expire_all()
info = get_asset_info_by_id(db_session, asset_info_id=result["asset_info_id"])
assert "custom_key" in info.user_metadata
assert info.user_metadata["custom_key"] == "custom_value"
def test_raises_on_invalid_id(self, db_session):
with pytest.raises(ValueError, match="not found"):
update_asset_info_full(
db_session,
asset_info_id=str(uuid.uuid4()),
name="test",
)
class TestFetchAssetInfoAndAsset:
def test_returns_tuple_when_exists(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
mime_type="image/png",
)
db_session.flush()
fetched = fetch_asset_info_and_asset(
db_session,
asset_info_id=result["asset_info_id"],
)
assert fetched is not None
info, asset = fetched
assert info.name == "test"
assert asset.hash == asset_hash
assert asset.mime_type == "image/png"
def test_returns_none_when_missing(self, db_session):
result = fetch_asset_info_and_asset(
db_session,
asset_info_id=str(uuid.uuid4()),
)
assert result is None
def test_respects_owner_visibility(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
owner_id="user1",
)
db_session.flush()
fetched = fetch_asset_info_and_asset(
db_session,
asset_info_id=result["asset_info_id"],
owner_id="different-user",
)
assert fetched is None
class TestFetchAssetInfoAssetAndTags:
def test_returns_tuple_with_tags(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
tags=["alpha", "beta"],
)
db_session.flush()
fetched = fetch_asset_info_asset_and_tags(
db_session,
asset_info_id=result["asset_info_id"],
)
assert fetched is not None
info, asset, tags = fetched
assert info.name == "test"
assert asset.hash == asset_hash
assert set(tags) == {"alpha", "beta"}
def test_returns_empty_tags_when_none(self, db_session, tmp_path):
test_file = tmp_path / "test.png"
test_file.write_bytes(b"data")
asset_hash = make_unique_hash()
result = ingest_fs_asset(
db_session,
asset_hash=asset_hash,
abs_path=str(test_file),
size_bytes=4,
mtime_ns=1000000,
info_name="test",
)
db_session.flush()
fetched = fetch_asset_info_asset_and_tags(
db_session,
asset_info_id=result["asset_info_id"],
)
assert fetched is not None
info, asset, tags = fetched
assert tags == []
def test_returns_none_when_missing(self, db_session):
result = fetch_asset_info_asset_and_tags(
db_session,
asset_info_id=str(uuid.uuid4()),
)
assert result is None