feat(dokument): Dokumente-Modul (Roadmap Phase 5)
Polymorphe Datei-Anhänge (PDF/JPEG/PNG/WebP, Whitelist statt Blacklist -
owasp-Grundsatz) an beliebige Ressource, gleiches entitaet_typ/entitaet_id-
Muster wie Mangel/Historie. Lokale Ablage (settings.upload_dir, kein Cloud-
Zwang, Self-Hosting-Anforderung), server-generierter Dateiname verhindert
Path-Traversal/Namenskollisionen.
POST /dokumente (multipart), GET /dokumente (Filter Pflicht: entitaet_typ +
entitaet_id), GET /dokumente/{id}/download, DELETE /dokumente/{id}. Hochladen:
alle Mitarbeiter+, Löschen: Materialverantwortliche+Leitung+Admin.
Frontend: wiederverwendbares DokumentePanel (Upload/Liste/Download/Löschen),
eingebunden in ObjektSection (je Objekt) und MangelListePage (Fotos zu
Mängeln) - weitere Ressourcen (Geräteinstanz, Fahrzeugdetails, Benutzer)
können denselben Baustein später einfach wiederverwenden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dokument_hochladen_und_liste(client, objekt_mit_position, mitarbeiter_user):
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
hochgeladen = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id), "beschreibung": "Zulassung"},
|
||||
files={"datei": ("zulassung.pdf", b"%PDF-1.4 fake", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert hochgeladen.status_code == 201
|
||||
body = hochgeladen.json()
|
||||
assert body["dateiname"] == "zulassung.pdf"
|
||||
assert body["groesse_bytes"] > 0
|
||||
|
||||
liste = await client.get(
|
||||
f"/api/v1/dokumente?entitaet_typ=objekt&entitaet_id={objekt.id}", headers=auth_header(token)
|
||||
)
|
||||
assert len(liste.json()) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_liefert_inhalt(client, objekt_mit_position, mitarbeiter_user):
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
hochgeladen = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id)},
|
||||
files={"datei": ("foto.png", b"\x89PNG fake", "image/png")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
dokument_id = hochgeladen.json()["id"]
|
||||
|
||||
download = await client.get(f"/api/v1/dokumente/{dokument_id}/download", headers=auth_header(token))
|
||||
assert download.status_code == 200
|
||||
assert download.content == b"\x89PNG fake"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unerlaubter_dateityp_wird_abgelehnt(client, objekt_mit_position, mitarbeiter_user):
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id)},
|
||||
files={"datei": ("boese.html", b"<script>alert(1)</script>", "text/html")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 415
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_materialverantwortlicher_kann_dokument_loeschen(
|
||||
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
||||
):
|
||||
objekt, _material = objekt_mit_position
|
||||
mitarbeiter_token = await login(client, "mitarbeiter1")
|
||||
hochgeladen = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id)},
|
||||
files={"datei": ("bericht.pdf", b"%PDF-1.4 x", "application/pdf")},
|
||||
headers=auth_header(mitarbeiter_token),
|
||||
)
|
||||
dokument_id = hochgeladen.json()["id"]
|
||||
|
||||
verantwortlicher_token = await login(client, "materialverantwortlicher1")
|
||||
geloescht = await client.delete(f"/api/v1/dokumente/{dokument_id}", headers=auth_header(verantwortlicher_token))
|
||||
assert geloescht.status_code == 204
|
||||
|
||||
liste = await client.get(
|
||||
f"/api/v1/dokumente?entitaet_typ=objekt&entitaet_id={objekt.id}", headers=auth_header(verantwortlicher_token)
|
||||
)
|
||||
assert liste.json() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mitarbeiter_darf_dokument_nicht_loeschen(client, objekt_mit_position, mitarbeiter_user):
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
hochgeladen = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id)},
|
||||
files={"datei": ("x.pdf", b"%PDF-1.4 x", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
dokument_id = hochgeladen.json()["id"]
|
||||
|
||||
response = await client.delete(f"/api/v1/dokumente/{dokument_id}", headers=auth_header(token))
|
||||
assert response.status_code == 403
|
||||
Reference in New Issue
Block a user