TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
"""flush_pending / sync_library against a stubbed API (the server side is covered by the real
|
|
|
|
|
local server in tools/smoke_server.py)."""
|
|
|
|
|
import pytest
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
from diora_tui import api, progress, remote
|
|
|
|
|
from diora_tui.config import RemoteConfig
|
|
|
|
|
|
|
|
|
|
T = 1_760_000_000_000
|
|
|
|
|
CFG = RemoteConfig("http://x", "tok", "key")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def fresh_store(tmp_path, monkeypatch):
|
|
|
|
|
store = progress.ProgressStore(tmp_path / "p.json")
|
|
|
|
|
monkeypatch.setattr(progress, "_default", store)
|
|
|
|
|
return store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flush_sends_read_at_and_device_and_clears_the_queue(fresh_store, monkeypatch):
|
|
|
|
|
fresh_store.write("s7", "3:0.5", 0.25, T, now=T)
|
|
|
|
|
sent = []
|
|
|
|
|
|
|
|
|
|
def fake_post(server, token, book_id, **kw):
|
|
|
|
|
sent.append((book_id, kw))
|
|
|
|
|
return {"ok": True, "stored": True, "read_at": T, "position_anchor": "3:0.5", "scroll_fraction": 0.25, "server_now": T}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(api, "post_progress", fake_post)
|
|
|
|
|
assert remote.flush_pending(CFG) == 1
|
|
|
|
|
assert sent == [(7, {"anchor": "3:0.5", "fraction": 0.25, "read_at": T, "device": "diora TUI", "force": False})]
|
|
|
|
|
assert fresh_store.get("s7").pending is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flush_keeps_the_queue_when_offline(fresh_store, monkeypatch):
|
|
|
|
|
fresh_store.write("s7", "3:0.5", 0.25, T, now=T)
|
|
|
|
|
|
|
|
|
|
def offline(*a, **kw):
|
|
|
|
|
raise requests.ConnectionError("no network")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(api, "post_progress", offline)
|
|
|
|
|
assert remote.flush_pending(CFG) == 0
|
|
|
|
|
assert fresh_store.get("s7").pending is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flush_drops_the_queue_of_a_deleted_book(fresh_store, monkeypatch):
|
|
|
|
|
fresh_store.write("s7", "3:0.5", 0.25, T, now=T)
|
|
|
|
|
|
|
|
|
|
def gone(*a, **kw):
|
|
|
|
|
raise api.ApiError("Buch 7 nicht gefunden (gelöscht oder anderes Konto?)")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(api, "post_progress", gone)
|
|
|
|
|
remote.flush_pending(CFG)
|
|
|
|
|
assert fresh_store.get("s7").pending is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flush_ignores_local_books(fresh_store, monkeypatch):
|
|
|
|
|
fresh_store.write("abcdef0123456789", "3:0.5", 0.25, T, now=T) # queue=True by default, but not a server key
|
|
|
|
|
monkeypatch.setattr(api, "post_progress", lambda *a, **k: pytest.fail("must not be sent"))
|
|
|
|
|
assert remote.flush_pending(CFG) == 0
|
TUI: Bibliothek wie im Web (Weiterlesen-Karte, Status- und Ordner-Chips, Suche, Sortierung, Länge)
library.py hält die Regeln der Web-Bibliothek (53-library.js) als reine Funktionen:
Status Angefangen/Ungelesen/Gelesen, Ordner (mehrere je Buch, Reihenfolge aus den
verschlüsselten Bibliothekseinstellungen, "Ohne Ordner"), Suche ohne Akzente, sechs
Sortierungen mit natürlicher Zahlenordnung, Weiterlesen-Karte aus den zuletzt gelesenen,
Bücher/Zeitschriften. library_screen.py zeigt sie an (Tasten 1-4, o, /, s, m, c; Chips und
Karte klickbar). Metadaten (Ordner, Seitenzahl, Zeitschrift) und die Ordnerreihenfolge
kommen beim Abgleich; fehlt die Länge, rechnet die TUI sie für geladene Bücher aus dem Text
(1800 Zeichen je Seite). crypto.encrypt für Stufe 3. 54 Tests, Smoke prüft Ordner/Länge/Prefs
gegen den lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:18:49 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_encrypt_roundtrip_matches_decrypt():
|
|
|
|
|
import base64
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
from diora_tui import crypto
|
|
|
|
|
|
|
|
|
|
key = base64.b64encode(os.urandom(32)).decode()
|
|
|
|
|
iv, ct = crypto.encrypt(key, "Zäune".encode())
|
|
|
|
|
assert len(bytes.fromhex(iv)) == 12 and crypto.decrypt(key, iv, ct).decode() == "Zäune"
|
|
|
|
|
with pytest.raises(crypto.DecryptError):
|
|
|
|
|
crypto.decrypt(base64.b64encode(os.urandom(32)).decode(), iv, ct)
|