storage: JSON files -> SQLite (users/history/searches/events)
- one-time import from the legacy JSON (left in place as snapshot); ghost rows (int vs str ep_id) deduped on import, unrepresentable after - history POST is now an upsert: the (user, slug_id, ep_id) PK is the merge key, so the unguarded read-modify-write (lost updates) and the corruption-swallow (JSONDecodeError -> [] -> fresh overwrite) are gone - searches: NOCASE PK upsert + cap 8 - events table + position/search logging (watch-time trajectory), 90-day retention pruned at boot - account delete cascades all four tables - 22/22 tests green; old JSON files untouched
This commit is contained in:
1 parent
2c0ee8eae3
commit
202bfa41c3
5 files changed
+222
-105
No files matched your search
+10
-5
@@ -54,13 +54,18 @@ def server():
|
||||
data = Path(tempfile.mkdtemp(prefix="aniweb-test-"))
|
||||
os.environ["ANIWEB_DATA"] = str(data)
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from server import users # noqa: E402
|
||||
from server import db, users # noqa: E402
|
||||
db.init() # schema before users.add / seeding
|
||||
for name, pw in USERS:
|
||||
users.add(name, pw)
|
||||
(data / "history").mkdir()
|
||||
(data / "history" / "admin.json").write_text(json.dumps([
|
||||
{"title": "Seed Anime", "slug_id": "seed-1", "season_id": 1, "ep_id": "1",
|
||||
"lang": "eng", "ep": "1", "pos": 10.0, "t": time.time()}]))
|
||||
c = db.connect()
|
||||
seed_t = time.time()
|
||||
c.execute("INSERT INTO history VALUES (?,?,?,?,?)",
|
||||
("admin", "seed-1", 1, seed_t, json.dumps(
|
||||
{"title": "Seed Anime", "slug_id": "seed-1", "season_id": 1,
|
||||
"ep_id": 1, "lang": "eng", "ep": "1", "pos": 10.0, "t": seed_t})))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "server.app:app",
|
||||
|
||||
+17
-6
@@ -96,7 +96,7 @@ def test_no_self_signup(base=BASE):
|
||||
jar = login("admin", "adminpw12345", base)
|
||||
assert req("POST", "/signup", {"username": "zed", "password": "whatever123"},
|
||||
jar=jar, base=base) == 405
|
||||
assert "zed" not in json.load(open(_users_file()))
|
||||
assert "zed" not in _users()
|
||||
|
||||
|
||||
def test_history_requires_ep_id(base=BASE):
|
||||
@@ -172,8 +172,8 @@ def test_self_delete(base=BASE):
|
||||
# give dave a searches file too: account delete must remove it
|
||||
assert req("POST", "/api/searches", {"q": "dave-search"}, jar=jar, base=base) == 200
|
||||
assert req("POST", "/account/delete", jar=jar, base=base) == 200
|
||||
assert "dave" not in json.load(open(_users_file()))
|
||||
assert not os.path.exists(os.path.join(os.environ["ANIWEB_DATA"], "searches", "dave.json"))
|
||||
assert "dave" not in _users()
|
||||
assert _search_count("dave") == 0 # account delete cascades the rows
|
||||
# old session cookie is dead immediately (middleware re-checks existence)
|
||||
assert req("GET", "/api/me", jar=jar, base=base) == 401
|
||||
jar2 = CJ()
|
||||
@@ -230,6 +230,17 @@ def _json(method, path, jar=None, base=BASE):
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
|
||||
def _users_file():
|
||||
import os
|
||||
return os.path.join(os.environ["ANIWEB_DATA"], "users.json")
|
||||
def _users():
|
||||
import sqlite3
|
||||
c = sqlite3.connect(os.path.join(os.environ["ANIWEB_DATA"], "aniweb.db"))
|
||||
names = [r[0] for r in c.execute("SELECT name FROM users")]
|
||||
c.close()
|
||||
return names
|
||||
|
||||
|
||||
def _search_count(user):
|
||||
import sqlite3
|
||||
c = sqlite3.connect(os.path.join(os.environ["ANIWEB_DATA"], "aniweb.db"))
|
||||
n = c.execute("SELECT COUNT(*) FROM searches WHERE user=?", (user,)).fetchone()[0]
|
||||
c.close()
|
||||
return n
|
||||
Reference in new issue
Block a user