mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
a7bf42ef49
Signed-off-by: Claude Code <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
275 lines
9.2 KiB
Python
275 lines
9.2 KiB
Python
"""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
|