"""User journey UC-005: reader visibility with mutation denial.""" import pytest from django.db import IntegrityError 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 Space, SpaceMembership from coulomb_social.apps.spaces.services import ( can_write_space, copy_space_page, create_space, put_space_page, transfer_space_page, ) def make_member(subject, tenant="tenant:a"): return Member.objects.create( tenant_id=tenant, user=User.objects.create_user(username=subject), user_engine_user_id=subject, issuer="https://issuer.test", subject=subject, ) @pytest.fixture def shared(tmp_path, settings, client): settings.CONTENT_ROOT = str(tmp_path / "content") owner = make_member("owner") reader = make_member("reader") outsider = make_member("outsider") space = create_space(owner, title="Shared", slug="shared") put_space_page(owner, space, "notes", title="Notes", body="Original work") SpaceMembership.objects.create(space=space, member=reader, role="viewer") client.force_login(reader.user) return owner, reader, outsider, space @pytest.mark.django_db @pytest.mark.parametrize("route", ["edit", "archive", "page_new", "page_edit", "page_delete", "page_copy", "page_transfer"]) def test_reader_cannot_mutate_by_direct_request(shared, client, route): _, _, _, space = shared kwargs = {"slug": space.slug} if route in {"page_edit", "page_delete", "page_copy", "page_transfer"}: kwargs["page_slug"] = "notes" response = client.post(reverse("spaces:" + route, kwargs=kwargs), {"title": "Changed", "body": "Tampered", "dest_space": "shared", "dest_slug": "copy"}) assert response.status_code == 403 assert pageops.read_page("shared", "notes").body == "Original work\n" assert not pageops.read_page("shared", "copy").exists space.refresh_from_db() assert space.title == "Shared" assert space.is_active @pytest.mark.django_db def test_reader_sees_read_only_controls_outsider_sees_no_content(shared, client): _, _, outsider, space = shared url = reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=notes" response = client.get(url) assert response.status_code == 200 assert b"Original work" in response.content assert b"Read-only access" in response.content assert b"Edit page" not in response.content assert b"New page" not in response.content client.force_login(outsider.user) response = client.get(url) assert response.status_code == 404 assert b"Original work" not in response.content @pytest.mark.django_db def test_member_can_edit_but_viewer_cannot_use_services(shared): _, reader, _, space = shared with pytest.raises(pageops.PageOpsError, match="read-only"): put_space_page(reader, space, "notes", title="Changed") membership = SpaceMembership.objects.get(space=space, member=reader) membership.role = "member" membership.save() put_space_page(reader, space, "notes", title="Changed", body="Allowed") assert pageops.read_page("shared", "notes").body == "Allowed\n" @pytest.mark.django_db @pytest.mark.parametrize("operation", ["copy", "move"]) def test_destination_read_only_denies_transfer_without_source_change(shared, operation): owner, _, _, source = shared other = make_member("destination-owner") destination = create_space(other, title="Destination", slug="destination") SpaceMembership.objects.create(space=destination, member=owner, role="viewer") with pytest.raises(pageops.PageOpsError, match="read-only"): if operation == "copy": copy_space_page(owner, source, "notes", dest_space=destination, dest_slug="copy") else: transfer_space_page(owner, source, "notes", destination) assert pageops.read_page("shared", "notes").exists assert not pageops.read_page("destination", "notes").exists assert not pageops.read_page("destination", "copy").exists @pytest.mark.django_db(transaction=True) def test_same_slug_other_tenant_cannot_be_written(shared): from tests.legacy_slug import without_global_slug_constraint owner, _, _, _ = shared with pytest.raises(IntegrityError): Space.objects.create(tenant_id="tenant:b", slug="shared", title="Foreign", created_by=owner) assert pageops.read_page("shared", "notes").body == "Original work\n" with without_global_slug_constraint(): foreign = Space.objects.create(tenant_id="tenant:b", slug="shared", title="Foreign", created_by=owner) assert not can_write_space(owner, foreign) with pytest.raises(pageops.PageOpsError, match="read-only"): put_space_page(owner, foreign, "notes", title="Changed") assert pageops.read_page("shared", "notes").body == "Original work\n" @pytest.mark.django_db def test_space_without_grants_is_not_tenant_public(client): member = make_member("ungranted") space = Space.objects.create(tenant_id=member.tenant_id, slug="private", title="Private") client.force_login(member.user) assert client.get(reverse("spaces:detail", kwargs={"slug": space.slug})).status_code == 404 assert b"Private" not in client.get(reverse("core:app_home")).content @pytest.mark.django_db def test_explicit_reader_role_overrides_creator_write_grant(shared): owner, _, _, space = shared membership = SpaceMembership.objects.get(space=space, member=owner) membership.role = "viewer" membership.save() assert not can_write_space(owner, space) with pytest.raises(pageops.PageOpsError, match="read-only"): put_space_page(owner, space, "notes", title="Should not change")