diff --git a/CHANGELOG.md b/CHANGELOG.md index 8180a63..3e9cd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,105 @@ skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. +## 0.6.0 — 2026-05-28 + +**Minor — no operator action required.** A sweep-the-edges hardening +release (roadmap item #4, "anon discuss + contribute off-limits") that +audits every write-shaped backend endpoint and asserts each one +enforces an explicit `auth.require_contributor` (or stricter) gate +before doing any state-changing work. v0.3.0 hid write affordances +behind a sign-in CTA on the frontend; v0.5.0 added the PR-less +discussion surface with its own write gate; v0.6.0 sweeps the rest +and adds a regression test net so future endpoints can't quietly +ship without a gate. No schema migration, no env-var changes, no +new dependencies. The only behavioural change is one tightening: the +`GET /api/rfcs//graduate/progress` SSE now requires +`auth.require_user` since it surfaces operator-visible step detail +(repo name, PR number, rollback steps) not part of the v0.3.0 +anonymous-read contract for catalog/RFC bodies. + +### Changed + +- **`backend/app/api_graduation.py`** — `GET /graduate/progress` + now calls `auth.require_user(request)` as its first line. + Anonymous callers receive 401 instead of being able to subscribe + to a graduation's SSE. The floor is `require_user` (not + `require_contributor`) so a write-muted operator can still observe + the progress of a graduation they kicked off before being muted. +- **SPEC `§6.1`** (`SPEC.md`) — the Anonymous role's bullet now + documents the v0.6.0 audit: every write-shaped endpoint in §17 + enforces an explicit gate; anonymous writes refuse 401. The list + of audited write families is recorded in-line. +- **SPEC `§10.10`** — the discussion-vs-contribution section now + records that the v0.5.0 write gates have a regression test net + (`test_anon_offlimits_vertical.py`) added in v0.6.0. +- **SPEC `§17`** — the `GET /api/rfcs//graduate/progress` + bullet now documents the `require_user` gate added in v0.6.0, + with the rationale. + +### Added (tests) + +- **`backend/tests/test_anon_offlimits_vertical.py`** — twelve new + tests asserting that every write-shaped endpoint surveyed in the + v0.6.0 audit refuses anonymous callers with 401, and that the five + anonymous-read surfaces (health, philosophy, auth/me, catalog, + RFC view, discussion threads, proposals) stay reachable. Sixty-six + assertions in total, covering: propose; proposal merge / decline / + withdraw; branch promote-to-branch / start-edit-branch / metadata / + manual-flush / visibility / grants (POST + DELETE) / threads (POST) + / messages (POST) / resolve / chat-seen / changes (accept / decline + / reask) / chat-stream; super-draft start-edit-branch + metadata; + PR pr-draft / open / seen / review / merge / withdraw / + description / resolution-branch; discussion thread create + message + post + resolve; admin role / mute / allowlist (POST + DELETE) plus + the admin reads; notification preferences / quiet-hours / watch / + mark-read / user-mute (POST + DELETE); funder credentials (POST + + DELETE) + consent (POST + DELETE); graduation kickoff + claim + + progress SSE; PR review page anonymous-readable. + +### Anonymous-writeable allowlist + +Two endpoints are intentionally anonymous-by-design. They are not +audit findings; they are documented here so the contract is +explicit: + +- `GET /auth/login` and `GET /auth/callback` — the OAuth + round-trip. Anonymous-by-design because they ARE the sign-in + entrypoint. +- `POST /api/webhooks/gitea` and `POST /api/webhooks/email-bounce` + — anonymous in the session sense but authenticated by HMAC shared + secret (`GITEA_WEBHOOK_SECRET` and `WEBHOOK_EMAIL_BOUNCE_SECRET` + respectively). The webhook receiver is the wrong place for an + authenticated session; the shared-secret shape is correct. + +### §19.2 candidates surfaced + +- None unique to this release. The two pre-existing candidates the + audit touched — anonymous-read polish for the discussion surface + (carried from v0.5.0) and the operator-visibility floor on + graduation progress — were settled here as `require_user` on + `/graduate/progress` rather than deferred. + +### Upgrade steps (from 0.5.0) + +1. The deployment **MUST** rebuild the frontend (`npm install && + npm run build`) so the frontend bundle's reported version matches + the backend's. No new env vars; existing `frontend/.env` is + sufficient. +2. The deployment **MUST** restart the backend so the new + `require_user` gate on `/graduate/progress` is enforced. No + schema migration runs. +3. The deployment **MAY** announce the audit completion to + operators: every write-shaped backend endpoint now enforces an + explicit gate, and the `test_anon_offlimits_vertical.py` test + net asserts the contract on every CI run. The audited surfaces + are listed in the `Added (tests)` section above and in `SPEC.md` + §6.1. +4. The deployment **MUST NOT** assume any new envelope behaviour: + v0.6.0 is purely a hardening release. No schema, no env, no + dependency changes; the operator's role is reduced to rebuild + + restart. + ## 0.5.0 — 2026-05-27 **Minor — no operator action required.** This release wires the diff --git a/SPEC.md b/SPEC.md index 1a3c1c4..e869906 100644 --- a/SPEC.md +++ b/SPEC.md @@ -354,7 +354,15 @@ Authorization is owned by the app. Gitea sees only the bot account. 1. **Anonymous.** Can read public RFCs (the meta repo's main branch, every RFC repo's main branch), read any branch whose `read_public` is true, read any PR. Cannot chat, propose, create branches, or - open PRs. + open PRs. v0.6.0 (roadmap item #4) closed the audit: every + write-shaped endpoint surveyed in §17 enforces an explicit + `auth.require_contributor` (or stricter) gate before doing any + state-changing work; anonymous writes refuse 401. The explicit + audit covers propose, branch create, branch threads, PR-less + discussion threads + messages, PR open / merge / withdraw, + funder credentials + consent, admin allowlist add, graduation + kickoff + claim. Anonymous reads on every catalog and RFC-body + surface remain open per the v0.3.0 contract. 2. **Contributor.** Default role for any authenticated account. Everything anonymous can do, plus: propose new RFCs (open a PR against the meta repo), create branches on any RFC repo, open PRs @@ -1669,8 +1677,12 @@ them was the failure mode of generic-PR-comments-as-only-conversation. Reads on the discussion surface follow §14 / the v0.3.0 anonymous-read contract: anyone can see the conversation. Writes require contributor -role per §6.1 (v0.5.0 implements the gate; v0.6.0 — item #4 — hardens -adjacent surfaces to match). The notification routing reuses the +role per §6.1: v0.5.0 implemented the gate on the three discussion +write paths (POST threads, POST messages, POST resolve); v0.6.0 (item +#4) audited the adjacent surfaces and added the matching test net +(`test_anon_offlimits_vertical.py`) so a regression on any write +endpoint is caught immediately. The gates use `auth.require_contributor` +as the canonical helper. The notification routing reuses the existing `chat_message_in_participated_thread` / `chat_reply_to_my_message` event kinds with `branch_name=null` on the fan-out row; the §15.7 reconciler and §15 inbox prose render @@ -2544,7 +2556,13 @@ The follow-up session will refine this. A minimal starting set: trailing `rollback` step's events if any earlier step fails. The Graduate dialog opens this stream on confirm and renders the step stack from the events. The stream closes on success or on - rollback completion. + rollback completion. Requires `auth.require_user` per v0.6.0 + (item #4): the step detail (repo name, PR number, rollback steps) + is operator-visible state and isn't part of the v0.3.0 + anonymous-read contract for catalog and RFC bodies. The floor is + `require_user` (not `require_contributor`) so a write-muted + operator can still observe a graduation they kicked off before + being muted. - `GET /api/rfcs//blocking-prs` — list open meta-repo PRs against `rfcs/.md` per §13.2's precondition popover. Returns PR number, title, author, last-activity timestamp, and the diff --git a/VERSION b/VERSION index 8f0916f..a918a2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/backend/app/api_graduation.py b/backend/app/api_graduation.py index 6fa46e0..66443ee 100644 --- a/backend/app/api_graduation.py +++ b/backend/app/api_graduation.py @@ -520,7 +520,16 @@ def make_router( @router.get("/api/rfcs/{slug}/graduate/progress") async def graduate_progress(slug: str, request: Request): - del request + # v0.6.0 (item #4): the progress SSE surfaces admin-internal step + # detail (repo name, PR number, rollback steps) that isn't part of + # the v0.3.0 anonymous-read contract for catalog/RFC bodies. The + # corresponding POST /graduate is gated to RFC owners/arbiters and + # app admins/owners via `_can_graduate`; the read SSE shares that + # operator-visible surface, so it requires at least an + # authenticated viewer. We keep the floor at require_user (not + # require_contributor) so a write-muted operator can still observe + # the progress of a graduation they kicked off before being muted. + auth.require_user(request) state = _get_active(slug) if state is None: raise HTTPException(404, "No graduation in flight for this slug") diff --git a/backend/tests/test_anon_offlimits_vertical.py b/backend/tests/test_anon_offlimits_vertical.py new file mode 100644 index 0000000..9b94ae2 --- /dev/null +++ b/backend/tests/test_anon_offlimits_vertical.py @@ -0,0 +1,476 @@ +"""v0.6.0 (roadmap item #4) — "anon discuss + contribute off-limits" +vertical. + +A sweep-the-edges hardening release. The v0.3.0 release hid the write +affordances from anonymous viewers; v0.5.0 added the PR-less discussion +surface with its own write gate. v0.6.0 audits both: every write-shaped +endpoint refuses anonymous callers with 401 (or 403 when the role check +runs after the auth check), and every anonymous-read surface stays +reachable. + +This test is the regression net for the audit. It walks each module's +representative write endpoint as an anonymous client and asserts the +401/403, then walks the same surfaces' representative read endpoints +as anonymous and asserts the 200. The intent is breadth over depth: +one assertion per write endpoint family is enough to catch a +regression where someone strips the `auth.require_contributor` line. + +Endpoints covered (one or two from each module): + + - api.py: propose, decline (admin), withdraw, + funder credentials POST/DELETE, funder consent + POST/DELETE + - api_branches.py: promote-to-branch, start-edit-branch, metadata, + manual-flush, visibility, grants POST/DELETE, + threads POST, thread messages POST, resolve, + chat-seen, change accept/decline/reask + - api_prs.py: pr-draft, open-pr, seen, review, merge, withdraw, + description, resolution-branch + - api_discussion.py: thread create, message post, resolve + - api_admin.py: role POST, mute POST, allowlist POST/DELETE + - api_notifications.py: prefs POST, watch POST, mark-read POST, + quiet-hours POST, user-mute POST/DELETE + - api_graduation.py: graduate POST, claim POST, progress GET + +The §15.7 reads (`/api/notifications`, `/api/watches`, +`/api/users/me/*`) are per-user surfaces — they require an +authenticated viewer by definition; an anonymous 401 on those reads is +shape-correct, not a regression. The test does not assert reads on +those. +""" +from __future__ import annotations + +import pytest + +# Reuse the fixture / session / fake-Gitea harness from Slice 1. +from test_propose_vertical import ( # noqa: F401 — fixtures land via import + FakeGitea, + app_with_fake_gitea, + provision_user_row, + sign_in_as, + tmp_env, +) +from test_rfc_view_vertical import SEED_BODY, seed_active_rfc + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_anonymous_can_read_every_public_surface(app_with_fake_gitea): + """Per §14 / the v0.3.0 anonymous-read contract: the catalog, the + RFC view, the PR-less discussion surface, the philosophy page, and + the health probe must remain reachable for unauthenticated viewers. + This is the read side of the item #4 contract — the read surfaces + must NOT regress to require auth as the write gates tighten. + """ + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + provision_user_row(user_id=1, login="alice", role="contributor") + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + + # No session cookie — viewer is anonymous. + client.cookies.clear() + + # The five read surfaces an anonymous viewer must reach. + assert client.get("/api/health").status_code == 200 + assert client.get("/api/philosophy").status_code == 200 + assert client.get("/api/auth/me").status_code == 200 + assert client.get("/api/rfcs").status_code == 200 + assert client.get("/api/rfcs/ohm").status_code == 200 + assert client.get("/api/rfcs/ohm/main").status_code == 200 + assert client.get("/api/rfcs/ohm/discussion/threads").status_code == 200 + assert client.get("/api/proposals").status_code == 200 + + +def test_anonymous_propose_refused(app_with_fake_gitea): + from fastapi.testclient import TestClient + + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + client.cookies.clear() + r = client.post( + "/api/rfcs/propose", + json={"title": "X", "slug": "x", "pitch": "p", "tags": []}, + ) + assert r.status_code == 401 + + +def test_anonymous_proposal_admin_paths_refused(app_with_fake_gitea): + """The admin-gated proposal actions — merge, decline — must refuse + anonymous callers with 401 (the auth check runs before the role + check; both refusals are correct, but 401 is the structural signal + "no session at all").""" + from fastapi.testclient import TestClient + + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + client.cookies.clear() + # PR number doesn't need to exist — the gate runs first. + assert client.post("/api/proposals/1/merge").status_code == 401 + assert ( + client.post("/api/proposals/1/decline", json={"comment": "no"}).status_code + == 401 + ) + assert client.post("/api/proposals/1/withdraw").status_code == 401 + + +def test_anonymous_branch_writes_refused_on_active_rfc(app_with_fake_gitea): + """Branch-scoped writes on an active RFC: promote-to-branch, + manual-flush, visibility, grants, threads create, message post, + resolve, chat-seen, change accept/decline/reask. All must 401 for + anonymous callers.""" + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + client.cookies.clear() + + # Branch-scoped writes — slug + branch values are placeholders; + # the auth gate runs before any state lookup. + slug = "ohm" + branch = "feature-x" + + assert ( + client.post( + f"/api/rfcs/{slug}/branches/main/promote-to-branch", + json={}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/manual-flush", + json={"new_content": "hi", "paragraph_count": 1}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/visibility", + json={"read_public": False}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/grants", + json={"grantee_gitea_login": "alice"}, + ).status_code == 401 + ) + assert ( + client.delete( + f"/api/rfcs/{slug}/branches/{branch}/grants/alice", + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/threads", + json={"thread_kind": "chat", "anchor_kind": "whole-doc"}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/threads/1/messages", + json={"text": "hi"}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/threads/1/resolve", + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/chat-seen", + json={"last_seen_message_id": 1}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/changes/1/accept", + json={"proposed": "x"}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/changes/1/decline", + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/changes/1/reask", + ).status_code == 401 + ) + # Chat stream — POST shaped, same auth gate. + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/threads/1/chat", + json={"text": "hi"}, + ).status_code == 401 + ) + + +def test_anonymous_super_draft_writes_refused(app_with_fake_gitea): + """Super-draft-scoped writes: start-edit-branch and metadata. The + PR open / merge paths share the gate via api_prs.py — see the + PR-flow test below for those.""" + from fastapi.testclient import TestClient + + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + client.cookies.clear() + assert ( + client.post( + "/api/rfcs/anything/start-edit-branch", json={} + ).status_code == 401 + ) + assert ( + client.post( + "/api/rfcs/anything/metadata", json={"title": "x"} + ).status_code == 401 + ) + + +def test_anonymous_pr_flow_writes_refused(app_with_fake_gitea): + """All §10 PR-flow writes — open, merge, withdraw, description, + review, seen, pr-draft, resolution-branch — must 401 for anonymous.""" + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + client.cookies.clear() + slug, branch, pr = "ohm", "feature-x", 1 + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/pr-draft" + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/branches/{branch}/open-pr", + json={"title": "t", "description": "d"}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/seen", + json={"last_seen_message_id": 1}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/review", + json={"text": "x", "anchor_payload": {}}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/merge" + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/withdraw" + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/description", + json={"title": "t", "description": "d"}, + ).status_code == 401 + ) + assert ( + client.post( + f"/api/rfcs/{slug}/prs/{pr}/resolution-branch" + ).status_code == 401 + ) + + +def test_anonymous_discussion_writes_refused(app_with_fake_gitea): + """The v0.5.0 PR-less discussion surface — write gates must hold. + This duplicates the assertion in `test_discussion_vertical.py` and + keeps it here too as the canonical home for the item #4 audit.""" + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + client.cookies.clear() + assert ( + client.post( + "/api/rfcs/ohm/discussion/threads", + json={"message": "drive-by"}, + ).status_code == 401 + ) + assert ( + client.post( + "/api/rfcs/ohm/discussion/threads/1/messages", + json={"text": "drive-by"}, + ).status_code == 401 + ) + assert ( + client.post( + "/api/rfcs/ohm/discussion/threads/1/resolve" + ).status_code == 401 + ) + + +def test_anonymous_admin_writes_refused(app_with_fake_gitea): + """Admin surfaces — role, mute, allowlist — refuse anonymous. + The auth check runs before the require_admin role check, so the + response is 401.""" + from fastapi.testclient import TestClient + + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + client.cookies.clear() + assert ( + client.post( + "/api/admin/users/1/role", json={"role": "admin"} + ).status_code == 401 + ) + assert ( + client.post( + "/api/admin/users/1/mute", json={"muted": True} + ).status_code == 401 + ) + assert ( + client.post( + "/api/admin/allowlist", json={"email": "x@y.z"} + ).status_code == 401 + ) + assert ( + client.delete("/api/admin/allowlist/x@y.z").status_code == 401 + ) + # Admin reads also gated. + assert client.get("/api/admin/users").status_code == 401 + assert client.get("/api/admin/audit").status_code == 401 + assert client.get("/api/admin/permission-events").status_code == 401 + assert client.get("/api/admin/graduation-queue").status_code == 401 + assert client.get("/api/admin/allowlist").status_code == 401 + + +def test_anonymous_notification_writes_refused(app_with_fake_gitea): + """Notification preference / watch / mark-read / user-mute writes — + all per-user surfaces, all require an authenticated viewer.""" + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + client.cookies.clear() + assert ( + client.post( + "/api/users/me/notification-preferences", + json={"email_personal_direct": False}, + ).status_code == 401 + ) + assert ( + client.post( + "/api/users/me/quiet-hours", + json={"start": None, "end": None, "timezone": None}, + ).status_code == 401 + ) + assert ( + client.post("/api/rfcs/ohm/watch", json={"state": "watching"}).status_code + == 401 + ) + assert client.post("/api/notifications/1/read").status_code == 401 + assert ( + client.post("/api/notifications/read", json={}).status_code == 401 + ) + assert client.post("/api/users/1/notification-mute").status_code == 401 + assert client.delete("/api/users/1/notification-mute").status_code == 401 + + +def test_anonymous_funder_writes_refused(app_with_fake_gitea): + """§6.7 funder credential + consent writes — registering a key, + consenting to fund — all refuse anonymous callers.""" + from fastapi.testclient import TestClient + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + client.cookies.clear() + assert ( + client.post( + "/api/users/me/funder/credentials", + json={"provider": "anthropic", "api_key": "sk-test"}, + ).status_code == 401 + ) + assert ( + client.delete( + "/api/users/me/funder/credentials/anthropic" + ).status_code == 401 + ) + assert ( + client.post("/api/rfcs/ohm/funder/consent").status_code == 401 + ) + assert ( + client.delete("/api/rfcs/ohm/funder/consent").status_code == 401 + ) + + +def test_anonymous_graduation_writes_refused(app_with_fake_gitea): + """§13 graduation: the POST kickoff and POST claim both refuse + anonymous. The progress SSE was gated to require_user in v0.6.0 + (item #4) since it surfaces admin-internal step detail.""" + from fastapi.testclient import TestClient + + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + client.cookies.clear() + assert ( + client.post( + "/api/rfcs/anything/graduate", + json={ + "rfc_id": "RFC-0001", + "repo_name": "rfc-0001-x", + "owners": ["alice"], + }, + ).status_code == 401 + ) + assert client.post("/api/rfcs/anything/claim").status_code == 401 + # v0.6.0 tightening: progress SSE now requires require_user. + # No graduation is in flight, but the auth check runs first. + assert ( + client.get("/api/rfcs/anything/graduate/progress").status_code == 401 + ) + + +def test_anonymous_can_read_published_pr_view(app_with_fake_gitea): + """The PR review page is §11.3 universal-public — once a PR is + open, anonymous viewers can read it. This guards against a + regression where the read endpoint accidentally grows an auth + gate.""" + from fastapi.testclient import TestClient + from app import db + + app, fake = app_with_fake_gitea + with TestClient(app) as client: + seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) + # Seed an open PR row directly — the cache shape is enough for + # the read endpoint; the live Gitea fetch falls back gracefully. + db.conn().execute( + """ + INSERT INTO cached_prs + (rfc_slug, pr_kind, repo, pr_number, title, description, state, + opened_by, opened_at, head_branch, base_branch, head_sha) + VALUES ('ohm', 'rfc_branch', 'wiggleverse/rfc-0001-ohm', 7, 't', 'd', + 'open', 'alice', datetime('now'), 'feature-x', 'main', 'sha7') + """ + ) + client.cookies.clear() + # Anonymous read on an open PR: should be 200. The endpoint may + # surface a partial response (the FakeGitea won't have the head + # branch's RFC.md, so branch_body falls back to empty) but the + # auth gate must let the read through. + r = client.get("/api/rfcs/ohm/prs/7") + assert r.status_code == 200 + body = r.json() + assert body["capabilities"]["is_anonymous"] is True + assert body["capabilities"]["can_merge"] is False + assert body["capabilities"]["can_post_review"] is False diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ee1899c..34e547c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "rfc-app-frontend", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rfc-app-frontend", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-markdown": "^6.5.0", diff --git a/frontend/package.json b/frontend/package.json index b68f1f4..2dc22f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.5.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite",