feat(admin): add subscription-status mutation route

Wires is_admin(), SUBSCRIPTION_STATUSES/get_current/set_status, and
log_action() into POST /admin/actions/subscription-status: validates
the requested status and that subscription_id matches the user's
current subscription, applies the change, and logs an audit entry.
Registers the admin blueprint in init_auth() and documents ADMIN_EMAIL
in .template.env.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-07-03 09:46:22 +02:00
parent 2f2cd151fb
commit e483d7af4d
5 changed files with 163 additions and 0 deletions
+38
View File
@@ -10,3 +10,41 @@ def users_db_path(monkeypatch, tmp_path):
reset_conn_for_tests()
yield db_path
reset_conn_for_tests()
@pytest.fixture
def admin_app(users_db_path, monkeypatch):
from flask import Flask
from src.auth.setup import init_auth
from src.subscriptions.setup import init_subscriptions
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050")
monkeypatch.setenv("FRISBII_PLAN_SIMPLE", "plan_simple")
monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien")
monkeypatch.setenv("FRISBII_WEBHOOK_SECRET", "s3cr3t")
flask_app = Flask(__name__)
flask_app.config["WTF_CSRF_ENABLED"] = False
init_auth(flask_app)
init_subscriptions(flask_app)
return flask_app
@pytest.fixture
def admin_client(admin_app):
return admin_app.test_client()
@pytest.fixture
def logged_in_admin_client(admin_app, monkeypatch):
from src.auth import db as auth_db
monkeypatch.setenv("ADMIN_EMAIL", "admin@ex.fr")
uid = auth_db.create_user("admin@ex.fr", "hash")
auth_db.set_email_verified(uid)
client = admin_app.test_client()
with client.session_transaction() as sess:
sess["_user_id"] = str(uid)
sess["_fresh"] = True
return client, uid
+76
View File
@@ -0,0 +1,76 @@
import itertools
from src.auth import db as auth_db
from src.subscriptions import db as sub_db
_email_counter = itertools.count(1)
def _make_target_with_subscription():
uid = auth_db.create_user(f"target{next(_email_counter)}@ex.fr", "hash")
_handle, sub_id = sub_db.create_pending(uid, "cust-1", "simple")
return uid, sub_id
def test_subscription_status_requires_admin(admin_client):
resp = admin_client.post(
"/admin/actions/subscription-status",
data={"user_id": "1", "subscription_id": "1", "status": "active"},
)
assert resp.status_code == 404
def test_subscription_status_rejects_invalid_status(logged_in_admin_client):
client, _admin_uid = logged_in_admin_client
uid, sub_id = _make_target_with_subscription()
resp = client.post(
"/admin/actions/subscription-status",
data={"user_id": str(uid), "subscription_id": str(sub_id), "status": "bogus"},
)
assert resp.status_code == 302
assert resp.headers["Location"] == f"/admin/user/{uid}?error=invalid_status"
assert sub_db.get_current(uid)["status"] == "pending"
def test_subscription_status_rejects_mismatched_subscription(logged_in_admin_client):
client, _admin_uid = logged_in_admin_client
uid, _sub_id = _make_target_with_subscription()
other_uid, other_sub_id = _make_target_with_subscription()
resp = client.post(
"/admin/actions/subscription-status",
data={
"user_id": str(uid),
"subscription_id": str(other_sub_id),
"status": "active",
},
)
assert resp.status_code == 302
assert resp.headers["Location"] == f"/admin/user/{uid}?error=invalid_status"
assert sub_db.get_current(other_uid)["status"] == "pending"
def test_subscription_status_success_updates_and_logs(logged_in_admin_client):
from src.admin.db import list_actions
client, _admin_uid = logged_in_admin_client
uid, sub_id = _make_target_with_subscription()
resp = client.post(
"/admin/actions/subscription-status",
data={"user_id": str(uid), "subscription_id": str(sub_id), "status": "active"},
)
assert resp.status_code == 302
assert resp.headers["Location"] == f"/admin/user/{uid}?status_changed=1"
assert sub_db.get_current(uid)["status"] == "active"
actions = list_actions()
assert len(actions) == 1
assert actions[0]["action"] == "subscription_status_change"
assert actions[0]["target_user_id"] == uid
assert actions[0]["details"] == "pending → active"
assert actions[0]["admin_email"] == "admin@ex.fr"