Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/tests/test_hscript_runtime.py

229 lines
6.6 KiB
Python

"""تست‌های هسته HScript: نحو، امنیت، محدودیت منابع."""
from __future__ import annotations
import pytest
from app.services.hscript.errors import ResourceLimitErrorHS, SecurityErrorHS
from app.services.hscript.limits import ResourceLimits
from app.services.hscript.runtime import run_script, validate_script
class _DummyDB:
"""Session جعلی — Gateway در این تست‌ها صدا زده نمی‌شود مگر صریح."""
pass
def _run(source: str, *, preview: bool = True, params: dict | None = None):
return run_script(
source,
db=_DummyDB(), # type: ignore[arg-type]
business_id=1,
user_id=1,
params=params,
preview=preview,
)
def test_validate_ok():
src = "x = 1\nreport.title(\"ok\")\n"
assert validate_script(src)["ok"] is True
def test_forbidden_import():
r = validate_script("import os\n")
assert r["ok"] is False
assert r["error"]["code"] == "E030_SECURITY"
def test_kpi_and_table_without_gateway():
src = """
report.title("Demo")
rows = [{"name": "A", "total": 10}, {"name": "B", "total": 20}]
t = table(rows)
report.kpi("جمع", t.sum("total"), format="currency")
report.table(t, columns=["name", "total"])
report.bar_chart(t, x="name", y="total", title="مقایسه")
"""
result = _run(src)
assert result.ok, result.error
assert result.spec is not None
types = [b["type"] for b in result.spec["blocks"]]
assert "title" in types
assert "kpi" in types
assert "table" in types
assert "chart" in types
def test_functions_and_loops():
src = """
def double(n):
return n * 2
total = 0
for i in range(5):
total = total + double(i)
report.kpi("total", total)
"""
result = _run(src)
assert result.ok, result.error
kpi = next(b for b in result.spec["blocks"] if b["type"] == "kpi")
assert kpi["value"] == 20
def test_infinite_loop_capped():
lim = ResourceLimits(max_loop_iterations=50, max_steps=10_000, max_execution_ms=2000)
src = """
i = 0
while True:
i = i + 1
"""
result = run_script(src, db=_DummyDB(), business_id=1, user_id=1, limits=lim, preview=True) # type: ignore[arg-type]
assert result.ok is False
assert result.error["code"] == "E050_RESOURCE_LIMIT"
def test_dunder_blocked():
src = "x = __import__\n"
r = validate_script(src)
# __import__ as identifier might parse as name then fail at runtime, or security at lexer for import keyword
# Actually __import__ is IDENT, blocked at lookup
result = _run("x = __builtins__\n")
assert result.ok is False
assert result.error["code"] in ("E030_SECURITY", "E010_NAME")
def test_attribute_underscore_blocked():
src = """
rows = table([{"a": 1}])
x = rows.__class__
"""
result = _run(src)
assert result.ok is False
assert result.error["code"] == "E030_SECURITY"
def test_params_injection_stripped():
src = """
report.kpi("bid", param.get("business_id") if False else param.get("note", "none"))
"""
# param is dict — use index
src = """
report.kpi("note", param["note"])
"""
result = _run(src, params={"business_id": 999, "note": "hello"})
assert result.ok, result.error
kpi = next(b for b in result.spec["blocks"] if b["type"] == "kpi")
assert kpi["value"] == "hello"
def test_elif_else():
src = """
x = 2
if x == 1:
msg = "one"
elif x == 2:
msg = "two"
else:
msg = "other"
report.text(msg)
"""
result = _run(src)
assert result.ok, result.error
assert any(b.get("text") == "two" for b in result.spec["blocks"])
def test_sanitize_datetime_and_decimal_from_gateway(monkeypatch):
"""فیلدهای date/datetime/Decimal اسناد نباید اجرای اسکریپت را بشکنند."""
from datetime import date, datetime
from decimal import Decimal
from unittest.mock import patch
src = """
rows = invoices.all(limit=10)
report.kpi("n", rows.count())
report.table(rows, columns=["code", "document_date", "total_debit"])
"""
fake = {
"items": [
{
"id": 1,
"code": "INV-1",
"document_date": date(2026, 1, 2),
"registered_at": datetime(2026, 1, 2, 10, 30, 0),
"total_debit": Decimal("1500.50"),
"total_credit": Decimal("0"),
}
]
}
with patch("app.services.document_service.list_documents", return_value=fake):
result = _run(src)
assert result.ok, result.error
table = next(b for b in result.spec["blocks"] if b["type"] == "table")
row = table["rows"][0]
assert row["code"] == "INV-1"
# با تقویم پیش‌فرض jalali قالب می‌شود
assert "/" in str(row["document_date"])
assert row["total_debit"] == 1500.5
def test_report_calendar_jalali_formats_table_dates():
src = """
report.calendar("jalali")
rows = table([{"document_date": "2026-07-20", "code": "A"}])
report.table(rows, columns=["code", "document_date"])
report.kpi("d", format_date("2026-07-20"))
"""
result = _run(src)
assert result.ok, result.error
assert result.spec["meta"]["calendar_type"] == "jalali"
table = next(b for b in result.spec["blocks"] if b["type"] == "table")
assert table["rows"][0]["document_date"] == "1405/04/29"
kpi = next(b for b in result.spec["blocks"] if b["type"] == "kpi")
assert kpi["value"] == "1405/04/29"
def test_report_calendar_gregorian_keeps_iso_style():
src = """
report.calendar("gregorian")
rows = table([{"document_date": "2026-07-20", "code": "A"}])
report.table(rows, columns=["document_date"])
"""
result = _run(src)
assert result.ok, result.error
assert result.spec["meta"]["calendar_type"] == "gregorian"
table = next(b for b in result.spec["blocks"] if b["type"] == "table")
assert table["rows"][0]["document_date"] == "2026-07-20"
def test_parse_jalali_filter_date():
from app.services.hscript.dates import parse_date
assert parse_date("1404/01/01", calendar="jalali") == "2025-03-21"
def test_format_number_thousands_and_currency():
from app.services.hscript.number_format import format_number
assert format_number(1234567, "currency") == "1,234,567"
assert format_number(1234.5, "number:2") == "1,234.50"
assert format_number(12.5, "percent") == "12.5%"
def test_report_number_format_and_table_formats():
src = """
report.number_format(style="western")
report.kpi("m", 1500000, format="currency")
rows = table([{"code": "A", "total": 2500000}])
report.table(rows, columns=["code", "total"], formats={"total": "currency"})
report.text(format_number(9999, "number"))
"""
result = _run(src)
assert result.ok, result.error
assert result.spec["meta"]["number_format"]["thousands_sep"] == ","
kpi = next(b for b in result.spec["blocks"] if b["type"] == "kpi")
assert kpi["format"] == "currency"
table = next(b for b in result.spec["blocks"] if b["type"] == "table")
assert table["formats"]["total"] == "currency"
assert any(b.get("text") == "9,999" for b in result.spec["blocks"] if b.get("type") == "text")