Files
rfc-app/backend/tests/test_metadata_edit_endpoint.py
T
Ben Stull 46c957cff5 feat(slice4): Owner-gated collection migrate endpoint (§22.4a PUC-5)
POST .../collections/{cid}/migrate — now safe to ship since all write paths
are sidecar-aware. Idempotent, one commit per collection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:01:18 -07:00

191 lines
7.3 KiB
Python

"""SLICE-4 — single-entry metadata edit endpoint (PUC-1, §6.4).
Through the real API: contributor+ gating (INV-4), schema validation at the
write boundary, direct commit to the sidecar with lazy migration, re-ingest,
and the GET RFC `meta` + `can_edit_meta` exposure. Reuses the fake-Gitea harness.
"""
from __future__ import annotations
import asyncio
import json
import yaml
from fastapi.testclient import TestClient
from app import cache, db, gitea as gitea_mod
from app.config import load_config
from test_propose_vertical import ( # noqa: F401 (fixtures)
app_with_fake_gitea,
provision_user_row,
sign_in_as,
tmp_env,
)
PID = "default"
CID = "default"
META = f"/api/projects/{PID}/collections/{CID}"
def _refresh():
cfg = load_config()
asyncio.run(cache.refresh_meta_repo(cfg, gitea_mod.Gitea(cfg)))
def _set_fields(schema):
db.conn().execute(
"UPDATE collections SET config_json = ? WHERE id = 'default'",
(json.dumps({"fields": schema}),))
def _seed_legacy(fake, slug, *, state="active", **front):
fm = {"slug": slug, "title": slug.title(), "state": state, **front}
body = yaml.safe_dump(fm, sort_keys=False).strip()
fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {
"content": f"---\n{body}\n---\n\nBody.\n", "sha": slug}
def _seed_migrated(fake, slug, sidecar):
fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {
"content": "Body.\n", "sha": f"{slug}-md"}
fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.meta.yaml")] = {
"content": sidecar, "sha": f"{slug}-sc"}
def _login_owner(client):
provision_user_row(user_id=1, login="ben", role="owner")
sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")
def test_edit_meta_sets_value_commits_sidecar_and_lazy_migrates(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0", "P1", "P2"]},
"tags": {"type": "tags"}})
_seed_legacy(fake, "a", priority="P1", tags=["x"])
_refresh()
_login_owner(client)
r = client.post(f"{META}/rfcs/a/meta", json={"values": {"priority": "P0"}})
assert r.status_code == 200, r.text
assert r.json()["meta"]["priority"] == "P0"
# sidecar written, .md lazy-migrated to body-only
sc = yaml.safe_load(
fake.files[("wiggleverse", "meta", "main", "rfcs/a.meta.yaml")]["content"])
assert sc["priority"] == "P0"
assert "---" not in fake.files[("wiggleverse", "meta", "main", "rfcs/a.md")]["content"]
# cache reflects the new value
row = db.conn().execute(
"SELECT meta_json FROM cached_rfcs WHERE slug='a'").fetchone()
assert json.loads(row["meta_json"])["priority"] == "P0"
def test_edit_meta_rejects_value_outside_enum(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0", "P1"]}})
_seed_legacy(fake, "a", priority="P1")
_refresh()
_login_owner(client)
r = client.post(f"{META}/rfcs/a/meta", json={"values": {"priority": "ZZZ"}})
assert r.status_code == 422, r.text
# nothing committed
assert ("wiggleverse", "meta", "main", "rfcs/a.meta.yaml") not in fake.files
def test_edit_meta_rejects_unknown_field(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0"]}})
_seed_legacy(fake, "a", priority="P0")
_refresh()
_login_owner(client)
r = client.post(f"{META}/rfcs/a/meta", json={"values": {"nope": "x"}})
assert r.status_code == 422, r.text
def test_edit_meta_forbidden_for_anonymous(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0"]}})
_seed_legacy(fake, "a", priority="P0")
_refresh()
r = client.post(f"{META}/rfcs/a/meta", json={"values": {"priority": "P0"}})
assert r.status_code == 403, r.text
def test_edit_meta_on_already_migrated_entry(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0", "P1"]}})
_seed_migrated(fake, "a", "slug: a\ntitle: A\nstate: active\npriority: P1\n")
_refresh()
_login_owner(client)
r = client.post(f"{META}/rfcs/a/meta", json={"values": {"priority": "P0"}})
assert r.status_code == 200, r.text
sc = yaml.safe_load(
fake.files[("wiggleverse", "meta", "main", "rfcs/a.meta.yaml")]["content"])
assert sc["priority"] == "P0"
# .md untouched (still body-only)
assert fake.files[("wiggleverse", "meta", "main", "rfcs/a.md")]["content"] == "Body.\n"
def test_get_rfc_exposes_meta_and_can_edit(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_set_fields({"priority": {"type": "enum", "values": ["P0", "P1"]}})
_seed_legacy(fake, "a", priority="P1")
_refresh()
# anonymous: meta present, can_edit_meta False
r = client.get(f"{META}/rfcs/a")
assert r.status_code == 200, r.text
assert r.json()["meta"]["priority"] == "P1"
assert r.json()["can_edit_meta"] is False
# owner: can_edit_meta True
_login_owner(client)
r = client.get(f"{META}/rfcs/a")
assert r.json()["can_edit_meta"] is True
# ---- Owner-gated collection migrate endpoint (PUC-5) ----
def test_migrate_collection_endpoint_owner(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_legacy(fake, "a", priority="P1", tags=["x"])
_seed_legacy(fake, "b", priority="P0")
_refresh()
_login_owner(client)
r = client.post(f"{META}/migrate")
assert r.status_code == 200, r.text
assert r.json()["committed"] is True
assert set(r.json()["migrated"]) == {"a", "b"}
# both entries now body-only + sidecar
for slug in ("a", "b"):
assert "---" not in fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")]["content"]
assert ("wiggleverse", "meta", "main", f"rfcs/{slug}.meta.yaml") in fake.files
def test_migrate_collection_forbidden_for_contributor(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_legacy(fake, "a", priority="P1")
_refresh()
provision_user_row(user_id=2, login="carol", role="contributor")
sign_in_as(client, user_id=2, gitea_login="carol",
display_name="Carol", role="contributor")
r = client.post(f"{META}/migrate")
assert r.status_code == 403, r.text
def test_migrate_collection_idempotent(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_legacy(fake, "a", priority="P1")
_refresh()
_login_owner(client)
assert client.post(f"{META}/migrate").json()["committed"] is True
# second run: nothing left to migrate
r2 = client.post(f"{META}/migrate")
assert r2.status_code == 200, r2.text
assert r2.json()["committed"] is False