Files
ComfyUI/tests-unit/assets_test/schemas_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

510 lines
18 KiB
Python

"""
Comprehensive tests for Pydantic schemas in the assets API.
"""
import pytest
from pydantic import ValidationError
from app.assets.api.schemas_in import (
ListAssetsQuery,
UpdateAssetBody,
CreateFromHashBody,
UploadAssetSpec,
SetPreviewBody,
TagsAdd,
TagsRemove,
TagsListQuery,
ScheduleAssetScanBody,
)
class TestListAssetsQuery:
def test_defaults(self):
q = ListAssetsQuery()
assert q.limit == 20
assert q.offset == 0
assert q.sort == "created_at"
assert q.order == "desc"
assert q.include_tags == []
assert q.exclude_tags == []
assert q.name_contains is None
assert q.metadata_filter is None
def test_csv_tags_parsing_string(self):
q = ListAssetsQuery.model_validate({"include_tags": "a,b,c"})
assert q.include_tags == ["a", "b", "c"]
def test_csv_tags_parsing_with_whitespace(self):
q = ListAssetsQuery.model_validate({"include_tags": " a , b , c "})
assert q.include_tags == ["a", "b", "c"]
def test_csv_tags_parsing_list(self):
q = ListAssetsQuery.model_validate({"include_tags": ["a", "b", "c"]})
assert q.include_tags == ["a", "b", "c"]
def test_csv_tags_parsing_list_with_csv(self):
q = ListAssetsQuery.model_validate({"include_tags": ["a,b", "c"]})
assert q.include_tags == ["a", "b", "c"]
def test_csv_tags_exclude_tags(self):
q = ListAssetsQuery.model_validate({"exclude_tags": "x,y,z"})
assert q.exclude_tags == ["x", "y", "z"]
def test_csv_tags_empty_string(self):
q = ListAssetsQuery.model_validate({"include_tags": ""})
assert q.include_tags == []
def test_csv_tags_none(self):
q = ListAssetsQuery.model_validate({"include_tags": None})
assert q.include_tags == []
def test_metadata_filter_json_string(self):
q = ListAssetsQuery.model_validate({"metadata_filter": '{"key": "value"}'})
assert q.metadata_filter == {"key": "value"}
def test_metadata_filter_dict(self):
q = ListAssetsQuery.model_validate({"metadata_filter": {"key": "value"}})
assert q.metadata_filter == {"key": "value"}
def test_metadata_filter_none(self):
q = ListAssetsQuery.model_validate({"metadata_filter": None})
assert q.metadata_filter is None
def test_metadata_filter_empty_string(self):
q = ListAssetsQuery.model_validate({"metadata_filter": ""})
assert q.metadata_filter is None
def test_metadata_filter_invalid_json(self):
with pytest.raises(ValidationError) as exc_info:
ListAssetsQuery.model_validate({"metadata_filter": "not json"})
assert "must be JSON" in str(exc_info.value)
def test_metadata_filter_non_object_json(self):
with pytest.raises(ValidationError) as exc_info:
ListAssetsQuery.model_validate({"metadata_filter": "[1, 2, 3]"})
assert "must be a JSON object" in str(exc_info.value)
def test_limit_bounds_min(self):
with pytest.raises(ValidationError):
ListAssetsQuery.model_validate({"limit": 0})
def test_limit_bounds_max(self):
with pytest.raises(ValidationError):
ListAssetsQuery.model_validate({"limit": 501})
def test_limit_bounds_valid(self):
q = ListAssetsQuery.model_validate({"limit": 500})
assert q.limit == 500
def test_offset_bounds_min(self):
with pytest.raises(ValidationError):
ListAssetsQuery.model_validate({"offset": -1})
def test_sort_enum_valid(self):
for sort_val in ["name", "created_at", "updated_at", "size", "last_access_time"]:
q = ListAssetsQuery.model_validate({"sort": sort_val})
assert q.sort == sort_val
def test_sort_enum_invalid(self):
with pytest.raises(ValidationError):
ListAssetsQuery.model_validate({"sort": "invalid"})
def test_order_enum_valid(self):
for order_val in ["asc", "desc"]:
q = ListAssetsQuery.model_validate({"order": order_val})
assert q.order == order_val
def test_order_enum_invalid(self):
with pytest.raises(ValidationError):
ListAssetsQuery.model_validate({"order": "invalid"})
class TestUpdateAssetBody:
def test_requires_at_least_one_field(self):
with pytest.raises(ValidationError) as exc_info:
UpdateAssetBody.model_validate({})
assert "at least one of" in str(exc_info.value)
def test_name_only(self):
body = UpdateAssetBody.model_validate({"name": "new_name"})
assert body.name == "new_name"
assert body.tags is None
assert body.user_metadata is None
def test_tags_only(self):
body = UpdateAssetBody.model_validate({"tags": ["tag1", "tag2"]})
assert body.tags == ["tag1", "tag2"]
def test_user_metadata_only(self):
body = UpdateAssetBody.model_validate({"user_metadata": {"key": "value"}})
assert body.user_metadata == {"key": "value"}
def test_tags_must_be_list_of_strings(self):
with pytest.raises(ValidationError) as exc_info:
UpdateAssetBody.model_validate({"tags": "not_a_list"})
assert "list" in str(exc_info.value).lower()
def test_tags_must_contain_strings(self):
with pytest.raises(ValidationError) as exc_info:
UpdateAssetBody.model_validate({"tags": [1, 2, 3]})
assert "string" in str(exc_info.value).lower()
def test_multiple_fields(self):
body = UpdateAssetBody.model_validate({
"name": "new_name",
"tags": ["tag1"],
"user_metadata": {"foo": "bar"}
})
assert body.name == "new_name"
assert body.tags == ["tag1"]
assert body.user_metadata == {"foo": "bar"}
class TestCreateFromHashBody:
def test_valid_blake3(self):
body = CreateFromHashBody(
hash="blake3:" + "a" * 64,
name="test"
)
assert body.hash.startswith("blake3:")
assert body.name == "test"
def test_valid_blake3_lowercase(self):
body = CreateFromHashBody(
hash="BLAKE3:" + "A" * 64,
name="test"
)
assert body.hash == "blake3:" + "a" * 64
def test_rejects_sha256(self):
with pytest.raises(ValidationError) as exc_info:
CreateFromHashBody(hash="sha256:" + "a" * 64, name="test")
assert "blake3" in str(exc_info.value).lower()
def test_rejects_no_colon(self):
with pytest.raises(ValidationError) as exc_info:
CreateFromHashBody(hash="a" * 64, name="test")
assert "blake3:<hex>" in str(exc_info.value)
def test_rejects_invalid_hex(self):
with pytest.raises(ValidationError) as exc_info:
CreateFromHashBody(hash="blake3:" + "g" * 64, name="test")
assert "hex" in str(exc_info.value).lower()
def test_rejects_empty_digest(self):
with pytest.raises(ValidationError) as exc_info:
CreateFromHashBody(hash="blake3:", name="test")
assert "hex" in str(exc_info.value).lower()
def test_default_tags_empty(self):
body = CreateFromHashBody(hash="blake3:" + "a" * 64, name="test")
assert body.tags == []
def test_default_user_metadata_empty(self):
body = CreateFromHashBody(hash="blake3:" + "a" * 64, name="test")
assert body.user_metadata == {}
def test_tags_normalized_lowercase(self):
body = CreateFromHashBody(
hash="blake3:" + "a" * 64,
name="test",
tags=["TAG1", "Tag2"]
)
assert body.tags == ["tag1", "tag2"]
def test_tags_deduplicated(self):
body = CreateFromHashBody(
hash="blake3:" + "a" * 64,
name="test",
tags=["tag", "TAG", "tag"]
)
assert body.tags == ["tag"]
def test_tags_csv_parsing(self):
body = CreateFromHashBody(
hash="blake3:" + "a" * 64,
name="test",
tags="a,b,c"
)
assert body.tags == ["a", "b", "c"]
def test_whitespace_stripping(self):
body = CreateFromHashBody(
hash=" blake3:" + "a" * 64 + " ",
name=" test "
)
assert body.hash == "blake3:" + "a" * 64
assert body.name == "test"
class TestUploadAssetSpec:
def test_first_tag_must_be_root_type_models(self):
spec = UploadAssetSpec.model_validate({"tags": ["models", "loras"]})
assert spec.tags[0] == "models"
def test_first_tag_must_be_root_type_input(self):
spec = UploadAssetSpec.model_validate({"tags": ["input"]})
assert spec.tags[0] == "input"
def test_first_tag_must_be_root_type_output(self):
spec = UploadAssetSpec.model_validate({"tags": ["output"]})
assert spec.tags[0] == "output"
def test_rejects_invalid_first_tag(self):
with pytest.raises(ValidationError) as exc_info:
UploadAssetSpec.model_validate({"tags": ["invalid"]})
assert "models, input, output" in str(exc_info.value)
def test_models_requires_category_tag(self):
with pytest.raises(ValidationError) as exc_info:
UploadAssetSpec.model_validate({"tags": ["models"]})
assert "category tag" in str(exc_info.value)
def test_input_does_not_require_second_tag(self):
spec = UploadAssetSpec.model_validate({"tags": ["input"]})
assert spec.tags == ["input"]
def test_output_does_not_require_second_tag(self):
spec = UploadAssetSpec.model_validate({"tags": ["output"]})
assert spec.tags == ["output"]
def test_tags_empty_rejected(self):
with pytest.raises(ValidationError):
UploadAssetSpec.model_validate({"tags": []})
def test_tags_csv_parsing(self):
spec = UploadAssetSpec.model_validate({"tags": "models,loras"})
assert spec.tags == ["models", "loras"]
def test_tags_json_array_parsing(self):
spec = UploadAssetSpec.model_validate({"tags": '["models", "loras"]'})
assert spec.tags == ["models", "loras"]
def test_tags_normalized_lowercase(self):
spec = UploadAssetSpec.model_validate({"tags": ["MODELS", "LORAS"]})
assert spec.tags == ["models", "loras"]
def test_tags_deduplicated(self):
spec = UploadAssetSpec.model_validate({"tags": ["models", "loras", "models"]})
assert spec.tags == ["models", "loras"]
def test_hash_validation_valid_blake3(self):
spec = UploadAssetSpec.model_validate({
"tags": ["input"],
"hash": "blake3:" + "a" * 64
})
assert spec.hash == "blake3:" + "a" * 64
def test_hash_validation_rejects_sha256(self):
with pytest.raises(ValidationError):
UploadAssetSpec.model_validate({
"tags": ["input"],
"hash": "sha256:" + "a" * 64
})
def test_hash_none_allowed(self):
spec = UploadAssetSpec.model_validate({"tags": ["input"], "hash": None})
assert spec.hash is None
def test_hash_empty_string_becomes_none(self):
spec = UploadAssetSpec.model_validate({"tags": ["input"], "hash": ""})
assert spec.hash is None
def test_name_optional(self):
spec = UploadAssetSpec.model_validate({"tags": ["input"]})
assert spec.name is None
def test_name_max_length(self):
with pytest.raises(ValidationError):
UploadAssetSpec.model_validate({
"tags": ["input"],
"name": "x" * 513
})
def test_user_metadata_json_string(self):
spec = UploadAssetSpec.model_validate({
"tags": ["input"],
"user_metadata": '{"key": "value"}'
})
assert spec.user_metadata == {"key": "value"}
def test_user_metadata_dict(self):
spec = UploadAssetSpec.model_validate({
"tags": ["input"],
"user_metadata": {"key": "value"}
})
assert spec.user_metadata == {"key": "value"}
def test_user_metadata_empty_string(self):
spec = UploadAssetSpec.model_validate({
"tags": ["input"],
"user_metadata": ""
})
assert spec.user_metadata == {}
def test_user_metadata_invalid_json(self):
with pytest.raises(ValidationError) as exc_info:
UploadAssetSpec.model_validate({
"tags": ["input"],
"user_metadata": "not json"
})
assert "must be JSON" in str(exc_info.value)
class TestSetPreviewBody:
def test_valid_uuid(self):
body = SetPreviewBody.model_validate({"preview_id": "550e8400-e29b-41d4-a716-446655440000"})
assert body.preview_id == "550e8400-e29b-41d4-a716-446655440000"
def test_none_allowed(self):
body = SetPreviewBody.model_validate({"preview_id": None})
assert body.preview_id is None
def test_empty_string_becomes_none(self):
body = SetPreviewBody.model_validate({"preview_id": ""})
assert body.preview_id is None
def test_whitespace_only_becomes_none(self):
body = SetPreviewBody.model_validate({"preview_id": " "})
assert body.preview_id is None
def test_invalid_uuid(self):
with pytest.raises(ValidationError) as exc_info:
SetPreviewBody.model_validate({"preview_id": "not-a-uuid"})
assert "UUID" in str(exc_info.value)
def test_default_is_none(self):
body = SetPreviewBody.model_validate({})
assert body.preview_id is None
class TestTagsAdd:
def test_non_empty_required(self):
with pytest.raises(ValidationError):
TagsAdd.model_validate({"tags": []})
def test_valid_tags(self):
body = TagsAdd.model_validate({"tags": ["tag1", "tag2"]})
assert body.tags == ["tag1", "tag2"]
def test_tags_normalized_lowercase(self):
body = TagsAdd.model_validate({"tags": ["TAG1", "Tag2"]})
assert body.tags == ["tag1", "tag2"]
def test_tags_whitespace_stripped(self):
body = TagsAdd.model_validate({"tags": [" tag1 ", " tag2 "]})
assert body.tags == ["tag1", "tag2"]
def test_tags_deduplicated(self):
body = TagsAdd.model_validate({"tags": ["tag", "TAG", "tag"]})
assert body.tags == ["tag"]
def test_empty_strings_filtered(self):
body = TagsAdd.model_validate({"tags": ["tag1", "", " ", "tag2"]})
assert body.tags == ["tag1", "tag2"]
def test_missing_tags_field_fails(self):
with pytest.raises(ValidationError):
TagsAdd.model_validate({})
class TestTagsRemove:
def test_non_empty_required(self):
with pytest.raises(ValidationError):
TagsRemove.model_validate({"tags": []})
def test_valid_tags(self):
body = TagsRemove.model_validate({"tags": ["tag1", "tag2"]})
assert body.tags == ["tag1", "tag2"]
def test_inherits_normalization(self):
body = TagsRemove.model_validate({"tags": ["TAG1", "Tag2"]})
assert body.tags == ["tag1", "tag2"]
class TestTagsListQuery:
def test_defaults(self):
q = TagsListQuery()
assert q.prefix is None
assert q.limit == 100
assert q.offset == 0
assert q.order == "count_desc"
assert q.include_zero is True
def test_prefix_normalized_lowercase(self):
q = TagsListQuery.model_validate({"prefix": "PREFIX"})
assert q.prefix == "prefix"
def test_prefix_whitespace_stripped(self):
q = TagsListQuery.model_validate({"prefix": " prefix "})
assert q.prefix == "prefix"
def test_prefix_whitespace_only_fails_min_length(self):
# After stripping, whitespace-only prefix becomes empty, which fails min_length=1
# The min_length check happens before the normalizer can return None
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"prefix": " "})
def test_prefix_min_length(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"prefix": ""})
def test_prefix_max_length(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"prefix": "x" * 257})
def test_limit_bounds_min(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"limit": 0})
def test_limit_bounds_max(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"limit": 1001})
def test_limit_bounds_valid(self):
q = TagsListQuery.model_validate({"limit": 1000})
assert q.limit == 1000
def test_offset_bounds_min(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"offset": -1})
def test_offset_bounds_max(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"offset": 10_000_001})
def test_order_valid_values(self):
for order_val in ["count_desc", "name_asc"]:
q = TagsListQuery.model_validate({"order": order_val})
assert q.order == order_val
def test_order_invalid(self):
with pytest.raises(ValidationError):
TagsListQuery.model_validate({"order": "invalid"})
def test_include_zero_bool(self):
q = TagsListQuery.model_validate({"include_zero": False})
assert q.include_zero is False
class TestScheduleAssetScanBody:
def test_valid_roots(self):
body = ScheduleAssetScanBody.model_validate({"roots": ["models"]})
assert body.roots == ["models"]
def test_multiple_roots(self):
body = ScheduleAssetScanBody.model_validate({"roots": ["models", "input", "output"]})
assert body.roots == ["models", "input", "output"]
def test_empty_roots_rejected(self):
with pytest.raises(ValidationError):
ScheduleAssetScanBody.model_validate({"roots": []})
def test_invalid_root_rejected(self):
with pytest.raises(ValidationError):
ScheduleAssetScanBody.model_validate({"roots": ["invalid"]})
def test_missing_roots_rejected(self):
with pytest.raises(ValidationError):
ScheduleAssetScanBody.model_validate({})