Skip to main content
Glama
test_security_validators.pyβ€’29.1 kB
"""Comprehensive tests for security.validators module.""" import pytest from mnemex.security.validators import ( ALLOWED_RELATION_TYPES, ALLOWED_TARGETS, MAX_CONTENT_LENGTH, MAX_LIST_LENGTH, MAX_TAG_LENGTH, validate_entity, validate_list_length, validate_positive_int, validate_relation_type, validate_score, validate_string_length, validate_tag, validate_target, validate_uuid, ) class TestValidateUuid: """Tests for validate_uuid function.""" def test_valid_uuid_lowercase(self): """Test valid lowercase UUIDs.""" uuid1 = "123e4567-e89b-12d3-a456-426614174000" assert validate_uuid(uuid1) == uuid1.lower() uuid2 = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" assert validate_uuid(uuid2) == uuid2.lower() def test_valid_uuid_uppercase(self): """Test valid uppercase UUIDs are converted to lowercase.""" uuid_upper = "123E4567-E89B-12D3-A456-426614174000" expected = "123e4567-e89b-12d3-a456-426614174000" assert validate_uuid(uuid_upper) == expected def test_valid_uuid_mixed_case(self): """Test valid mixed-case UUIDs are converted to lowercase.""" uuid_mixed = "123e4567-E89B-12d3-A456-426614174000" expected = "123e4567-e89b-12d3-a456-426614174000" assert validate_uuid(uuid_mixed) == expected def test_valid_uuid_with_hyphens(self): """Test valid UUIDs with proper hyphen placement.""" uuid_valid = "550e8400-e29b-41d4-a716-446655440000" assert validate_uuid(uuid_valid) == uuid_valid def test_invalid_uuid_too_short(self): """Test rejection of UUIDs that are too short.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123e4567-e89b-12d3-a456") def test_invalid_uuid_too_long(self): """Test rejection of UUIDs that are too long.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123e4567-e89b-12d3-a456-426614174000-extra") def test_invalid_uuid_wrong_characters(self): """Test rejection of UUIDs with invalid characters.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123g4567-e89b-12d3-a456-426614174000") with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123e4567-e89b-12d3-a456-42661417400z") def test_invalid_uuid_no_hyphens(self): """Test rejection of UUIDs without hyphens.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123e4567e89b12d3a456426614174000") def test_invalid_uuid_wrong_hyphen_placement(self): """Test rejection of UUIDs with wrong hyphen placement.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("123e456-7e89b-12d3-a456-426614174000") def test_non_string_input_integer(self): """Test rejection of non-string inputs (integer).""" with pytest.raises(ValueError, match="must be a string, got int"): validate_uuid(12345) def test_non_string_input_none(self): """Test rejection of None input.""" with pytest.raises(ValueError, match="must be a string, got NoneType"): validate_uuid(None) def test_non_string_input_list(self): """Test rejection of list input.""" with pytest.raises(ValueError, match="must be a string, got list"): validate_uuid(["123e4567-e89b-12d3-a456-426614174000"]) def test_empty_string(self): """Test rejection of empty string.""" with pytest.raises(ValueError, match="must be a valid UUID"): validate_uuid("") def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="user_id"): validate_uuid("invalid", field_name="user_id") with pytest.raises(ValueError, match="memory_uuid"): validate_uuid(123, field_name="memory_uuid") def test_uuid_truncation_in_error_message(self): """Test that very long invalid UUIDs are truncated in error messages.""" long_invalid = "x" * 100 with pytest.raises(ValueError, match=r"\.\.\."): validate_uuid(long_invalid) class TestValidateStringLength: """Tests for validate_string_length function.""" def test_valid_string_within_limit(self): """Test valid strings within length limits.""" assert validate_string_length("hello", 10) == "hello" assert validate_string_length("test string", 50) == "test string" assert validate_string_length("a" * 100, 100) == "a" * 100 def test_valid_string_at_exact_limit(self): """Test strings at exact length limit.""" assert validate_string_length("a" * 50, 50) == "a" * 50 def test_empty_string_allow_empty_true(self): """Test empty strings with allow_empty=True.""" assert validate_string_length("", 10, allow_empty=True) == "" def test_empty_string_allow_empty_false(self): """Test empty strings with allow_empty=False.""" with pytest.raises(ValueError, match="cannot be empty"): validate_string_length("", 10, allow_empty=False) def test_string_too_long(self): """Test rejection of strings exceeding max_length.""" with pytest.raises( ValueError, match=r"exceeds maximum length of 10 characters \(got 15 characters\)" ): validate_string_length("a" * 15, 10) def test_string_exceeds_max_content_length(self): """Test rejection of very long strings.""" long_string = "x" * (MAX_CONTENT_LENGTH + 1) with pytest.raises(ValueError, match="exceeds maximum length"): validate_string_length(long_string, MAX_CONTENT_LENGTH) def test_non_string_input_integer(self): """Test rejection of non-string inputs (integer).""" with pytest.raises(ValueError, match="must be a string, got int"): validate_string_length(123, 10) def test_non_string_input_list(self): """Test rejection of list input.""" with pytest.raises(ValueError, match="must be a string, got list"): validate_string_length(["test"], 10) def test_none_with_allow_none_true(self): """Test None input with allow_none=True.""" assert validate_string_length(None, 10, allow_none=True) is None def test_none_with_allow_none_false(self): """Test None input with allow_none=False.""" with pytest.raises(ValueError, match="cannot be None"): validate_string_length(None, 10, allow_none=False) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="content"): validate_string_length("a" * 100, 10, field_name="content") with pytest.raises(ValueError, match="title"): validate_string_length("", 10, field_name="title", allow_empty=False) def test_length_formatting_with_commas(self): """Test that large numbers are formatted with commas in error messages.""" with pytest.raises(ValueError, match=r"1,000"): validate_string_length("a" * 2000, 1000) class TestValidatePositiveInt: """Tests for validate_positive_int function.""" def test_valid_positive_integers(self): """Test valid positive integers.""" assert validate_positive_int(1) == 1 assert validate_positive_int(42) == 42 assert validate_positive_int(1000) == 1000 def test_zero_with_default_min(self): """Test rejection of zero with default min_value=1.""" with pytest.raises(ValueError, match="must be >= 1, got 0"): validate_positive_int(0) def test_zero_with_custom_min_zero(self): """Test acceptance of zero with min_value=0.""" assert validate_positive_int(0, min_value=0) == 0 def test_negative_numbers(self): """Test rejection of negative numbers.""" with pytest.raises(ValueError, match="must be >= 1, got -1"): validate_positive_int(-1) with pytest.raises(ValueError, match="must be >= 1, got -100"): validate_positive_int(-100) def test_non_integer_input_float(self): """Test rejection of float inputs.""" with pytest.raises(ValueError, match="must be an integer, got float"): validate_positive_int(3.14) with pytest.raises(ValueError, match="must be an integer, got float"): validate_positive_int(1.0) def test_non_integer_input_string(self): """Test rejection of string inputs.""" with pytest.raises(ValueError, match="must be an integer, got str"): validate_positive_int("42") def test_non_integer_input_boolean(self): """Test rejection of boolean inputs (bool is subclass of int in Python).""" with pytest.raises(ValueError, match="must be an integer, got bool"): validate_positive_int(True) with pytest.raises(ValueError, match="must be an integer, got bool"): validate_positive_int(False) def test_custom_min_value(self): """Test custom min_value parameter.""" assert validate_positive_int(10, min_value=10) == 10 assert validate_positive_int(100, min_value=50) == 100 with pytest.raises(ValueError, match="must be >= 10, got 5"): validate_positive_int(5, min_value=10) def test_custom_max_value(self): """Test custom max_value parameter.""" assert validate_positive_int(50, max_value=100) == 50 assert validate_positive_int(100, max_value=100) == 100 with pytest.raises(ValueError, match="must be <= 100, got 150"): validate_positive_int(150, max_value=100) def test_custom_min_and_max_value(self): """Test custom min and max values together.""" assert validate_positive_int(50, min_value=10, max_value=100) == 50 with pytest.raises(ValueError, match="must be >= 10"): validate_positive_int(5, min_value=10, max_value=100) with pytest.raises(ValueError, match="must be <= 100"): validate_positive_int(150, min_value=10, max_value=100) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="limit"): validate_positive_int(0, field_name="limit") with pytest.raises(ValueError, match="count"): validate_positive_int("invalid", field_name="count") def test_large_positive_integer(self): """Test very large positive integers.""" large_int = 10**10 assert validate_positive_int(large_int) == large_int class TestValidateScore: """Tests for validate_score function.""" def test_valid_scores_in_range(self): """Test valid scores within [0.0, 1.0] range.""" assert validate_score(0.5) == 0.5 assert validate_score(0.75) == 0.75 assert validate_score(0.123456) == 0.123456 def test_edge_case_zero(self): """Test edge case of exactly 0.0.""" assert validate_score(0.0) == 0.0 def test_edge_case_one(self): """Test edge case of exactly 1.0.""" assert validate_score(1.0) == 1.0 def test_integer_zero_accepted(self): """Test integer 0 is accepted and converted to float.""" result = validate_score(0) assert result == 0.0 assert isinstance(result, float) def test_integer_one_accepted(self): """Test integer 1 is accepted and converted to float.""" result = validate_score(1) assert result == 1.0 assert isinstance(result, float) def test_negative_score(self): """Test rejection of negative scores.""" with pytest.raises(ValueError, match="must be between 0.0 and 1.0, got -0.1"): validate_score(-0.1) with pytest.raises(ValueError, match="must be between 0.0 and 1.0, got -1.0"): validate_score(-1.0) def test_score_greater_than_one(self): """Test rejection of scores > 1.0.""" with pytest.raises(ValueError, match="must be between 0.0 and 1.0, got 1.1"): validate_score(1.1) with pytest.raises(ValueError, match="must be between 0.0 and 1.0, got 2.5"): validate_score(2.5) def test_non_numeric_input_string(self): """Test rejection of non-numeric inputs (string).""" with pytest.raises(ValueError, match="must be a number, got str"): validate_score("0.5") def test_non_numeric_input_none(self): """Test rejection of None input.""" with pytest.raises(ValueError, match="must be a number, got NoneType"): validate_score(None) def test_non_numeric_input_list(self): """Test rejection of list input.""" with pytest.raises(ValueError, match="must be a number, got list"): validate_score([0.5]) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="confidence"): validate_score(1.5, field_name="confidence") with pytest.raises(ValueError, match="importance"): validate_score("invalid", field_name="importance") class TestValidateTag: """Tests for validate_tag function.""" def test_valid_tags_alphanumeric(self): """Test valid alphanumeric tags.""" assert validate_tag("tag") == "tag" assert validate_tag("tag123") == "tag123" assert validate_tag("TAG") == "TAG" def test_valid_tags_with_hyphens(self): """Test valid tags with hyphens.""" assert validate_tag("my-tag") == "my-tag" assert validate_tag("machine-learning") == "machine-learning" def test_valid_tags_with_underscores(self): """Test valid tags with underscores.""" assert validate_tag("my_tag") == "my_tag" assert validate_tag("python_code") == "python_code" def test_valid_tags_mixed_allowed_characters(self): """Test valid tags with mix of alphanumeric, hyphens, underscores.""" assert validate_tag("my-tag_123") == "my-tag_123" assert validate_tag("ML-model_v2") == "ML-model_v2" def test_whitespace_stripped(self): """Test that leading/trailing whitespace is stripped.""" assert validate_tag(" tag ") == "tag" assert validate_tag("\ttag\t") == "tag" def test_invalid_characters_spaces(self): """Test rejection of tags with spaces.""" with pytest.raises(ValueError, match="contains invalid characters"): validate_tag("my tag") def test_invalid_characters_special_chars(self): """Test rejection of tags with special characters.""" with pytest.raises(ValueError, match="contains invalid characters"): validate_tag("tag!") with pytest.raises(ValueError, match="contains invalid characters"): validate_tag("tag@email") with pytest.raises(ValueError, match="contains invalid characters"): validate_tag("tag#hash") def test_empty_tag_after_strip(self): """Test rejection of empty tags after stripping.""" with pytest.raises(ValueError, match="cannot be empty"): validate_tag("") with pytest.raises(ValueError, match="cannot be empty"): validate_tag(" ") def test_tag_too_long(self): """Test rejection of tags exceeding MAX_TAG_LENGTH.""" long_tag = "a" * (MAX_TAG_LENGTH + 1) with pytest.raises(ValueError, match=f"exceeds maximum length of {MAX_TAG_LENGTH}"): validate_tag(long_tag) def test_tag_at_max_length(self): """Test tags at exactly MAX_TAG_LENGTH are accepted.""" max_tag = "a" * MAX_TAG_LENGTH assert validate_tag(max_tag) == max_tag def test_non_string_input(self): """Test rejection of non-string inputs.""" with pytest.raises(ValueError, match="must be a string, got int"): validate_tag(123) with pytest.raises(ValueError, match="must be a string, got list"): validate_tag(["tag"]) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="category"): validate_tag("invalid tag", field_name="category") with pytest.raises(ValueError, match="label"): validate_tag("", field_name="label") def test_tag_truncation_in_error_message(self): """Test that long invalid tags are truncated in error messages.""" long_invalid = "x" * 60 + " invalid" with pytest.raises(ValueError, match="Got:"): validate_tag(long_invalid) class TestValidateEntity: """Tests for validate_entity function.""" def test_valid_entities_alphanumeric(self): """Test valid alphanumeric entities.""" assert validate_entity("entity") == "entity" assert validate_entity("Entity123") == "Entity123" def test_valid_entities_with_hyphens(self): """Test valid entities with hyphens.""" assert validate_entity("my-entity") == "my-entity" assert validate_entity("project-alpha") == "project-alpha" def test_valid_entities_with_underscores(self): """Test valid entities with underscores.""" assert validate_entity("user_123") == "user_123" def test_valid_entities_with_spaces(self): """Test valid entities with spaces.""" assert validate_entity("Claude AI") == "Claude AI" assert validate_entity("Project Alpha") == "Project Alpha" def test_valid_entities_mixed_allowed_characters(self): """Test valid entities with mix of allowed characters.""" assert validate_entity("My-Project_v2 Beta") == "My-Project_v2 Beta" def test_whitespace_stripped(self): """Test that leading/trailing whitespace is stripped.""" assert validate_entity(" entity ") == "entity" assert validate_entity("\tentity\t") == "entity" def test_multiple_spaces_normalized(self): """Test that multiple spaces are normalized to single space.""" assert validate_entity("Claude AI") == "Claude AI" assert validate_entity("Project Alpha") == "Project Alpha" assert validate_entity("A B C") == "A B C" def test_invalid_characters_special_chars(self): """Test rejection of entities with invalid special characters.""" with pytest.raises(ValueError, match="contains invalid characters"): validate_entity("entity!") with pytest.raises(ValueError, match="contains invalid characters"): validate_entity("entity@domain") with pytest.raises(ValueError, match="contains invalid characters"): validate_entity("entity#tag") def test_empty_entity_after_strip(self): """Test rejection of empty entities after stripping.""" with pytest.raises(ValueError, match="cannot be empty"): validate_entity("") with pytest.raises(ValueError, match="cannot be empty"): validate_entity(" ") def test_entity_too_long(self): """Test rejection of entities exceeding MAX_TAG_LENGTH.""" long_entity = "a" * (MAX_TAG_LENGTH + 1) with pytest.raises(ValueError, match=f"exceeds maximum length of {MAX_TAG_LENGTH}"): validate_entity(long_entity) def test_entity_at_max_length(self): """Test entities at exactly MAX_TAG_LENGTH are accepted.""" max_entity = "a" * MAX_TAG_LENGTH assert validate_entity(max_entity) == max_entity def test_non_string_input(self): """Test rejection of non-string inputs.""" with pytest.raises(ValueError, match="must be a string, got int"): validate_entity(123) with pytest.raises(ValueError, match="must be a string, got dict"): validate_entity({"name": "entity"}) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="person"): validate_entity("invalid!", field_name="person") with pytest.raises(ValueError, match="project_name"): validate_entity("", field_name="project_name") def test_entity_truncation_in_error_message(self): """Test that long invalid entities are truncated in error messages.""" long_invalid = "x" * 60 + "!" with pytest.raises(ValueError, match="Got:"): validate_entity(long_invalid) class TestValidateListLength: """Tests for validate_list_length function.""" def test_valid_empty_list(self): """Test valid empty list.""" assert validate_list_length([], 10) == [] def test_valid_list_within_limit(self): """Test valid lists within length limits.""" assert validate_list_length([1, 2, 3], 10) == [1, 2, 3] assert validate_list_length(["a", "b"], 5) == ["a", "b"] def test_valid_list_at_exact_limit(self): """Test lists at exact length limit.""" items = list(range(10)) assert validate_list_length(items, 10) == items def test_list_too_long(self): """Test rejection of lists exceeding max_length.""" long_list = list(range(20)) with pytest.raises( ValueError, match=r"exceeds maximum length of 10 items \(got 20 items\)" ): validate_list_length(long_list, 10) def test_list_exceeds_max_list_length(self): """Test rejection of very long lists.""" long_list = list(range(MAX_LIST_LENGTH + 1)) with pytest.raises(ValueError, match="exceeds maximum length"): validate_list_length(long_list, MAX_LIST_LENGTH) def test_non_list_input_tuple(self): """Test rejection of non-list inputs (tuple).""" with pytest.raises(ValueError, match="must be a list, got tuple"): validate_list_length((1, 2, 3), 10) def test_non_list_input_string(self): """Test rejection of string input.""" with pytest.raises(ValueError, match="must be a list, got str"): validate_list_length("not a list", 10) def test_non_list_input_dict(self): """Test rejection of dict input.""" with pytest.raises(ValueError, match="must be a list, got dict"): validate_list_length({"key": "value"}, 10) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="tags"): validate_list_length(list(range(20)), 10, field_name="tags") with pytest.raises(ValueError, match="memory_ids"): validate_list_length("not a list", 10, field_name="memory_ids") def test_list_with_various_types(self): """Test lists containing various types of items.""" mixed_list = [1, "two", 3.0, None, {"key": "value"}] assert validate_list_length(mixed_list, 10) == mixed_list class TestValidateRelationType: """Tests for validate_relation_type function.""" def test_valid_relation_types(self): """Test all valid relation types from ALLOWED_RELATION_TYPES.""" for rel_type in ALLOWED_RELATION_TYPES: assert validate_relation_type(rel_type) == rel_type def test_specific_valid_types(self): """Test specific known valid relation types.""" assert validate_relation_type("related") == "related" assert validate_relation_type("causes") == "causes" assert validate_relation_type("supports") == "supports" assert validate_relation_type("contradicts") == "contradicts" assert validate_relation_type("has_decision") == "has_decision" assert validate_relation_type("consolidated_from") == "consolidated_from" def test_invalid_relation_type(self): """Test rejection of invalid relation types.""" with pytest.raises(ValueError, match="must be one of"): validate_relation_type("unknown") with pytest.raises(ValueError, match="must be one of"): validate_relation_type("invalid_type") def test_empty_string(self): """Test rejection of empty string.""" with pytest.raises(ValueError, match="must be one of"): validate_relation_type("") def test_case_sensitive(self): """Test that relation type validation is case-sensitive.""" with pytest.raises(ValueError, match="must be one of"): validate_relation_type("RELATED") with pytest.raises(ValueError, match="must be one of"): validate_relation_type("Related") def test_non_string_input(self): """Test rejection of non-string inputs.""" with pytest.raises(ValueError, match="must be a string, got int"): validate_relation_type(123) with pytest.raises(ValueError, match="must be a string, got list"): validate_relation_type(["related"]) def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="rel_type"): validate_relation_type("invalid", field_name="rel_type") with pytest.raises(ValueError, match="relationship"): validate_relation_type(123, field_name="relationship") def test_error_message_shows_allowed_values(self): """Test that error message shows sorted allowed values.""" with pytest.raises(ValueError, match="must be one of") as exc_info: validate_relation_type("invalid") error_message = str(exc_info.value) for rel_type in ALLOWED_RELATION_TYPES: assert rel_type in error_message class TestValidateTarget: """Tests for validate_target function.""" def test_valid_targets(self): """Test all valid targets from ALLOWED_TARGETS.""" for target in ALLOWED_TARGETS: assert validate_target(target) == target def test_valid_obsidian_target(self): """Test the specific 'obsidian' target.""" assert validate_target("obsidian") == "obsidian" def test_invalid_target(self): """Test rejection of invalid targets.""" with pytest.raises(ValueError, match="must be one of"): validate_target("unknown") with pytest.raises(ValueError, match="must be one of"): validate_target("markdown") def test_empty_string(self): """Test rejection of empty string.""" with pytest.raises(ValueError, match="must be one of"): validate_target("") def test_case_sensitive(self): """Test that target validation is case-sensitive.""" with pytest.raises(ValueError, match="must be one of"): validate_target("OBSIDIAN") with pytest.raises(ValueError, match="must be one of"): validate_target("Obsidian") def test_non_string_input(self): """Test rejection of non-string inputs.""" with pytest.raises(ValueError, match="must be a string, got int"): validate_target(123) with pytest.raises(ValueError, match="must be a string, got list"): validate_target(["obsidian"]) def test_path_like_input_rejected(self): """Test rejection of path-like strings (target is not a path).""" with pytest.raises(ValueError, match="must be one of"): validate_target("/path/to/vault") with pytest.raises(ValueError, match="must be one of"): validate_target("./vault") def test_error_message_explains_target_vs_path(self): """Test that error message explains target is a backend, not a path.""" with pytest.raises(ValueError, match="storage backend") as exc_info: validate_target("invalid") error_message = str(exc_info.value) assert "not a file path" in error_message assert "LTM_VAULT_PATH" in error_message def test_custom_field_name_in_errors(self): """Test that custom field_name appears in error messages.""" with pytest.raises(ValueError, match="backend"): validate_target("invalid", field_name="backend") with pytest.raises(ValueError, match="storage_type"): validate_target(123, field_name="storage_type") def test_error_message_shows_allowed_values(self): """Test that error message shows sorted allowed values.""" with pytest.raises(ValueError, match="must be one of") as exc_info: validate_target("invalid") error_message = str(exc_info.value) for target in ALLOWED_TARGETS: assert target in error_message

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mnemexai/mnemex'

If you have feedback or need assistance with the MCP directory API, please join our Discord server