test_memory_url_validation.py•9.45 kB
"""Tests for memory URL validation functionality."""
import pytest
from pydantic import ValidationError
from basic_memory.schemas.memory import (
    normalize_memory_url,
    validate_memory_url_path,
    memory_url,
)
class TestValidateMemoryUrlPath:
    """Test the validate_memory_url_path function."""
    def test_valid_paths(self):
        """Test that valid paths pass validation."""
        valid_paths = [
            "notes/meeting",
            "projects/basic-memory",
            "research/findings-2025",
            "specs/search",
            "docs/api-spec",
            "folder/subfolder/note",
            "single-note",
            "notes/with-hyphens",
            "notes/with_underscores",
            "notes/with123numbers",
            "pattern/*",  # Wildcard pattern matching
            "deep/*/pattern",
        ]
        for path in valid_paths:
            assert validate_memory_url_path(path), f"Path '{path}' should be valid"
    def test_invalid_empty_paths(self):
        """Test that empty/whitespace paths fail validation."""
        invalid_paths = [
            "",
            "   ",
            "\t",
            "\n",
            "  \n  ",
        ]
        for path in invalid_paths:
            assert not validate_memory_url_path(path), f"Path '{path}' should be invalid"
    def test_invalid_double_slashes(self):
        """Test that paths with double slashes fail validation."""
        invalid_paths = [
            "notes//meeting",
            "//root",
            "folder//subfolder/note",
            "path//with//multiple//doubles",
            "memory//test",
        ]
        for path in invalid_paths:
            assert not validate_memory_url_path(path), (
                f"Path '{path}' should be invalid (double slashes)"
            )
    def test_invalid_protocol_schemes(self):
        """Test that paths with protocol schemes fail validation."""
        invalid_paths = [
            "http://example.com",
            "https://example.com/path",
            "file://local/path",
            "ftp://server.com",
            "invalid://test",
            "custom://scheme",
        ]
        for path in invalid_paths:
            assert not validate_memory_url_path(path), (
                f"Path '{path}' should be invalid (protocol scheme)"
            )
    def test_invalid_characters(self):
        """Test that paths with invalid characters fail validation."""
        invalid_paths = [
            "notes<with>brackets",
            'notes"with"quotes',
            "notes|with|pipes",
            "notes?with?questions",
        ]
        for path in invalid_paths:
            assert not validate_memory_url_path(path), (
                f"Path '{path}' should be invalid (invalid chars)"
            )
class TestNormalizeMemoryUrl:
    """Test the normalize_memory_url function."""
    def test_valid_normalization(self):
        """Test that valid URLs are properly normalized."""
        test_cases = [
            ("specs/search", "memory://specs/search"),
            ("memory://specs/search", "memory://specs/search"),
            ("notes/meeting-2025", "memory://notes/meeting-2025"),
            ("memory://notes/meeting-2025", "memory://notes/meeting-2025"),
            ("pattern/*", "memory://pattern/*"),
            ("memory://pattern/*", "memory://pattern/*"),
        ]
        for input_url, expected in test_cases:
            result = normalize_memory_url(input_url)
            assert result == expected, (
                f"normalize_memory_url('{input_url}') should return '{expected}', got '{result}'"
            )
    def test_empty_url(self):
        """Test that empty URLs raise ValueError."""
        with pytest.raises(ValueError, match="cannot be empty"):
            normalize_memory_url(None)
        with pytest.raises(ValueError, match="cannot be empty"):
            normalize_memory_url("")
    def test_invalid_double_slashes(self):
        """Test that URLs with double slashes raise ValueError."""
        invalid_urls = [
            "memory//test",
            "notes//meeting",
            "//root",
            "memory://path//with//doubles",
        ]
        for url in invalid_urls:
            with pytest.raises(ValueError, match="contains double slashes"):
                normalize_memory_url(url)
    def test_invalid_protocol_schemes(self):
        """Test that URLs with other protocol schemes raise ValueError."""
        invalid_urls = [
            "http://example.com",
            "https://example.com/path",
            "file://local/path",
            "invalid://test",
        ]
        for url in invalid_urls:
            with pytest.raises(ValueError, match="contains protocol scheme"):
                normalize_memory_url(url)
    def test_whitespace_only(self):
        """Test that whitespace-only URLs raise ValueError."""
        whitespace_urls = [
            "   ",
            "\t",
            "\n",
            "  \n  ",
        ]
        for url in whitespace_urls:
            with pytest.raises(ValueError, match="cannot be empty or whitespace"):
                normalize_memory_url(url)
    def test_invalid_characters(self):
        """Test that URLs with invalid characters raise ValueError."""
        invalid_urls = [
            "notes<brackets>",
            'notes"quotes"',
            "notes|pipes|",
            "notes?questions?",
        ]
        for url in invalid_urls:
            with pytest.raises(ValueError, match="contains invalid characters"):
                normalize_memory_url(url)
class TestMemoryUrlPydanticValidation:
    """Test the MemoryUrl Pydantic type validation."""
    def test_valid_urls_pass_validation(self):
        """Test that valid URLs pass Pydantic validation."""
        valid_urls = [
            "specs/search",
            "memory://specs/search",
            "notes/meeting-2025",
            "projects/basic-memory/docs",
            "pattern/*",
        ]
        for url in valid_urls:
            # Should not raise an exception
            result = memory_url.validate_python(url)
            assert result.startswith("memory://"), (
                f"Validated URL should start with memory://, got {result}"
            )
    def test_invalid_urls_fail_validation(self):
        """Test that invalid URLs fail Pydantic validation with clear errors."""
        invalid_test_cases = [
            ("memory//test", "double slashes"),
            ("invalid://test", "protocol scheme"),
            ("   ", "empty or whitespace"),
            ("notes<brackets>", "invalid characters"),
        ]
        for url, expected_error in invalid_test_cases:
            with pytest.raises(ValidationError) as exc_info:
                memory_url.validate_python(url)
            error_msg = str(exc_info.value)
            assert "value_error" in error_msg, f"Should be a value_error for '{url}'"
    def test_empty_string_fails_validation(self):
        """Test that empty strings fail validation."""
        with pytest.raises(ValidationError, match="cannot be empty"):
            memory_url.validate_python("")
    def test_very_long_urls_fail_maxlength(self):
        """Test that very long URLs fail MaxLen validation."""
        long_url = "a" * 3000  # Exceeds MaxLen(2028)
        with pytest.raises(ValidationError, match="at most 2028"):
            memory_url.validate_python(long_url)
    def test_whitespace_stripped(self):
        """Test that whitespace is properly stripped."""
        urls_with_whitespace = [
            "  specs/search  ",
            "\tprojects/basic-memory\t",
            "\nnotes/meeting\n",
        ]
        for url in urls_with_whitespace:
            result = memory_url.validate_python(url)
            assert not result.startswith(" ") and not result.endswith(" "), (
                f"Whitespace should be stripped from '{url}'"
            )
            assert "memory://" in result, "Result should contain memory:// prefix"
class TestMemoryUrlErrorMessages:
    """Test that error messages are clear and helpful."""
    def test_double_slash_error_message(self):
        """Test specific error message for double slashes."""
        with pytest.raises(ValueError) as exc_info:
            normalize_memory_url("memory//test")
        error_msg = str(exc_info.value)
        assert "memory//test" in error_msg
        assert "double slashes" in error_msg
    def test_protocol_scheme_error_message(self):
        """Test specific error message for protocol schemes."""
        with pytest.raises(ValueError) as exc_info:
            normalize_memory_url("http://example.com")
        error_msg = str(exc_info.value)
        assert "http://example.com" in error_msg
        assert "protocol scheme" in error_msg
    def test_empty_error_message(self):
        """Test specific error message for empty paths."""
        with pytest.raises(ValueError) as exc_info:
            normalize_memory_url("   ")
        error_msg = str(exc_info.value)
        assert "empty or whitespace" in error_msg
    def test_invalid_characters_error_message(self):
        """Test specific error message for invalid characters."""
        with pytest.raises(ValueError) as exc_info:
            normalize_memory_url("notes<brackets>")
        error_msg = str(exc_info.value)
        assert "notes<brackets>" in error_msg
        assert "invalid characters" in error_msg