""" Tester för Tenant Management endpoints. """ import uuid from datetime import datetime, timezone import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.models import Tenant, TenantStatus, TenantType, AuditLog, AuditAction # ─── CREATE ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_tenant(client: AsyncClient, auth_headers: dict): response = await client.post( "/api/v1/tenants", json={ "name": "Nya Kommunen", "slug": "nya-kommunen", "tenant_type": "municipality", "contact_email": "kontakt@nyakommun.se", "city": "Göteborg", "country": "SE", "plan": "professional", "max_users": 50, }, headers=auth_headers, ) assert response.status_code == 201 data = response.json() assert data["name"] == "Nya Kommunen" assert data["slug"] == "nya-kommunen" assert data["status"] == "pending" assert data["schema_name"] == "tenant_nya-kommunen" assert data["plan"] == "professional" assert data["max_users"] == 50 @pytest.mark.asyncio async def test_create_tenant_duplicate_slug(client: AsyncClient, auth_headers: dict): # Skapa första await client.post( "/api/v1/tenants", json={ "name": "Duplicate Test", "slug": "duplicate-test", "tenant_type": "company", "contact_email": "a@test.se", }, headers=auth_headers, ) # Försök skapa andra med samma slug response = await client.post( "/api/v1/tenants", json={ "name": "Duplicate Test 2", "slug": "duplicate-test", "tenant_type": "company", "contact_email": "b@test.se", }, headers=auth_headers, ) assert response.status_code == 409 assert "already exists" in response.json()["detail"] @pytest.mark.asyncio async def test_create_tenant_invalid_org_number(client: AsyncClient, auth_headers: dict): response = await client.post( "/api/v1/tenants", json={ "name": "Bad Org", "slug": "bad-org", "tenant_type": "company", "contact_email": "test@bad.se", "org_number": "12345", # För kort }, headers=auth_headers, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_tenant_unauthorized(client: AsyncClient): response = await client.post( "/api/v1/tenants", json={ "name": "Unauthorized", "slug": "unauthorized", "tenant_type": "company", "contact_email": "test@unauth.se", }, ) assert response.status_code == 403 # ─── LIST ───────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_list_tenants(client: AsyncClient, auth_headers: dict): response = await client.get("/api/v1/tenants", headers=auth_headers) assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data assert "page" in data assert "pages" in data @pytest.mark.asyncio async def test_list_tenants_pagination(client: AsyncClient, auth_headers: dict): response = await client.get("/api/v1/tenants?page=1&page_size=5", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["page"] == 1 assert data["page_size"] == 5 @pytest.mark.asyncio async def test_list_tenants_filter_by_status(client: AsyncClient, auth_headers: dict): response = await client.get("/api/v1/tenants?status=active", headers=auth_headers) assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["status"] == "active" @pytest.mark.asyncio async def test_list_tenants_search(client: AsyncClient, auth_headers: dict): response = await client.get("/api/v1/tenants?search=stockholm", headers=auth_headers) assert response.status_code == 200 # ─── GET ONE ────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_tenant(client: AsyncClient, auth_headers: dict, test_tenant: Tenant): response = await client.get(f"/api/v1/tenants/{test_tenant.id}", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["id"] == str(test_tenant.id) assert data["name"] == test_tenant.name @pytest.mark.asyncio async def test_get_tenant_not_found(client: AsyncClient, auth_headers: dict): fake_id = uuid.uuid4() response = await client.get(f"/api/v1/tenants/{fake_id}", headers=auth_headers) assert response.status_code == 404 # ─── UPDATE ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_update_tenant(client: AsyncClient, auth_headers: dict, test_tenant: Tenant): response = await client.patch( f"/api/v1/tenants/{test_tenant.id}", json={"name": "Updated Name", "city": "Malmö"}, headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["name"] == "Updated Name" assert data["city"] == "Malmö" @pytest.mark.asyncio async def test_update_tenant_not_found(client: AsyncClient, auth_headers: dict): fake_id = uuid.uuid4() response = await client.patch( f"/api/v1/tenants/{fake_id}", json={"name": "Updated"}, headers=auth_headers, ) assert response.status_code == 404 # ─── DELETE ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_delete_tenant(client: AsyncClient, auth_headers: dict, db_session: AsyncSession): # Skapa en tenant att radera tenant = Tenant( id=uuid.uuid4(), name="Delete Me", slug="delete-me", tenant_type=TenantType.COMPANY, status=TenantStatus.ACTIVE, schema_name="tenant_delete_me", contact_email="delete@me.se", plan="basic", max_users=10, max_projects=5, storage_quota_mb=1024, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ) db_session.add(tenant) await db_session.commit() response = await client.delete(f"/api/v1/tenants/{tenant.id}", headers=auth_headers) assert response.status_code == 200 data = response.json() assert "deleted" in data["message"].lower() # Verifiera mjuk-radering await db_session.refresh(tenant) assert tenant.is_deleted == "Y" assert tenant.status == TenantStatus.CANCELLED @pytest.mark.asyncio async def test_hard_delete_tenant(client: AsyncClient, auth_headers: dict, db_session: AsyncSession): tenant = Tenant( id=uuid.uuid4(), name="Hard Delete Me", slug="hard-delete-me", tenant_type=TenantType.COMPANY, status=TenantStatus.ACTIVE, schema_name="tenant_hard_delete_me", contact_email="hard@delete.se", plan="basic", max_users=10, max_projects=5, storage_quota_mb=1024, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ) db_session.add(tenant) await db_session.commit() response = await client.delete(f"/api/v1/tenants/{tenant.id}/hard", headers=auth_headers) assert response.status_code == 200 # Verifiera permanent borttagning result = await db_session.get(Tenant, tenant.id) assert result is None # ─── STATUS MANAGEMENT ──────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_activate_tenant(client: AsyncClient, auth_headers: dict, db_session: AsyncSession): tenant = Tenant( id=uuid.uuid4(), name="Pending Tenant", slug="pending-tenant", tenant_type=TenantType.MUNICIPALITY, status=TenantStatus.PENDING, schema_name="tenant_pending_tenant", contact_email="pending@test.se", plan="basic", max_users=10, max_projects=5, storage_quota_mb=1024, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ) db_session.add(tenant) await db_session.commit() response = await client.post( f"/api/v1/tenants/{tenant.id}/activate", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["status"] == "active" assert data["activated_at"] is not None @pytest.mark.asyncio async def test_suspend_tenant(client: AsyncClient, auth_headers: dict, test_tenant: Tenant): response = await client.post( f"/api/v1/tenants/{test_tenant.id}/suspend", json={"reason": "Missed payment"}, headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["status"] == "suspended" assert data["suspended_reason"] == "Missed payment" @pytest.mark.asyncio async def test_cancel_tenant(client: AsyncClient, auth_headers: dict, test_tenant: Tenant): response = await client.post( f"/api/v1/tenants/{test_tenant.id}/cancel?reason=Customer request", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["status"] == "cancelled" assert data["cancellation_reason"] == "Customer request" # ─── STATS ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_tenant_stats(client: AsyncClient, auth_headers: dict): response = await client.get("/api/v1/tenants/stats", headers=auth_headers) assert response.status_code == 200 data = response.json() assert "total_tenants" in data assert "active_tenants" in data assert "by_type" in data assert "by_plan" in data # ─── AUDIT LOGS ─────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_tenant_audit_logs(client: AsyncClient, auth_headers: dict, test_tenant: Tenant): response = await client.get( f"/api/v1/tenants/{test_tenant.id}/audit-logs", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data # ─── VALIDATION ─────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_tenant_invalid_slug(client: AsyncClient, auth_headers: dict): response = await client.post( "/api/v1/tenants", json={ "name": "Bad Slug", "slug": "bad slug!", # Ogiltiga tecken "tenant_type": "company", "contact_email": "test@slug.se", }, headers=auth_headers, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_tenant_missing_required(client: AsyncClient, auth_headers: dict): response = await client.post( "/api/v1/tenants", json={ "slug": "missing-name", "tenant_type": "company", }, headers=auth_headers, ) assert response.status_code == 422