Files
mempalace/tests/test_config.py
T
Arnold Wender bcd07916a3 fix(security): normalize MEMPALACE_PALACE_PATH env var with abspath+expanduser
MEMPALACE_PALACE_PATH (and legacy MEMPAL_PALACE_PATH) read from the
environment was returned as-is from Config.palace_path, while the
sibling --palace CLI path gets os.path.abspath() applied at
mcp_server.py:62. That inconsistency means env-var callers can end
up with literal '~' or unresolved '..' segments in the path, which
(a) breaks user intuition and (b) lets a caller who can set env vars
on the target user's session redirect palace storage to an
unexpected location.

Apply os.path.abspath(os.path.expanduser(...)) to the env-var branch
so both code paths converge on the same resolved absolute path.

Closes #1163
2026-04-24 11:06:30 +02:00

156 lines
4.3 KiB
Python

import os
import json
import tempfile
import pytest
from mempalace.config import MempalaceConfig, sanitize_kg_value, sanitize_name
def test_default_config():
cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
assert "palace" in cfg.palace_path
assert cfg.collection_name == "mempalace_drawers"
def test_config_from_file():
tmpdir = tempfile.mkdtemp()
with open(os.path.join(tmpdir, "config.json"), "w") as f:
json.dump({"palace_path": "/custom/palace"}, f)
cfg = MempalaceConfig(config_dir=tmpdir)
assert cfg.palace_path == "/custom/palace"
def test_env_override():
os.environ["MEMPALACE_PALACE_PATH"] = "/env/palace"
cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
assert cfg.palace_path == "/env/palace"
del os.environ["MEMPALACE_PALACE_PATH"]
def test_env_path_expanduser():
os.environ["MEMPALACE_PALACE_PATH"] = "~/mempalace-test"
try:
cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
# Tilde must be expanded to match the --palace CLI code path.
assert "~" not in cfg.palace_path
assert cfg.palace_path.endswith("mempalace-test")
assert cfg.palace_path == os.path.expanduser("~/mempalace-test")
finally:
del os.environ["MEMPALACE_PALACE_PATH"]
def test_env_path_abspath_collapses_traversal():
os.environ["MEMPALACE_PALACE_PATH"] = "/tmp/palace/../mempalace-test"
try:
cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
# .. segments must be collapsed, not preserved literally.
assert ".." not in cfg.palace_path
assert cfg.palace_path == "/tmp/mempalace-test"
finally:
del os.environ["MEMPALACE_PALACE_PATH"]
def test_env_path_legacy_alias_normalized():
# Legacy MEMPAL_PALACE_PATH gets the same normalization treatment.
os.environ.pop("MEMPALACE_PALACE_PATH", None)
os.environ["MEMPAL_PALACE_PATH"] = "~/legacy-alias/../mempalace-test"
try:
cfg = MempalaceConfig(config_dir=tempfile.mkdtemp())
assert "~" not in cfg.palace_path
assert ".." not in cfg.palace_path
assert cfg.palace_path == os.path.expanduser("~/mempalace-test")
finally:
del os.environ["MEMPAL_PALACE_PATH"]
def test_init():
tmpdir = tempfile.mkdtemp()
cfg = MempalaceConfig(config_dir=tmpdir)
cfg.init()
assert os.path.exists(os.path.join(tmpdir, "config.json"))
# --- sanitize_name ---
def test_sanitize_name_ascii():
assert sanitize_name("hello") == "hello"
def test_sanitize_name_latvian():
assert sanitize_name("Jānis") == "Jānis"
def test_sanitize_name_cjk():
assert sanitize_name("太郎") == "太郎"
def test_sanitize_name_cyrillic():
assert sanitize_name("Алексей") == "Алексей"
def test_sanitize_name_rejects_leading_underscore():
with pytest.raises(ValueError):
sanitize_name("_foo")
def test_sanitize_name_rejects_path_traversal():
with pytest.raises(ValueError):
sanitize_name("../etc/passwd")
def test_sanitize_name_rejects_empty():
with pytest.raises(ValueError):
sanitize_name("")
# --- sanitize_kg_value ---
def test_kg_value_accepts_commas():
assert sanitize_kg_value("Alice, Bob, and Carol") == "Alice, Bob, and Carol"
def test_kg_value_accepts_colons():
assert sanitize_kg_value("role: engineer") == "role: engineer"
def test_kg_value_accepts_parentheses():
assert sanitize_kg_value("Python (programming)") == "Python (programming)"
def test_kg_value_accepts_slashes():
assert sanitize_kg_value("owner/repo") == "owner/repo"
def test_kg_value_accepts_hash():
assert sanitize_kg_value("issue #123") == "issue #123"
def test_kg_value_accepts_unicode():
assert sanitize_kg_value("Jānis Bērziņš") == "Jānis Bērziņš"
def test_kg_value_strips_whitespace():
assert sanitize_kg_value(" hello ") == "hello"
def test_kg_value_rejects_empty():
with pytest.raises(ValueError):
sanitize_kg_value("")
def test_kg_value_rejects_whitespace_only():
with pytest.raises(ValueError):
sanitize_kg_value(" ")
def test_kg_value_rejects_null_bytes():
with pytest.raises(ValueError):
sanitize_kg_value("hello\x00world")
def test_kg_value_rejects_over_length():
with pytest.raises(ValueError):
sanitize_kg_value("a" * 129)