"""Member-facing space/page CRUD, copy, transfer (CSOC-WP-0006).""" from pathlib import Path import pytest from django.urls import reverse from coulomb_social.apps.members.models import Member from coulomb_social.apps.spaces import pageops from coulomb_social.apps.spaces.content import load_space_page from coulomb_social.apps.spaces.models import Space def _login(client, settings, *, subject: str = "u1", tenant: str = "tenant:a", name: str = "User"): settings.DEBUG = True settings.OIDC_ENABLED = False settings.DEFAULT_TENANT_ID = tenant return client.post( reverse("identity:dev_login"), { "subject": subject, "issuer": "https://local.dev/issuer", "name": name, "tenant": tenant, }, ) @pytest.fixture def content_root(tmp_path, settings): root = tmp_path / "content" settings.CONTENT_ROOT = str(root) return root @pytest.mark.django_db def test_create_space_writes_index_and_lists(client, settings, content_root): _login(client, settings) r = client.post( reverse("spaces:create"), { "title": "Garden", "abstractor": "Grow ideas", "slug": "garden", "visual": "assets/cover.jpg", }, ) assert r.status_code == 302 assert r.url.endswith("/app/spaces/garden/") space = Space.objects.get(slug="garden") assert space.title == "Garden" assert space.description == "Grow ideas" assert space.visual == "assets/cover.jpg" assert space.abstractor == "Grow ideas" index = pageops.read_page("garden", "index") assert index.exists assert index.title == "Garden" home = client.get(reverse("core:app_home")) assert home.status_code == 200 assert b"Garden" in home.content assert b"Grow ideas" in home.content @pytest.mark.django_db def test_edit_space_updates_index_frontmatter(client, settings, content_root): _login(client, settings) client.post( reverse("spaces:create"), {"title": "Lab", "slug": "lab", "abstractor": "Old", "visual": ""}, ) r = client.post( reverse("spaces:edit", kwargs={"slug": "lab"}), {"title": "Lab 2", "abstractor": "New abstract", "visual": "assets/v.jpg"}, ) assert r.status_code == 302 space = Space.objects.get(slug="lab") assert space.title == "Lab 2" assert space.description == "New abstract" assert space.visual == "assets/v.jpg" index = pageops.read_page("lab", "index") assert index.title == "Lab 2" assert index.abstractor == "New abstract" @pytest.mark.django_db def test_archive_space_hides_from_list(client, settings, content_root): _login(client, settings) client.post( reverse("spaces:create"), {"title": "Temp", "slug": "temp", "abstractor": "", "visual": ""}, ) r = client.post(reverse("spaces:archive", kwargs={"slug": "temp"})) assert r.status_code == 302 assert not Space.objects.get(slug="temp").is_active home = client.get(reverse("core:app_home")) assert home.status_code == 200 # Flash message may mention the title; the space must not remain linked. assert b"/app/spaces/temp/" not in home.content assert b"No spaces yet" in home.content @pytest.mark.django_db def test_page_crud_copy_transfer(client, settings, content_root): _login(client, settings) client.post( reverse("spaces:create"), {"title": "Alpha", "slug": "alpha", "abstractor": "", "visual": ""}, ) client.post( reverse("spaces:create"), {"title": "Beta", "slug": "beta", "abstractor": "", "visual": ""}, ) # create page r = client.post( reverse("spaces:page_new", kwargs={"slug": "alpha"}), { "title": "Notes", "slug": "notes", "abstractor": "Scratch pad", "visual": "", "body": "# Notes\n\nHello body.\n", }, ) assert r.status_code == 302 assert "page=notes" in r.url doc = pageops.read_page("alpha", "notes") assert doc.exists assert doc.abstractor == "Scratch pad" # render from content plane space = Space.objects.get(slug="alpha") rendered = load_space_page(space, "notes") assert rendered.error is None assert rendered.source == "content-plane" assert "Hello body" in rendered.html detail = client.get(reverse("spaces:detail", kwargs={"slug": "alpha"}) + "?page=notes") assert detail.status_code == 200 assert b"Hello body" in detail.content assert b"Notes" in detail.content # edit r = client.post( reverse("spaces:page_edit", kwargs={"slug": "alpha", "page_slug": "notes"}), { "title": "Notes v2", "slug": "notes", "abstractor": "Updated", "visual": "assets/n.jpg", "body": "# Notes v2\n\nUpdated body.\n", }, ) assert r.status_code == 302 doc = pageops.read_page("alpha", "notes") assert doc.title == "Notes v2" assert "Updated body" in doc.body # copy within space r = client.post( reverse("spaces:page_copy", kwargs={"slug": "alpha", "page_slug": "notes"}), { "dest_space": "alpha", "dest_slug": "notes-copy", "title": "", }, ) assert r.status_code == 302 copy = pageops.read_page("alpha", "notes-copy") assert copy.exists assert copy.title == "Notes v2 (copy)" # transfer to beta r = client.post( reverse("spaces:page_transfer", kwargs={"slug": "alpha", "page_slug": "notes-copy"}), { "dest_space": "beta", "dest_slug": "notes-copy", }, ) assert r.status_code == 302 assert not pageops.read_page("alpha", "notes-copy").exists moved = pageops.read_page("beta", "notes-copy") assert moved.exists assert moved.title == "Notes v2 (copy)" # delete remaining notes r = client.post( reverse("spaces:page_delete", kwargs={"slug": "alpha", "page_slug": "notes"}) ) assert r.status_code == 302 assert not pageops.read_page("alpha", "notes").exists @pytest.mark.django_db def test_page_ops_require_membership_isolation(client, settings, content_root): _login(client, settings, subject="u1", tenant="tenant:a") client.post( reverse("spaces:create"), {"title": "Mine", "slug": "mine", "abstractor": "", "visual": ""}, ) # other tenant cannot see client.logout() _login(client, settings, subject="u2", tenant="tenant:b", name="Other") r = client.get(reverse("spaces:detail", kwargs={"slug": "mine"})) assert r.status_code == 404 r = client.post( reverse("spaces:page_new", kwargs={"slug": "mine"}), {"title": "X", "slug": "x", "body": "nope"}, ) assert r.status_code == 404 @pytest.mark.django_db def test_cannot_delete_index_via_view(client, settings, content_root): _login(client, settings) client.post( reverse("spaces:create"), {"title": "Keep", "slug": "keep", "abstractor": "", "visual": ""}, ) # URL for delete index would work but pageops rejects r = client.post( reverse("spaces:page_delete", kwargs={"slug": "keep", "page_slug": "index"}) ) assert r.status_code == 302 assert pageops.read_page("keep", "index").exists