commits
tags
import pytest
from rpa import config, flows
@pytest.fixture(autouse=True)
def tmp_flows_dir(tmp_path, monkeypatch):
d = tmp_path / "flows"
d.mkdir()
monkeypatch.setattr(config, "FLOWS_DIR", d)
return d
def graph(nodes, edges=None, **extra):
return {"format": "graph", "nodes": nodes, "edges": edges or [], "settings": {}, **extra}
def test_crud_round_trip():
assert flows.list_flows() == []
flows.save_flow("mitt flöde", graph([{"id": "n1", "type": "goto", "url": "https://x.se"}]))
assert flows.list_flows() == ["mitt flöde"]
loaded = flows.load_flow("mitt flöde")
assert loaded["nodes"][0]["url"] == "https://x.se"
assert loaded["settings"] == {}
assert flows.delete_flow("mitt flöde") is True
assert flows.list_flows() == []
assert flows.delete_flow("mitt flöde") is False
def test_old_steps_flow_is_migrated_on_load(tmp_flows_dir):
import json
(tmp_flows_dir / "gammalt.json").write_text(
json.dumps({"steps": [{"type": "goto", "url": "https://x.se"}], "settings": {}}),
encoding="utf-8",
)
loaded = flows.load_flow("gammalt")
assert loaded["format"] == "graph"
assert [n["type"] for n in loaded["nodes"]] == ["goto"]
assert loaded["nodes"][0]["url"] == "https://x.se"
def test_invalid_name_rejected():
with pytest.raises(flows.InvalidFlowName):
flows.flow_path("../secret")
with pytest.raises(flows.InvalidFlowName):
flows.flow_path("a" * 200)
def test_load_missing_raises():
with pytest.raises(flows.FlowNotFound):
flows.load_flow("nope")
def test_save_lenient_by_default_strict_on_request():
half = graph([{"id": "n1", "type": "goto"}]) # url saknas
flows.save_flow("wip", half) # lenient - ok
with pytest.raises(flows.InvalidFlow):
flows.save_flow("wip", half, strict=True)
def test_save_rejects_structural_errors_even_lenient():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("bad", graph([{"id": "n1", "type": "nonesuch"}]))
def test_save_rejects_dangling_edge():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("bad", graph(
[{"id": "n1", "type": "wait", "seconds": 1}],
[{"id": "e1", "source": "n1", "target": "ghost"}],
))
def test_node_positions_round_trip():
flow = graph([{"id": "abc123", "type": "goto", "url": "https://x.se",
"position": {"x": 40, "y": 120}}])
flows.save_flow("laid out", flow)
loaded = flows.load_flow("laid out")
assert loaded["nodes"][0]["id"] == "abc123"
assert loaded["nodes"][0]["position"] == {"x": 40, "y": 120}
def test_inputs_and_sets_round_trip():
flow = graph(
[{"id": "n1", "type": "goto", "url": "https://x.se/{org_id}"}],
inputs=[{"name": "org_id", "label": "Org", "default": "4"}],
input_sets={"Test": {"org_id": "9"}},
)
flows.save_flow("params", flow)
loaded = flows.load_flow("params")
assert loaded["inputs"][0]["name"] == "org_id"
assert loaded["input_sets"] == {"Test": {"org_id": "9"}}
def test_input_without_name_rejected():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("bad", graph([], inputs=[{"label": "x"}]))
def test_missing_flow_defaults_inputs():
flows.save_flow("plain", graph([]))
loaded = flows.load_flow("plain")
assert loaded["inputs"] == [] and loaded["input_sets"] == {}
def test_layout_omitted_when_empty():
flows.save_flow("nolayout", graph([], layout={}))
import json
raw = json.loads((flows.config.FLOWS_DIR / "nolayout.json").read_text(encoding="utf-8"))
assert "layout" not in raw
# --- subflöden ---
def _with_subflows(**extra):
return graph(
[{"id": "n1", "type": "call_flow", "target": "#hjalp",
"in": [{"name": "x", "value": "1"}], "out": [{"from": "y", "to": "r"}]}],
subflows=[{
"id": "hjalp", "name": "Hjälpare",
"nodes": [{"id": "s1", "type": "set_var", "var": "y", "text": "{x}"}],
"edges": [], "start": "s1", "inputs": [{"name": "x"}],
}],
**extra,
)
def test_subflows_round_trip():
flows.save_flow("m", _with_subflows())
loaded = flows.load_flow("m")
assert loaded["subflows"][0]["id"] == "hjalp"
assert loaded["subflows"][0]["nodes"][0]["type"] == "set_var"
assert loaded["nodes"][0]["type"] == "call_flow"
def test_subflow_id_slugged_from_name_when_missing():
g = graph([], subflows=[{"name": "Hämta Bilagor",
"nodes": [], "edges": []}])
flows.save_flow("s", g)
assert flows.load_flow("s")["subflows"][0]["id"] == "h-mta-bilagor"
def test_subflows_omitted_when_empty():
import json
flows.save_flow("nosub", graph([], subflows=[]))
raw = json.loads((flows.config.FLOWS_DIR / "nosub.json").read_text(encoding="utf-8"))
assert "subflows" not in raw
def test_call_flow_to_unknown_local_subflow_rejected():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("bad", graph(
[{"id": "n1", "type": "call_flow", "target": "#saknas"}],
subflows=[{"id": "finns", "name": "F", "nodes": [], "edges": []}],
))
def test_cyclic_local_subflows_rejected():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("cyc", graph(
[{"id": "n1", "type": "call_flow", "target": "#a"}],
subflows=[
{"id": "a", "name": "A", "nodes": [{"id": "x", "type": "call_flow", "target": "#b"}], "edges": []},
{"id": "b", "name": "B", "nodes": [{"id": "y", "type": "call_flow", "target": "#a"}], "edges": []},
],
))
def test_broken_subflow_node_rejected():
with pytest.raises(flows.InvalidFlow):
flows.save_flow("bs", graph([], subflows=[
{"id": "s", "name": "S", "nodes": [{"id": "n", "type": "nonesuch"}], "edges": []},
]))