mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-02-04 02:30:21 +08:00
- 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>
598 lines
18 KiB
Python
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
|