472 lines
17 KiB
Python
472 lines
17 KiB
Python
# =================== AIPass ====================
|
|
# Name: test_json_handler.py
|
|
# Description: JSON Handler Tests (from seedgo template)
|
|
# Version: 1.0.0
|
|
# Created: 2026-03-27
|
|
# Modified: 2026-03-27
|
|
# =============================================
|
|
|
|
"""
|
|
JSON Handler Tests for API branch.
|
|
|
|
Adapted from seedgo universal template (DPLAN-0059).
|
|
Covers 8 test quality categories for json_handler:
|
|
- default_factory, validate, get_path, ensure_exists,
|
|
load, save, log_operation, ensure_module
|
|
"""
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
import types
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
|
|
# ============ BRANCH CONFIG ============
|
|
BRANCH_MODULE = "api"
|
|
# =======================================
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dynamic import with cross-branch guard bypass
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
|
|
_json_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers.json"
|
|
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
|
|
|
|
if _handler_pkg not in sys.modules:
|
|
_stub = types.ModuleType(_handler_pkg)
|
|
_handlers_dir = (
|
|
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
|
|
)
|
|
_stub.__path__ = [str(_handlers_dir)]
|
|
sys.modules[_handler_pkg] = _stub
|
|
|
|
_mod = importlib.import_module(_json_mod_path)
|
|
json_handler = _mod
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JSON_DIR variable discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_JSON_DIR_ATTR: str | None = None
|
|
_JSON_DIR_CANDIDATES = [
|
|
f"{BRANCH_MODULE.upper()}_JSON_DIR",
|
|
"JSON_DIR",
|
|
"BRANCH_JSON_DIR",
|
|
"_JSON_DIR",
|
|
]
|
|
|
|
for _candidate in _JSON_DIR_CANDIDATES:
|
|
if hasattr(_mod, _candidate):
|
|
_JSON_DIR_ATTR = _candidate
|
|
break
|
|
|
|
if _JSON_DIR_ATTR is None:
|
|
pytest.skip(
|
|
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler — "
|
|
f"tried: {_JSON_DIR_CANDIDATES}",
|
|
allow_module_level=True,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Default factory discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get_default_for_type(json_type: str, module_name: str = "test_mod") -> Any:
|
|
"""Call whichever default factory the branch exposes."""
|
|
for fn_name in ("_create_default", "_get_default_template", "_get_default"):
|
|
fn = getattr(_mod, fn_name, None)
|
|
if fn is not None:
|
|
return fn(json_type, module_name)
|
|
return None
|
|
|
|
|
|
def _has_default_factory() -> bool:
|
|
for fn_name in ("_create_default", "_get_default_template", "_get_default"):
|
|
if hasattr(_mod, fn_name):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _default_factory_raises_on_unknown() -> bool:
|
|
for fn_name in ("_create_default", "_get_default_template", "_get_default"):
|
|
fn = getattr(_mod, fn_name, None)
|
|
if fn is not None:
|
|
try:
|
|
fn("__nonexistent_type__", "test_mod")
|
|
except ValueError:
|
|
return True
|
|
except Exception:
|
|
return False
|
|
return False
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Isolation fixture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Redirect JSON operations to tmp_path for test isolation."""
|
|
assert _JSON_DIR_ATTR is not None
|
|
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
def _json_dir_as_path(tmp_path: Path) -> Path:
|
|
assert _JSON_DIR_ATTR is not None
|
|
val = getattr(_mod, _JSON_DIR_ATTR)
|
|
return Path(val) if isinstance(val, str) else val
|
|
|
|
|
|
# ============================================================================
|
|
# Group 1 — _create_default / default templates
|
|
# ============================================================================
|
|
|
|
def test_default_config_returns_dict_with_required_keys() -> None:
|
|
if not _has_default_factory():
|
|
pytest.skip("Branch has no default factory function")
|
|
result = _get_default_for_type("config", "test_mod")
|
|
assert isinstance(result, dict)
|
|
assert "module_name" in result
|
|
assert "version" in result
|
|
assert "config" in result
|
|
|
|
|
|
def test_default_data_returns_dict_with_date_keys() -> None:
|
|
if not _has_default_factory():
|
|
pytest.skip("Branch has no default factory function")
|
|
result = _get_default_for_type("data", "test_mod")
|
|
assert isinstance(result, dict)
|
|
assert "created" in result
|
|
assert "last_updated" in result
|
|
|
|
|
|
def test_default_log_returns_empty_list() -> None:
|
|
if not _has_default_factory():
|
|
pytest.skip("Branch has no default factory function")
|
|
result = _get_default_for_type("log", "test_mod")
|
|
assert isinstance(result, list)
|
|
assert len(result) == 0
|
|
|
|
|
|
def test_default_unknown_type_raises_value_error() -> None:
|
|
if not _default_factory_raises_on_unknown():
|
|
pytest.skip("Branch default factory does not raise ValueError")
|
|
with pytest.raises(ValueError, match="[Uu]nknown"):
|
|
_get_default_for_type("__nonexistent__", "test_mod")
|
|
|
|
|
|
# ============================================================================
|
|
# Group 2 — validate_json_structure
|
|
# ============================================================================
|
|
|
|
def test_validate_valid_config() -> None:
|
|
data = {"module_name": "x", "version": "1.0.0", "config": {}}
|
|
assert json_handler.validate_json_structure(data, "config") is True
|
|
|
|
|
|
def test_validate_config_missing_key() -> None:
|
|
data = {"module_name": "x", "version": "1.0.0"}
|
|
assert json_handler.validate_json_structure(data, "config") is False
|
|
|
|
|
|
def test_validate_config_not_dict() -> None:
|
|
assert json_handler.validate_json_structure([1, 2, 3], "config") is False
|
|
|
|
|
|
def test_validate_valid_data() -> None:
|
|
data = {"created": "2026-01-01", "last_updated": "2026-01-01"}
|
|
assert json_handler.validate_json_structure(data, "data") is True
|
|
|
|
|
|
def test_validate_data_missing_key() -> None:
|
|
data = {"created": "2026-01-01"}
|
|
assert json_handler.validate_json_structure(data, "data") is False
|
|
|
|
|
|
def test_validate_data_not_dict() -> None:
|
|
assert json_handler.validate_json_structure("not a dict", "data") is False
|
|
|
|
|
|
def test_validate_valid_log() -> None:
|
|
assert json_handler.validate_json_structure([], "log") is True
|
|
assert json_handler.validate_json_structure([{"entry": 1}], "log") is True
|
|
|
|
|
|
def test_validate_log_not_list() -> None:
|
|
assert json_handler.validate_json_structure({"not": "a list"}, "log") is False
|
|
|
|
|
|
def test_validate_unknown_type_returns_false() -> None:
|
|
assert json_handler.validate_json_structure({}, "nonexistent_type") is False
|
|
|
|
|
|
def test_validate_none_input_returns_false() -> None:
|
|
assert json_handler.validate_json_structure(None, "config") is False
|
|
assert json_handler.validate_json_structure(None, "data") is False
|
|
assert json_handler.validate_json_structure(None, "log") is False
|
|
|
|
|
|
# ============================================================================
|
|
# Group 3 — get_json_path
|
|
# ============================================================================
|
|
|
|
def test_get_json_path_returns_path_type(tmp_path: Path) -> None:
|
|
result = json_handler.get_json_path("mymod", "config")
|
|
assert isinstance(result, (Path, str))
|
|
|
|
|
|
def test_get_json_path_filename_pattern(tmp_path: Path) -> None:
|
|
result = json_handler.get_json_path("mymod", "config")
|
|
name = Path(result).name if isinstance(result, str) else result.name
|
|
assert name == "mymod_config.json"
|
|
|
|
|
|
def test_get_json_path_different_combos_differ(tmp_path: Path) -> None:
|
|
path_a = str(json_handler.get_json_path("alpha", "log"))
|
|
path_b = str(json_handler.get_json_path("beta", "data"))
|
|
assert path_a != path_b
|
|
|
|
|
|
# ============================================================================
|
|
# Group 4 — ensure_json_exists
|
|
# ============================================================================
|
|
|
|
def test_ensure_creates_file_when_missing(tmp_path: Path) -> None:
|
|
result = json_handler.ensure_json_exists("ens_mod", "config")
|
|
assert result is True
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
created = json_dir / "ens_mod_config.json"
|
|
assert created.exists()
|
|
|
|
|
|
def test_ensure_preserves_valid_existing_file(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
target = json_dir / "keep_data.json"
|
|
original = {"created": "2025-01-01", "last_updated": "2025-06-01", "custom_key": "preserve_me"}
|
|
target.write_text(json.dumps(original), encoding="utf-8")
|
|
|
|
json_handler.ensure_json_exists("keep", "data")
|
|
|
|
data = json.loads(target.read_text(encoding="utf-8"))
|
|
assert data["custom_key"] == "preserve_me"
|
|
|
|
|
|
def test_ensure_regenerates_corrupt_json(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
target = json_dir / "bad_log.json"
|
|
target.write_bytes(b"\x00\x01NOT VALID JSON{{{")
|
|
|
|
json_handler.ensure_json_exists("bad", "log")
|
|
|
|
data = json.loads(target.read_text(encoding="utf-8"))
|
|
assert isinstance(data, list)
|
|
|
|
|
|
def test_ensure_regenerates_invalid_structure(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
target = json_dir / "wrong_config.json"
|
|
target.write_text(json.dumps({"wrong": "structure"}), encoding="utf-8")
|
|
|
|
json_handler.ensure_json_exists("wrong", "config")
|
|
|
|
data = json.loads(target.read_text(encoding="utf-8"))
|
|
assert "module_name" in data
|
|
assert "version" in data
|
|
assert "config" in data
|
|
|
|
|
|
def test_ensure_returns_bool(tmp_path: Path) -> None:
|
|
result = json_handler.ensure_json_exists("bool_mod", "data")
|
|
assert isinstance(result, bool)
|
|
assert result is True
|
|
|
|
|
|
# ============================================================================
|
|
# Group 5 — load_json
|
|
# ============================================================================
|
|
|
|
def test_load_creates_default_when_missing(tmp_path: Path) -> None:
|
|
result = json_handler.load_json("fresh_mod", "log")
|
|
assert result is not None
|
|
assert isinstance(result, list)
|
|
|
|
|
|
def test_load_returns_existing_content(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
payload = {"created": "2025-01-01", "last_updated": "2025-06-15", "x": 42}
|
|
target = json_dir / "exist_data.json"
|
|
target.write_text(json.dumps(payload), encoding="utf-8")
|
|
|
|
result = json_handler.load_json("exist", "data")
|
|
assert isinstance(result, dict)
|
|
assert result["x"] == 42
|
|
|
|
|
|
def test_load_returns_dict_for_config(tmp_path: Path) -> None:
|
|
result = json_handler.load_json("cfg_mod", "config")
|
|
assert isinstance(result, dict)
|
|
|
|
|
|
def test_load_returns_list_for_log(tmp_path: Path) -> None:
|
|
result = json_handler.load_json("log_mod", "log")
|
|
assert isinstance(result, list)
|
|
|
|
|
|
# ============================================================================
|
|
# Group 6 — save_json
|
|
# ============================================================================
|
|
|
|
def test_save_roundtrip(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
data = {"module_name": "rt", "version": "1.0.0", "config": {"key": "val"}}
|
|
json_handler.save_json("rt", "config", data)
|
|
|
|
loaded = json_handler.load_json("rt", "config")
|
|
assert loaded is not None
|
|
assert loaded["config"]["key"] == "val"
|
|
|
|
|
|
def test_save_returns_true(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
data = {"module_name": "sv", "version": "1.0.0", "config": {}}
|
|
result = json_handler.save_json("sv", "config", data)
|
|
assert result is True
|
|
|
|
|
|
def test_save_rejects_invalid_structure(tmp_path: Path) -> None:
|
|
"""save_json returns False for invalid structure."""
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
result = json_handler.save_json("bad", "config", {"missing": "keys"})
|
|
assert result is False
|
|
|
|
|
|
def test_save_data_updates_last_updated(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
today = datetime.now().date().isoformat()
|
|
data = {"created": "2025-01-01", "last_updated": "2025-01-01"}
|
|
json_handler.save_json("ts", "data", data)
|
|
|
|
on_disk = json.loads((json_dir / "ts_data.json").read_text(encoding="utf-8"))
|
|
assert on_disk["last_updated"] == today
|
|
|
|
|
|
def test_save_writes_valid_json_to_disk(tmp_path: Path) -> None:
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
json_dir.mkdir(parents=True, exist_ok=True)
|
|
entries = [{"timestamp": "t1", "operation": "test"}]
|
|
json_handler.save_json("disk", "log", entries)
|
|
|
|
raw = (json_dir / "disk_log.json").read_text(encoding="utf-8")
|
|
parsed = json.loads(raw)
|
|
assert isinstance(parsed, list)
|
|
assert len(parsed) == 1
|
|
|
|
|
|
# ============================================================================
|
|
# Group 7 — log_operation
|
|
# ============================================================================
|
|
|
|
def test_log_operation_appends_entry(tmp_path: Path) -> None:
|
|
json_handler.log_operation("deploy", module_name="logmod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
log = json.loads((json_dir / "logmod_log.json").read_text(encoding="utf-8"))
|
|
assert len(log) >= 1
|
|
assert log[-1]["operation"] == "deploy"
|
|
|
|
|
|
def test_log_operation_returns_bool(tmp_path: Path) -> None:
|
|
result = json_handler.log_operation("test_op", module_name="boolmod")
|
|
assert isinstance(result, bool)
|
|
assert result is True
|
|
|
|
|
|
def test_log_operation_entry_has_timestamp(tmp_path: Path) -> None:
|
|
json_handler.log_operation("check_ts", module_name="tsmod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
log = json.loads((json_dir / "tsmod_log.json").read_text(encoding="utf-8"))
|
|
assert "timestamp" in log[-1]
|
|
|
|
|
|
def test_log_operation_includes_data_when_provided(tmp_path: Path) -> None:
|
|
json_handler.log_operation("with_data", data={"count": 5}, module_name="datamod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
log = json.loads((json_dir / "datamod_log.json").read_text(encoding="utf-8"))
|
|
assert "data" in log[-1]
|
|
assert log[-1]["data"]["count"] == 5
|
|
|
|
|
|
def test_log_operation_multiple_calls_accumulate(tmp_path: Path) -> None:
|
|
json_handler.log_operation("first", module_name="accmod")
|
|
json_handler.log_operation("second", module_name="accmod")
|
|
json_handler.log_operation("third", module_name="accmod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
log = json.loads((json_dir / "accmod_log.json").read_text(encoding="utf-8"))
|
|
assert len(log) >= 3
|
|
ops = [e["operation"] for e in log[-3:]]
|
|
assert ops == ["first", "second", "third"]
|
|
|
|
|
|
# ============================================================================
|
|
# Group 8 — ensure_module_jsons
|
|
# ============================================================================
|
|
|
|
def test_ensure_module_jsons_creates_all_three(tmp_path: Path) -> None:
|
|
json_handler.ensure_module_jsons("triple")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
assert (json_dir / "triple_config.json").exists()
|
|
assert (json_dir / "triple_data.json").exists()
|
|
assert (json_dir / "triple_log.json").exists()
|
|
|
|
|
|
def test_ensure_module_jsons_returns_true(tmp_path: Path) -> None:
|
|
result = json_handler.ensure_module_jsons("retmod")
|
|
assert result is True
|
|
|
|
|
|
def test_ensure_module_jsons_files_pass_validation(tmp_path: Path) -> None:
|
|
json_handler.ensure_module_jsons("valid_mod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
|
|
config = json.loads((json_dir / "valid_mod_config.json").read_text(encoding="utf-8"))
|
|
assert json_handler.validate_json_structure(config, "config") is True
|
|
|
|
data = json.loads((json_dir / "valid_mod_data.json").read_text(encoding="utf-8"))
|
|
assert json_handler.validate_json_structure(data, "data") is True
|
|
|
|
log = json.loads((json_dir / "valid_mod_log.json").read_text(encoding="utf-8"))
|
|
assert json_handler.validate_json_structure(log, "log") is True
|
|
|
|
|
|
def test_ensure_module_jsons_data_has_correct_keys(tmp_path: Path) -> None:
|
|
json_handler.ensure_module_jsons("keymod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
data = json.loads((json_dir / "keymod_data.json").read_text(encoding="utf-8"))
|
|
assert "created" in data
|
|
assert "last_updated" in data
|
|
|
|
|
|
def test_ensure_module_jsons_log_is_empty_list(tmp_path: Path) -> None:
|
|
json_handler.ensure_module_jsons("listmod")
|
|
json_dir = _json_dir_as_path(tmp_path)
|
|
log = json.loads((json_dir / "listmod_log.json").read_text(encoding="utf-8"))
|
|
assert isinstance(log, list)
|
|
assert len(log) == 0
|