Files
timemaster/backend/tests/test_reseller.py
T
patrickandClaude Opus 4.8 9e1ba2bd91 feat: Mandant/Reseller ohne E-Mail anlegbar (Temp-Passwort statt Einladung)
- admin_email bzw. reseller email optional; ohne E-Mail wird interne Login-
  Kennung (<name>@<slug>.local) + einmaliges Temp-Passwort erzeugt, Account
  sofort aktiv, kein Mailversand
- TenantOut/ResellerOut: initial_password (einmalig) ergänzt
- LoginRequest.email: str statt EmailStr (Kennung muss kein zustellbares
  Postfach sein; .local-Domains sind sonst nicht einloggbar)
- Frontend: E-Mail-Felder optional, CredDialog zeigt Login + Temp-Passwort einmalig
- Test: Anlage ohne E-Mail + Login mit generierten Zugangsdaten; 173/173 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:34:23 +02:00

130 lines
5.3 KiB
Python

"""Tests für Reseller-Rolle + Mandantenverwaltung + RLS-Isolation."""
import uuid
import pytest
from httpx import AsyncClient
from sqlalchemy import text
from app.core.security import hash_password
from app.models.user import User, UserRole
async def _make_user(db, *, email, role, company_id=None, password="Secret123"):
await db.execute(text("SET LOCAL app.bypass_rls = 'on'"))
user = User(
id=uuid.uuid4(), company_id=company_id, email=email,
password_hash=hash_password(password),
first_name=role.title(), last_name="User",
role=UserRole(role), is_active=True,
)
db.add(user)
await db.commit()
return user.id
async def _login(client, email, password="Secret123"):
r = await client.post("/api/v1/auth/login", json={"email": email, "password": password})
assert r.status_code == 200, r.text
return {"Authorization": f"Bearer {r.json()['access_token']}"}
@pytest.mark.asyncio(loop_scope="session")
async def test_superadmin_creates_reseller(client: AsyncClient, db_session):
await _make_user(db_session, email="sa@platform.de", role="SUPER_ADMIN")
h = await _login(client, "sa@platform.de")
r = await client.post("/api/v1/admin/resellers", json={
"email": "reseller-a@partner.de", "first_name": "Rita", "last_name": "Reseller",
}, headers=h)
assert r.status_code == 201, r.text
assert r.json()["company_count"] == 0
lst = await client.get("/api/v1/admin/resellers", headers=h)
assert any(x["email"] == "reseller-a@partner.de" for x in lst.json())
@pytest.mark.asyncio(loop_scope="session")
async def test_reseller_creates_and_sees_only_own_company(client: AsyncClient, db_session):
rid_a = await _make_user(db_session, email="res-a@p.de", role="RESELLER")
rid_b = await _make_user(db_session, email="res-b@p.de", role="RESELLER")
ha = await _login(client, "res-a@p.de")
hb = await _login(client, "res-b@p.de")
# Reseller A legt eine Firma an
r = await client.post("/api/v1/reseller/companies", json={
"name": "Alpha GmbH", "admin_email": "admin@alpha.de",
"admin_first_name": "Al", "admin_last_name": "Pha",
}, headers=ha)
assert r.status_code == 201, r.text
assert r.json()["name"] == "Alpha GmbH"
# Reseller B legt eine andere Firma an
r2 = await client.post("/api/v1/reseller/companies", json={
"name": "Beta GmbH", "admin_email": "admin@beta.de",
"admin_first_name": "Be", "admin_last_name": "Ta",
}, headers=hb)
assert r2.status_code == 201, r2.text
# A sieht nur Alpha, NICHT Beta (RLS-Isolation)
la = await client.get("/api/v1/reseller/companies", headers=ha)
names_a = {c["name"] for c in la.json()}
assert "Alpha GmbH" in names_a
assert "Beta GmbH" not in names_a
lb = await client.get("/api/v1/reseller/companies", headers=hb)
names_b = {c["name"] for c in lb.json()}
assert "Beta GmbH" in names_b
assert "Alpha GmbH" not in names_b
assert rid_a != rid_b
@pytest.mark.asyncio(loop_scope="session")
async def test_create_company_without_email_returns_temp_password(client: AsyncClient, db_session):
await _make_user(db_session, email="res-noemail@p.de", role="RESELLER")
h = await _login(client, "res-noemail@p.de")
r = await client.post("/api/v1/reseller/companies", json={
"name": "Delta GmbH", "admin_first_name": "De", "admin_last_name": "Lta",
}, headers=h)
assert r.status_code == 201, r.text
body = r.json()
assert body["initial_password"], "Temp-Passwort muss zurückgegeben werden"
assert body["admin_email"], "Login-Kennung muss erzeugt werden"
# Mit den generierten Zugangsdaten kann sich der Admin sofort einloggen
ok = await client.post("/api/v1/auth/login", json={
"email": body["admin_email"], "password": body["initial_password"],
})
assert ok.status_code == 200, ok.text
@pytest.mark.asyncio(loop_scope="session")
async def test_reseller_cannot_access_admin_endpoints(client: AsyncClient, db_session):
await _make_user(db_session, email="res-c@p.de", role="RESELLER")
h = await _login(client, "res-c@p.de")
r = await client.get("/api/v1/admin/tenants", headers=h)
assert r.status_code == 403
@pytest.mark.asyncio(loop_scope="session")
async def test_deactivated_tenant_blocks_login(client: AsyncClient, db_session):
rid = await _make_user(db_session, email="res-d@p.de", role="RESELLER")
h = await _login(client, "res-d@p.de")
create = await client.post("/api/v1/reseller/companies", json={
"name": "Gamma GmbH", "admin_email": "admin@gamma.de",
"admin_first_name": "Ga", "admin_last_name": "Mma",
}, headers=h)
company_id = create.json()["id"]
# Direkt einen aktiven Mitarbeiter in Gamma anlegen (per DB) und Login testen
await _make_user(db_session, email="emp@gamma.de", role="EMPLOYEE", company_id=company_id)
ok = await client.post("/api/v1/auth/login", json={"email": "emp@gamma.de", "password": "Secret123"})
assert ok.status_code == 200
# Firma deaktivieren → Login gesperrt
await client.patch(f"/api/v1/reseller/companies/{company_id}", json={"is_active": False}, headers=h)
blocked = await client.post("/api/v1/auth/login", json={"email": "emp@gamma.de", "password": "Secret123"})
assert blocked.status_code == 403
assert rid is not None