""" Unit and integration tests for app/utils/input_validation.py. Covers all validators: sort field/order, search query, task ID, and setting key. """ import pytest from fastapi import HTTPException # --------------------------------------------------------------------------- # validate_sort_field # --------------------------------------------------------------------------- @pytest.mark.unit class TestValidateSortField: """Tests for validate_sort_field.""" def test_accepts_valid_sort_fields(self): """All declared sort fields should be accepted.""" from app.utils.input_validation import ALLOWED_SORT_FIELDS, validate_sort_field for field in ALLOWED_SORT_FIELDS: assert validate_sort_field(field) == field def test_rejects_unknown_field(self): """An unknown sort field should raise 422.""" from app.utils.input_validation import validate_sort_field with pytest.raises(HTTPException) as exc_info: validate_sort_field("nonexistent_field") assert exc_info.value.status_code == 422 def test_rejects_sql_injection_attempt(self): """A SQL-injection-style field name should raise 422.""" from app.utils.input_validation import validate_sort_field with pytest.raises(HTTPException) as exc_info: validate_sort_field("id; DROP TABLE files; --") assert exc_info.value.status_code == 422 def test_rejects_empty_string(self): """An empty string should raise 422.""" from app.utils.input_validation import validate_sort_field with pytest.raises(HTTPException) as exc_info: validate_sort_field("") assert exc_info.value.status_code == 422 def test_error_message_lists_allowed_values(self): """Error detail should mention the allowed values.""" from app.utils.input_validation import validate_sort_field with pytest.raises(HTTPException) as exc_info: validate_sort_field("bad_field") assert "created_at" in exc_info.value.detail # --------------------------------------------------------------------------- # validate_sort_order # --------------------------------------------------------------------------- @pytest.mark.unit class TestValidateSortOrder: """Tests for validate_sort_order.""" def test_accepts_asc(self): from app.utils.input_validation import validate_sort_order assert validate_sort_order("asc") == "asc" def test_accepts_desc(self): from app.utils.input_validation import validate_sort_order assert validate_sort_order("desc") == "desc" def test_rejects_uppercase_asc(self): """Validation is case-sensitive; 'ASC' should be rejected.""" from app.utils.input_validation import validate_sort_order with pytest.raises(HTTPException) as exc_info: validate_sort_order("ASC") assert exc_info.value.status_code == 422 def test_rejects_arbitrary_string(self): from app.utils.input_validation import validate_sort_order with pytest.raises(HTTPException) as exc_info: validate_sort_order("random") assert exc_info.value.status_code == 422 def test_rejects_empty_string(self): from app.utils.input_validation import validate_sort_order with pytest.raises(HTTPException) as exc_info: validate_sort_order("") assert exc_info.value.status_code == 422 # --------------------------------------------------------------------------- # validate_search_query # --------------------------------------------------------------------------- @pytest.mark.unit class TestValidateSearchQuery: """Tests for validate_search_query.""" def test_returns_none_for_none_input(self): from app.utils.input_validation import validate_search_query assert validate_search_query(None) is None def test_returns_none_for_blank_string(self): from app.utils.input_validation import validate_search_query assert validate_search_query(" ") is None def test_strips_whitespace(self): from app.utils.input_validation import validate_search_query assert validate_search_query(" hello ") == "hello" def test_accepts_normal_query(self): from app.utils.input_validation import validate_search_query assert validate_search_query("invoice 2024") == "invoice 2024" def test_rejects_too_long_query(self): """A query longer than 255 characters should raise 422.""" from app.utils.input_validation import MAX_SEARCH_QUERY_LENGTH, validate_search_query long_query = "a" * (MAX_SEARCH_QUERY_LENGTH + 1) with pytest.raises(HTTPException) as exc_info: validate_search_query(long_query) assert exc_info.value.status_code == 422 def test_accepts_query_at_max_length(self): """A query exactly at the maximum length should be accepted.""" from app.utils.input_validation import MAX_SEARCH_QUERY_LENGTH, validate_search_query exact_query = "a" * MAX_SEARCH_QUERY_LENGTH assert validate_search_query(exact_query) == exact_query # --------------------------------------------------------------------------- # validate_task_id # --------------------------------------------------------------------------- @pytest.mark.unit class TestValidateTaskId: """Tests for validate_task_id.""" def test_accepts_valid_uuid(self): from app.utils.input_validation import validate_task_id valid_uuid = "550e8400-e29b-41d4-a716-446655440000" assert validate_task_id(valid_uuid) == valid_uuid def test_accepts_uppercase_uuid(self): """UUID validation should be case-insensitive.""" from app.utils.input_validation import validate_task_id upper_uuid = "550E8400-E29B-41D4-A716-446655440000" assert validate_task_id(upper_uuid) == upper_uuid def test_rejects_non_v4_uuid(self): """A syntactically valid UUID with a version other than 4 should be rejected.""" from app.utils.input_validation import validate_task_id # Version 1 UUID (version digit is '1', not '4') with pytest.raises(HTTPException) as exc_info: validate_task_id("550e8400-e29b-11d4-a716-446655440000") assert exc_info.value.status_code == 422 def test_rejects_short_string(self): from app.utils.input_validation import validate_task_id with pytest.raises(HTTPException) as exc_info: validate_task_id("abc-123") assert exc_info.value.status_code == 422 def test_rejects_sql_injection(self): from app.utils.input_validation import validate_task_id with pytest.raises(HTTPException) as exc_info: validate_task_id("'; DROP TABLE processing_logs; --") assert exc_info.value.status_code == 422 def test_rejects_path_traversal(self): from app.utils.input_validation import validate_task_id with pytest.raises(HTTPException) as exc_info: validate_task_id("../../etc/passwd") assert exc_info.value.status_code == 422 def test_rejects_empty_string(self): from app.utils.input_validation import validate_task_id with pytest.raises(HTTPException) as exc_info: validate_task_id("") assert exc_info.value.status_code == 422 def test_error_message_mentions_uuid(self): from app.utils.input_validation import validate_task_id with pytest.raises(HTTPException) as exc_info: validate_task_id("not-a-uuid") assert "UUID" in exc_info.value.detail # --------------------------------------------------------------------------- # validate_setting_key # --------------------------------------------------------------------------- @pytest.mark.unit class TestValidateSettingKey: """Tests for validate_setting_key.""" def test_accepts_known_setting_key(self): """A key that exists in SETTING_METADATA should be accepted.""" from app.utils.input_validation import validate_setting_key # 'workdir' is always defined in SETTING_METADATA assert validate_setting_key("workdir") == "workdir" def test_rejects_unknown_key_with_404(self): """An unknown but syntactically valid key should raise 404.""" from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("totally_unknown_key_xyz") assert exc_info.value.status_code == 404 def test_rejects_key_with_special_characters(self): """A key with special characters (e.g., injection attempt) should raise 400.""" from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("__class__") assert exc_info.value.status_code == 400 def test_rejects_key_with_dot_notation(self): """Dot-separated attribute traversal should be rejected.""" from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("model_fields") # model_fields is a Pydantic internal but not in SETTING_METADATA -> 404 assert exc_info.value.status_code == 404 def test_rejects_dunder_attributes(self): """Double-underscore attributes should be rejected (bad format).""" from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("__dict__") assert exc_info.value.status_code == 400 def test_rejects_empty_key(self): from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("") assert exc_info.value.status_code == 400 def test_rejects_key_starting_with_digit(self): """Keys starting with a digit should be rejected.""" from app.utils.input_validation import validate_setting_key with pytest.raises(HTTPException) as exc_info: validate_setting_key("1nvalid_key") assert exc_info.value.status_code == 400