feat: wire license.effective_tier into tiers.py; add dev_override priority
This commit is contained in:
parent
bf2d0f81c7
commit
58ebd57c49
3 changed files with 99 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,3 +29,4 @@ scrapers/.debug/
|
||||||
scrapers/raw_scrapes/
|
scrapers/raw_scrapes/
|
||||||
|
|
||||||
compose.override.yml
|
compose.override.yml
|
||||||
|
config/license.json
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,32 @@ def tier_label(feature: str) -> str:
|
||||||
if required is None:
|
if required is None:
|
||||||
return ""
|
return ""
|
||||||
return "🔒 Paid" if required == "paid" else "⭐ Premium"
|
return "🔒 Paid" if required == "paid" else "⭐ Premium"
|
||||||
|
|
||||||
|
|
||||||
|
def effective_tier(
|
||||||
|
profile=None,
|
||||||
|
license_path=None,
|
||||||
|
public_key_path=None,
|
||||||
|
) -> str:
|
||||||
|
"""Return the effective tier for this installation.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. profile.dev_tier_override (developer mode override)
|
||||||
|
2. License JWT verification (offline RS256 check)
|
||||||
|
3. "free" (fallback)
|
||||||
|
|
||||||
|
license_path and public_key_path default to production paths when None.
|
||||||
|
Pass explicit paths in tests to avoid touching real files.
|
||||||
|
"""
|
||||||
|
if profile and getattr(profile, "dev_tier_override", None):
|
||||||
|
return profile.dev_tier_override
|
||||||
|
|
||||||
|
from scripts.license import effective_tier as _license_tier
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
if license_path is not None:
|
||||||
|
kwargs["license_path"] = _Path(license_path)
|
||||||
|
if public_key_path is not None:
|
||||||
|
kwargs["public_key_path"] = _Path(public_key_path)
|
||||||
|
return _license_tier(**kwargs)
|
||||||
|
|
|
||||||
69
tests/test_license_tier_integration.py
Normal file
69
tests/test_license_tier_integration.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from unittest.mock import patch
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
import jwt as pyjwt
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def license_env(tmp_path):
|
||||||
|
"""Returns (private_pem, public_path, license_path) for tier integration tests."""
|
||||||
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
private_pem = private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
public_pem = private_key.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
public_path = tmp_path / "public.pem"
|
||||||
|
public_path.write_bytes(public_pem)
|
||||||
|
license_path = tmp_path / "license.json"
|
||||||
|
return private_pem, public_path, license_path
|
||||||
|
|
||||||
|
|
||||||
|
def _write_jwt_license(license_path, private_pem, tier="paid", days=30):
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
token = pyjwt.encode({
|
||||||
|
"sub": "CFG-PRNG-TEST", "product": "peregrine", "tier": tier,
|
||||||
|
"iat": now, "exp": now + timedelta(days=days),
|
||||||
|
}, private_pem, algorithm="RS256")
|
||||||
|
license_path.write_text(json.dumps({"jwt": token, "grace_until": None}))
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_tier_free_without_license(tmp_path):
|
||||||
|
from app.wizard.tiers import effective_tier
|
||||||
|
tier = effective_tier(
|
||||||
|
profile=None,
|
||||||
|
license_path=tmp_path / "missing.json",
|
||||||
|
public_key_path=tmp_path / "key.pem",
|
||||||
|
)
|
||||||
|
assert tier == "free"
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_tier_paid_with_valid_license(license_env):
|
||||||
|
private_pem, public_path, license_path = license_env
|
||||||
|
_write_jwt_license(license_path, private_pem, tier="paid")
|
||||||
|
from app.wizard.tiers import effective_tier
|
||||||
|
tier = effective_tier(profile=None, license_path=license_path,
|
||||||
|
public_key_path=public_path)
|
||||||
|
assert tier == "paid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_tier_dev_override_takes_precedence(license_env):
|
||||||
|
"""dev_tier_override wins even when a valid license is present."""
|
||||||
|
private_pem, public_path, license_path = license_env
|
||||||
|
_write_jwt_license(license_path, private_pem, tier="paid")
|
||||||
|
|
||||||
|
class FakeProfile:
|
||||||
|
dev_tier_override = "premium"
|
||||||
|
|
||||||
|
from app.wizard.tiers import effective_tier
|
||||||
|
tier = effective_tier(profile=FakeProfile(), license_path=license_path,
|
||||||
|
public_key_path=public_path)
|
||||||
|
assert tier == "premium"
|
||||||
Loading…
Reference in a new issue