Compare commits
3 commits
1448782564
...
d9ecdb15fb
| Author | SHA1 | Date | |
|---|---|---|---|
| d9ecdb15fb | |||
| cde019cacf | |||
| 545b57c723 |
6 changed files with 328 additions and 15 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -13,3 +13,9 @@ __pycache__/
|
|||
.claude/worktrees/
|
||||
Ban Yaro - Google Play package/
|
||||
/unsplash/
|
||||
|
||||
# Selbst-gehostete Vektor-Tiles (groß, gehören nicht ins Repo)
|
||||
tiles/build/
|
||||
*.pmtiles
|
||||
*.osm.pbf
|
||||
*.mbtiles
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -24,6 +24,7 @@ TAR_EXCLUDE := --exclude='.git' \
|
|||
--exclude='./backend/__pycache__' \
|
||||
--exclude='./.env' \
|
||||
--exclude='./*.db' \
|
||||
--exclude='./tiles' \
|
||||
--exclude='./.DS_Store'
|
||||
|
||||
.PHONY: help deploy deploy-clean staging release sync push restart build stop status \
|
||||
|
|
|
|||
|
|
@ -371,6 +371,13 @@ app.mount("/js", StaticFiles(directory=f"{STATIC_DIR}/js"), name="js")
|
|||
app.mount("/icons", StaticFiles(directory=f"{STATIC_DIR}/icons"), name="icons")
|
||||
app.mount("/img", StaticFiles(directory=f"{STATIC_DIR}/img"), name="img")
|
||||
|
||||
# Selbst-gehostete Vektor-Tiles (.pmtiles) — liegen im data-Volume, NICHT im Image.
|
||||
# Starlette FileResponse beherrscht Range-Requests (206) → MapLibre/pmtiles-Protokoll.
|
||||
# Guard: Mount nur wenn das Verzeichnis existiert (sonst No-Op, z. B. lokal/Prod ohne Tiles).
|
||||
_TILES_DIR = os.getenv("TILES_DIR", "/data/tiles")
|
||||
if os.path.isdir(_TILES_DIR):
|
||||
app.mount("/tiles", StaticFiles(directory=_TILES_DIR), name="tiles")
|
||||
|
||||
# User-generierte Medien (Fotos aus Tagebuch, Giftköder-Alarm, etc.)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -149,25 +149,74 @@ async def put_world_config(body: WorldConfigIn, user=Depends(get_current_user)):
|
|||
# ----------------------------------------------------------
|
||||
# DELETE /profile/account — Konto unwiderruflich löschen
|
||||
# ----------------------------------------------------------
|
||||
# Spalten, die eine HANDLUNG referenzieren (Moderator/Admin/Ersteller),
|
||||
# nicht Eigentum des Users. Beim Löschen auf NULL setzen statt die fremde
|
||||
# Zeile (z. B. einen Partner-Code oder eine moderierte Einreichung) mitzureißen.
|
||||
_ACTOR_COLUMNS = {
|
||||
("wiki_foto_submissions", "reviewed_by"),
|
||||
("osm_poi_edits", "mod_id"),
|
||||
("partner_codes", "created_by"),
|
||||
("outreach_log", "sent_by"),
|
||||
("upgrade_requests", "fulfilled_by"),
|
||||
}
|
||||
|
||||
|
||||
@router.delete('/account')
|
||||
async def delete_account(user=Depends(get_current_user)):
|
||||
"""Löscht das Konto und alle zugehörigen Daten unwiderruflich."""
|
||||
"""Löscht das Konto und ALLE zugehörigen Daten unwiderruflich (DSGVO + App-Store-Gl. 4).
|
||||
|
||||
FK-sicher und schema-robust: ermittelt per Introspektion alle Tabellen, die
|
||||
auf users(id) verweisen. CASCADE-Tabellen werden beim users-DELETE automatisch
|
||||
geleert; NO-ACTION/RESTRICT-Eigentumstabellen löschen wir explizit; Aktions-
|
||||
Spalten (Moderator/Admin) setzen wir auf NULL. `defer_foreign_keys` macht die
|
||||
Reihenfolge irrelevant — geprüft wird erst beim Commit.
|
||||
"""
|
||||
uid = user['id']
|
||||
with db() as conn:
|
||||
# Alle Hunde-IDs des Users
|
||||
dog_ids = [r['id'] for r in conn.execute(
|
||||
"SELECT id FROM dogs WHERE user_id=?", (uid,)).fetchall()]
|
||||
for did in dog_ids:
|
||||
conn.execute("DELETE FROM diary WHERE dog_id=?", (did,))
|
||||
conn.execute("DELETE FROM health WHERE dog_id=?", (did,))
|
||||
conn.execute("DELETE FROM training_sessions WHERE dog_id=?", (did,))
|
||||
conn.execute("DELETE FROM training_streaks WHERE dog_id=?", (did,))
|
||||
conn.execute("DELETE FROM expenses WHERE dog_id=?", (did,))
|
||||
conn.execute("DELETE FROM dogs WHERE user_id=?", (uid,))
|
||||
conn.execute("DELETE FROM upgrade_requests WHERE user_id=?", (uid,))
|
||||
conn.execute("DELETE FROM push_subscriptions WHERE user_id=?", (uid,))
|
||||
conn.execute("DELETE FROM notifications WHERE user_id=?", (uid,))
|
||||
conn.execute("DELETE FROM forum_posts WHERE user_id=?", (uid,))
|
||||
# FK-Prüfung bis zum Commit aufschieben → Löschreihenfolge egal.
|
||||
conn.execute("PRAGMA defer_foreign_keys=ON")
|
||||
|
||||
tables = [r['name'] for r in conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
||||
).fetchall()]
|
||||
|
||||
# Tabellen merken, deren User-Bezug bereits über eine FK behandelt wurde
|
||||
# (gelöscht oder genullt), damit der Spalten-Scan sie nicht doppelt anfasst.
|
||||
handled_fk_cols: set[tuple] = set()
|
||||
|
||||
# --- 1) Formale FKs auf users(id) ---
|
||||
for tbl in tables:
|
||||
try:
|
||||
fks = conn.execute(f"PRAGMA foreign_key_list({tbl})").fetchall()
|
||||
except Exception:
|
||||
continue
|
||||
for fk in fks:
|
||||
if fk['table'] != 'users':
|
||||
continue
|
||||
col = fk['from']
|
||||
handled_fk_cols.add((tbl, col))
|
||||
on_delete = (fk['on_delete'] or '').upper()
|
||||
if on_delete == 'CASCADE':
|
||||
continue # wird durch den finalen users-DELETE mitgelöscht
|
||||
if on_delete == 'SET NULL' or (tbl, col) in _ACTOR_COLUMNS:
|
||||
conn.execute(f"UPDATE {tbl} SET {col}=NULL WHERE {col}=?", (uid,))
|
||||
else:
|
||||
# NO ACTION / RESTRICT auf einer Eigentums-Spalte → Zeilen löschen.
|
||||
conn.execute(f"DELETE FROM {tbl} WHERE {col}=?", (uid,))
|
||||
|
||||
# --- 2) Eigentums-Spalten OHNE formale FK (z. B. events.user_id) ---
|
||||
# Manche Tabellen tragen user_id/owner_id ohne REFERENCES-Klausel. Die fängt
|
||||
# die FK-Introspektion nicht — für ein echtes „alle Daten löschen" hier nach.
|
||||
for tbl in tables:
|
||||
try:
|
||||
cols = {r['name'] for r in conn.execute(f"PRAGMA table_info({tbl})").fetchall()}
|
||||
except Exception:
|
||||
continue
|
||||
for col in ('user_id', 'owner_id'):
|
||||
if col in cols and (tbl, col) not in handled_fk_cols and (tbl, col) not in _ACTOR_COLUMNS:
|
||||
conn.execute(f"DELETE FROM {tbl} WHERE {col}=?", (uid,))
|
||||
|
||||
# Räumt alle verbliebenen ON-DELETE-CASCADE-Tabellen automatisch ab.
|
||||
conn.execute("DELETE FROM users WHERE id=?", (uid,))
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
|
|
|||
189
docs/TILE_SERVER_HANDOVER.md
Normal file
189
docs/TILE_SERVER_HANDOVER.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# Übergabe: Selbst-gehosteter Tile-Server (Karte + Touren auf eigenen Vektortiles)
|
||||
|
||||
> **An den Kollegen, der das hier im `banyaro`-Repo weiterbaut.**
|
||||
> Geschrieben aus dem `banyaro-ios`-Kontext heraus, nachdem der App-Store-Resubmit
|
||||
> (Build 1.0(5)) raus war. Dies ist der **Vorbereitungs- + Staging-Test-Plan**. Bitte
|
||||
> erst „Kontext & Entscheidung" lesen, dann den Staging-Spike ausführen, dann die
|
||||
> offenen Punkte mit René klären, bevor irgendwas nach Produktion geht.
|
||||
|
||||
---
|
||||
|
||||
## 1. Worum geht's
|
||||
|
||||
Wir wollen **Karten selbst hosten** (analog Pocket Earth), statt vom öffentlichen
|
||||
OSM-Raster-Server zu ziehen. Damit bauen wir **Karte UND Touren** auf eigenen
|
||||
Vektortiles auf — Web (PWA) und nativ (iOS).
|
||||
|
||||
**Warum überhaupt:**
|
||||
- **Lizenz/Policy:** Die PWA nutzt aktuell `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`
|
||||
(`backend/static/js/ui.js` → `Map.OSM_URL`, `offline-indicator.js`). Die
|
||||
[OSMF-Tile-Usage-Policy](https://operations.osmfoundation.org/policies/tiles/)
|
||||
verbietet heavy/kommerzielle Nutzung — auf Dauer kein tragfähiges Fundament.
|
||||
- **Offline:** iOS soll Regionen offline vorhalten (Pocket-Earth-Modell). Mit eigenen
|
||||
Tiles ist das sauber machbar, mit dem öffentlichen Raster-Server nicht.
|
||||
- **Kontrolle & Konsistenz:** Eine Tile-/Style-Quelle für Web + iOS, kein
|
||||
Drittanbieter-Limit, Retina/Vektor-Styling, eigenes Karten-Design möglich.
|
||||
- **Companion-Prinzip:** Substanz lebt auf banyaro.app — die Tile-Infrastruktur gehört
|
||||
genau hierher (DS/Docker/NPM/Staging sind alle in diesem Repo). Die iOS-App ist nur
|
||||
das native Fenster.
|
||||
|
||||
**Bewusst NICHT Teil des laufenden App-Store-Resubmits** — das ist eigener Scope nach
|
||||
der Freigabe (andere Risikoklasse: Infra/Ops, Heim-Uplink-Verfügbarkeit; kein offener
|
||||
Apple-Punkt). Siehe iOS-Memory `project_app_review_build3` / `project_build4_karte`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ist-Zustand (im Repo verifiziert)
|
||||
|
||||
- **Karte Web:** Leaflet + `leaflet.markercluster`, Raster-Basemap vom öffentlichen
|
||||
OSM-Server. Helper in `backend/static/js/ui.js` (`Map.OSM_URL`, `L.tileLayer(...)`),
|
||||
weitere Vorkommen in `ui.js:1006` und `offline-indicator.js:193`.
|
||||
- **Karte iOS:** aktuell Apple **MapKit** (im `banyaro-ios`-Repo). Soll später auf
|
||||
MapLibre Native + Offline-Regionen umgestellt werden — **separater iOS-Workstream**,
|
||||
dieser Tile-Server ist die Voraussetzung dafür.
|
||||
- **Deployment:** ein Docker-Service `banyaro` (FastAPI :8000 → DS :3010) hinter
|
||||
**NPM (Nginx Proxy Manager)**. Static-Files via FastAPI-`StaticFiles`-Mounts
|
||||
(`/css`, `/js`, `/icons`, `/img` in `backend/main.py:369–372`). Volume `./data:/data`.
|
||||
- **Staging:** eigener Container `banyaro-staging`, `docker-compose.staging.yml`,
|
||||
Pfad `/volume1/docker/banyaro-staging`, URL **https://staging.banyaro.app**,
|
||||
Deploy via `make staging` (pusht `develop`).
|
||||
- **DS-Zugang:** Host `ds` (10.47.11.10, SSH-Port 4711), `sudo docker`.
|
||||
- **Tile-Pipeline:** existiert noch **nicht** (keine pbf/mbtiles/pmtiles im Repo).
|
||||
Es gibt konzeptionell validierte OSM-POI-Pipeline-Notizen (iOS-Memory
|
||||
`project_build4_pois`) — separat von den Basemap-Tiles, aber gleiche pbf-Quelle.
|
||||
|
||||
---
|
||||
|
||||
## 3. Empfohlene Architektur: **planetiler → PMTiles → MapLibre**
|
||||
|
||||
Für einen **Heim-Server (DiskStation)** mit **Offline-Anspruch** ist das die klar beste
|
||||
Wahl — leichter als ein klassischer Raster-Renderer und ohne laufenden Render-Prozess:
|
||||
|
||||
```
|
||||
OSM-Extract (.osm.pbf, Geofabrik)
|
||||
│ planetiler (OpenMapTiles-Schema, einmalig + bei Updates)
|
||||
▼
|
||||
region.pmtiles ← EIN Single-File-Tile-Archiv (Vektor)
|
||||
│ per HTTP Range-Requests ausgeliefert (nginx/FastAPI StaticFiles → 206)
|
||||
▼
|
||||
MapLibre ── Web: MapLibre GL JS + pmtiles-Protokoll (ersetzt Leaflet-Raster)
|
||||
└─ iOS: MapLibre Native, Offline = .pmtiles lokal auf dem Gerät
|
||||
+ Style-JSON + Glyphs (Fonts) + Sprite (statisch gehostet)
|
||||
+ Touren = GeoJSON-Linien-Layer obendrauf (Basemap-unabhängig)
|
||||
```
|
||||
|
||||
**Warum PMTiles (und nicht ein Raster-Tile-Server):**
|
||||
- **Kein Server-Prozess, kein PostGIS, kein renderd/Mapnik.** Eine Datei, ausgeliefert
|
||||
per Range-Request — die DS muss zur Laufzeit **nichts rendern**. Ideal für schwache
|
||||
Hardware + Heim-Uplink.
|
||||
- **Offline trivial:** dieselbe `.pmtiles` lokal auf dem iPhone → MapLibre liest direkt.
|
||||
Das ist das Pocket-Earth-Modell.
|
||||
- **Vektor:** Retina-scharf, eigenes Styling, Labels drehbar, klein.
|
||||
|
||||
**Verworfen — `openstreetmap-tile-server` / renderd+Mapnik (Raster):** schwerer
|
||||
PostGIS-Import, CPU-Rendering pro Request, große Storage, schlechte Offline-Story,
|
||||
kein Vektor-Styling. Für unseren Fall strikt unterlegen.
|
||||
|
||||
**Stack-Komponenten:**
|
||||
| Teil | Tool | Hinweis |
|
||||
|------|------|---------|
|
||||
| Tile-Generierung | [planetiler](https://github.com/onthegomap/planetiler) (Docker) | OpenMapTiles-Schema, Output direkt `.pmtiles` |
|
||||
| Auslieferung | nginx/NPM **oder** FastAPI `StaticFiles` | beide können Range-Requests (206); für Spike reicht StaticFiles |
|
||||
| Web-Client | `maplibre-gl` + `pmtiles` (JS) | ersetzt Leaflet schrittweise (Feature-Flag) |
|
||||
| iOS-Client | MapLibre Native (eigener Workstream) | Offline-Region = lokale `.pmtiles` |
|
||||
| Style | OpenMapTiles-kompatibel (z. B. Positron/OSM-Bright) + Glyphs + Sprite | selbst hosten für volle Unabhängigkeit |
|
||||
|
||||
---
|
||||
|
||||
## 4. Staging-Spike (konkret, zum Loslegen)
|
||||
|
||||
Ziel: eine kleine Region als `.pmtiles` erzeugen, auf **staging** ausliefern, mit
|
||||
MapLibre rendern — und Größe/Zeit/Performance/Range-Requests messen. **Erst klein**
|
||||
(Bayern), nicht gleich DACH.
|
||||
|
||||
### 4.1 Extract + Tiles erzeugen (lokal/Build-Maschine, NICHT auf der DS)
|
||||
```bash
|
||||
mkdir -p tiles/build && cd tiles/build
|
||||
# planetiler lädt den Geofabrik-Extract selbst und schreibt direkt PMTiles:
|
||||
docker run --rm -v "$PWD:/data" ghcr.io/onthegomap/planetiler:latest \
|
||||
--download --area=bayern --output=/data/bayern.pmtiles
|
||||
# Ergebnis: bayern.pmtiles (grobe Schätzung: paar hundert MB, wenige Minuten)
|
||||
```
|
||||
> DACH gibt es bei Geofabrik nicht als ein Extract → später `germany` + `austria` +
|
||||
> `switzerland` per `osmium merge` zusammenführen, dann planetiler darauf. Für den
|
||||
> Spike reicht `bayern`.
|
||||
|
||||
### 4.2 Auf Staging ausliefern (einfachster Weg: FastAPI StaticFiles vom data-Volume)
|
||||
Die große Datei NICHT ins Image bauen — ins `./data`-Volume legen und mounten.
|
||||
```python
|
||||
# backend/main.py — nahe der bestehenden StaticFiles-Mounts (Z. 369ff)
|
||||
import os
|
||||
_TILES_DIR = os.getenv("TILES_DIR", "/data/tiles")
|
||||
if os.path.isdir(_TILES_DIR):
|
||||
app.mount("/tiles", StaticFiles(directory=_TILES_DIR), name="tiles")
|
||||
```
|
||||
- `bayern.pmtiles` nach `/volume1/docker/banyaro-staging/data/tiles/` kopieren
|
||||
(scp zur DS), dann `make staging`.
|
||||
- Starlette `FileResponse` beherrscht Range-Requests → MapLibre/pmtiles bekommt 206.
|
||||
- **Verifizieren:** `curl -I -H "Range: bytes=0-1023" https://staging.banyaro.app/tiles/bayern.pmtiles`
|
||||
muss **HTTP/1.1 206 Partial Content** + `Accept-Ranges: bytes` liefern. Prüfen, dass
|
||||
NPM die Range-Header nicht verschluckt.
|
||||
|
||||
### 4.3 MapLibre-Testseite (Feature-Flag, Leaflet bleibt Fallback)
|
||||
- `maplibre-gl` + `pmtiles` einbinden, Protokoll registrieren:
|
||||
```js
|
||||
const p = new pmtiles.Protocol();
|
||||
maplibregl.addProtocol('pmtiles', p.tile);
|
||||
// source: { type:'vector', url:'pmtiles://https://staging.banyaro.app/tiles/bayern.pmtiles' }
|
||||
```
|
||||
- Style-JSON (OpenMapTiles-Schema) + Glyphs + Sprite hosten (z. B. unter `/tiles/style/`).
|
||||
Fertige freie Styles: OSM-Bright / Positron (maputnik-kompatibel). Glyphs z. B. aus
|
||||
`openmaptiles/fonts`. Für den Spike darf der Style minimal sein.
|
||||
- Touren-Polyline als GeoJSON-`line`-Layer auf die Map legen (ist basemap-unabhängig —
|
||||
sobald MapLibre läuft, ist die Tour nur noch ein Layer).
|
||||
|
||||
### 4.4 Messen & festhalten
|
||||
- Dateigröße + Generierungszeit (Bayern, dann hochrechnen auf DACH).
|
||||
- Render-Performance über den Heim-Uplink (erste Zoomstufen, Labels).
|
||||
- DS-Storage-Budget (`/volume1/...` frei?).
|
||||
- Range-Requests durch NPM ok? CORS nötig (falls Tiles auf anderer Subdomain als App)?
|
||||
|
||||
---
|
||||
|
||||
## 5. Offene Entscheidungen (mit René klären)
|
||||
|
||||
1. **Region-Scope:** DACH am Stück vs. Deutschland-only vs. **herunterladbare
|
||||
Regionen** (für iOS-Offline granular). DACH-PMTiles grob ~2–3 GB (verifizieren).
|
||||
2. **Hosting-Pfad:** Pfad (`/tiles` an der App) vs. eigene Subdomain
|
||||
(`tiles.banyaro.app`) hinter NPM. Subdomain = sauberer cachebar, aber CORS-Setup.
|
||||
Für Skalierung ggf. nginx direkt statt FastAPI StaticFiles (kein App-CPU).
|
||||
3. **Style:** welcher Basis-Style (Positron/OSM-Bright/eigenes Hunde-Theme), Glyphs/Sprite
|
||||
selbst hosten.
|
||||
4. **Update-Kadenz:** wie oft aus frischem OSM-Extract neu generieren (monatlich?).
|
||||
planetiler-Rerun + Datei tauschen (atomar). Cron/Make-Target dafür.
|
||||
5. **iOS-Workstream:** MapLibre Native + Offline-Region-Download ersetzt MapKit —
|
||||
eigener Build-4-Task im `banyaro-ios`-Repo, **nach** dieser Infra.
|
||||
6. **Attribution (Pflicht):** „© OpenStreetMap contributors" (ODbL) muss in Web **und**
|
||||
iOS sichtbar sein. Bei eigenem Style ggf. zusätzliche Datenquellen-Hinweise.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vorgeschlagene Reihenfolge
|
||||
|
||||
1. Staging-Spike (Abschnitt 4) mit **Bayern** — Proof of Concept, Zahlen sammeln.
|
||||
2. Entscheidungen aus Abschnitt 5 mit René.
|
||||
3. DACH-PMTiles generieren (osmium merge → planetiler), Update-Make-Target.
|
||||
4. PWA: MapLibre hinter Feature-Flag produktiv, Leaflet-Pfad rausnehmen wenn stabil.
|
||||
5. iOS (separat): MapLibre Native + Offline-Regionen, MapKit ablösen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Querverweise
|
||||
|
||||
- iOS-Memory: `project_build4_karte` (Entscheidung OSM/MapLibre/Offline),
|
||||
`project_build4_pois` (POI-Pipeline aus pbf — gleiche Datenquelle),
|
||||
`project_osm_contribution` (OSM-Beitragskreislauf, gehört in die PWA),
|
||||
`project_companion` (Companion-Prinzip), `project_app_review_build3` (warum nicht im Resubmit).
|
||||
- NPM/IPv6/Reverse-Proxy-Stolpersteine auf der DS: Skill `synology-troubleshooting`.
|
||||
- Deploy: `make staging` (→ staging.banyaro.app), `make deploy` (→ Produktion, deployt
|
||||
den **Arbeitsbaum**). `make bump` NUR bei Frontend-Asset-Änderungen (SW-Cache).
|
||||
61
tests/test_account_deletion.py
Normal file
61
tests/test_account_deletion.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
Account-Löschung (DSGVO + App-Store-Gl. 4): muss FK-sicher ALLE Daten entfernen,
|
||||
auch wenn der User Zeilen in Tabellen ohne ON DELETE CASCADE hat (routes, places,
|
||||
walks, events, forum_threads, …). Regressionstest gegen den alten, FK-unvollständigen
|
||||
Delete, der am finalen `DELETE FROM users` scheiterte, sobald solche Zeilen existierten.
|
||||
"""
|
||||
import secrets
|
||||
|
||||
|
||||
def _make_user(client):
|
||||
from database import db
|
||||
email = f"del-{secrets.token_hex(4)}@example.com"
|
||||
pw, name = "TestPass123!", f"deltest{secrets.token_hex(3)}"
|
||||
r = client.post("/api/auth/register", json={"email": email, "password": pw, "name": name})
|
||||
assert r.status_code == 200, r.text
|
||||
with db() as conn:
|
||||
conn.execute("UPDATE users SET email_verified=1 WHERE email=?", (email,))
|
||||
uid = conn.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()["id"]
|
||||
token = client.post("/api/auth/login", json={"email": email, "password": pw}).json()["token"]
|
||||
return uid, {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_delete_account_with_noncascade_data(client):
|
||||
from database import db
|
||||
uid, headers = _make_user(client)
|
||||
dog_id = client.post("/api/dogs", headers=headers,
|
||||
json={"name": "Rex", "rasse": "Mix", "is_public": False}).json()["id"]
|
||||
|
||||
# Direkt Zeilen in den Tabellen anlegen, die users(id) OHNE Cascade referenzieren —
|
||||
# genau die, die den alten Delete blockiert haben.
|
||||
with db() as conn:
|
||||
conn.execute("INSERT INTO routes (user_id, name, gps_track) VALUES (?,?,?)",
|
||||
(uid, "Testrunde", "[]"))
|
||||
conn.execute("INSERT INTO places (user_id, name, typ, lat, lon) VALUES (?,?,?,?,?)",
|
||||
(uid, "Hundewiese", "freilauf", 52.5, 13.4))
|
||||
conn.execute("INSERT INTO walks (user_id, titel, datum, uhrzeit, lat, lon) VALUES (?,?,?,?,?,?)",
|
||||
(uid, "Gassi-Treff", "2026-07-01", "18:00", 52.5, 13.4))
|
||||
conn.execute("INSERT INTO events (user_id, titel, datum) VALUES (?,?,?)",
|
||||
(uid, "Hundewanderung", "2026-07-02"))
|
||||
conn.execute("INSERT INTO forum_threads (user_id, titel) VALUES (?,?)",
|
||||
(uid, "Hallo Forum"))
|
||||
|
||||
resp = client.delete("/api/profile/account", headers=headers)
|
||||
assert resp.status_code == 200, f"Delete failed: {resp.status_code} {resp.text}"
|
||||
assert resp.json()["status"] == "deleted"
|
||||
|
||||
with db() as conn:
|
||||
assert conn.execute("SELECT 1 FROM users WHERE id=?", (uid,)).fetchone() is None
|
||||
for tbl in ("routes", "places", "walks", "events", "forum_threads", "dogs"):
|
||||
cnt = conn.execute(f"SELECT COUNT(*) c FROM {tbl} WHERE user_id=?", (uid,)).fetchone()["c"]
|
||||
assert cnt == 0, f"{tbl} hat noch {cnt} Zeile(n) nach Account-Löschung"
|
||||
|
||||
|
||||
def test_delete_account_minimal_user(client):
|
||||
"""Auch ein User ganz ohne Zusatzdaten lässt sich löschen."""
|
||||
from database import db
|
||||
uid, headers = _make_user(client)
|
||||
resp = client.delete("/api/profile/account", headers=headers)
|
||||
assert resp.status_code == 200, resp.text
|
||||
with db() as conn:
|
||||
assert conn.execute("SELECT 1 FROM users WHERE id=?", (uid,)).fetchone() is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue