Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Failing after 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a new environment variable `CLUB_FEATURE_ENFORCE` to control club feature access, allowing values of 1, true, or yes for activation. - Updated the backend logic to check for club feature enforcement, raising HTTP exceptions when access is denied without an active club context. - Enhanced the admin rights router with a new endpoint to check the enforcement status of club features. - Incremented application version to 0.8.202 to reflect these changes.
226 lines
6.7 KiB
Python
226 lines
6.7 KiB
Python
"""M5: ai_calls Verbrauch + Hard-Block (CLUB_FEATURE_ENFORCE)."""
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from club_features import (
|
|
club_feature_enforcement_enabled,
|
|
consume_club_feature,
|
|
consume_club_feature_with_usage,
|
|
merge_feature_usage_into_response,
|
|
probe_club_feature_access,
|
|
)
|
|
|
|
|
|
def _fake_cur():
|
|
class C:
|
|
def execute(self, *a, **k):
|
|
pass
|
|
|
|
def fetchone(self):
|
|
return None
|
|
|
|
return C()
|
|
|
|
|
|
def test_probe_blocks_when_enforce_and_limit_exceeded(monkeypatch):
|
|
monkeypatch.setenv("CLUB_FEATURE_ENFORCE", "1")
|
|
monkeypatch.setattr("club_features.get_cursor", lambda conn: _fake_cur())
|
|
monkeypatch.setattr(
|
|
"club_quota_bypass.is_club_feature_quota_bypassed",
|
|
lambda *a, **k: False,
|
|
)
|
|
monkeypatch.setattr(
|
|
"club_features.check_club_feature_access",
|
|
lambda club_id, feature_id, conn=None: {
|
|
"allowed": False,
|
|
"limit": 0,
|
|
"used": 0,
|
|
"remaining": 0,
|
|
"reason": "feature_disabled",
|
|
"plan_id": "free",
|
|
},
|
|
)
|
|
monkeypatch.setattr("club_feature_logger.log_club_feature_usage", lambda **kwargs: None)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
probe_club_feature_access(
|
|
feature_id="ai_calls",
|
|
action="suggest",
|
|
club_id=12,
|
|
profile_id=3,
|
|
endpoint="POST /exercises/ai/suggest",
|
|
conn=object(),
|
|
)
|
|
assert exc.value.status_code == 403
|
|
assert "ai_calls" in str(exc.value.detail)
|
|
|
|
|
|
def test_probe_allows_when_enforce_off(monkeypatch):
|
|
monkeypatch.setenv("CLUB_FEATURE_ENFORCE", "0")
|
|
monkeypatch.setattr("club_features.get_cursor", lambda conn: _fake_cur())
|
|
monkeypatch.setattr(
|
|
"club_quota_bypass.is_club_feature_quota_bypassed",
|
|
lambda *a, **k: False,
|
|
)
|
|
monkeypatch.setattr(
|
|
"club_features.check_club_feature_access",
|
|
lambda club_id, feature_id, conn=None: {
|
|
"allowed": False,
|
|
"limit": 0,
|
|
"used": 0,
|
|
"remaining": 0,
|
|
"reason": "feature_disabled",
|
|
"plan_id": "free",
|
|
},
|
|
)
|
|
monkeypatch.setattr("club_feature_logger.log_club_feature_usage", lambda **kwargs: None)
|
|
|
|
access = probe_club_feature_access(
|
|
feature_id="ai_calls",
|
|
action="suggest",
|
|
club_id=12,
|
|
profile_id=3,
|
|
conn=object(),
|
|
)
|
|
assert access["allowed"] is False
|
|
|
|
|
|
def test_consume_skips_without_club_id(monkeypatch):
|
|
calls = []
|
|
|
|
def _inc(*args, **kwargs):
|
|
calls.append(1)
|
|
|
|
monkeypatch.setattr("club_features.increment_club_feature_usage", _inc)
|
|
consume_club_feature(feature_id="ai_calls", club_id=None, profile_id=1)
|
|
assert calls == []
|
|
|
|
|
|
def test_consume_logs_usage_after_increment(monkeypatch):
|
|
logs = []
|
|
|
|
monkeypatch.setattr("club_features.get_cursor", lambda conn: _fake_cur())
|
|
monkeypatch.setattr(
|
|
"club_quota_bypass.is_club_feature_quota_bypassed",
|
|
lambda *a, **k: False,
|
|
)
|
|
monkeypatch.setattr("club_features.increment_club_feature_usage", lambda *a, **k: None)
|
|
monkeypatch.setattr(
|
|
"club_features.check_club_feature_access",
|
|
lambda club_id, feature_id, conn=None: {
|
|
"allowed": True,
|
|
"used": 1,
|
|
"limit": 30,
|
|
"remaining": 29,
|
|
"plan_id": "club",
|
|
"reason": "within_limit",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"club_feature_logger.log_club_feature_usage",
|
|
lambda **kwargs: logs.append(kwargs),
|
|
)
|
|
|
|
consume_club_feature(
|
|
feature_id="ai_calls",
|
|
club_id=5,
|
|
profile_id=9,
|
|
portal_role="trainer",
|
|
action="suggest",
|
|
conn=object(),
|
|
)
|
|
assert len(logs) == 1
|
|
assert logs[0]["phase"] == "consume"
|
|
assert logs[0]["feature_id"] == "ai_calls"
|
|
assert logs[0]["club_id"] == 5
|
|
|
|
|
|
def test_consume_increments_once_per_call(monkeypatch):
|
|
calls = []
|
|
|
|
def _inc(club_id, feature_id, **kwargs):
|
|
calls.append((club_id, feature_id, kwargs.get("action")))
|
|
|
|
monkeypatch.setattr("club_features.get_cursor", lambda conn: _fake_cur())
|
|
monkeypatch.setattr(
|
|
"club_quota_bypass.is_club_feature_quota_bypassed",
|
|
lambda *a, **k: False,
|
|
)
|
|
monkeypatch.setattr("club_features.increment_club_feature_usage", _inc)
|
|
monkeypatch.setattr(
|
|
"club_features.check_club_feature_access",
|
|
lambda club_id, feature_id, conn=None: {
|
|
"allowed": True,
|
|
"used": 1,
|
|
"limit": 30,
|
|
"remaining": 29,
|
|
"plan_id": "club",
|
|
"reason": "within_limit",
|
|
},
|
|
)
|
|
monkeypatch.setattr("club_feature_logger.log_club_feature_usage", lambda **kwargs: None)
|
|
consume_club_feature(
|
|
feature_id="ai_calls",
|
|
club_id=5,
|
|
profile_id=9,
|
|
portal_role="trainer",
|
|
action="suggest",
|
|
conn=object(),
|
|
)
|
|
assert calls == [(5, "ai_calls", "suggest")]
|
|
|
|
|
|
def test_merge_feature_usage_into_response():
|
|
out = merge_feature_usage_into_response(
|
|
{"ok": True},
|
|
{"ai_calls": {"used": 3, "limit": 30}},
|
|
)
|
|
assert out["ok"] is True
|
|
assert out["feature_usage"]["ai_calls"]["used"] == 3
|
|
assert merge_feature_usage_into_response({"x": 1}, None) == {"x": 1}
|
|
|
|
|
|
def test_consume_with_usage_returns_snapshot(monkeypatch):
|
|
monkeypatch.setattr("club_features.get_cursor", lambda conn: _fake_cur())
|
|
monkeypatch.setattr(
|
|
"club_quota_bypass.is_club_feature_quota_bypassed",
|
|
lambda *a, **k: False,
|
|
)
|
|
monkeypatch.setattr("club_features.consume_club_feature", lambda **kwargs: None)
|
|
monkeypatch.setattr(
|
|
"club_features.club_feature_usage_for_api",
|
|
lambda cur, **kwargs: {"used": 4, "limit": 30, "allowed": True},
|
|
)
|
|
|
|
usage = consume_club_feature_with_usage(
|
|
feature_id="ai_calls",
|
|
club_id=7,
|
|
profile_id=1,
|
|
portal_role="trainer",
|
|
action="suggest",
|
|
cur=_fake_cur(),
|
|
conn=object(),
|
|
)
|
|
assert usage["ai_calls"]["used"] == 4
|
|
|
|
|
|
def test_probe_blocks_no_club_context_when_enforce(monkeypatch):
|
|
monkeypatch.setenv("CLUB_FEATURE_ENFORCE", "1")
|
|
monkeypatch.setattr("club_feature_logger.log_club_feature_usage", lambda **kwargs: None)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
probe_club_feature_access(
|
|
feature_id="ai_calls",
|
|
action="suggest",
|
|
club_id=None,
|
|
profile_id=3,
|
|
endpoint="POST /exercises/ai/suggest",
|
|
)
|
|
assert exc.value.status_code == 403
|
|
assert "Vereinskontext" in str(exc.value.detail)
|
|
|
|
|
|
def test_club_feature_enforcement_env_default_off(monkeypatch):
|
|
monkeypatch.delenv("CLUB_FEATURE_ENFORCE", raising=False)
|
|
assert club_feature_enforcement_enabled() is False
|