coulomb-social/tests/test_reichelag_workflow.py
tegwick dd83c90d73
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m43s
Implement space sharing, member portability and recoverable content edits
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a119cf-56d0-7fa3-840b-eb8732e1db0b
2026-10-08 09:17:31 +02:00

153 lines
8 KiB
Python

"""UC-008 synthetic acceptance; contains no private ReichelAg notes."""
import base64
import json
from pathlib import Path
import pytest
from django.urls import reverse
from coulomb_social.apps.members.models import Member, User
from coulomb_social.apps.spaces import pageops
from coulomb_social.apps.spaces.models import SpaceMembership
from coulomb_social.apps.spaces.services import create_space, put_space_page
@pytest.mark.usecase("CSOC-UC-008")
@pytest.mark.django_db
def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings, client):
settings.CONTENT_ROOT = str(tmp_path / "content")
user = User.objects.create_user(username="writer")
member = Member.objects.create(
user=user, tenant_id="tenant:a", user_engine_user_id="writer",
issuer="https://issuer.test", subject="writer",
)
space = create_space(member, title="Synthetic weekly calls", slug="weekly-calls")
scenario = json.loads((Path(__file__).parent / "usecases/fixtures/reichelag-weekly-call.json")
.read_text(encoding="utf-8"))
body = scenario["previous_body"]
put_space_page(member, space, "previous-call", title="Previous call", body=body)
client.force_login(user)
response = client.post(reverse("spaces:page_copy", kwargs={
"slug": space.slug, "page_slug": "previous-call",
}), {"dest_space": space.slug, "dest_slug": "next-call", "title": "Next call"})
assert response.status_code == 302
assert pageops.read_page(space.slug, "next-call").body == body
assert pageops.read_page(space.slug, "previous-call").body == body
edited = scenario["next_body"]
response = client.post(reverse("spaces:page_edit", kwargs={
"slug": space.slug, "page_slug": "next-call",
}), {"title": "Next call", "slug": "next-call", "body": edited,
"revision": pageops.read_page(space.slug, "next-call").revision})
assert response.status_code == 302
assert pageops.read_page(space.slug, "previous-call").body == body
response = client.get(reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=next-call")
assert response.status_code == 200
assert b"Copy content to clipboard" in response.content
assert b"Duplicate page" in response.content
assert b'id="page-content"' in response.content
assert b"Follow-up point" in response.content
assert b"spaces/copy-content.js" in response.content
assert b"Completed point" not in response.content
# A reader can export what they can read, without getting authoring controls.
membership = SpaceMembership.objects.get(space=space, member=member)
membership.role = "viewer"
membership.save()
response = client.get(reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=next-call")
assert b"Copy content to clipboard" in response.content
assert b"Duplicate page" not in response.content
unavailable = client.get(
reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=missing"
)
assert b"Copy content to clipboard" not in unavailable.content
@pytest.mark.usecase("CSOC-UC-008")
@pytest.mark.django_db
def test_protocol_naming_rename_and_collision_safety(tmp_path, settings, client):
settings.CONTENT_ROOT = str(tmp_path / "content")
user = User.objects.create_user(username="protocol-writer")
member = Member.objects.create(
user=user, tenant_id="tenant:synthetic", user_engine_user_id="protocol-writer",
issuer="https://issuer.test", subject="protocol-writer",
)
space = create_space(member, title="Synthetic calls", slug="synthetic-calls")
scenario = json.loads((Path(__file__).parent / "usecases/fixtures/reichelag-weekly-call.json")
.read_text(encoding="utf-8"))
image = base64.b64decode(scenario["visual_base64"])
pageops.assets_dir(space.slug).joinpath("cover.png").write_bytes(image)
source = pageops.write_page(
space.slug, "previous-call", title="Previous call", abstractor="Board call",
body="## Agenda\n\n- Open point\n", visual="assets/cover.png",
extra_meta={"source_id": "synthetic-record", "source_modified_at": 12345},
)
source_bytes = Path(source.path).read_bytes()
client.force_login(user)
copy_url = reverse("spaces:page_copy", kwargs={
"slug": space.slug, "page_slug": source.slug,
})
# Enter a title once; the optional filename is generated on the server.
response = client.post(copy_url, {"title": "Next Weekly Call", "dest_slug": ""})
assert response.status_code == 302
assert response.url.endswith("?page=next-weekly-call")
duplicate = pageops.read_page(space.slug, "next-weekly-call")
assert duplicate.title == "Next Weekly Call" and duplicate.body == source.body
visual_path = pageops.space_dir(space.slug) / duplicate.visual
assert visual_path.read_bytes() == image
edit_url = reverse("spaces:page_edit", kwargs={
"slug": space.slug, "page_slug": duplicate.slug,
})
fields = {"title": "Renamed call", "slug": source.slug,
"revision": duplicate.revision,
"abstractor": duplicate.abstractor, "visual": duplicate.visual,
"body": duplicate.body}
response = client.post(edit_url, fields)
assert response.status_code == 200
assert b"already exists" in response.content
assert Path(source.path).read_bytes() == source_bytes
assert pageops.read_page(space.slug, duplicate.slug).title == duplicate.title
# A distinct filename renames the Markdown file and the return address.
fields["slug"] = "renamed-call"
response = client.post(edit_url, fields)
assert response.status_code == 302 and response.url.endswith("?page=renamed-call")
assert not pageops.read_page(space.slug, duplicate.slug).exists
renamed = pageops.read_page(space.slug, "renamed-call")
assert renamed.title == "Renamed call"
assert renamed.body == source.body and renamed.abstractor == source.abstractor
assert renamed.visual == duplicate.visual and visual_path.is_file()
assert renamed.meta["source_id"] == "synthetic-record"
assert renamed.meta["source_modified_at"] == "12345"
assert Path(source.path).read_bytes() == source_bytes
# Changing title alone preserves the filename and imported source metadata.
fields.update(title="Corrected title", slug="renamed-call", revision=renamed.revision)
response = client.post(reverse("spaces:page_edit", kwargs={
"slug": space.slug, "page_slug": "renamed-call",
}), fields)
assert response.status_code == 302
renamed = pageops.read_page(space.slug, "renamed-call")
assert renamed.title == "Corrected title" and renamed.meta["source_id"] == "synthetic-record"
# Explicit copy filename overrides the title-derived name.
response = client.post(copy_url, {"title": "Another call", "dest_slug": "custom-name"})
assert response.status_code == 302 and response.url.endswith("?page=custom-name")
# Duplicate collision leaves the existing file and form input intact.
protected = Path(renamed.path).read_bytes()
response = client.post(copy_url, {"title": "Renamed call", "dest_slug": ""})
assert response.status_code == 200 and b"already exists" in response.content
assert b'value="Renamed call"' in response.content
assert Path(renamed.path).read_bytes() == protected
# Renaming to index is refused; changing an index title cannot move its file.
fields["slug"] = "index"
fields["revision"] = renamed.revision
response = client.post(reverse("spaces:page_edit", kwargs={
"slug": space.slug, "page_slug": renamed.slug,
}), fields)
assert response.status_code == 200 and b"already exists" in response.content
response = client.post(reverse("spaces:page_edit", kwargs={
"slug": space.slug, "page_slug": "index",
}), {"title": "Index title", "slug": "move-index", "body": "Index notes\n",
"revision": pageops.read_page(space.slug, "index").revision})
assert response.status_code == 302
assert pageops.read_page(space.slug, "index").title == "Index title"
assert not pageops.read_page(space.slug, "move-index").exists