- infrasynth.configs: typed multi-tenant config store (registry, service, secrets, cache) + public config_changed/config_reset signals and API - emit the declared-but-dead signals (features flags/overrides, scheduler task_completed/task_failed, tenancy tenant_updated, audit model_changed) and per-model audit field exclusions - security: permission catalog (security_permission), Django-style model-derived AutoPermission, PermissionRegistry, RoleAssignment, global-or-tenant Grant/Revoke, catalog API - consolidate the permission surface: PermissionRegistry only (drop the settings dict), IsAuthenticatedAndPermitted aliases HybridPermission, require_permission replaced by required_permissions + require_all - packaging: add [build-system]; add Forgejo publish workflow (.forgejo)
165 lines
4 KiB
Python
165 lines
4 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
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_config_registry():
|
|
"""Clears the global ConfigRegistry for the test, then restores it."""
|
|
from infrasynth.configs.registry import ConfigRegistry
|
|
|
|
snapshot = dict(ConfigRegistry._definitions)
|
|
ConfigRegistry.clear()
|
|
yield
|
|
ConfigRegistry._definitions = snapshot
|