Land CSOC-WP-0006 PageOps: member space/page CRUD, copy, transfer
Thin CONTENT_ROOT content plane (ADR-0003/0004) with space visual field, product UI for Title/Abstract/Visual, page list sidebar, and tests. Update capability model and smoke checklist; mark WP-0006 finished.
This commit is contained in:
parent
00dbb86c93
commit
affed64839
24 changed files with 1812 additions and 151 deletions
229
tests/test_space_crud.py
Normal file
229
tests/test_space_crud.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue