AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6
@@ -630,3 +630,110 @@ async def test_creating_a_tag_that_differs_only_in_case_returns_the_existing_one
|
||||
assert (await second.get_json())["id"] == (await first.get_json())["id"]
|
||||
listing = (await (await app_client.get("/api/labels")).get_json())["labels"]
|
||||
assert len(listing) == 1
|
||||
|
||||
|
||||
# --- One save path for a note's text (#5164) ---------------------------------
|
||||
#
|
||||
# A body edit is a sequence: keep a revision, rename the note, lift #tags, commit,
|
||||
# queue link previews. It was written out once per route, and the copies drifted —
|
||||
# restoring a revision skipped the previews. These pin the sequence at each door.
|
||||
|
||||
|
||||
async def _signed_in(app_client, who: str):
|
||||
reg = await app_client.post(
|
||||
"/api/auth/register",
|
||||
json={"email": f"{who}@example.test", "password": "a-long-enough-password"},
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
|
||||
|
||||
def _record_previews(monkeypatch, module: str) -> list[tuple[str, str]]:
|
||||
"""Capture what a write path queues for unfurling, instead of fetching."""
|
||||
queued: list[tuple[str, str]] = []
|
||||
monkeypatch.setattr(f"{module}.schedule_unfurls", lambda nid, body: queued.append((str(nid), body)))
|
||||
return queued
|
||||
|
||||
|
||||
async def test_restoring_a_revision_queues_previews_for_its_links(app_client, db, monkeypatch):
|
||||
"""The bug that motivated the shared path: restore was the one copy of the edit
|
||||
sequence without the unfurl step, so a link brought back by a restore stayed a
|
||||
bare URL on every surface."""
|
||||
await _signed_in(app_client, "restore")
|
||||
queued = _record_previews(monkeypatch, "inkwell.notes")
|
||||
|
||||
note = await (await app_client.post("/api/notes", json={"body": "read https://example.com/a"})).get_json()
|
||||
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "no link any more"})
|
||||
revisions = (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"]
|
||||
assert [r["body"] for r in revisions] == ["read https://example.com/a"]
|
||||
|
||||
queued.clear()
|
||||
resp = await app_client.post(f"/api/notes/{note['id']}/revisions/{revisions[0]['id']}/restore")
|
||||
assert resp.status_code == 200
|
||||
assert (await resp.get_json())["body"] == "read https://example.com/a"
|
||||
assert queued == [(note["id"], "read https://example.com/a")], "the restored link gets its preview"
|
||||
|
||||
|
||||
async def test_restoring_keeps_the_text_it_replaces(app_client, db):
|
||||
"""Restore snapshots unconditionally — inside an editing session too — so a
|
||||
restore can itself be undone."""
|
||||
await _signed_in(app_client, "undo")
|
||||
note = await (await app_client.post("/api/notes", json={"body": "first"})).get_json()
|
||||
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "second"})
|
||||
rev = (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"][0]
|
||||
|
||||
await app_client.post(f"/api/notes/{note['id']}/revisions/{rev['id']}/restore")
|
||||
bodies = [r["body"] for r in (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"]]
|
||||
assert sorted(bodies) == ["first", "second"]
|
||||
|
||||
|
||||
async def test_each_route_that_writes_text_queues_previews_only_when_it_changed(app_client, db, monkeypatch):
|
||||
await _signed_in(app_client, "routes")
|
||||
queued = _record_previews(monkeypatch, "inkwell.notes")
|
||||
|
||||
note = await (await app_client.post("/api/notes", json={"body": "https://example.com/new"})).get_json()
|
||||
assert queued == [(note["id"], "https://example.com/new")], "create"
|
||||
|
||||
queued.clear()
|
||||
await app_client.patch(f"/api/notes/{note['id']}", json={"pinned": True})
|
||||
assert queued == [], "a pin is not a text change"
|
||||
|
||||
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "https://example.com/new\n- [ ] milk"})
|
||||
assert queued == [(note["id"], "https://example.com/new\n- [ ] milk")], "PATCH"
|
||||
|
||||
queued.clear()
|
||||
await app_client.patch(f"/api/notes/{note['id']}/items/0", json={"checked": True})
|
||||
assert queued == [(note["id"], "https://example.com/new\n- [x] milk")], "ticking an item"
|
||||
|
||||
|
||||
async def test_a_pushed_note_is_named_tagged_and_queued_like_a_typed_one(app_client, db, monkeypatch):
|
||||
await _signed_in(app_client, "push")
|
||||
queued = _record_previews(monkeypatch, "inkwell.sync")
|
||||
nid = str(uuid.uuid4())
|
||||
|
||||
resp = await app_client.post(
|
||||
"/api/sync/push",
|
||||
json={"changes": [{"entity": "note", "id": nid, "op": "upsert", "body": "Trip https://example.com/t\n#travel",
|
||||
"edited_at": "2026-01-01T00:00:00Z", "created_at": "2026-01-01T00:00:00Z"}]},
|
||||
)
|
||||
assert (await resp.get_json())["results"][0]["status"] == "created"
|
||||
|
||||
note = await (await app_client.get(f"/api/notes/{nid}")).get_json()
|
||||
assert note["body"] == "Trip https://example.com/t", "the standalone tag was lifted"
|
||||
assert [lb["name"] for lb in note["labels"]] == ["travel"]
|
||||
assert note["display_title"] == "Trip https://example.com/t"
|
||||
assert queued == [(nid, "Trip https://example.com/t")], "queued with the text as stored"
|
||||
|
||||
|
||||
async def test_a_pushed_edit_keeps_the_clients_edit_time_when_a_tag_is_lifted(app_client, db):
|
||||
"""Last-write-wins compares edit times, so the stored one has to be the client's.
|
||||
Lifting a tag rewrites the body in a second flush, and the column's onupdate
|
||||
used to stamp the server's clock over the time the client sent."""
|
||||
await _signed_in(app_client, "edited")
|
||||
nid = str(uuid.uuid4())
|
||||
await app_client.post(
|
||||
"/api/sync/push",
|
||||
json={"changes": [{"entity": "note", "id": nid, "op": "upsert", "body": "milk\n#groceries",
|
||||
"edited_at": "2026-01-01T00:00:00Z", "created_at": "2026-01-01T00:00:00Z"}]},
|
||||
)
|
||||
note = await (await app_client.get(f"/api/notes/{nid}")).get_json()
|
||||
assert datetime.fromisoformat(note["updated_at"]) == datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
Reference in New Issue
Block a user