"""End-to-end check of the sync against a LOCAL diora server (never a real account): login, protected credentials, book list, lazy download, a position going out, another device reading later, an offline catch-up that must lose. Throwaway state in a temp folder. python tools/smoke_server.py http://127.0.0.1:8001 It can run again on the same database (the furthest place only ever grows). The server is the diora-web checkout run with its own throwaway database, seeded like diora-android's tools/seed_testserver.py does. """ import os import sys import tempfile import time from pathlib import Path home = tempfile.mkdtemp(prefix="diora-tui-smoke-") for var in ("LOCALAPPDATA", "APPDATA", "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"): os.environ[var] = home os.environ["HOME"] = home os.environ["DIORA_TUI_HOME"] = home from diora_tui import api, blocks, config, crypto, epub, login, progress, remote # noqa: E402 server, username, password = sys.argv[1].rstrip("/"), sys.argv[2], sys.argv[3] if "127.0.0.1" not in server and "localhost" not in server: sys.exit("only against a local test server") def check(what, ok, detail=""): print(("OK " if ok else "FAIL ") + what + (f" [{detail}]" if detail else "")) if not ok: sys.exit(1) token, name = login.web_login(server, username, password) key_b64 = crypto.derive_key_b64(name, password) cfg = config.RemoteConfig(server, token, key_b64, name) config.save(cfg) back = config.load() check("credentials stored protected and read back", back == cfg and (sys.platform != "win32" or "dpapi:" in Path(config._CONFIG_FILE).read_text())) check("no token or key in the config file in the clear", token not in Path(config._CONFIG_FILE).read_text() and key_b64 not in Path(config._CONFIG_FILE).read_text()) lib = Path(home) / "lib" res = remote.sync_library(lib, cfg) check("book list pulled, nothing downloaded", res.books >= 1 and not list(lib.glob("*.epub")), f"{res.books} books") store = progress.default() check("server clock offset learnt", abs(store.offset) < 5000, f"{store.offset} ms") entry = sorted(__import__("diora_tui.library_meta", fromlist=["x"]).remote_entries().items())[0] skey, info = entry path = remote.download_book(lib, cfg, info["server_id"], info["title"]) check("book downloaded on demand and keyed by the server id", path.exists() and epub._book_id(path) == skey, path.name) book = blocks.load_flat_book(path) # folders, length and the folder order come from the encrypted meta and prefs import json # noqa: E402 import requests # noqa: E402 H = {"Authorization": f"Bearer {token}"} raw_books = {b["id"]: b for b in api.fetch_books(server, token)[0]} b0 = raw_books[info["server_id"]] meta = json.loads(crypto.decrypt(key_b64, b0["meta_iv"], b0["meta_ct"])) meta.update(folders=["Smoke B", "Smoke A"], pages=123) iv, ct = crypto.encrypt(key_b64, json.dumps(meta).encode()) check("meta written", requests.post(f"{server}/books/{info['server_id']}/meta/", json={"meta_ct": ct, "meta_iv": iv}, headers=H).ok) iv, ct = crypto.encrypt(key_b64, json.dumps({"folder_order": ["Smoke B", "Smoke A"]}).encode()) check("prefs written", requests.post(f"{server}/books/prefs/", json={"prefs_ct": ct, "prefs_iv": iv}, headers=H).ok) remote.sync_library(lib, cfg) from diora_tui import library, library_meta # noqa: E402 e = library_meta.remote_entries()[skey] check("folders and length pulled from the meta", e["folders"] == ["Smoke B", "Smoke A"] and e["pages"] == 123, str(e)) check("folder order pulled from the prefs", library_meta.get_extra("folder_order") == ["Smoke B", "Smoke A"]) rows = library.build_rows([path], library_meta.remote_entries(), {}, {}, epub._book_id) check("a row carries them", [r for r in rows if r.key == skey][0].folders == ["Smoke B", "Smoke A"]) # the reader arrives at block 12 now now = progress.now_ms() a1 = blocks.format_anchor(12, 0.5) store.write(skey, a1, blocks.fraction_for_anchor(book, 12, 0.5), now) check("position queued", store.get(skey).pending is not None) check("flushed", remote.flush_pending(cfg) == 1 and store.get(skey).pending is None) row = [b for b in api.fetch_books(server, token)[0] if b["id"] == info["server_id"]][0] check("server holds it with device and a real fraction", row["position_anchor"] == a1 and row["device"] == "diora TUI" and 0 < row["scroll_fraction"] < 1 and abs(row["read_at"] - now) < 10_000, f"fraction {row['scroll_fraction']:.4f}, read_at diff {row['read_at'] - now} ms") # another device reads somewhere else, later time.sleep(0.05) other = blocks.format_anchor(3, 0.0) api.post_progress(server, token, info["server_id"], anchor=other, fraction=0.02, read_at=progress.now_ms(), device="Handy") remote.sync_library(lib, cfg) st = store.get(skey) check("a pull shows the later position of the other device", st.anchor == other and st.device == "Handy") check("the furthest place is kept (never behind what was read)", progress.cmp_progress(st.furthest_anchor, 0, a1, 0) >= 0, st.furthest_anchor) # an offline catch-up from before that must not become the continue point store.write(skey, blocks.format_anchor(30, 0.0), 0.5, progress.now_ms() - 120_000) remote.flush_pending(cfg) row = [b for b in api.fetch_books(server, token)[0] if b["id"] == info["server_id"]][0] check("an older write loses on the server", row["position_anchor"] == other and row["device"] == "Handy") check("...and locally", store.get(skey).anchor == other and store.get(skey).pending is None) check("...but raises the furthest place", progress.cmp_progress(row["furthest_anchor"], 0, blocks.format_anchor(30, 0.0), 0) >= 0, row["furthest_anchor"]) # highlights and bookmarks: ours go up merged with another device's, in the web's format from diora_tui import annotations as an # noqa: E402 astore = an.default() sid = info["server_id"] theirs = [{"id": "web-1", "type": "highlight", "anchor": an.make_anchor("c", 3, 0, 3, 4, "abcd"), "color": "red", "note": "", "createdAt": "2026-10-26T10:00:00.000Z"}] iv, ct = crypto.encrypt(key_b64, json.dumps(theirs).encode()) check("another device wrote a highlight", requests.post(f"{server}/books/{sid}/highlights/", json={"ct": ct, "iv": iv}, headers=H).ok) remote.pull_annotations(cfg, sid, skey) check("pulled", [h["id"] for h in astore.get(skey).highlights] == ["web-1"]) mine = an.new_highlight(an.make_anchor("c", 5, 2, 5, 9, "efghijk"), "green", "meine Notiz") astore.add(skey, "highlights", mine) astore.add(skey, "bookmarks", an.new_bookmark("c", "c", 0.5)) # meanwhile the other device adds one more theirs.append({**theirs[0], "id": "web-2", "color": "blue"}) iv, ct = crypto.encrypt(key_b64, json.dumps(theirs).encode()) requests.post(f"{server}/books/{sid}/highlights/", json={"ct": ct, "iv": iv}, headers=H) check("pushed", remote.push_annotations(cfg) == 2 and astore.dirty_keys() == []) got = an.decode(key_b64, requests.get(f"{server}/books/{sid}/highlights/", headers=H).json()) check("the server holds both devices' highlights", [h["id"] for h in got] == ["web-1", "web-2", mine["id"]] and got[2]["note"] == "meine Notiz") bms = an.decode(key_b64, requests.get(f"{server}/books/{sid}/bookmarks/", headers=H).json()) check("and the bookmark", len(bms) == 1 and bms[0]["label"] == "c") astore.delete(skey, "highlights", "web-1") remote.push_annotations(cfg) got = an.decode(key_b64, requests.get(f"{server}/books/{sid}/highlights/", headers=H).json()) check("a deletion goes up too", [h["id"] for h in got] == ["web-2", mine["id"]]) print("all good")