aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
355 lines
12 KiB
Python
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
|