Kansho/backend/tests/test_journal_trash.py
Lars 9d79e3e92f
Some checks failed
Deploy Development / deploy (push) Successful in 51s
Test Suite / pytest-backend (push) Failing after 2m32s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 15s
Run backend tests with pytest after Dev deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 14:47:04 +02:00

154 lines
8.6 KiB
Python

"""Journal trash, undelete, and hard delete. Run from backend/: python tests/test_journal_trash.py"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-trash-media")
os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1"
Path(os.environ["KANSHO_MEDIA_ROOT"]).mkdir(parents=True, exist_ok=True)
from fastapi.testclient import TestClient
from main import app
from media_store import media_root
def expect(ok: bool, message: str) -> None:
if not ok:
raise SystemExit(f"FAIL: {message}")
print(f"OK {message}")
def header(token: str) -> dict:
return {"X-Auth-Token": token}
PNG = bytes.fromhex(
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
"0000000a49444154789c6360000002000100ffff03000006000557bf0000000049454e44ae426082"
)
def main() -> None:
with TestClient(app) as client:
setup = client.post("/api/auth/setup", json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"})
headers = header(setup.json()["token"])
other = client.post(
"/api/users",
headers=headers,
json={"email": "ute@example.test", "name": "Ute", "password": "user-pass", "role": "user"},
)
expect(other.status_code == 200, "second profile")
other_headers = header(client.post("/api/auth/login", json={"email": "ute@example.test", "password": "user-pass"}).json()["token"])
space = client.post("/api/journal/spaces", headers=headers, json={"title": "Alltag"})
space_id = space.json()["id"]
day = client.post(f"/api/journal/spaces/{space_id}/days", headers=headers, json={"calendar_date": "2026-08-20"})
day_id = day.json()["day"]["id"]
conv = client.post(f"/api/journal/days/{day_id}/conversations", headers=headers, json={"title": "Gespräch"})
client.post(f"/api/journal/conversations/{conv.json()['id']}/turn", headers=headers, json={"body": "Heute war der Markt voll."})
saved = client.post(
"/api/journal/entries",
headers=headers,
json={"journal_day_id": day_id, "title": "Markt", "body": "Heute war der Markt voll.", "origin": "user_edit"},
)
entry_id = saved.json()["id"]
second = client.post(
"/api/journal/entries",
headers=headers,
json={"journal_day_id": day_id, "title": "Abend", "body": "Später am Hafen.", "origin": "user_edit"},
)
second_id = second.json()["id"]
media = client.post(
f"/api/journal/entries/{entry_id}/media",
headers=headers,
files={"file": ("shot.png", PNG, "image/png")},
)
expect(media.status_code == 200, "media attached")
media_id = media.json()["id"]
rel = media.json()["rel_path"]
expect((media_root() / rel).is_file(), "media file exists")
listed = client.get(f"/api/journal/spaces/{space_id}/entries", headers=headers).json()
expect(len(listed) == 2, "two active entries")
expect(listed[0]["calendar_date"] == "2026-08-20", "same day")
expect(listed[0]["id"] != listed[1]["id"], "same-day entries are distinct")
expect(all(item["id"] in {entry_id, second_id} for item in listed), "list is the two entries")
hidden_list = client.get(f"/api/journal/spaces/{space_id}/entries", headers=other_headers)
expect(hidden_list.status_code == 404, "entry list isolated")
hidden_trash = client.get(f"/api/journal/spaces/{space_id}/trash", headers=other_headers)
expect(hidden_trash.status_code == 404, "trash list isolated")
deleted = client.delete(f"/api/journal/entries/{entry_id}", headers=headers)
expect(deleted.status_code == 200, "soft delete")
after_soft = client.get(f"/api/journal/spaces/{space_id}/entries", headers=headers).json()
expect(all(item["id"] != entry_id for item in after_soft), "soft-deleted entry leaves chronology")
expect(any(item["id"] == second_id for item in after_soft), "other entry remains")
trash = client.get(f"/api/journal/spaces/{space_id}/trash", headers=headers).json()
expect(any(item["id"] == entry_id for item in trash), "soft-deleted appears in trash")
missing = client.get(f"/api/journal/entries/{entry_id}", headers=headers)
expect(missing.status_code == 404, "deleted entry is not a normal GET")
stolen_delete = client.delete(f"/api/journal/entries/{second_id}", headers=other_headers)
expect(stolen_delete.status_code == 404, "foreign profile cannot soft-delete")
stolen_undelete = client.post(f"/api/journal/entries/{entry_id}/undelete", headers=other_headers, json={})
expect(stolen_undelete.status_code == 404, "foreign profile cannot undelete")
stolen_purge = client.post(f"/api/journal/entries/{entry_id}/purge", headers=other_headers, json={"confirm": True})
expect(stolen_purge.status_code == 404, "foreign profile cannot purge")
version_restore = client.post(
f"/api/journal/entries/{entry_id}/restore",
headers=headers,
json={"version_id": saved.json()["current_version_id"]},
)
expect(version_restore.status_code == 404, "version restore is not trash undelete")
back = client.post(f"/api/journal/entries/{entry_id}/undelete", headers=headers, json={})
expect(back.status_code == 200, f"undelete {back.text}")
expect(back.json().get("deleted_at") in (None, ""), "undelete clears deleted_at")
restored_list = client.get(f"/api/journal/spaces/{space_id}/entries", headers=headers).json()
expect(any(item["id"] == entry_id for item in restored_list), "undeleted entry returns to chronology")
expect(client.get(f"/api/journal/spaces/{space_id}/trash", headers=headers).json() == [], "trash empty after undelete")
versions = client.get(f"/api/journal/entries/{entry_id}/versions", headers=headers).json()
expect(len(versions) == 1, "undelete does not append a version")
client.delete(f"/api/journal/entries/{entry_id}", headers=headers)
unconfirmed = client.post(f"/api/journal/entries/{entry_id}/purge", headers=headers, json={"confirm": False})
expect(unconfirmed.status_code == 400, "purge without confirm fails closed")
still = client.get(f"/api/journal/spaces/{space_id}/trash", headers=headers).json()
expect(any(item["id"] == entry_id for item in still), "unconfirmed purge leaves the entry")
active_purge = client.post(f"/api/journal/entries/{second_id}/purge", headers=headers, json={"confirm": True})
expect(active_purge.status_code == 400, "purge of active entry is rejected")
conv_before = client.get(f"/api/journal/conversations/{conv.json()['id']}", headers=headers)
expect(conv_before.status_code == 200, "source dialogue still there before purge")
purged = client.post(f"/api/journal/entries/{entry_id}/purge", headers=headers, json={"confirm": True})
expect(purged.status_code == 200 and purged.json().get("purged"), f"purge {purged.text}")
expect(purged.json().get("conversations_untouched") is True, "source dialogues are not deleted")
expect(client.get(f"/api/journal/conversations/{conv.json()['id']}", headers=headers).status_code == 200, "dialogue remains")
expect(client.get(f"/api/journal/entries/{entry_id}", headers=headers).status_code == 404, "purged entry gone")
expect(client.get(f"/api/journal/spaces/{space_id}/trash", headers=headers).json() == [], "purged entry leaves trash")
expect(not (media_root() / rel).exists(), "unreferenced media file removed")
expect(client.get(f"/api/journal/media/{media_id}", headers=headers).status_code == 404, "media row gone")
again = client.post(f"/api/journal/entries/{entry_id}/purge", headers=headers, json={"confirm": True})
expect(again.status_code == 404, "second purge of same id is not found")
again_undelete = client.post(f"/api/journal/entries/{entry_id}/undelete", headers=headers, json={})
expect(again_undelete.status_code == 404, "cannot undelete a purged entry")
leftover = client.get(f"/api/journal/spaces/{space_id}/entries", headers=headers).json()
expect(all(item["id"] != entry_id for item in leftover), "purged id absent from chronology")
expect(any(item["id"] == second_id for item in leftover), "untouched entry remains")
if __name__ == "__main__":
main()