Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 7m5s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
263 lines
9.8 KiB
Python
263 lines
9.8 KiB
Python
"""Import ADR-0004 tree into content plane (CSOC-WP-0001-T04 rehearsal path)."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from django.core.management import call_command
|
|
|
|
from coulomb_social.apps.members.models import Member, User
|
|
from coulomb_social.apps.spaces import pageops
|
|
from coulomb_social.apps.spaces.content import load_space_page
|
|
from coulomb_social.apps.spaces.import_tree import import_space_tree
|
|
from coulomb_social.apps.spaces.models import Space, SpaceMembership
|
|
|
|
|
|
@pytest.fixture
|
|
def content_root(tmp_path, settings):
|
|
root = tmp_path / "content"
|
|
settings.CONTENT_ROOT = str(root)
|
|
return root
|
|
|
|
|
|
def _write_sample_tree(root: Path) -> Path:
|
|
tree = root / "sample-space"
|
|
pages = tree / "pages"
|
|
assets = tree / "assets"
|
|
pages.mkdir(parents=True)
|
|
assets.mkdir(parents=True)
|
|
(pages / "index.md").write_text(
|
|
"---\n"
|
|
'title: "Sample Space"\n'
|
|
'abstractor: "From export"\n'
|
|
'visual: ""\n'
|
|
'space: "sample-space"\n'
|
|
"bubble_type: space\n"
|
|
'source_created_at: "2001-01-02T03:04:05Z"\n'
|
|
"---\n\n"
|
|
"# Sample Space\n\nLanding.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(pages / "notes.md").write_text(
|
|
"---\n"
|
|
'title: "Notes"\n'
|
|
'abstractor: "Scratch"\n'
|
|
'visual: "assets/notes-visual.png"\n'
|
|
'space: "sample-space"\n'
|
|
"bubble_type: article\n"
|
|
'source_created_at: "2001-01-03T04:05:06Z"\n'
|
|
"---\n\n"
|
|
"# Notes\n\nBody text.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(assets / "notes-visual.png").write_bytes(b"\x89PNG\r\n")
|
|
return tree
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_space_tree(content_root, tmp_path):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
user = User.objects.create_user(username="imp")
|
|
member = Member.objects.create(
|
|
tenant_id="tenant:a",
|
|
user=user,
|
|
user_engine_user_id="usr_imp",
|
|
issuer="https://local.dev/issuer",
|
|
subject="imp-sub",
|
|
display_name="Importer",
|
|
)
|
|
report = import_space_tree(
|
|
tree,
|
|
tenant_id="tenant:a",
|
|
member=member,
|
|
)
|
|
assert report.pages_copied == 2
|
|
assert report.assets_copied == 1
|
|
assert report.space_created
|
|
|
|
space = Space.objects.get(tenant_id="tenant:a", slug="sample-space")
|
|
assert space.title == "Sample Space"
|
|
assert space.description == "From export"
|
|
assert SpaceMembership.objects.filter(space=space, member=member).exists()
|
|
|
|
notes = pageops.read_page("sample-space", "notes")
|
|
assert notes.exists
|
|
assert notes.title == "Notes"
|
|
assert "Body text" in notes.body
|
|
assert (pageops.space_dir("sample-space") / "assets" / "notes-visual.png").is_file()
|
|
|
|
rendered = load_space_page(space, "notes")
|
|
assert rendered.error is None
|
|
assert rendered.source == "content-plane"
|
|
assert "Body text" in rendered.html
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_refuses_overwrite_without_replace(content_root, tmp_path):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
import_space_tree(tree, tenant_id="tenant:a")
|
|
with pytest.raises(pageops.PageOpsError):
|
|
import_space_tree(tree, tenant_id="tenant:a", replace=False)
|
|
report = import_space_tree(tree, tenant_id="tenant:a", replace=True)
|
|
assert report.space_updated or report.space_created
|
|
assert report.pages_copied == 2
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_content_tree_command(content_root, tmp_path, capsys):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
call_command("import_content_tree", str(tree), "--tenant", "tenant:cmd", "--dry-run")
|
|
out = capsys.readouterr().out
|
|
assert "Dry run" in out
|
|
assert not Space.objects.filter(slug="sample-space").exists()
|
|
|
|
call_command(
|
|
"import_content_tree",
|
|
str(tree),
|
|
"--tenant",
|
|
"tenant:cmd",
|
|
"--slug",
|
|
"cmd-space",
|
|
)
|
|
space = Space.objects.get(slug="cmd-space", tenant_id="tenant:cmd")
|
|
assert space.title == "Sample Space"
|
|
assert pageops.read_page("cmd-space", "index").exists
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_rejects_other_tenant_before_writes(content_root, tmp_path):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
member = Member.objects.create(
|
|
tenant_id="tenant:b", user=User.objects.create_user(username="other"),
|
|
user_engine_user_id="other", issuer="https://issuer.test", subject="other",
|
|
)
|
|
with pytest.raises(pageops.PageOpsError, match="another tenant"):
|
|
import_space_tree(tree, tenant_id="tenant:a", member=member)
|
|
assert not content_root.exists()
|
|
assert not Space.objects.exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_rejects_cross_tenant_slug(content_root, tmp_path):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
import_space_tree(tree, tenant_id="tenant:a")
|
|
before = pageops.page_file("sample-space", "index").read_bytes()
|
|
with pytest.raises(pageops.PageOpsError, match="another tenant"):
|
|
import_space_tree(tree, tenant_id="tenant:b", replace=True)
|
|
assert pageops.page_file("sample-space", "index").read_bytes() == before
|
|
assert not Space.objects.filter(tenant_id="tenant:b").exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_rejects_symlink_in_dry_run(content_root, tmp_path):
|
|
from django.core.management.base import CommandError
|
|
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
(tree / "assets" / "outside").symlink_to(tmp_path)
|
|
with pytest.raises(CommandError, match="symlink"):
|
|
call_command("import_content_tree", str(tree), "--dry-run")
|
|
assert not content_root.exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_import_rolls_back_content_on_publish_error(content_root, tmp_path, monkeypatch):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
import_space_tree(tree, tenant_id="tenant:a")
|
|
path = pageops.page_file("sample-space", "index")
|
|
original = path.read_bytes()
|
|
meta, _ = pageops.parse_frontmatter((tree / "pages" / "index.md").read_text())
|
|
meta["title"] = "Changed"
|
|
(tree / "pages" / "index.md").write_text(pageops.dump_frontmatter(meta, '# Changed\n'))
|
|
real_rename = Path.rename
|
|
|
|
def fail_publish(self, target):
|
|
if self.name == "staged":
|
|
raise OSError("simulated publish failure")
|
|
return real_rename(self, target)
|
|
|
|
monkeypatch.setattr(Path, "rename", fail_publish)
|
|
with pytest.raises(OSError, match="simulated"):
|
|
import_space_tree(tree, tenant_id="tenant:a", replace=True)
|
|
assert path.read_bytes() == original
|
|
assert Space.objects.get(slug="sample-space").title == "Sample Space"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_single_import_subject_cannot_fall_back_to_other_tenant(content_root, tmp_path):
|
|
from django.core.management.base import CommandError
|
|
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
Member.objects.create(
|
|
tenant_id="tenant:b", user=User.objects.create_user(username="foreign"),
|
|
user_engine_user_id="foreign", issuer="https://issuer.test", subject="foreign",
|
|
)
|
|
with pytest.raises(CommandError, match="exactly one"):
|
|
call_command("import_content_tree", str(tree), tenant="tenant:a",
|
|
member_subject="foreign", dry_run=True)
|
|
assert not content_root.exists()
|
|
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
def test_import_restores_tree_when_database_commit_fails(content_root, tmp_path, monkeypatch):
|
|
from django.db import DatabaseError, connection
|
|
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
import_space_tree(tree, tenant_id="tenant:a")
|
|
path = pageops.page_file("sample-space", "index")
|
|
original = path.read_bytes()
|
|
meta, _ = pageops.parse_frontmatter((tree / "pages" / "index.md").read_text())
|
|
meta["title"] = "Changed"
|
|
(tree / "pages" / "index.md").write_text(pageops.dump_frontmatter(meta, '# Changed\n'))
|
|
|
|
def fail_commit():
|
|
raise DatabaseError("simulated database commit failure")
|
|
|
|
monkeypatch.setattr(connection, "commit", fail_commit)
|
|
with pytest.raises(DatabaseError):
|
|
import_space_tree(tree, tenant_id="tenant:a", replace=True)
|
|
assert path.read_bytes() == original
|
|
assert Space.objects.get(slug="sample-space").title == "Sample Space"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@pytest.mark.parametrize("failure", ["overlap", "page-name", "metadata"])
|
|
def test_shared_validation_rejects_bad_trees(content_root, tmp_path, failure):
|
|
from django.core.management.base import CommandError
|
|
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
if failure == "overlap":
|
|
import shutil
|
|
|
|
content_root.mkdir()
|
|
shutil.copytree(tree, content_root / "source")
|
|
tree = content_root / "source"
|
|
elif failure == "page-name":
|
|
(tree / "pages" / "notes.md").rename(tree / "pages" / "Bad Name.md")
|
|
else:
|
|
(tree / "pages" / "index.md").write_text(
|
|
'---\ntitle: "' + ('x' * 256) + '"\n---\n\nbody\n'
|
|
)
|
|
with pytest.raises(CommandError):
|
|
call_command("import_content_tree", str(tree), dry_run=True)
|
|
assert not Space.objects.exists()
|
|
assert not (content_root / "spaces").exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_failed_restore_preserves_previous_tree(content_root, tmp_path, monkeypatch):
|
|
tree = _write_sample_tree(tmp_path / "export")
|
|
import_space_tree(tree, tenant_id="tenant:a")
|
|
original = pageops.page_file("sample-space", "index").read_bytes()
|
|
real_rename = Path.rename
|
|
|
|
def fail_publish_and_restore(self, target):
|
|
if self.name in {"staged", "previous"}:
|
|
raise OSError("simulated filesystem failure")
|
|
return real_rename(self, target)
|
|
|
|
monkeypatch.setattr(Path, "rename", fail_publish_and_restore)
|
|
with pytest.raises(pageops.PageOpsError, match="retained"):
|
|
import_space_tree(tree, tenant_id="tenant:a", replace=True)
|
|
saved = list((content_root / "spaces").glob(".import-*/previous/pages/index.md"))
|
|
assert len(saved) == 1
|
|
assert saved[0].read_bytes() == original
|
|
assert Space.objects.get(slug="sample-space").title == "Sample Space"
|