Co-authored-by: MajoorWaldi <129623145+MajoorWaldi@users.noreply.github.com>
671 lines
23 KiB
Python
671 lines
23 KiB
Python
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from mjr_am_backend.routes.core import security as sec
|
|
from mjr_am_backend.settings import (
|
|
_AI_VERBOSE_LOGS_KEY,
|
|
_INPUT_DIRECTORY_KEY,
|
|
_METADATA_FALLBACK_IMAGE_KEY,
|
|
_METADATA_FALLBACK_MEDIA_KEY,
|
|
_OUTPUT_DIRECTORY_KEY,
|
|
_PROBE_BACKEND_KEY,
|
|
_SECURITY_API_TOKEN_HASH_KEY,
|
|
_SECURITY_API_TOKEN_KEY,
|
|
_SETTINGS_VERSION_KEY,
|
|
_VECTOR_CAPTION_ON_INDEX_KEY,
|
|
_WORKFLOW_ROOTS_KEY,
|
|
AppSettings,
|
|
)
|
|
from mjr_am_backend.shared import Result
|
|
|
|
|
|
class _DB:
|
|
def __init__(self):
|
|
self.store = {}
|
|
self.fail_write = False
|
|
self.fail_delete = False
|
|
|
|
async def aquery(self, sql, params=()):
|
|
if "SELECT value FROM metadata" in sql:
|
|
key = params[0]
|
|
if key in self.store:
|
|
return Result.Ok([{"value": self.store[key]}])
|
|
return Result.Ok([])
|
|
return Result.Ok([])
|
|
|
|
async def aexecute(self, sql, params=()):
|
|
if "INSERT OR REPLACE INTO metadata" in sql:
|
|
if self.fail_write:
|
|
return Result.Err("DB", "write-failed")
|
|
self.store[str(params[0])] = str(params[1])
|
|
return Result.Ok("ok")
|
|
if "DELETE FROM metadata" in sql:
|
|
if self.fail_delete:
|
|
return Result.Err("DB", "delete-failed")
|
|
self.store.pop(str(params[0]), None)
|
|
return Result.Ok("ok")
|
|
return Result.Ok("ok")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_token_env(monkeypatch):
|
|
keys = (
|
|
"MAJOOR_API_TOKEN",
|
|
"MJR_API_TOKEN",
|
|
"MAJOOR_API_TOKEN_HASH",
|
|
"MJR_API_TOKEN_HASH",
|
|
"MAJOOR_API_TOKEN_PEPPER",
|
|
)
|
|
for k in keys:
|
|
monkeypatch.delenv(k, raising=False)
|
|
yield
|
|
try:
|
|
sec._reset_security_state_for_tests()
|
|
except Exception:
|
|
pass
|
|
for k in keys:
|
|
monkeypatch.delenv(k, raising=False)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_write_delete_setting_smoke():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
assert await s._read_setting("x") is None
|
|
w = await s._write_setting("x", "1")
|
|
assert w.ok
|
|
assert await s._read_setting("x") == "1"
|
|
d = await s._delete_setting("x")
|
|
assert d.ok
|
|
assert await s._read_setting("x") is None
|
|
|
|
|
|
def test_hash_and_token_env(monkeypatch):
|
|
s = AppSettings(_DB())
|
|
monkeypatch.setenv("MAJOOR_API_TOKEN_PEPPER", "pep")
|
|
h = s._hash_api_token("abc")
|
|
assert h and len(h) == 64
|
|
|
|
s._set_api_token_env("tok", "hash", include_plain=True)
|
|
assert os.environ.get("MAJOOR_API_TOKEN") == "tok"
|
|
s._set_api_token_env("tok", "hash2", include_plain=False)
|
|
assert os.environ.get("MAJOOR_API_TOKEN") is None
|
|
assert os.environ.get("MAJOOR_API_TOKEN_HASH") == "hash2"
|
|
monkeypatch.delenv("MAJOOR_API_TOKEN_HASH", raising=False)
|
|
monkeypatch.delenv("MJR_API_TOKEN_HASH", raising=False)
|
|
monkeypatch.delenv("MJR_API_TOKEN", raising=False)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_api_token_from_env(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
monkeypatch.setenv("MAJOOR_API_TOKEN", "envtok")
|
|
t = await s._get_or_create_api_token_locked()
|
|
assert t == "envtok"
|
|
assert db.store.get(_SECURITY_API_TOKEN_HASH_KEY)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_api_token_from_hash_loads_hash_without_regeneration(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
db.store[_SECURITY_API_TOKEN_HASH_KEY] = s._hash_api_token("old")
|
|
monkeypatch.delenv("MAJOOR_API_TOKEN", raising=False)
|
|
t = await s._get_or_create_api_token_locked()
|
|
assert t == ""
|
|
assert os.environ.get("MAJOOR_API_TOKEN") is None
|
|
assert os.environ.get("MAJOOR_API_TOKEN_HASH") == db.store[_SECURITY_API_TOKEN_HASH_KEY]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_security_prefs_defaults_and_set(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
monkeypatch.setenv("MAJOOR_ALLOW_DELETE", "0")
|
|
|
|
prefs = await s.get_security_prefs()
|
|
assert prefs["safe_mode"] is False
|
|
assert prefs["allow_write"] is True
|
|
assert isinstance(prefs["allow_delete"], bool)
|
|
assert prefs["allow_delete"] is False
|
|
assert prefs["allow_rename"] is True
|
|
assert prefs["allow_open_in_folder"] is True
|
|
assert prefs["allow_reset_index"] is True
|
|
|
|
out = await s.set_security_prefs(
|
|
{
|
|
"allow_write": True,
|
|
"require_auth": True,
|
|
"allow_insecure_token_transport": True,
|
|
"apiToken": "abc",
|
|
}
|
|
)
|
|
assert out.ok
|
|
assert db.store.get("allow_write") == "1"
|
|
assert db.store.get("require_auth") == "1"
|
|
assert db.store.get("allow_insecure_token_transport") == "1"
|
|
assert os.environ.get("MAJOOR_REQUIRE_AUTH") == "1"
|
|
assert os.environ.get("MAJOOR_ALLOW_INSECURE_TOKEN_TRANSPORT") == "1"
|
|
assert db.store.get(_SECURITY_API_TOKEN_KEY) == "abc"
|
|
assert db.store.get(_SECURITY_API_TOKEN_HASH_KEY)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_security_prefs_invalid_and_db_error():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
bad = await s.set_security_prefs({})
|
|
assert bad.code == "INVALID_INPUT"
|
|
|
|
db.fail_write = True
|
|
err = await s.set_security_prefs({"allow_write": True})
|
|
assert err.code == "DB_ERROR"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rotate_and_bootstrap_token():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
rot = await s.rotate_api_token()
|
|
assert rot.ok and rot.data.get("api_token")
|
|
assert db.store.get(_SECURITY_API_TOKEN_KEY) == rot.data.get("api_token")
|
|
|
|
boot = await s.bootstrap_api_token()
|
|
assert boot.ok and boot.data.get("api_token")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bootstrap_token_can_be_recovered_after_restart_when_managed_by_settings():
|
|
db = _DB()
|
|
first = AppSettings(db)
|
|
|
|
saved = await first.set_security_prefs({"apiToken": "persisted-token-123456"})
|
|
assert saved.ok
|
|
assert db.store.get(_SECURITY_API_TOKEN_KEY) == "persisted-token-123456"
|
|
|
|
restarted = AppSettings(db)
|
|
boot = await restarted.bootstrap_api_token()
|
|
assert boot.ok
|
|
assert boot.data.get("api_token") == "persisted-token-123456"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bootstrap_token_recovers_legacy_hash_only_configuration():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
db.store[_SECURITY_API_TOKEN_HASH_KEY] = s._hash_api_token("old-legacy-token-123456")
|
|
|
|
boot = await s.bootstrap_api_token()
|
|
assert boot.ok
|
|
assert boot.data.get("api_token")
|
|
assert db.store.get(_SECURITY_API_TOKEN_KEY) == boot.data.get("api_token")
|
|
assert db.store.get(_SECURITY_API_TOKEN_HASH_KEY) == s._hash_api_token(boot.data.get("api_token"))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_version_read_get_bump_cache():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
db.store[_SETTINGS_VERSION_KEY] = "not-int"
|
|
assert await s._read_settings_version() == 0
|
|
|
|
bump = await s._bump_settings_version_locked()
|
|
assert bump.ok and int(bump.data) > 0
|
|
|
|
v1 = await s._get_settings_version()
|
|
v2 = await s._get_settings_version()
|
|
assert v1 == v2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_probe_backend_get_set_paths(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
db.store[_PROBE_BACKEND_KEY] = "invalid"
|
|
mode = await s.get_probe_backend()
|
|
assert mode in {"auto", "exiftool", "ffprobe", "both"}
|
|
|
|
bad = await s.set_probe_backend("xxx")
|
|
assert bad.code == "INVALID_INPUT"
|
|
|
|
ok = await s.set_probe_backend("ffprobe")
|
|
assert ok.ok and ok.data == "ffprobe"
|
|
|
|
monkeypatch.setattr(s, "_cache_ttl_s", -1)
|
|
assert await s.get_probe_backend() == "ffprobe"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vector_caption_on_index_get_set_and_startup_restore(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
monkeypatch.delenv("MJR_AM_VECTOR_CAPTION_ON_INDEX", raising=False)
|
|
monkeypatch.delenv("MAJOOR_VECTOR_CAPTION_ON_INDEX", raising=False)
|
|
|
|
assert await s.get_vector_caption_on_index_enabled() is False
|
|
|
|
out = await s.set_vector_caption_on_index_enabled(True)
|
|
assert out.ok and out.data is True
|
|
assert db.store.get(_VECTOR_CAPTION_ON_INDEX_KEY) == "1"
|
|
assert os.environ.get("MJR_AM_VECTOR_CAPTION_ON_INDEX") == "1"
|
|
|
|
monkeypatch.setenv("MJR_AM_VECTOR_CAPTION_ON_INDEX", "0")
|
|
await s.apply_vector_search_override_on_startup()
|
|
assert os.environ.get("MJR_AM_VECTOR_CAPTION_ON_INDEX") == "1"
|
|
|
|
|
|
def test_cached_probe_backend_helper_paths():
|
|
s = AppSettings(_DB())
|
|
cache_key = _PROBE_BACKEND_KEY
|
|
# Seed with an expired entry (TTL already passed → monotonic timestamp 0)
|
|
s._cache._data[cache_key] = ("auto", 0.0, 1)
|
|
assert s._cached_probe_backend(cache_key, 1) == ""
|
|
|
|
# Seed with fresh timestamp but wrong version → should also miss
|
|
s._cache.put(cache_key, "auto", version=1)
|
|
assert s._cached_probe_backend(cache_key, 2) == ""
|
|
|
|
|
|
def test_versioned_ttl_cache_evicts_oldest_entry():
|
|
s = AppSettings(_DB())
|
|
|
|
s._cache = s._cache.__class__(ttl_s=s._cache_ttl_s, maxsize=2)
|
|
s._cache.put("first", "1", version=0)
|
|
s._cache.put("second", "2", version=0)
|
|
s._cache.put("third", "3", version=0)
|
|
|
|
assert s._cache.get("first", version=0) is None
|
|
assert s._cache.get("second", version=0) == "2"
|
|
assert s._cache.get("third", version=0) == "3"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_metadata_fallback_get_set_paths():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
prefs = await s.get_metadata_fallback_prefs()
|
|
assert set(prefs.keys()) == {"image", "media"}
|
|
|
|
bad = await s.set_metadata_fallback_prefs()
|
|
assert bad.code == "INVALID_INPUT"
|
|
|
|
ok = await s.set_metadata_fallback_prefs(image=False, media=True)
|
|
assert ok.ok
|
|
assert db.store[_METADATA_FALLBACK_IMAGE_KEY] == "0"
|
|
assert db.store[_METADATA_FALLBACK_MEDIA_KEY] == "1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_metadata_fallback_db_error():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
db.fail_write = True
|
|
out = await s.set_metadata_fallback_prefs(image=True)
|
|
assert out.code == "DB_ERROR"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_scoped_settings_fallback_and_isolation(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
db.store["allow_write"] = "1"
|
|
db.store[_PROBE_BACKEND_KEY] = "ffprobe"
|
|
db.store[_METADATA_FALLBACK_IMAGE_KEY] = "0"
|
|
db.store[_METADATA_FALLBACK_MEDIA_KEY] = "1"
|
|
monkeypatch.delenv("MAJOOR_ALLOW_WRITE", raising=False)
|
|
|
|
token = sec._CURRENT_USER_ID.set("alice")
|
|
try:
|
|
assert await s.get_probe_backend() == "ffprobe"
|
|
assert await s.get_metadata_fallback_prefs() == {"image": False, "media": True}
|
|
assert (await s.get_security_prefs())["allow_write"] is True
|
|
|
|
probe_res = await s.set_probe_backend("both")
|
|
metadata_res = await s.set_metadata_fallback_prefs(image=True, media=False)
|
|
prefs_res = await s.set_security_prefs({"allow_write": False})
|
|
|
|
assert probe_res.ok is True
|
|
assert metadata_res.ok is True
|
|
assert prefs_res.ok is True
|
|
finally:
|
|
sec._CURRENT_USER_ID.reset(token)
|
|
|
|
alice_probe_key = s._storage_key(_PROBE_BACKEND_KEY, user_scoped=True, user_id="alice")
|
|
alice_image_key = s._storage_key(_METADATA_FALLBACK_IMAGE_KEY, user_scoped=True, user_id="alice")
|
|
alice_media_key = s._storage_key(_METADATA_FALLBACK_MEDIA_KEY, user_scoped=True, user_id="alice")
|
|
alice_version_key = s._settings_version_write_key(user_scoped=True, user_id="alice")
|
|
|
|
assert db.store[_PROBE_BACKEND_KEY] == "ffprobe"
|
|
assert db.store[_METADATA_FALLBACK_IMAGE_KEY] == "0"
|
|
assert db.store[_METADATA_FALLBACK_MEDIA_KEY] == "1"
|
|
assert db.store["allow_write"] == "0"
|
|
assert db.store[alice_probe_key] == "both"
|
|
assert db.store[alice_image_key] == "1"
|
|
assert db.store[alice_media_key] == "0"
|
|
assert alice_version_key in db.store
|
|
assert os.environ.get("MAJOOR_ALLOW_WRITE") == "0"
|
|
|
|
token = sec._CURRENT_USER_ID.set("bob")
|
|
try:
|
|
assert await s.get_probe_backend() == "ffprobe"
|
|
assert await s.get_metadata_fallback_prefs() == {"image": False, "media": True}
|
|
assert (await s.get_security_prefs())["allow_write"] is False
|
|
finally:
|
|
sec._CURRENT_USER_ID.reset(token)
|
|
|
|
|
|
def test_metadata_helpers_direct():
|
|
s = AppSettings(_DB())
|
|
payload = s._normalize_metadata_fallback_write_payload(image="0", media="1")
|
|
assert payload[_METADATA_FALLBACK_IMAGE_KEY] is False
|
|
assert payload[_METADATA_FALLBACK_MEDIA_KEY] is True
|
|
|
|
s._cache.put(_METADATA_FALLBACK_IMAGE_KEY, "1", version=0)
|
|
s._cache.put(_METADATA_FALLBACK_MEDIA_KEY, "0", version=0)
|
|
cur = s._current_metadata_fallback_prefs_from_cache()
|
|
assert cur == {"image": True, "media": False}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_output_directory_set_get_clear(tmp_path: Path, monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
target = str(tmp_path)
|
|
override_file = tmp_path / "output-override.txt"
|
|
|
|
class _FP:
|
|
output_directory = ""
|
|
|
|
@staticmethod
|
|
def set_output_directory(v):
|
|
_FP.output_directory = v
|
|
|
|
@staticmethod
|
|
def get_output_directory():
|
|
return "C:/orig"
|
|
|
|
monkeypatch.setitem(sys.modules, "folder_paths", _FP)
|
|
monkeypatch.setattr("mjr_am_backend.settings._OUTPUT_DIR_OVERRIDE_FILE_PATH", override_file)
|
|
|
|
set_res = await s.set_output_directory(target)
|
|
assert set_res.ok
|
|
assert db.store[_OUTPUT_DIRECTORY_KEY]
|
|
assert override_file.read_text(encoding="utf-8").strip() == str(tmp_path.resolve())
|
|
got = await s.get_output_directory()
|
|
assert got
|
|
|
|
clear_res = await s.set_output_directory("")
|
|
assert clear_res.ok
|
|
assert _OUTPUT_DIRECTORY_KEY not in db.store
|
|
assert not override_file.exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_output_directory_db_errors(tmp_path: Path):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
db.fail_write = True
|
|
out1 = await s.set_output_directory(str(tmp_path))
|
|
assert out1.code == "DB_ERROR"
|
|
|
|
db.fail_write = False
|
|
db.fail_delete = True
|
|
out2 = await s.set_output_directory("")
|
|
assert out2.code == "DB_ERROR"
|
|
|
|
|
|
@pytest.fixture
|
|
def input_override_file(tmp_path, monkeypatch):
|
|
path = tmp_path / "input-override.txt"
|
|
monkeypatch.setattr("mjr_am_backend.settings._INPUT_DIR_OVERRIDE_FILE_PATH", path)
|
|
monkeypatch.setattr("mjr_am_backend.config._INPUT_DIR_OVERRIDE_FILE_PATH", path)
|
|
for name in ("MJR_AM_INPUT_DIRECTORY", "MAJOOR_INPUT_DIRECTORY"):
|
|
monkeypatch.delenv(name, raising=False)
|
|
return path
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_input_directory_persistence_restart_clear(tmp_path, monkeypatch, input_override_file):
|
|
from types import SimpleNamespace
|
|
|
|
from mjr_am_backend.adapters.comfy_core import get_input_directory
|
|
from mjr_am_backend.features.runtime.bootstrap import apply_startup_settings
|
|
from mjr_am_backend.routes.core import paths
|
|
|
|
default = tmp_path / "cli-input"
|
|
custom = tmp_path / "custom"
|
|
default.mkdir()
|
|
custom.mkdir()
|
|
monkeypatch.setitem(sys.modules, "folder_paths", None)
|
|
monkeypatch.setitem(sys.modules, "comfy.cli_args", None)
|
|
monkeypatch.setattr(sys, "argv", ["main.py", "--input-directory", str(default)])
|
|
monkeypatch.setattr(paths, "get_runtime_output_root", lambda: str(tmp_path / "output"))
|
|
db = _DB()
|
|
settings = AppSettings(db)
|
|
assert await settings.get_input_directory() is None
|
|
assert get_input_directory() == str(default)
|
|
assert paths._is_path_allowed(default / "a.png")
|
|
saved = await settings.set_input_directory(str(custom))
|
|
assert saved.ok and saved.data == str(custom)
|
|
assert db.store[_INPUT_DIRECTORY_KEY] == str(custom)
|
|
assert input_override_file.read_text().strip() == str(custom)
|
|
assert paths._is_path_allowed(custom / "a.png")
|
|
assert not paths._is_path_allowed(default / "a.png")
|
|
assert not paths._is_path_allowed(tmp_path / "outside.png")
|
|
(custom / "escape").symlink_to(default, target_is_directory=True)
|
|
assert not paths._is_path_allowed(custom / "escape" / "a.png")
|
|
|
|
for name in ("MJR_AM_INPUT_DIRECTORY", "MAJOOR_INPUT_DIRECTORY"):
|
|
monkeypatch.delenv(name)
|
|
# The early-startup file resolves before the database is initialized.
|
|
assert get_input_directory() == str(custom)
|
|
input_override_file.unlink()
|
|
restarted = AppSettings(db)
|
|
warnings = []
|
|
await apply_startup_settings(
|
|
SimpleNamespace(apply_input_directory_override_on_startup=restarted.apply_input_directory_override_on_startup),
|
|
warn=lambda *args: warnings.append(args),
|
|
)
|
|
assert not warnings
|
|
assert await restarted.get_input_directory() == str(custom)
|
|
assert get_input_directory() == str(custom)
|
|
cleared = await restarted.set_input_directory("")
|
|
assert cleared.ok
|
|
assert _INPUT_DIRECTORY_KEY not in db.store
|
|
assert not input_override_file.exists()
|
|
assert await restarted.get_input_directory() is None
|
|
assert get_input_directory() == str(default)
|
|
assert not paths._is_path_allowed(custom / "a.png")
|
|
assert paths._is_path_allowed(default / "a.png")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_input_directory_validation_and_db_errors(tmp_path, input_override_file):
|
|
db = _DB()
|
|
settings = AppSettings(db)
|
|
not_dir = tmp_path / "file"
|
|
not_dir.write_text("x")
|
|
for invalid in (str(tmp_path / "missing"), str(not_dir), "bad\0path"):
|
|
result = await settings.set_input_directory(invalid)
|
|
assert result.code == "INVALID_INPUT"
|
|
assert _INPUT_DIRECTORY_KEY not in db.store
|
|
assert not input_override_file.exists()
|
|
db.fail_write = True
|
|
result = await settings.set_input_directory(str(tmp_path))
|
|
assert result.code == "DB_ERROR"
|
|
assert not input_override_file.exists()
|
|
db.fail_write = False
|
|
assert (await settings.set_input_directory(str(tmp_path))).ok
|
|
db.fail_delete = True
|
|
result = await settings.set_input_directory("")
|
|
assert result.code == "DB_ERROR"
|
|
assert input_override_file.exists()
|
|
assert await settings.get_input_directory() == str(tmp_path)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_workflow_roots_set_get_clear(tmp_path: Path, monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
root_a = tmp_path / "wf-a"
|
|
root_b = tmp_path / "wf-b"
|
|
|
|
out = await s.set_workflow_roots([str(root_a), str(root_b)])
|
|
|
|
assert out.ok
|
|
assert out.data == [str(root_a.resolve()), str(root_b.resolve())]
|
|
assert db.store[_WORKFLOW_ROOTS_KEY] == "\n".join(out.data)
|
|
assert await s.get_workflow_roots() == out.data
|
|
assert os.environ["MJR_AM_WORKFLOW_DIRECTORIES"] == os.pathsep.join(out.data)
|
|
|
|
clear = await s.set_workflow_roots("")
|
|
assert clear.ok
|
|
assert await s.get_workflow_roots() == []
|
|
assert _WORKFLOW_ROOTS_KEY not in db.store
|
|
assert "MJR_AM_WORKFLOW_DIRECTORIES" not in os.environ
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_huggingface_token_rejects_reused_security_api_token():
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
s._runtime_api_token = "mjr_api_secret_123"
|
|
|
|
blocked = await s.set_huggingface_token("mjr_api_secret_123")
|
|
assert blocked.code == "INVALID_INPUT"
|
|
assert "huggingface_token" not in db.store
|
|
|
|
ok = await s.set_huggingface_token("hf_abcdefghijklmnopqrstuvwxyz")
|
|
assert ok.ok is True
|
|
assert db.store.get("huggingface_token") == "hf_abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ai_verbose_logs_get_set_and_startup_restore(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
assert await s.get_ai_verbose_logs_enabled() is False
|
|
|
|
out = await s.set_ai_verbose_logs_enabled(True)
|
|
assert out.ok is True
|
|
assert db.store.get(_AI_VERBOSE_LOGS_KEY) == "1"
|
|
assert os.environ.get("MAJOOR_AI_VERBOSE_LOGS") == "1"
|
|
|
|
monkeypatch.delenv("MAJOOR_AI_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_AI_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MAJOOR_VERBOSE_AI_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_VERBOSE_AI_LOGS", raising=False)
|
|
|
|
await s.apply_ai_verbose_logs_on_startup()
|
|
assert os.environ.get("MAJOOR_AI_VERBOSE_LOGS") == "1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_route_verbose_logs_get_set_and_startup_restore(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
assert await s.get_route_verbose_logs_enabled() is False
|
|
|
|
out = await s.set_route_verbose_logs_enabled(True)
|
|
assert out.ok is True
|
|
assert db.store.get("route_verbose_logs") == "1"
|
|
assert os.environ.get("MAJOOR_ROUTE_VERBOSE_LOGS") == "1"
|
|
|
|
monkeypatch.delenv("MAJOOR_ROUTE_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_ROUTE_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MAJOOR_VERBOSE_ROUTE_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_VERBOSE_ROUTE_LOGS", raising=False)
|
|
|
|
await s.apply_route_verbose_logs_on_startup()
|
|
assert os.environ.get("MAJOOR_ROUTE_VERBOSE_LOGS") == "1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_startup_verbose_logs_get_set_and_startup_restore(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
assert await s.get_startup_verbose_logs_enabled() is False
|
|
|
|
out = await s.set_startup_verbose_logs_enabled(True)
|
|
assert out.ok is True
|
|
assert db.store.get("startup_verbose_logs") == "1"
|
|
assert os.environ.get("MAJOOR_STARTUP_VERBOSE_LOGS") == "1"
|
|
|
|
monkeypatch.delenv("MAJOOR_STARTUP_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_STARTUP_VERBOSE_LOGS", raising=False)
|
|
monkeypatch.delenv("MAJOOR_VERBOSE_STARTUP_LOGS", raising=False)
|
|
monkeypatch.delenv("MJR_AM_VERBOSE_STARTUP_LOGS", raising=False)
|
|
|
|
await s.apply_startup_verbose_logs_on_startup()
|
|
assert os.environ.get("MAJOOR_STARTUP_VERBOSE_LOGS") == "1"
|
|
|
|
|
|
def test_output_env_helpers(monkeypatch):
|
|
s = AppSettings(_DB())
|
|
monkeypatch.setenv("MAJOOR_OUTPUT_DIRECTORY", "x")
|
|
monkeypatch.setenv("MJR_AM_OUTPUT_DIRECTORY", "x")
|
|
s._clear_output_directory_env_vars()
|
|
assert os.environ.get("MAJOOR_OUTPUT_DIRECTORY") is None
|
|
|
|
monkeypatch.setenv("MAJOOR_ORIGINAL_OUTPUT_DIRECTORY", "C:/orig")
|
|
assert s._restore_output_directory_target() == "C:/orig"
|
|
|
|
s._clear_original_output_directory_env()
|
|
assert os.environ.get("MAJOOR_ORIGINAL_OUTPUT_DIRECTORY") is None
|
|
|
|
|
|
def test_apply_comfy_output_directory_and_current(monkeypatch):
|
|
s = AppSettings(_DB())
|
|
|
|
class _FP1:
|
|
output_directory = ""
|
|
|
|
@staticmethod
|
|
def set_output_directory(v):
|
|
_FP1.output_directory = v
|
|
|
|
@staticmethod
|
|
def get_output_directory():
|
|
return "C:/out"
|
|
|
|
monkeypatch.setitem(sys.modules, "folder_paths", _FP1)
|
|
s._apply_comfy_output_directory("C:/new")
|
|
assert _FP1.output_directory == "C:/new"
|
|
assert s._get_current_comfy_output_directory() == "C:/out"
|
|
|
|
|
|
def test_extract_token_payload_aliases():
|
|
s = AppSettings(_DB())
|
|
assert s._extract_token_from_prefs_payload({"api_token": "x"}) == "x"
|
|
assert s._extract_token_from_prefs_payload({"apiToken": "y"}) == "y"
|
|
assert s._extract_token_from_prefs_payload({}) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ensure_security_bootstrap_and_warn_bump(monkeypatch):
|
|
db = _DB()
|
|
s = AppSettings(db)
|
|
|
|
async def _tok():
|
|
return "tok"
|
|
|
|
monkeypatch.setattr(s, "_get_or_create_api_token_locked", _tok)
|
|
await s.ensure_security_bootstrap()
|
|
|
|
async def _bad_bump(*_args, **_kwargs):
|
|
return Result.Err("DB_ERROR", "x")
|
|
|
|
monkeypatch.setattr(s, "_bump_settings_version_locked", _bad_bump)
|
|
await s._warn_if_bump_fails("x")
|