Add Apache-2.0 @4n6h4x0r/stem-branch 0.8.0, a seven-governors adapter, and POST /api/qizheng, /api/western, /api/ephemeris_events. BUG-700 remains blocked (do not call --pillars). BUG-701 and BUG-702 are resolved. BUG-703 is investigating (panchanga Lahiri). Skill is not bumped.
123 lines
3.6 KiB
Python
123 lines
3.6 KiB
Python
"""HTTP contract for POST /api/qizheng."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from io import BytesIO
|
|
|
|
import pytest
|
|
|
|
from scripts.api_heavy_compute_gate import reset_heavy_compute_gate
|
|
from scripts.jyotish_api_server import (
|
|
DEFAULT_ALLOWED_HOSTS,
|
|
DEFAULT_ALLOWED_ORIGINS,
|
|
JyotishAPIHandler,
|
|
)
|
|
|
|
SAMPLE = {
|
|
"year": 1990,
|
|
"month": 4,
|
|
"day": 9,
|
|
"hour": 13,
|
|
"minute": 24,
|
|
"lat": 31.19,
|
|
"lon": 121.44,
|
|
"tz": 8,
|
|
}
|
|
|
|
|
|
class _FakeHeaders(dict):
|
|
def get(self, key, default=None):
|
|
return super().get(key, default)
|
|
|
|
|
|
class _FakeServer:
|
|
allowed_origins = DEFAULT_ALLOWED_ORIGINS
|
|
allowed_hosts = DEFAULT_ALLOWED_HOSTS
|
|
|
|
|
|
class _PostCaptureHandler(JyotishAPIHandler):
|
|
def __init__(self, path: str, payload: dict) -> None:
|
|
raw = json.dumps(payload).encode("utf-8")
|
|
self.headers = _FakeHeaders(
|
|
{
|
|
"Content-Length": str(len(raw)),
|
|
"Content-Type": "application/json",
|
|
}
|
|
)
|
|
self.server = _FakeServer()
|
|
self.path = path
|
|
self.rfile = BytesIO(raw)
|
|
self.wfile = BytesIO()
|
|
self.status_code = None
|
|
self.response_headers = []
|
|
self.client_address = ("test-qizheng", 0)
|
|
|
|
def send_response(self, code, message=None): # noqa: ANN001
|
|
self.status_code = code
|
|
|
|
def send_header(self, key, value): # noqa: ANN001
|
|
self.response_headers.append((key, value))
|
|
|
|
def end_headers(self):
|
|
return None
|
|
|
|
def payload(self) -> dict:
|
|
return json.loads(self.wfile.getvalue().decode("utf-8"))
|
|
|
|
|
|
@pytest.fixture
|
|
def api_env(monkeypatch):
|
|
monkeypatch.setenv("JYOTISH_API_RATE_LIMIT_PER_MINUTE", "0")
|
|
reset_heavy_compute_gate()
|
|
yield
|
|
reset_heavy_compute_gate()
|
|
|
|
|
|
def test_qizheng_endpoint_returns_eleven_bodies_and_twelve_palaces(api_env) -> None:
|
|
handler = _PostCaptureHandler("/api/qizheng", SAMPLE)
|
|
handler.do_POST()
|
|
assert handler.status_code == 200
|
|
payload = handler.payload()
|
|
assert payload["success"] is True
|
|
assert payload["coordinate_system"] == "qizheng_mansion_degrees_from_jiao"
|
|
assert len(payload["bodies"]) == 11
|
|
assert len(payload["palaces"]) == 12
|
|
assert payload["ascendant"]["palace"] == "午宮"
|
|
assert payload["dignities"]["status"] == "unclosed"
|
|
assert "神煞" not in payload["boundary"]
|
|
|
|
|
|
def test_qizheng_missing_lat_returns_400(api_env) -> None:
|
|
body = {key: value for key, value in SAMPLE.items() if key != "lat"}
|
|
handler = _PostCaptureHandler("/api/qizheng", body)
|
|
handler.do_POST()
|
|
assert handler.status_code == 400
|
|
payload = handler.payload()
|
|
assert payload["success"] is False
|
|
assert payload["error_code"] == "ERR_BAD_REQUEST"
|
|
assert "lat" in payload["error"]
|
|
|
|
|
|
def test_qizheng_missing_lon_returns_400(api_env) -> None:
|
|
body = {key: value for key, value in SAMPLE.items() if key != "lon"}
|
|
handler = _PostCaptureHandler("/api/qizheng", body)
|
|
handler.do_POST()
|
|
assert handler.status_code == 400
|
|
assert handler.payload()["success"] is False
|
|
|
|
|
|
def test_qizheng_node_unavailable_returns_structured_error(api_env, monkeypatch) -> None:
|
|
from scripts.jyotish_api_server import _load_local_module
|
|
|
|
module = _load_local_module("qizheng_chart_engine")
|
|
monkeypatch.setattr(module.shutil, "which", lambda name: None)
|
|
handler = _PostCaptureHandler("/api/qizheng", SAMPLE)
|
|
handler.do_POST()
|
|
assert handler.status_code == 400
|
|
payload = handler.payload()
|
|
assert payload["success"] is False
|
|
assert payload["error_code"] == "ERR_BAD_REQUEST"
|
|
assert "node" in payload["error"]
|
|
assert "Traceback" not in payload["error"]
|