Removes helpers nothing calls, test-only wrappers and their tests, the unused config writer, three unused images, and the prompt registry the nodes wrote to but nothing read, which grew with every run.
275 lines
8.8 KiB
Python
275 lines
8.8 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,
|
|
sanitize_input,
|
|
validate_result_timeout,
|
|
validate_worker_threads,
|
|
)
|
|
|
|
|
|
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 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 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([" "])
|
|
|
|
|
|
class TestValidateResultTimeout(unittest.TestCase):
|
|
"""Test validate_result_timeout function (#149)."""
|
|
|
|
def test_zero_disables_auto_hide(self):
|
|
self.assertEqual(validate_result_timeout(0), 0)
|
|
|
|
def test_typical_value(self):
|
|
self.assertEqual(validate_result_timeout(30), 30)
|
|
|
|
def test_max_boundary(self):
|
|
self.assertEqual(validate_result_timeout(300), 300)
|
|
|
|
def test_returns_int(self):
|
|
self.assertIsInstance(validate_result_timeout(5), int)
|
|
|
|
def test_none_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
validate_result_timeout(None)
|
|
|
|
def test_garbage_string_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
validate_result_timeout("soon")
|
|
|
|
def test_numeric_string_coerced(self):
|
|
self.assertEqual(validate_result_timeout(" 10 "), 10)
|
|
|
|
def test_fractional_values_rejected(self):
|
|
for value in (1.5, "1.5"):
|
|
with self.assertRaises(ValueError):
|
|
validate_result_timeout(value)
|
|
|
|
def test_out_of_range_rejected_not_clamped(self):
|
|
for value in (-5, 301, 9999):
|
|
with self.assertRaises(ValueError):
|
|
validate_result_timeout(value)
|
|
|
|
def test_bool_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
validate_result_timeout(True)
|
|
|
|
|
|
class TestValidateWorkerThreads(unittest.TestCase):
|
|
"""validate_worker_threads(value, max_workers) -> int in [1, max_workers]."""
|
|
|
|
def test_accepts_ints_and_digit_strings_within_range(self):
|
|
self.assertEqual(validate_worker_threads(1, 8), 1)
|
|
self.assertEqual(validate_worker_threads(8, 8), 8)
|
|
self.assertEqual(validate_worker_threads("4", 8), 4)
|
|
|
|
def test_rejects_out_of_range_bool_and_junk(self):
|
|
for value in (0, -1, 9, True, False, "x", None, 2.5, [2]):
|
|
with self.assertRaises(ValueError, msg=repr(value)):
|
|
validate_worker_threads(value, 8)
|
|
|
|
def test_max_of_one_only_accepts_one(self):
|
|
self.assertEqual(validate_worker_threads(1, 1), 1)
|
|
with self.assertRaises(ValueError):
|
|
validate_worker_threads(2, 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|