Gate blocked actions behind an in-place terms acceptance dialog

A member whose account has not accepted the terms in force now gets one
dialog on the action they attempted instead of a dead-end refusal. The
client handler is the single TermsGate, wired into every Http POST helper
so the four optimistic controllers cannot swallow the gate into an error
flash, and the original request is replayed once the acceptance is
recorded. Reading the site and deleting an account stay unblocked.

apple.md is the source brief the compliance research documents reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:25:57 +02:00
co-authored by Claude Opus 5
parent 8e9d3fad98
commit 91fac7fd67
38 changed files with 393 additions and 58 deletions
+51
View File
@@ -5,6 +5,7 @@ import time
import requests
from devplacepy.database import get_setting, get_table, refresh_snapshot, set_setting
from devplacepy.routers.auth.terms import TERMS_ACCEPTANCE_CODE
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
@@ -152,3 +153,53 @@ def test_bumping_the_version_gates_writes_but_never_reads(app_server):
assert allowed.status_code == 200
finally:
set_setting("terms_version", original)
def test_the_refusal_tells_a_fetch_client_how_to_resolve_it(app_server):
session, _, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
bumped = f"{original}-contract"
set_setting("terms_version", bumped)
try:
blocked = None
for _ in range(40):
blocked = session.post(
f"{BASE_URL}/posts/create",
data={"title": "gated", "content": "This write must be gated."},
headers=JSON,
)
if blocked.status_code == 403:
break
time.sleep(0.5)
assert blocked.status_code == 403
error = blocked.json()["error"]
assert error["code"] == TERMS_ACCEPTANCE_CODE
assert error["redirect"] == "/auth/accept-terms"
assert error["terms_version"] == bumped
assert error["message"]
finally:
set_setting("terms_version", original)
def test_a_browser_form_post_is_redirected_to_the_acceptance_page(app_server):
session, _, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
set_setting("terms_version", f"{original}-redirect")
try:
blocked = None
for _ in range(40):
blocked = session.post(
f"{BASE_URL}/posts/create",
data={"title": "gated", "content": "This write must be gated."},
headers={"Accept": "text/html"},
allow_redirects=False,
)
if blocked.status_code == 303:
break
time.sleep(0.5)
assert blocked.status_code == 303
assert blocked.headers["location"] == "/auth/accept-terms"
finally:
set_setting("terms_version", original)