dodil/erp-platform
tests/test_gate.py
"""The gate (0.8): plan / migrate / init refuse a design the customer has not signed as a diagram, or whose
signed BPMN no longer matches what its source generates; `--unsigned "<reason>"` goes on loudly and records it;
serve warns; `signoff verify --all` is the CI check."""
import json
import shutil
import pytest
from erp_platform import compose, compose_init, gate, signoff
from test_bpmn import MD
from test_compose import bricks, composition, make_brick # noqa: F401
from test_compose_init import world, quiet # noqa: F401
@pytest.fixture
def folder(bricks):
make_brick(bricks, "zz_a", "a", ["list_a"], ["a_rows"])
path = composition(bricks, ("a", "zz_a", "0.1.0"))
shutil.copy(MD, bricks / "ERP_INVOICING.md")
return bricks, path
def sign(root):
return signoff.record("invoicing", None, None, None, "Dana Owner, MD", directory=root, source="ERP_INVOICING.md")
def test_plan_and_migrate_refuse_an_unsigned_module(folder, capsys):
root, path = folder
for verb in ("plan", "migrate"):
assert compose.main([verb, str(path)]) == 1
err = capsys.readouterr().err
assert f"erp-compose {verb}: REFUSED" in err and "invoicing: UNSIGNED" in err
assert "erp-compose bpmn --from ERP_<MODULE>.md" in err
def test_a_signed_module_passes_and_a_changed_design_is_refused_by_stable_id(folder, capsys):
root, path = folder
rec = sign(root)
assert rec["source"] == {"kind": "bpmn", "from": "ERP_INVOICING.md", "workflow": None}
assert (root / "processes" / "invoicing.bpmn").exists() and rec["scenario_ids"] == [] and rec["deviations"] == []
assert compose.main(["plan", str(path)]) == 0
assert "sign-off invoicing: signed" in capsys.readouterr().err
md = root / "ERP_INVOICING.md"
md.write_text(md.read_text().replace("| cancel_draft | draft | cancelled | biller |",
"| cancel_draft | draft | cancelled | approver |"))
assert compose.main(["plan", str(path)]) == 1
err = capsys.readouterr().err
assert "invoicing: DRIFTED" in err and "Task_cancel_draft" in err and "Lane_invoice_approver" in err
def test_unsigned_goes_on_under_a_banner_and_is_recorded(folder, capsys):
root, path = folder
assert compose.main(["plan", str(path), "--unsigned", " "]) == 1 # a reason, in words
assert compose.main(["plan", str(path), "--unsigned", "demo for the sales call; not for the customer"]) == 0
err = capsys.readouterr().err
assert gate.BAR in err and "UNSIGNED BUILD" in err
rec = json.loads((root / "signoff" / "invoicing.unsigned.json").read_text())
assert rec["reason"] == "demo for the sales call; not for the customer" and rec["state"] == "unsigned"
assert rec["events"][0]["command"] == "erp-compose plan"
def test_a_folder_with_no_design_at_all_is_refused_too(bricks, capsys):
make_brick(bricks, "zz_a", "a", ["list_a"], ["a_rows"])
path = composition(bricks, ("a", "zz_a", "0.1.0"))
assert compose.main(["plan", str(path)]) == 1
assert "nothing has been designed and signed" in capsys.readouterr().err
def test_serve_warns_and_does_not_refuse(folder):
root, _ = folder
said = []
assert gate.enforce("erp-compose serve", root, refuse=False, say=said.append) is True
assert any("WARNING" in s for s in said) and any("invoicing: UNSIGNED" in s for s in said)
def test_a_sign_off_from_before_0_8_cannot_be_regenerated_and_is_refused(folder):
root, _ = folder
sign(root)
rec = json.loads((root / "signoff" / "invoicing" / "invoicing.json").read_text())
rec.pop("source")
(root / "signoff" / "invoicing" / "invoicing.json").write_text(json.dumps(rec))
[row] = gate.check(root)
assert row["state"] == "unverifiable" and "record it again" in row["detail"]
def test_verify_all_is_the_ci_check(folder, capsys):
root, path = folder
assert signoff.main(["verify", "--all", "--dir", str(root)]) == 1
assert "signoff invoicing: UNSIGNED" in capsys.readouterr().out
sign(root)
assert signoff.main(["verify", "--all", "--dir", str(root)]) == 0
(root / "compose.yaml").write_text((root / "compose.yaml").read_text() + "\nmodules: [invoicing, projects]\n")
assert signoff.main(["verify", "--all", "--dir", str(root)]) == 1 # projects: unsigned
capsys.readouterr()
assert compose.main(["signoff", "verify", "--all", "--dir", str(root), "--json"]) == 1
rows = {r["module"]: r["state"] for r in json.loads(capsys.readouterr().out)}
assert rows == {"invoicing": "signed", "projects": "unsigned"}
def test_record_refuses_a_bpmn_that_is_not_what_the_source_draws(folder, tmp_path):
root, _ = folder
other = tmp_path / "other.bpmn"
other.write_text(compose_bpmn_of(root, "dunning")) # a part of the process is not the process
with pytest.raises(signoff.SignoffError, match="is not the process ERP_INVOICING.md generates now"):
signoff.record("invoicing", other, None, None, "Dana", directory=root, source="ERP_INVOICING.md")
def compose_bpmn_of(root, workflow):
from erp_platform import bpmn
return bpmn.generate(root / "ERP_INVOICING.md", workflow)
def test_init_refuses_a_designed_unsigned_module_and_guides_a_fresh_folder(world):
out = world["out"]
said = []
assert compose_init.init(["demo"], root=out, no_install=True, say=said.append) == 0
assert any("no ERP_<MODULE>.md here for demo" in s for s in said)
shutil.copy(MD, out / "ERP_DEMO.md")
with pytest.raises(compose_init.InitFailed, match="not signed"):
compose_init.init(["demo"], root=out, no_install=True, say=quiet)
assert compose_init.init(["demo"], root=out, no_install=True, unsigned="spike", say=quiet) == 0
assert json.loads((out / "signoff" / "demo.unsigned.json").read_text())["reason"] == "spike"
signoff.record("demo", None, None, None, "Dana", directory=out, source="ERP_DEMO.md")
assert compose_init.init(["demo"], root=out, no_install=True, say=quiet) == 0