158 lines
8.6 KiB
Python
158 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))
|
|
|
|
from tests.harness import configure_test_engine
|
|
|
|
configure_test_engine()
|
|
|
|
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()
|