Files
Vito Sansevero e1bf06cb3d test: add comprehensive unit tests for validators, hashing, metadata, config, and prompt tracker
Adds 143 new tests across 5 files covering previously untested modules:
- test_validators.py: all 7 validator functions + edge cases
- test_hashing.py: content hash, duplicate detection
- test_metadata_extraction.py: ComfyUI PNG metadata parsing with real images
- test_config.py: GalleryConfig/PromptManagerConfig with mocked PromptServer
- test_prompt_tracker.py: thread-safe prompt tracking, timeouts, context manager

Total test count: 87 → 264
2026-02-07 12:33:48 -08:00

292 lines
9.2 KiB
Python

"""
Comprehensive tests for input validation utilities.
Tests all 7 validator functions in utils/validators.py including
edge cases, boundary conditions, and security-related inputs.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.validators import (
validate_prompt_text,
validate_rating,
validate_tags,
validate_category,
validate_workflow_name,
sanitize_input,
parse_tags_string,
)
class TestValidateCategory(unittest.TestCase):
"""Test validate_category function."""
def test_none_is_valid(self):
self.assertTrue(validate_category(None))
def test_empty_string_is_valid(self):
self.assertTrue(validate_category(""))
def test_whitespace_only_is_valid(self):
self.assertTrue(validate_category(" "))
def test_normal_category(self):
self.assertTrue(validate_category("landscape"))
def test_category_with_spaces(self):
self.assertTrue(validate_category("nature photos"))
def test_category_with_hyphens_underscores(self):
self.assertTrue(validate_category("sci-fi_art"))
def test_unicode_category(self):
self.assertTrue(validate_category("日本語カテゴリ"))
def test_max_length_boundary(self):
self.assertTrue(validate_category("x" * 100))
def test_exceeds_max_length(self):
with self.assertRaises(ValueError) as ctx:
validate_category("x" * 101)
self.assertIn("100", str(ctx.exception))
def test_non_string_raises(self):
with self.assertRaises(ValueError):
validate_category(123)
def test_list_raises(self):
with self.assertRaises(ValueError):
validate_category(["category"])
def test_control_characters_rejected(self):
with self.assertRaises(ValueError) as ctx:
validate_category("bad\x00category")
self.assertIn("control characters", str(ctx.exception))
def test_tab_character_rejected(self):
with self.assertRaises(ValueError):
validate_category("bad\tcategory")
def test_newline_rejected(self):
with self.assertRaises(ValueError):
validate_category("bad\ncategory")
class TestValidateWorkflowName(unittest.TestCase):
"""Test validate_workflow_name function."""
def test_none_is_valid(self):
self.assertTrue(validate_workflow_name(None))
def test_empty_string_is_valid(self):
self.assertTrue(validate_workflow_name(""))
def test_whitespace_only_is_valid(self):
self.assertTrue(validate_workflow_name(" "))
def test_normal_name(self):
self.assertTrue(validate_workflow_name("My Workflow"))
def test_name_with_special_chars(self):
self.assertTrue(validate_workflow_name("workflow-v2_final (copy)"))
def test_max_length_boundary(self):
self.assertTrue(validate_workflow_name("x" * 200))
def test_exceeds_max_length(self):
with self.assertRaises(ValueError) as ctx:
validate_workflow_name("x" * 201)
self.assertIn("200", str(ctx.exception))
def test_non_string_raises(self):
with self.assertRaises(ValueError):
validate_workflow_name(42)
def test_dict_raises(self):
with self.assertRaises(ValueError):
validate_workflow_name({"name": "workflow"})
class TestSanitizeInput(unittest.TestCase):
"""Test sanitize_input function."""
def test_normal_text_unchanged(self):
self.assertEqual(sanitize_input("hello world"), "hello world")
def test_strips_outer_whitespace(self):
self.assertEqual(sanitize_input(" hello "), "hello")
def test_removes_null_bytes(self):
self.assertEqual(sanitize_input("hello\x00world"), "helloworld")
def test_normalizes_windows_line_endings(self):
self.assertEqual(sanitize_input("line1\r\nline2"), "line1\nline2")
def test_normalizes_old_mac_line_endings(self):
self.assertEqual(sanitize_input("line1\rline2"), "line1\nline2")
def test_strips_whitespace_per_line(self):
result = sanitize_input(" line1 \n line2 ")
self.assertEqual(result, "line1\nline2")
def test_limits_consecutive_empty_lines(self):
text = "a\n\n\n\n\nb"
result = sanitize_input(text)
# Max 2 consecutive empty lines = 3 newlines between content
# 4+ newlines (3+ empty lines) should never appear
self.assertNotIn("\n\n\n\n", result)
self.assertIn("a", result)
self.assertIn("b", result)
def test_non_string_returns_empty(self):
self.assertEqual(sanitize_input(None), "")
self.assertEqual(sanitize_input(123), "")
self.assertEqual(sanitize_input([]), "")
def test_empty_string(self):
self.assertEqual(sanitize_input(""), "")
def test_preserves_single_newlines(self):
result = sanitize_input("line1\nline2\nline3")
self.assertEqual(result, "line1\nline2\nline3")
def test_mixed_line_endings(self):
result = sanitize_input("a\r\nb\rc\nd")
self.assertEqual(result, "a\nb\nc\nd")
class TestParseTagsString(unittest.TestCase):
"""Test parse_tags_string function."""
def test_simple_comma_separated(self):
result = parse_tags_string("tag1, tag2, tag3")
self.assertEqual(result, ["tag1", "tag2", "tag3"])
def test_extra_whitespace(self):
result = parse_tags_string(" tag1 , tag2 ")
self.assertEqual(result, ["tag1", "tag2"])
def test_empty_string_returns_empty_list(self):
self.assertEqual(parse_tags_string(""), [])
def test_none_returns_empty_list(self):
self.assertEqual(parse_tags_string(None), [])
def test_non_string_returns_empty_list(self):
self.assertEqual(parse_tags_string(123), [])
def test_single_tag(self):
self.assertEqual(parse_tags_string("solo"), ["solo"])
def test_deduplication(self):
result = parse_tags_string("tag1, tag1, tag2, tag1")
self.assertEqual(result, ["tag1", "tag2"])
def test_empty_segments_skipped(self):
result = parse_tags_string("tag1,,tag2,,,tag3")
self.assertEqual(result, ["tag1", "tag2", "tag3"])
def test_max_20_tags(self):
tags = ", ".join(f"tag{i}" for i in range(25))
result = parse_tags_string(tags)
self.assertEqual(len(result), 20)
def test_strips_null_bytes_from_tags(self):
result = parse_tags_string("clean\x00tag, normal")
self.assertIn("cleantag", result)
self.assertIn("normal", result)
class TestValidatePromptTextEdgeCases(unittest.TestCase):
"""Additional edge cases beyond test_basic.py coverage."""
def test_exactly_max_length(self):
self.assertTrue(validate_prompt_text("x" * 10000))
def test_one_over_max_length(self):
with self.assertRaises(ValueError):
validate_prompt_text("x" * 10001)
def test_single_character(self):
self.assertTrue(validate_prompt_text("a"))
def test_unicode_text(self):
self.assertTrue(validate_prompt_text("日本語プロンプト"))
def test_multiline_text(self):
self.assertTrue(validate_prompt_text("line1\nline2\nline3"))
def test_boolean_raises(self):
with self.assertRaises(ValueError):
validate_prompt_text(True)
def test_none_raises(self):
with self.assertRaises(ValueError):
validate_prompt_text(None)
class TestValidateRatingEdgeCases(unittest.TestCase):
"""Additional edge cases beyond test_basic.py coverage."""
def test_float_raises(self):
with self.assertRaises(ValueError):
validate_rating(3.5)
def test_negative_raises(self):
with self.assertRaises(ValueError):
validate_rating(-1)
def test_zero_raises(self):
with self.assertRaises(ValueError):
validate_rating(0)
def test_boolean_true_is_int(self):
# In Python, bool is a subclass of int: True == 1
# This tests the actual behavior
self.assertTrue(validate_rating(True))
class TestValidateTagsEdgeCases(unittest.TestCase):
"""Additional edge cases beyond test_basic.py coverage."""
def test_empty_list_is_valid(self):
self.assertTrue(validate_tags([]))
def test_comma_string_parsed(self):
self.assertTrue(validate_tags("tag1, tag2"))
def test_control_char_in_tag_rejected(self):
with self.assertRaises(ValueError) as ctx:
validate_tags(["bad\x00tag"])
self.assertIn("control characters", str(ctx.exception))
def test_tab_in_tag_rejected(self):
with self.assertRaises(ValueError):
validate_tags(["bad\ttag"])
def test_non_string_tag_in_list_rejected(self):
with self.assertRaises(ValueError):
validate_tags([123])
def test_dict_input_rejected(self):
with self.assertRaises(ValueError):
validate_tags({"tag": "value"})
def test_exactly_20_tags(self):
self.assertTrue(validate_tags([f"tag{i}" for i in range(20)]))
def test_exactly_50_char_tag(self):
self.assertTrue(validate_tags(["x" * 50]))
def test_whitespace_only_tag_rejected(self):
with self.assertRaises(ValueError):
validate_tags([" "])
if __name__ == "__main__":
unittest.main()