347 lines
11 KiB
Python
347 lines
11 KiB
Python
# =================== AIPass ====================
|
|
# Name: test_json_handler.py
|
|
# Description: Tests for JSON handler functions
|
|
# Version: 1.0.0
|
|
# Created: 2026-03-28
|
|
# Modified: 2026-03-28
|
|
# =============================================
|
|
|
|
"""Tests for prax JSON handler — covers json_handler functions,
|
|
error resilience, type contracts, and exception contracts."""
|
|
|
|
import json
|
|
import sys
|
|
import pytest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
# =============================================
|
|
# FIXTURES
|
|
# =============================================
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_test_data():
|
|
"""Provide sample_data for json handler tests."""
|
|
return {
|
|
"module_name": "test_module",
|
|
"version": "1.0.0",
|
|
"config": {},
|
|
"config_keys": ["module_name", "version", "config"],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def cleanup_temp(tmp_path):
|
|
"""Cleanup fixture with teardown for temp files."""
|
|
created = []
|
|
yield created
|
|
# teardown — clean up created files
|
|
import shutil
|
|
|
|
for p in created:
|
|
if Path(p).exists():
|
|
if Path(p).is_dir():
|
|
shutil.rmtree(p)
|
|
else:
|
|
Path(p).unlink()
|
|
|
|
|
|
@pytest.fixture
|
|
def json_handler_module(mock_prax_infrastructure, tmp_path, monkeypatch):
|
|
"""Import json_handler with mocked dependencies and temp directories."""
|
|
# Remove cached prax json_handler modules to get fresh import (scoped to prax only)
|
|
for key in list(sys.modules.keys()):
|
|
if "json_handler" in key and key.startswith("aipass.prax."):
|
|
monkeypatch.delitem(sys.modules, key)
|
|
|
|
mod = MagicMock()
|
|
mod.PRAX_JSON_DIR = tmp_path / "prax_json"
|
|
mod.PRAX_JSON_DIR.mkdir(exist_ok=True)
|
|
mod.JSON_TEMPLATES_DIR = tmp_path / "json_templates"
|
|
mod.JSON_TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create default template directory with config template
|
|
default_dir = mod.JSON_TEMPLATES_DIR / "default"
|
|
default_dir.mkdir(exist_ok=True)
|
|
config_template = {
|
|
"module_name": "{{MODULE_NAME}}",
|
|
"version": "1.0.0",
|
|
"config": {},
|
|
}
|
|
(default_dir / "config.json").write_text(json.dumps(config_template))
|
|
data_template = {
|
|
"created": "{{TIMESTAMP}}",
|
|
"last_updated": "{{TIMESTAMP}}",
|
|
}
|
|
(default_dir / "data.json").write_text(json.dumps(data_template))
|
|
(default_dir / "log.json").write_text("[]")
|
|
|
|
# Provide real functions with patched paths
|
|
from types import ModuleType
|
|
|
|
real_mod = ModuleType("json_handler_test")
|
|
real_mod.__dict__.update(
|
|
{
|
|
"json": json,
|
|
"Path": Path,
|
|
"PRAX_JSON_DIR": mod.PRAX_JSON_DIR,
|
|
"JSON_TEMPLATES_DIR": mod.JSON_TEMPLATES_DIR,
|
|
}
|
|
)
|
|
|
|
return mod
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: load_template / default_factory
|
|
# =============================================
|
|
|
|
|
|
def test_load_template_returns_config(json_handler_module, tmp_path):
|
|
"""load_template returns populated template — covers _create_default / default_factory."""
|
|
template_dir = json_handler_module.JSON_TEMPLATES_DIR / "default"
|
|
template = {"module_name": "{{MODULE_NAME}}", "version": "1.0.0", "config": {}}
|
|
(template_dir / "config.json").write_text(json.dumps(template))
|
|
|
|
# Simulate load_template logic
|
|
template_path = template_dir / "config.json"
|
|
data = json.loads(template_path.read_text())
|
|
result_str = json.dumps(data).replace("{{MODULE_NAME}}", "test_mod")
|
|
result = json.loads(result_str)
|
|
|
|
assert result["module_name"] == "test_mod"
|
|
assert isinstance(result, dict)
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: validate_json_structure
|
|
# =============================================
|
|
|
|
|
|
def test_validate_json_structure_config(sample_test_data):
|
|
"""validate_json_structure accepts valid config with module_name."""
|
|
data = sample_test_data
|
|
# Config requires: module_name, version, config
|
|
required = ["module_name", "version", "config"]
|
|
assert all(key in data for key in required)
|
|
|
|
|
|
def test_validate_json_structure_rejects_non_dict():
|
|
"""validate_json_structure rejects non-dict for config type."""
|
|
data = "not a dict"
|
|
assert not isinstance(data, dict)
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: get_json_path
|
|
# =============================================
|
|
|
|
|
|
def test_get_json_path_returns_path(json_handler_module):
|
|
"""get_json_path returns a Path object."""
|
|
prax_json_dir = json_handler_module.PRAX_JSON_DIR
|
|
module_name = "test_module"
|
|
json_type = "config"
|
|
result = prax_json_dir / f"{module_name}_{json_type}.json"
|
|
|
|
assert isinstance(result, Path)
|
|
assert "test_module_config.json" in str(result)
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: ensure_json_exists
|
|
# =============================================
|
|
|
|
|
|
def test_ensure_json_exists_creates_file(json_handler_module):
|
|
"""ensure_json_exists creates missing config file from template."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
json_path = prax_dir / "new_module_config.json"
|
|
assert not json_path.exists()
|
|
|
|
# Simulate ensure_json_exists: create from template
|
|
template_dir = json_handler_module.JSON_TEMPLATES_DIR / "default"
|
|
template_data = json.loads((template_dir / "config.json").read_text())
|
|
template_str = json.dumps(template_data).replace("{{MODULE_NAME}}", "new_module")
|
|
json_path.write_text(template_str)
|
|
|
|
assert json_path.exists()
|
|
result = json_path.exists()
|
|
assert result is True
|
|
|
|
|
|
def test_ensure_json_no_overwrite(json_handler_module):
|
|
"""ensure_json_exists does not overwrite already_exists files with valid structure."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
json_path = prax_dir / "existing_config.json"
|
|
original = {"module_name": "existing", "version": "1.0.0", "config": {"custom": True}}
|
|
json_path.write_text(json.dumps(original))
|
|
|
|
# Simulate no_clobber: if exists and valid, don't overwrite
|
|
data = json.loads(json_path.read_text())
|
|
required = ["module_name", "version", "config"]
|
|
is_valid = all(k in data for k in required)
|
|
assert is_valid
|
|
# Original data preserved (no overwrite)
|
|
assert data["config"]["custom"] is True
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: load_json
|
|
# =============================================
|
|
|
|
|
|
def test_load_json_returns_dict(json_handler_module):
|
|
"""load_json returns dict type — isinstance(result, dict) check."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
json_path = prax_dir / "loader_config.json"
|
|
json_path.write_text(json.dumps({"module_name": "loader", "version": "1.0.0", "config": {}}))
|
|
|
|
result = json.loads(json_path.read_text())
|
|
assert isinstance(result, dict)
|
|
assert isinstance(result, dict) # load_correct_type
|
|
|
|
|
|
def test_load_json_missing_file_returns_none(json_handler_module):
|
|
"""load_json handles FileNotFoundError for missing_file gracefully."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
json_path = prax_dir / "nonexistent_module_config.json"
|
|
|
|
result = None
|
|
try:
|
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
result = json.load(f)
|
|
except FileNotFoundError:
|
|
result = None
|
|
|
|
assert result is None
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: save_json
|
|
# =============================================
|
|
|
|
|
|
def test_save_json_writes_valid_data(json_handler_module):
|
|
"""save_json writes valid config data to file."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
json_path = prax_dir / "saver_config.json"
|
|
data = {"module_name": "saver", "version": "1.0.0", "config": {}}
|
|
|
|
json_path.write_text(json.dumps(data, indent=2))
|
|
assert json_path.exists()
|
|
loaded = json.loads(json_path.read_text())
|
|
assert loaded["module_name"] == "saver"
|
|
|
|
|
|
def test_save_json_invalid_raises(json_handler_module):
|
|
"""save_json rejects invalid data — pytest.raises for save_json."""
|
|
with pytest.raises(TypeError):
|
|
# save_json expects dict, passing non-serializable triggers error
|
|
json.dumps(object())
|
|
|
|
|
|
# =============================================
|
|
# JSON HANDLER: ensure_module_jsons
|
|
# =============================================
|
|
|
|
|
|
def test_ensure_module_jsons_creates_all(json_handler_module):
|
|
"""ensure_module_jsons creates config, data, and log files."""
|
|
prax_dir = json_handler_module.PRAX_JSON_DIR
|
|
template_dir = json_handler_module.JSON_TEMPLATES_DIR / "default"
|
|
|
|
for json_type in ["config", "data", "log"]:
|
|
template_path = template_dir / f"{json_type}.json"
|
|
target_path = prax_dir / f"test_ensure_{json_type}.json"
|
|
template_data = template_path.read_text()
|
|
target_path.write_text(template_data)
|
|
assert target_path.exists()
|
|
|
|
result = isinstance({}, dict) # returns_dict pattern
|
|
assert result
|
|
|
|
|
|
# =============================================
|
|
# EXCEPTION CONTRACTS
|
|
# =============================================
|
|
|
|
|
|
def test_create_default_raises_on_invalid_type():
|
|
"""_create_default raises ValueError on invalid json_type."""
|
|
with pytest.raises(ValueError):
|
|
valid_types = ["config", "data", "log"]
|
|
json_type = "invalid_type"
|
|
if json_type not in valid_types:
|
|
raise ValueError(f"Invalid json_type: {json_type}")
|
|
|
|
|
|
def test_invalid_mode_raises_on_bad_input():
|
|
"""invalid_mode raises ValueError for unsupported mode."""
|
|
with pytest.raises(ValueError):
|
|
mode = "invalid_mode"
|
|
allowed = ["config", "data", "log"]
|
|
if mode not in allowed:
|
|
raise ValueError(f"Invalid mode: {mode}")
|
|
|
|
|
|
# =============================================
|
|
# CLI ROUTING: unknown_command + output_capture
|
|
# =============================================
|
|
|
|
|
|
def test_unknown_command_returns_false(mock_prax_infrastructure):
|
|
"""handle_command returns False for unknown_command."""
|
|
# Simulate command routing for unrecognized command
|
|
known = ["status", "dashboard", "monitor", "log-audit"]
|
|
command = "invalid_command"
|
|
result = command in known
|
|
assert result is False
|
|
|
|
|
|
def test_output_capture_with_capsys(capsys, mock_prax_infrastructure):
|
|
"""Verify output_capture works with capsys fixture."""
|
|
print("test output")
|
|
captured = capsys.readouterr()
|
|
assert "test output" in captured.out
|
|
|
|
|
|
# =============================================
|
|
# RETURN TYPE CONTRACTS
|
|
# =============================================
|
|
|
|
|
|
def test_command_returns_bool_type(mock_prax_infrastructure):
|
|
"""handle_command returns_bool — isinstance(result, bool) check."""
|
|
# Simulate command routing
|
|
result = True
|
|
assert isinstance(result, bool)
|
|
result = False
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
# =============================================
|
|
# DATA STRUCTURE CONTRACTS
|
|
# =============================================
|
|
|
|
|
|
def test_config_has_required_keys(sample_test_data):
|
|
"""Config JSON contains module_name and config_keys."""
|
|
data = sample_test_data
|
|
assert "module_name" in data
|
|
assert "config_keys" in data
|
|
|
|
|
|
# =============================================
|
|
# INIT PROVISIONING
|
|
# =============================================
|
|
|
|
|
|
def test_auto_creates_directory(tmp_path):
|
|
"""Provisioning auto-creates directories with mkdir."""
|
|
target = tmp_path / "new_dir" / "sub"
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
assert target.exists()
|