Tests: pytest-Suite für Konten, Haushalt, Kanban, Reisen und Migrationen
- backend/tests: jede Test-Funktion bekommt eine frische Datenbank; test_migrationen.py baut ältere Datenbank-Stände nach und prüft die verlustfreie Umstellung (auch zweiter Start ohne Änderung) - requirements-dev.txt mit pytest - Fix (von den Tests gefunden): Eine neue, leere Datenbank bekam keine erste Liste "Zuhause"; jetzt einmalig beim ersten Start Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc4ce18593
commit
79cbaab092
10 files changed
+478
No files matched your search
@@ -0,0 +1,138 @@
|
||||
"""Alte Datenbank-Stände nachbauen und prüfen, dass der Start sie verlustfrei auf den aktuellen Stand bringt."""
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from app import db
|
||||
|
||||
T = "2026-01-01T00:00:00.000Z"
|
||||
|
||||
KANBAN_PROJECTS = """CREATE TABLE kanban_projects (
|
||||
id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', position REAL NOT NULL,
|
||||
completed_at TEXT, created_at TEXT NOT NULL, created_by INTEGER, updated_at TEXT NOT NULL, updated_by INTEGER)"""
|
||||
# Kanban-Aufgaben vor "erledigt seit"
|
||||
KANBAN_TASKS_OHNE_DONE = """CREATE TABLE kanban_tasks (
|
||||
id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES kanban_projects(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL, assignee INTEGER,
|
||||
priority INTEGER NOT NULL DEFAULT 2, position REAL NOT NULL, created_at TEXT NOT NULL, created_by INTEGER,
|
||||
updated_at TEXT NOT NULL, updated_by INTEGER)"""
|
||||
# Reise-Städte vor Höhe, Zeitzone und Hauptstadt
|
||||
TRAVEL_ALT = """CREATE TABLE travel_cities (
|
||||
id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, geoname_id INTEGER NOT NULL, name TEXT NOT NULL,
|
||||
admin1 TEXT NOT NULL DEFAULT '', country TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL,
|
||||
population INTEGER, created_at TEXT NOT NULL, UNIQUE (user_id, geoname_id))"""
|
||||
# Vorrat/Einkauf ganz alt: ohne Einheit
|
||||
HAUSHALT_OHNE_EINHEIT = """CREATE TABLE household_items (
|
||||
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL, quantity INTEGER NOT NULL,
|
||||
updated_at TEXT NOT NULL, updated_by INTEGER, UNIQUE (key, list))"""
|
||||
# Vorrat/Einkauf mit Einheit, aber ohne Listen (Stand auf dem Server vor den Listen)
|
||||
HAUSHALT_OHNE_LISTEN = """CREATE TABLE household_items (
|
||||
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL, quantity REAL NOT NULL,
|
||||
unit TEXT NOT NULL DEFAULT 'stk', updated_at TEXT NOT NULL, updated_by INTEGER, UNIQUE (key, list, unit))"""
|
||||
# Zwischenstand: mehrere Einkaufslisten, Vorrat ohne Liste (list_id NULL)
|
||||
ZWISCHENSTAND = [
|
||||
"""CREATE TABLE shopping_lists (id INTEGER PRIMARY KEY, name TEXT NOT NULL, everyone INTEGER NOT NULL DEFAULT 1,
|
||||
position REAL NOT NULL, created_at TEXT NOT NULL, created_by INTEGER)""",
|
||||
"""CREATE TABLE household_items (
|
||||
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL,
|
||||
list_id INTEGER REFERENCES shopping_lists(id) ON DELETE CASCADE, quantity REAL NOT NULL,
|
||||
unit TEXT NOT NULL DEFAULT 'stk', updated_at TEXT NOT NULL, updated_by INTEGER,
|
||||
CHECK ((list = 'einkauf') = (list_id IS NOT NULL)))""",
|
||||
"CREATE UNIQUE INDEX household_vorrat ON household_items (key, unit) WHERE list_id IS NULL",
|
||||
"CREATE UNIQUE INDEX household_einkauf ON household_items (list_id, key, unit) WHERE list_id IS NOT NULL",
|
||||
f"INSERT INTO shopping_lists VALUES (1, 'Einkaufsliste', 1, 1, '{T}', NULL)",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def old_db(tmp_path, monkeypatch):
|
||||
"""Leere Datei mit den angegebenen alten Tabellen; danach db.init() wie beim Serverstart."""
|
||||
path = tmp_path / "alt.db"
|
||||
monkeypatch.setattr(db, "DB_FILE", path)
|
||||
|
||||
def build(*statements):
|
||||
c = sqlite3.connect(path)
|
||||
for sql in statements:
|
||||
c.execute(sql)
|
||||
c.commit()
|
||||
c.close()
|
||||
db.init()
|
||||
conn = db.connect()
|
||||
return conn
|
||||
return build
|
||||
|
||||
|
||||
def household(conn):
|
||||
return sorted(tuple(r) for r in conn.execute("SELECT name, list, list_id, quantity, unit, checked FROM household_items"))
|
||||
|
||||
|
||||
def test_haushalt_ohne_einheit(old_db):
|
||||
conn = old_db(
|
||||
HAUSHALT_OHNE_EINHEIT,
|
||||
f"INSERT INTO household_items VALUES (1, 'milch', 'Milch', 'vorrat', 2, '{T}', NULL)",
|
||||
f"INSERT INTO household_items VALUES (2, 'brot', 'Brot', 'einkauf', 1, '{T}', NULL)",
|
||||
)
|
||||
assert [tuple(r) for r in conn.execute("SELECT id, name FROM shopping_lists")] == [(1, "Zuhause")]
|
||||
assert household(conn) == [("Brot", "einkauf", 1, 1, "stk", 0), ("Milch", "vorrat", 1, 2, "stk", 0)]
|
||||
|
||||
|
||||
def test_haushalt_ohne_listen(old_db):
|
||||
conn = old_db(
|
||||
HAUSHALT_OHNE_LISTEN,
|
||||
f"INSERT INTO household_items VALUES (1, 'mehl', 'Mehl', 'vorrat', 1.5, 'kg', '{T}', NULL)",
|
||||
f"INSERT INTO household_items VALUES (2, 'mehl', 'Mehl', 'einkauf', 2, 'stk', '{T}', NULL)",
|
||||
)
|
||||
assert [r["name"] for r in conn.execute("SELECT name FROM shopping_lists")] == ["Zuhause"]
|
||||
assert household(conn) == [("Mehl", "einkauf", 1, 2, "stk", 0), ("Mehl", "vorrat", 1, 1.5, "kg", 0)]
|
||||
|
||||
|
||||
def test_haushalt_zwischenstand(old_db):
|
||||
conn = old_db(
|
||||
*ZWISCHENSTAND,
|
||||
f"INSERT INTO household_items VALUES (1, 'reis', 'Reis', 'vorrat', NULL, 1, 'stk', '{T}', NULL)",
|
||||
f"INSERT INTO household_items VALUES (2, 'reis', 'Reis', 'einkauf', 1, 3, 'stk', '{T}', NULL)",
|
||||
)
|
||||
# "Einkaufsliste" heißt jetzt "Zuhause" und enthält auch den Vorrat
|
||||
assert [tuple(r) for r in conn.execute("SELECT id, name FROM shopping_lists")] == [(1, "Zuhause")]
|
||||
assert household(conn) == [("Reis", "einkauf", 1, 3, "stk", 0), ("Reis", "vorrat", 1, 1, "stk", 0)]
|
||||
indexes = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type = 'index'")}
|
||||
assert not {"household_vorrat", "household_einkauf"} & indexes
|
||||
|
||||
|
||||
def test_kanban_ohne_erledigt_zeitpunkt(old_db):
|
||||
conn = old_db(
|
||||
KANBAN_PROJECTS,
|
||||
KANBAN_TASKS_OHNE_DONE,
|
||||
f"INSERT INTO kanban_projects VALUES (1, 'P', '', 1, NULL, '{T}', NULL, '{T}', NULL)",
|
||||
f"INSERT INTO kanban_tasks VALUES (1, 1, 'fertig', '', 'erledigt', NULL, 2, 1, '{T}', NULL, '{T}', NULL)",
|
||||
f"INSERT INTO kanban_tasks VALUES (2, 1, 'offen', '', 'offen', NULL, 2, 2, '{T}', NULL, '{T}', NULL)",
|
||||
)
|
||||
rows = dict(conn.execute("SELECT title, done_at FROM kanban_tasks").fetchall())
|
||||
# Erledigte zählen ab dem Update – nicht ab dem alten Änderungsdatum (sonst sofort gelöscht)
|
||||
assert rows["fertig"] > T and rows["offen"] is None
|
||||
|
||||
|
||||
def test_reisen_ohne_neue_spalten(old_db):
|
||||
conn = old_db(
|
||||
TRAVEL_ALT,
|
||||
f"INSERT INTO travel_cities VALUES (1, 1, 2950159, 'Berlin', '', 'DE', 52.5, 13.4, NULL, '{T}')",
|
||||
)
|
||||
row = conn.execute("SELECT name, elevation, timezone, capital FROM travel_cities").fetchone()
|
||||
assert tuple(row) == ("Berlin", None, None, None)
|
||||
|
||||
|
||||
def test_zweiter_start_aendert_nichts(old_db):
|
||||
conn = old_db(
|
||||
HAUSHALT_OHNE_LISTEN,
|
||||
f"INSERT INTO household_items VALUES (1, 'mehl', 'Mehl', 'vorrat', 1, 'stk', '{T}', NULL)",
|
||||
)
|
||||
before = household(conn), [tuple(r) for r in conn.execute("SELECT * FROM shopping_lists")]
|
||||
db.init()
|
||||
assert (household(conn), [tuple(r) for r in conn.execute("SELECT * FROM shopping_lists")]) == before
|
||||
|
||||
|
||||
def test_geloeschte_listen_kommen_nicht_zurueck(conn):
|
||||
with conn:
|
||||
conn.execute("DELETE FROM shopping_lists")
|
||||
db.init()
|
||||
assert conn.execute("SELECT COUNT(*) FROM shopping_lists").fetchone()[0] == 0
|
||||
Reference in new issue
Block a user