Files
boc/landvex-admin-backend/tests/test_tenants.py
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

355 lines
12 KiB
Python

"""
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