feat: add module definition completeness validation and permissions
Add new validation rules MOD-020 to MOD-023 for module definition completeness and standardize permissions across all modules. Changes: - Add MOD-020: Module definitions must have required attributes - Add MOD-021: Modules with menus should have features - Add MOD-022: Feature modules should have permissions - Add MOD-023: Modules with routers should use get_*_with_routers pattern Module permissions added: - analytics: view, export, manage_dashboards - billing: view_tiers, manage_tiers, view_subscriptions, manage_subscriptions, view_invoices - cart: view, manage - checkout: view_settings, manage_settings - cms: view_pages, manage_pages, view_media, manage_media, manage_themes - loyalty: view_programs, manage_programs, view_rewards, manage_rewards - marketplace: view_integration, manage_integration, sync_products - messaging: view_messages, send_messages, manage_templates - payments: view_gateways, manage_gateways, view_transactions Module improvements: - Complete cart module with features and permissions - Complete checkout module with features and permissions - Add features to catalog module - Add version to cms module - Fix loyalty platform_router attachment - Add path definitions to payments module - Remove empty scheduled_tasks from dev_tools module Documentation: - Update module-system.md with new validation rules - Update architecture-rules.md with MOD-020 to MOD-023 Tests: - Add unit tests for module definition completeness - Add tests for permission structure validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
285
tests/integration/api/v1/platform/test_pricing.py
Normal file
285
tests/integration/api/v1/platform/test_pricing.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# tests/integration/api/v1/public/test_pricing.py
|
||||
"""Integration tests for platform pricing API endpoints.
|
||||
|
||||
Tests the /api/v1/public/pricing/* endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.models import (
|
||||
AddOnProduct,
|
||||
SubscriptionTier,
|
||||
TierCode,
|
||||
TIER_LIMITS,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.platform
|
||||
class TestPlatformPricingAPI:
|
||||
"""Test platform pricing endpoints at /api/v1/public/*."""
|
||||
|
||||
# =========================================================================
|
||||
# GET /api/v1/public/tiers
|
||||
# =========================================================================
|
||||
|
||||
def test_get_tiers_returns_all_public_tiers(self, client):
|
||||
"""Test getting all subscription tiers."""
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 4 # Essential, Professional, Business, Enterprise
|
||||
|
||||
def test_get_tiers_has_expected_fields(self, client):
|
||||
"""Test that tier response has all expected fields."""
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) > 0
|
||||
|
||||
tier = data[0]
|
||||
assert "code" in tier
|
||||
assert "name" in tier
|
||||
assert "price_monthly" in tier
|
||||
assert "price_monthly_cents" in tier
|
||||
assert "orders_per_month" in tier
|
||||
assert "products_limit" in tier
|
||||
assert "team_members" in tier
|
||||
assert "features" in tier
|
||||
assert "is_popular" in tier
|
||||
assert "is_enterprise" in tier
|
||||
|
||||
def test_get_tiers_includes_essential(self, client):
|
||||
"""Test that Essential tier is included."""
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
tier_codes = [t["code"] for t in data]
|
||||
assert TierCode.ESSENTIAL.value in tier_codes
|
||||
|
||||
def test_get_tiers_includes_professional(self, client):
|
||||
"""Test that Professional tier is included and marked as popular."""
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
professional = next(
|
||||
(t for t in data if t["code"] == TierCode.PROFESSIONAL.value), None
|
||||
)
|
||||
assert professional is not None
|
||||
assert professional["is_popular"] is True
|
||||
|
||||
def test_get_tiers_includes_enterprise(self, client):
|
||||
"""Test that Enterprise tier is included and marked appropriately."""
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
enterprise = next(
|
||||
(t for t in data if t["code"] == TierCode.ENTERPRISE.value), None
|
||||
)
|
||||
assert enterprise is not None
|
||||
assert enterprise["is_enterprise"] is True
|
||||
|
||||
def test_get_tiers_from_database(self, client, db):
|
||||
"""Test getting tiers from database when available."""
|
||||
# Create a tier in the database
|
||||
tier = SubscriptionTier(
|
||||
code="test_tier",
|
||||
name="Test Tier",
|
||||
description="A test tier",
|
||||
price_monthly_cents=9900,
|
||||
price_annual_cents=99000,
|
||||
orders_per_month=1000,
|
||||
products_limit=500,
|
||||
team_members=5,
|
||||
features=["feature1", "feature2"],
|
||||
is_active=True,
|
||||
is_public=True,
|
||||
display_order=99,
|
||||
)
|
||||
db.add(tier)
|
||||
db.commit()
|
||||
|
||||
response = client.get("/api/v1/public/tiers")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
tier_codes = [t["code"] for t in data]
|
||||
assert "test_tier" in tier_codes
|
||||
|
||||
# =========================================================================
|
||||
# GET /api/v1/public/tiers/{tier_code}
|
||||
# =========================================================================
|
||||
|
||||
def test_get_tier_by_code_success(self, client):
|
||||
"""Test getting a specific tier by code."""
|
||||
response = client.get(f"/api/v1/public/tiers/{TierCode.PROFESSIONAL.value}")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == TierCode.PROFESSIONAL.value
|
||||
assert data["name"] == TIER_LIMITS[TierCode.PROFESSIONAL]["name"]
|
||||
|
||||
def test_get_tier_by_code_essential(self, client):
|
||||
"""Test getting Essential tier details."""
|
||||
response = client.get(f"/api/v1/public/tiers/{TierCode.ESSENTIAL.value}")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == TierCode.ESSENTIAL.value
|
||||
assert data["price_monthly"] == TIER_LIMITS[TierCode.ESSENTIAL]["price_monthly_cents"] / 100
|
||||
|
||||
def test_get_tier_by_code_not_found(self, client):
|
||||
"""Test getting a non-existent tier returns 404."""
|
||||
response = client.get("/api/v1/public/tiers/nonexistent_tier")
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["message"].lower()
|
||||
|
||||
# =========================================================================
|
||||
# GET /api/v1/public/addons
|
||||
# =========================================================================
|
||||
|
||||
def test_get_addons_empty_when_none_configured(self, client):
|
||||
"""Test getting add-ons when none are configured."""
|
||||
response = client.get("/api/v1/public/addons")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_get_addons_returns_configured_addons(self, client, db):
|
||||
"""Test getting add-ons when configured in database."""
|
||||
# Create test add-on
|
||||
addon = AddOnProduct(
|
||||
code="test_domain",
|
||||
name="Custom Domain",
|
||||
description="Use your own domain",
|
||||
category="domain",
|
||||
price_cents=1500,
|
||||
billing_period="annual",
|
||||
is_active=True,
|
||||
display_order=1,
|
||||
)
|
||||
db.add(addon)
|
||||
db.commit()
|
||||
|
||||
response = client.get("/api/v1/public/addons")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
|
||||
addon_codes = [a["code"] for a in data]
|
||||
assert "test_domain" in addon_codes
|
||||
|
||||
def test_get_addons_has_expected_fields(self, client, db):
|
||||
"""Test that addon response has all expected fields."""
|
||||
addon = AddOnProduct(
|
||||
code="test_ssl",
|
||||
name="Premium SSL",
|
||||
description="EV certificate",
|
||||
category="security",
|
||||
price_cents=4900,
|
||||
billing_period="annual",
|
||||
is_active=True,
|
||||
display_order=1,
|
||||
)
|
||||
db.add(addon)
|
||||
db.commit()
|
||||
|
||||
response = client.get("/api/v1/public/addons")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) > 0
|
||||
|
||||
addon_response = data[0]
|
||||
assert "code" in addon_response
|
||||
assert "name" in addon_response
|
||||
assert "description" in addon_response
|
||||
assert "category" in addon_response
|
||||
assert "price" in addon_response
|
||||
assert "price_cents" in addon_response
|
||||
assert "billing_period" in addon_response
|
||||
|
||||
def test_get_addons_excludes_inactive(self, client, db):
|
||||
"""Test that inactive add-ons are excluded."""
|
||||
# Create active and inactive add-ons
|
||||
active_addon = AddOnProduct(
|
||||
code="active_addon",
|
||||
name="Active Addon",
|
||||
category="test",
|
||||
price_cents=1000,
|
||||
billing_period="monthly",
|
||||
is_active=True,
|
||||
display_order=1,
|
||||
)
|
||||
inactive_addon = AddOnProduct(
|
||||
code="inactive_addon",
|
||||
name="Inactive Addon",
|
||||
category="test",
|
||||
price_cents=1000,
|
||||
billing_period="monthly",
|
||||
is_active=False,
|
||||
display_order=2,
|
||||
)
|
||||
db.add_all([active_addon, inactive_addon])
|
||||
db.commit()
|
||||
|
||||
response = client.get("/api/v1/public/addons")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
addon_codes = [a["code"] for a in data]
|
||||
assert "active_addon" in addon_codes
|
||||
assert "inactive_addon" not in addon_codes
|
||||
|
||||
# =========================================================================
|
||||
# GET /api/v1/public/pricing
|
||||
# =========================================================================
|
||||
|
||||
def test_get_pricing_returns_complete_info(self, client):
|
||||
"""Test getting complete pricing information."""
|
||||
response = client.get("/api/v1/public/pricing")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
assert "tiers" in data
|
||||
assert "addons" in data
|
||||
assert "trial_days" in data
|
||||
assert "annual_discount_months" in data
|
||||
|
||||
def test_get_pricing_includes_trial_days(self, client):
|
||||
"""Test that pricing includes correct trial period."""
|
||||
response = client.get("/api/v1/public/pricing")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["trial_days"] == 30 # Updated from 14 to 30
|
||||
|
||||
def test_get_pricing_includes_annual_discount(self, client):
|
||||
"""Test that pricing includes annual discount info."""
|
||||
response = client.get("/api/v1/public/pricing")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["annual_discount_months"] == 2 # 2 months free
|
||||
|
||||
def test_get_pricing_tiers_not_empty(self, client):
|
||||
"""Test that pricing always includes tiers."""
|
||||
response = client.get("/api/v1/public/pricing")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["tiers"]) >= 4
|
||||
Reference in New Issue
Block a user