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