9ca07a3f81
- collections.py resolution helpers (default_collection_id, type, initial_state) - registry mirror writes project grouping fields + default-collection corpus fields - auth.project_of_rfc joins collections; project_member_role reads memberships - cache/api_*/funder writers+readers re-keyed to collection_id (cached_prs + denormalised project_id tags unchanged); api_deployment reads type/initial_state from the default collection - projects.py restamp detects bootstrap via collections; initial_state via collection - tests updated to the three-tier schema; test_migration_028 retired (superseded by 029) - add @S1 acceptance test (collection grain + N=1 serving) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
"""§22.4 (Plan B) — per-project RFC serving. A second project's corpus renders
|
|
under its own slug namespace via /api/projects/{pid}/rfcs[/{slug}], isolated
|
|
from the default project and gated by §22.5 visibility."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from test_propose_vertical import ( # noqa: F401
|
|
app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as,
|
|
)
|
|
|
|
|
|
def _add_project(pid, name, vis="public"):
|
|
# §22 three-tier: a project + its default collection (keyed by the project
|
|
# id in tests, so default_collection_id(pid) == pid).
|
|
from app import db
|
|
db.conn().execute(
|
|
"INSERT OR IGNORE INTO projects (id, name, content_repo, visibility) VALUES (?, ?, ?, ?)",
|
|
(pid, name, pid + "-content", vis),
|
|
)
|
|
db.conn().execute(
|
|
"INSERT OR IGNORE INTO collections (id, project_id, type, subfolder, initial_state, visibility, name) "
|
|
"VALUES (?, ?, 'document', '', 'super-draft', ?, ?)",
|
|
(pid, pid, vis, name),
|
|
)
|
|
|
|
|
|
def _add_rfc(slug, title, pid, state="active"):
|
|
from app import db
|
|
# entries key by the project's default collection (id == pid in these tests)
|
|
db.conn().execute(
|
|
"INSERT INTO cached_rfcs (slug, title, state, collection_id) VALUES (?, ?, ?, ?)",
|
|
(slug, title, state, pid),
|
|
)
|
|
|
|
|
|
def test_catalog_scoped_to_one_project(app_with_fake_gitea):
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
_add_project("ecomm", "Ecomm")
|
|
_add_rfc("intro", "Default Intro", "default")
|
|
_add_rfc("intro", "Ecomm Intro", "ecomm")
|
|
_add_rfc("only-ecomm", "Ecomm Only", "ecomm")
|
|
|
|
d = client.get("/api/projects/default/rfcs").json()["items"]
|
|
e = client.get("/api/projects/ecomm/rfcs").json()["items"]
|
|
d_slugs = {i["slug"] for i in d}
|
|
e_slugs = {i["slug"] for i in e}
|
|
assert "intro" in d_slugs and "only-ecomm" not in d_slugs
|
|
assert {"intro", "only-ecomm"} <= e_slugs
|
|
|
|
|
|
def test_entry_is_isolated_by_project(app_with_fake_gitea):
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
_add_project("ecomm", "Ecomm")
|
|
_add_rfc("intro", "Default Intro", "default")
|
|
_add_rfc("intro", "Ecomm Intro", "ecomm")
|
|
_add_rfc("only-ecomm", "Ecomm Only", "ecomm")
|
|
|
|
assert client.get("/api/projects/default/rfcs/intro").json()["title"] == "Default Intro"
|
|
assert client.get("/api/projects/ecomm/rfcs/intro").json()["title"] == "Ecomm Intro"
|
|
# a slug that exists only in ecomm 404s under default
|
|
assert client.get("/api/projects/ecomm/rfcs/only-ecomm").status_code == 200
|
|
assert client.get("/api/projects/default/rfcs/only-ecomm").status_code == 404
|
|
|
|
|
|
def test_gated_project_catalog_404s_for_anon(app_with_fake_gitea):
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
_add_project("secret", "Secret", vis="gated")
|
|
_add_rfc("hush", "Hush", "secret")
|
|
assert client.get("/api/projects/secret/rfcs").status_code == 404
|
|
assert client.get("/api/projects/secret/rfcs/hush").status_code == 404
|