Close the gaps between the documented contract (API-STANDARD, TENANCY, ENTITLEMENTS) and the implementation, and remove committed build artifacts. Security: - verify + process inbound webhooks (HMAC/handler verify, size limit, timestamp tolerance, idempotency via InboundEvent.external_id) - real 2FA login flow (pre-auth challenge; tokens only after verify/recovery) - wire HybridPermission into security/audit views; add API-key rotate and users/<id>/permissions|roles endpoints - tenant-scoped throttling on by default; webhook replay protection - verify MercadoPago webhook signatures - login brute-force guard, configurable password policy, real ALTCHA PoW Correctness: - apply verified billing webhooks idempotently (subscription/entitlement/ invoice/PaymentTransaction); scheduled payment lifecycle jobs - capture audit update diffs automatically; add audit retention purge - working notification retries, per-channel rate limits, log retention - pluggable virus scanner, upload-size limit, pipeline toggle - feature rollout %/environment targeting; settings-driven registrations - workflow guards (instance cap, route depth, self-assignment, clone on re-entry) - wire every previously-dead INFRASYNTH_* setting; drop truly dead ones Delivery: - README + CHANGELOG; CI format check + coverage gate - keep test media out of the tree; untrack .coverage, __pycache__, egg-info, docs/ and invoice artifacts
154 lines
3.7 KiB
Python
154 lines
3.7 KiB
Python
import uuid
|
|
|
|
import pytest
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.hashers import make_password
|
|
from rest_framework.test import APIClient
|
|
|
|
from infrasynth.tenancy.models import Tenant, TenantMembership
|
|
|
|
UserModel = get_user_model()
|
|
|
|
|
|
@pytest.fixture
|
|
def tenant(db):
|
|
return Tenant.objects.create(slug=f"t-{uuid.uuid4().hex[:8]}", name="Test Workspace")
|
|
|
|
|
|
@pytest.fixture
|
|
def membership_factory(db):
|
|
def create_membership(user, tenant, role="member", is_owner=False, is_active=True):
|
|
return TenantMembership.objects.create(
|
|
tenant=tenant,
|
|
user=user,
|
|
role=role,
|
|
is_owner=is_owner,
|
|
is_active=is_active,
|
|
)
|
|
|
|
return create_membership
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def bind_tenant_context(tenant):
|
|
"""Binds ``current_tenant`` for the whole test so scoped managers work."""
|
|
from infrasynth.tenancy.context import tenant_context
|
|
|
|
with tenant_context(tenant):
|
|
yield tenant
|
|
|
|
|
|
@pytest.fixture
|
|
def api_client(db):
|
|
return APIClient()
|
|
|
|
|
|
@pytest.fixture
|
|
def user(db, tenant):
|
|
user = UserModel.objects.create_user(
|
|
username="testuser",
|
|
email="test@example.com",
|
|
password="testpass123",
|
|
)
|
|
TenantMembership.objects.create(tenant=tenant, user=user, role="owner", is_owner=True)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_user(db, tenant):
|
|
user = UserModel.objects.create_superuser(
|
|
username="admin",
|
|
email="admin@example.com",
|
|
password="adminpass123",
|
|
)
|
|
TenantMembership.objects.create(tenant=tenant, user=user, role="owner", is_owner=True)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def member_user(db, tenant):
|
|
"""A non-owner member of the current tenant (no implicit permissions)."""
|
|
user = UserModel.objects.create_user(
|
|
username="member",
|
|
email="member@example.com",
|
|
password="memberpass123",
|
|
)
|
|
TenantMembership.objects.create(tenant=tenant, user=user, role="member", is_owner=False)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def member_client(member_user, db):
|
|
client = APIClient()
|
|
client.force_authenticate(user=member_user)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def authenticated_client(user, db):
|
|
client = APIClient()
|
|
client.force_authenticate(user=user)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_client(admin_user, db):
|
|
client = APIClient()
|
|
client.force_authenticate(user=admin_user)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def user_factory():
|
|
def create_user(**kwargs):
|
|
defaults = {
|
|
"username": "factory_user",
|
|
"email": "factory@example.com",
|
|
"password": make_password("factorypass123"),
|
|
}
|
|
defaults.update(kwargs)
|
|
return UserModel.objects.create(**defaults)
|
|
|
|
return create_user
|
|
|
|
|
|
@pytest.fixture
|
|
def role_factory():
|
|
from infrasynth.security.models import Role
|
|
|
|
def create_role(**kwargs):
|
|
defaults = {
|
|
"name": "Test Role",
|
|
"slug": "test-role",
|
|
"permissions": [],
|
|
}
|
|
defaults.update(kwargs)
|
|
return Role.objects.create(**defaults)
|
|
|
|
return create_role
|
|
|
|
|
|
@pytest.fixture
|
|
def media_root(tmp_path, settings):
|
|
settings.MEDIA_ROOT = str(tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_feature_cache():
|
|
from django.core.cache import cache
|
|
|
|
cache.clear()
|
|
yield
|
|
cache.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_feature_registry():
|
|
"""Clears the global FeatureRegistry for the test, then restores it."""
|
|
from infrasynth.features.registry import FeatureRegistry
|
|
|
|
snapshot = dict(FeatureRegistry._features)
|
|
FeatureRegistry._features.clear()
|
|
yield
|
|
FeatureRegistry._features = snapshot
|