Duplicate flows and dashboards: POST /flows/{name}/duplicate and /dashboards/{name}/duplicate copy the working copy into {name}_copy (then _copy_2, ...) as an unpublished draft with " (copy)" on the title. A flow copy takes its node sources as drafts and leaves origin and runtime.json behind; copying code nodes is a superuser's (the save check, now taking the replaced copy). Client regenerated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,24 @@ def test_a_user_keeps_everything_but_the_code(
|
||||
assert saved.status_code == 200, saved.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("save")
|
||||
def test_duplicating_code_nodes_is_a_superusers(
|
||||
client: TestClient,
|
||||
normal_user_token_headers: dict[str, str],
|
||||
superuser_token_headers: dict[str, str],
|
||||
) -> None:
|
||||
url = f"{PREFIX}/{FLOW}/duplicate"
|
||||
|
||||
refused = client.post(url, headers=normal_user_token_headers)
|
||||
assert refused.status_code == 403
|
||||
assert refused.json()["detail"].startswith(CODE_IS_SUPERUSERS)
|
||||
|
||||
copied = client.post(url, headers=superuser_token_headers)
|
||||
assert copied.status_code == 200, copied.text
|
||||
name = copied.json()["definition"]["name"]
|
||||
client.delete(f"{PREFIX}/{name}", headers=superuser_token_headers)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("save")
|
||||
def test_node_source_is_a_superusers_to_write(
|
||||
client: TestClient,
|
||||
|
||||
@@ -123,6 +123,22 @@ def test_an_unpublished_dashboard_can_be_renamed_and_deleted(store: DashboardSto
|
||||
assert not store.exists("home")
|
||||
|
||||
|
||||
def test_a_duplicate_is_an_unpublished_copy_of_the_working_copy(
|
||||
store: DashboardStore,
|
||||
):
|
||||
store.write(default_dashboard("house"))
|
||||
edited = store.write_draft(
|
||||
store.read("house", draft=True).model_copy(update={"title": "Edited"})
|
||||
)
|
||||
|
||||
copy = store.duplicate("house")
|
||||
|
||||
assert (copy.name, copy.title, copy.version) == ("house_copy", "Edited (copy)", 1)
|
||||
assert store.has_draft("house_copy") and not store.is_published("house_copy")
|
||||
assert store.read("house", draft=True).version == edited.version
|
||||
assert store.duplicate("house").name == "house_copy_2"
|
||||
|
||||
|
||||
def test_deleting_and_renaming(store: DashboardStore):
|
||||
store.write(default_dashboard("house"))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
from fluksio.flow.controller import FlowController
|
||||
from fluksio.flow.messages import MessageSpec
|
||||
from fluksio.flow.schemas import FlowDef, NodeDef
|
||||
from fluksio.flow.schemas import FlowDef, FlowOrigin, NodeDef
|
||||
from fluksio.flow.store import FlowStore, StaleVersion
|
||||
|
||||
SOURCE = "def process():\n return {}\n"
|
||||
@@ -181,3 +181,22 @@ def test_renaming_carries_the_draft_and_repoints_other_drafts(store: FlowStore):
|
||||
assert store.read_flow("display", draft=True).nodes[0].requires[0].name == (
|
||||
"warmth.temp"
|
||||
)
|
||||
|
||||
|
||||
def test_a_duplicate_is_a_local_draft_of_what_the_editor_shows(store: FlowStore):
|
||||
store.write_flow(a_flow(title="Warm").model_copy(update={"origin": FlowOrigin()}))
|
||||
store.write_node_source("heating", "sensor", SOURCE)
|
||||
store.write_node_source("heating", "sensor", EDITED, draft=True)
|
||||
store.write_enabled("heating", False)
|
||||
|
||||
copy = store.duplicate_flow("heating")
|
||||
|
||||
assert (copy.name, copy.title, copy.version) == ("heating_copy", "Warm (copy)", 1)
|
||||
assert copy.origin is None
|
||||
assert store.has_draft("heating_copy") and not store.is_published("heating_copy")
|
||||
assert store.read_node_source("heating_copy", "sensor", draft=True) == EDITED
|
||||
assert not (store.root / "heating_copy" / "runtime.json").exists()
|
||||
assert store.duplicate_flow("heating").name == "heating_copy_2"
|
||||
|
||||
store.publish_flow("heating_copy", copy.version)
|
||||
assert store.read_node_source("heating_copy", "sensor") == EDITED
|
||||
|
||||
Reference in New Issue
Block a user