mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-26 19:19:53 +00: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>
510 lines
18 KiB
Python
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({})
|