"""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