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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Pytest fixtures för LandveX Admin Backend.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/landvex_test")
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-landvex-2026")
|
||||
|
||||
from app.main import app
|
||||
from app.database import Base, get_db
|
||||
from app.models import Tenant, User, UserRole, UserStatus, TenantType, TenantStatus
|
||||
from app.security import get_password_hash
|
||||
|
||||
|
||||
TEST_DATABASE_URL = os.environ["DATABASE_URL"]
|
||||
|
||||
engine = create_async_engine(
|
||||
TEST_DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
future=True,
|
||||
)
|
||||
|
||||
TestSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
async with TestSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
def event_loop():
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session", autouse=True)
|
||||
async def setup_database():
|
||||
"""Skapa och tömma test-databasen."""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_session():
|
||||
"""Skapa en färsk session per test."""
|
||||
async with TestSessionLocal() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client():
|
||||
"""Async HTTP client för API-tester."""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_tenant(db_session: AsyncSession):
|
||||
"""Skapa en test-tenant."""
|
||||
tenant = Tenant(
|
||||
id=uuid.uuid4(),
|
||||
name="Test Municipality",
|
||||
slug="test-municipality",
|
||||
tenant_type=TenantType.MUNICIPALITY,
|
||||
status=TenantStatus.ACTIVE,
|
||||
schema_name="tenant_test_municipality",
|
||||
contact_email="test@kommun.se",
|
||||
city="Stockholm",
|
||||
country="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()
|
||||
await db_session.refresh(tenant)
|
||||
return tenant
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def admin_user(db_session: AsyncSession, test_tenant):
|
||||
"""Skapa en admin-användare."""
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="admin@landvex.se",
|
||||
hashed_password=get_password_hash("Admin123!"),
|
||||
first_name="Admin",
|
||||
last_name="User",
|
||||
role=UserRole.ADMIN,
|
||||
status=UserStatus.ACTIVE,
|
||||
is_superuser=True,
|
||||
tenant_id=test_tenant.id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def admin_token(admin_user):
|
||||
"""Generera JWT-token för admin."""
|
||||
from app.security import create_access_token
|
||||
return create_access_token(str(admin_user.id))
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth_headers(admin_token):
|
||||
"""Headers med Bearer-token för autentiserade requests."""
|
||||
return {"Authorization": f"Bearer {admin_token}"}
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Tester för Audit Log endpoints.
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import AuditLog, AuditAction
|
||||
from app.services.audit_service import AuditService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_audit_logs(client: AsyncClient, auth_headers: dict, db_session: AsyncSession):
|
||||
# Skapa några audit-loggar
|
||||
audit = AuditService(db_session)
|
||||
await audit.log(
|
||||
action=AuditAction.CREATE,
|
||||
entity_type="tenant",
|
||||
entity_id="test-tenant-1",
|
||||
actor_id="admin-user",
|
||||
new_values={"name": "Test Tenant"},
|
||||
)
|
||||
await audit.log(
|
||||
action=AuditAction.UPDATE,
|
||||
entity_type="tenant",
|
||||
entity_id="test-tenant-1",
|
||||
actor_id="admin-user",
|
||||
changed_fields=["name"],
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get("/api/v1/audit-logs", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "total" in data
|
||||
assert len(data["items"]) >= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_audit_logs_filtered_by_entity(
|
||||
client: AsyncClient, auth_headers: dict, db_session: AsyncSession
|
||||
):
|
||||
audit = AuditService(db_session)
|
||||
await audit.log(
|
||||
action=AuditAction.CREATE,
|
||||
entity_type="user",
|
||||
entity_id="user-123",
|
||||
actor_id="admin-user",
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/audit-logs?entity_type=user&entity_id=user-123",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
for item in data["items"]:
|
||||
assert item["entity_type"] == "user"
|
||||
assert item["entity_id"] == "user-123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_entity_audit_logs(client: AsyncClient, auth_headers: dict, db_session: AsyncSession):
|
||||
audit = AuditService(db_session)
|
||||
await audit.log(
|
||||
action=AuditAction.CREATE,
|
||||
entity_type="tenant",
|
||||
entity_id="entity-test-1",
|
||||
actor_id="admin-user",
|
||||
new_values={"name": "Entity Test"},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/audit-logs/entity/tenant/entity-test-1",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) >= 1
|
||||
assert data["items"][0]["entity_type"] == "tenant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_actor_audit_logs(client: AsyncClient, auth_headers: dict, db_session: AsyncSession):
|
||||
audit = AuditService(db_session)
|
||||
await audit.log(
|
||||
action=AuditAction.LOGIN,
|
||||
entity_type="session",
|
||||
entity_id="session-1",
|
||||
actor_id="test-actor-1",
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/audit-logs/actor/test-actor-1",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
for item in data["items"]:
|
||||
assert item["actor_id"] == "test-actor-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audit_log_sanitization(db_session: AsyncSession):
|
||||
"""Testa att känslig data saniteras i audit-loggar."""
|
||||
audit = AuditService(db_session)
|
||||
|
||||
log = await audit.log(
|
||||
action=AuditAction.CREATE,
|
||||
entity_type="user",
|
||||
entity_id="user-1",
|
||||
request_body='{"email": "test@test.se", "password": "supersecret123", "api_key": "abc123"}',
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
assert "supersecret123" not in (log.request_body or "")
|
||||
assert "***REDACTED***" in (log.request_body or "")
|
||||
assert "abc123" not in (log.request_body or "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audit_log_unauthorized(client: AsyncClient):
|
||||
response = await client.get("/api/v1/audit-logs")
|
||||
assert response.status_code == 403
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Tester för auth-endpoints."""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.models import User
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(client: AsyncClient, test_user: User):
|
||||
response = await client.post("/api/v1/auth/login", json={
|
||||
"email": test_user.email,
|
||||
"password": "password123",
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password(client: AsyncClient, test_user: User):
|
||||
response = await client.post("/api/v1/auth/login", json={
|
||||
"email": test_user.email,
|
||||
"password": "wrongpassword",
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_me_endpoint(client: AsyncClient, admin_token: str, test_user: User):
|
||||
response = await client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
assert data["role"] == "admin"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token(client: AsyncClient, admin_token: str):
|
||||
# Hämta refresh token via login
|
||||
response = await client.post("/api/v1/auth/login", json={
|
||||
"email": "admin@landvex.se",
|
||||
"password": "password123",
|
||||
})
|
||||
refresh = response.json()["refresh_token"]
|
||||
|
||||
response = await client.post("/api/v1/auth/refresh", json={"refresh_token": refresh})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Tester för invitation-flödet."""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.models import User
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invitation(client: AsyncClient, admin_token: str):
|
||||
response = await client.post("/api/v1/invitations", headers={"Authorization": f"Bearer {admin_token}"}, json={
|
||||
"email": "invited@landvex.se",
|
||||
"role": "viewer",
|
||||
})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["email"] == "invited@landvex.se"
|
||||
assert data["role"] == "viewer"
|
||||
assert "token" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_invitation(client: AsyncClient, admin_token: str):
|
||||
# Skapa inbjudan
|
||||
response = await client.post("/api/v1/invitations", headers={"Authorization": f"Bearer {admin_token}"}, json={
|
||||
"email": "validate@landvex.se",
|
||||
"role": "analyst",
|
||||
})
|
||||
token = response.json()["token"]
|
||||
|
||||
response = await client.get(f"/api/v1/invitations/validate/{token}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["valid"] is True
|
||||
assert data["email"] == "validate@landvex.se"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_invitation(client: AsyncClient, admin_token: str):
|
||||
# Skapa inbjudan
|
||||
response = await client.post("/api/v1/invitations", headers={"Authorization": f"Bearer {admin_token}"}, json={
|
||||
"email": "accept@landvex.se",
|
||||
"role": "manager",
|
||||
})
|
||||
token = response.json()["token"]
|
||||
|
||||
response = await client.post("/api/v1/invitations/accept", json={
|
||||
"token": token,
|
||||
"first_name": "Accepted",
|
||||
"last_name": "User",
|
||||
"password": "newpassword123",
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["email"] == "accept@landvex.se"
|
||||
assert data["role"] == "manager"
|
||||
assert data["status"] == "active"
|
||||
|
||||
# Försök acceptera igen — ska misslyckas
|
||||
response = await client.post("/api/v1/invitations/accept", json={
|
||||
"token": token,
|
||||
"first_name": "Accepted",
|
||||
"last_name": "User",
|
||||
"password": "newpassword123",
|
||||
})
|
||||
assert response.status_code == 400
|
||||
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tester för user CRUD."""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.models import User, Tenant
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user(client: AsyncClient, admin_token: str, test_tenant: Tenant):
|
||||
response = await client.post("/api/v1/users", headers={"Authorization": f"Bearer {admin_token}"}, json={
|
||||
"email": "newuser@landvex.se",
|
||||
"first_name": "New",
|
||||
"last_name": "User",
|
||||
"password": "securepass123",
|
||||
"role": "analyst",
|
||||
"tenant_id": str(test_tenant.id),
|
||||
})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["email"] == "newuser@landvex.se"
|
||||
assert data["role"] == "analyst"
|
||||
assert data["status"] == "active"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users(client: AsyncClient, admin_token: str):
|
||||
response = await client.get("/api/v1/users", headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "total" in data
|
||||
assert len(data["items"]) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user(client: AsyncClient, admin_token: str, test_user: User):
|
||||
response = await client.get(f"/api/v1/users/{test_user.id}", headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user(client: AsyncClient, admin_token: str, test_user: User):
|
||||
response = await client.patch(f"/api/v1/users/{test_user.id}", headers={"Authorization": f"Bearer {admin_token}"}, json={
|
||||
"first_name": "Updated",
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["first_name"] == "Updated"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user(client: AsyncClient, admin_token: str, test_tenant: Tenant, db_session):
|
||||
# Skapa en användare att ta bort
|
||||
from app.models import User, UserRole, UserStatus
|
||||
from app.security import get_password_hash
|
||||
import uuid
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="delete-me@landvex.se",
|
||||
first_name="Delete",
|
||||
last_name="Me",
|
||||
hashed_password=get_password_hash("password123"),
|
||||
role=UserRole.VIEWER,
|
||||
status=UserStatus.ACTIVE,
|
||||
tenant_id=test_tenant.id,
|
||||
)
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.delete(f"/api/v1/users/{user.id}", headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 204
|
||||
Reference in New Issue
Block a user