Server comments describe the code to a stranger (#6221)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
Rule 32 "Comments describe the product to a stranger" sweep over src/, alembic/ and tests/. Comments and docstrings now say what the code does and the reason as a present-tense constraint, briefly. Removed: - issue, task, note, rule, milestone and family-idea references - "used to" / before-and-after narration - quotes of decisions and incident stories from the deploying instance - paragraph-length arguments, where a line or two carries the fact Kept every hazard as a present-tense fact, e.g. timing oracles, the proxy hop direction, and migrations that must stay frozen. Also: - Three test names lose "still"/"used to"/"pre_m304" phrasing. - pyproject's description drops "FabledSword family". The history these comments carried stays in Scribe and in git log. A comment-stripping diff (Scribe snippet #6226 "codeonly.py — prove a diff changed only comments") shows no code changed apart from the renamed tests and the description string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+64
-120
@@ -1,17 +1,11 @@
|
||||
"""The real-Postgres lane (family rule 6).
|
||||
|
||||
Everything else in this suite is deliberately DB-free, which means the schema the
|
||||
migrations build has never been checked against the models that read it. That gap is
|
||||
what this file closes, and it is not theoretical: M13 dropped three columns and
|
||||
rebuilt a generated column, and until now `alembic upgrade head` ran for the first
|
||||
time when the operator's container started.
|
||||
"""The real-Postgres lane: the schema the migrations build, against the models and
|
||||
routes that use it.
|
||||
|
||||
Marked `integration` and excluded from the unit lane by `-m "not integration"`, so a
|
||||
workstation without Postgres runs the rest of the suite unchanged.
|
||||
|
||||
The schema comes from real migrations, never `metadata.create_all` (rule 82) — the
|
||||
point is to test what actually ships, and `create_all` would build a schema no
|
||||
deployment has ever seen.
|
||||
The schema comes from real migrations, never `metadata.create_all`, so it is the one
|
||||
that ships.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -92,7 +86,7 @@ async def owner(db):
|
||||
|
||||
|
||||
async def test_the_migrated_schema_matches_the_models(db, owner):
|
||||
"""The check that has never run: insert through the ORM, read it back.
|
||||
"""Insert through the ORM, read it back.
|
||||
|
||||
A column the models expect and the migrations never created — or the reverse —
|
||||
fails right here, instead of when a container starts.
|
||||
@@ -109,7 +103,7 @@ async def test_the_migrated_schema_matches_the_models(db, owner):
|
||||
|
||||
|
||||
async def test_the_dropped_columns_are_actually_gone(db):
|
||||
"""M13 dropped three. If a migration silently no-opped, this is where it shows."""
|
||||
"""Dropped columns are really gone; a migration that silently no-opped shows here."""
|
||||
cols = set(
|
||||
(
|
||||
await db.execute(
|
||||
@@ -143,11 +137,7 @@ async def test_the_dropped_columns_are_actually_gone(db):
|
||||
|
||||
|
||||
async def test_the_search_vector_was_rebuilt_over_the_name(db, owner):
|
||||
"""0026 had to drop and recreate a STORED GENERATED column.
|
||||
|
||||
Postgres refuses to drop a column another generated column depends on, so getting
|
||||
this wrong doesn't produce a subtly wrong ranking — it produces a migration that
|
||||
won't run at all. Worth proving the replacement actually indexes something.
|
||||
"""0026 recreates a stored generated column; the replacement must index something.
|
||||
"""
|
||||
note = Note(owner_id=owner.id, body="ferry tickets\nbook before friday", display_title="ferry tickets")
|
||||
db.add(note)
|
||||
@@ -173,12 +163,7 @@ async def test_the_search_vector_was_rebuilt_over_the_name(db, owner):
|
||||
|
||||
|
||||
async def test_a_note_keeps_its_prose_on_both_sides_of_its_list(db, owner):
|
||||
"""The shape M304 made expressible at all.
|
||||
|
||||
The old model could not hold this: a row had a position in a table and none in the
|
||||
text, so a checklist could only ever render AFTER the body. Prose, list, prose is
|
||||
the case that proves the storage changed, not just the styling.
|
||||
"""
|
||||
"""Prose, list, prose: a checklist can sit between two paragraphs."""
|
||||
body = "weekend shop\n\n- [ ] milk\n- [x] eggs\n\nback before six"
|
||||
note = Note(owner_id=owner.id, body=body, display_title=derive_display_title(body))
|
||||
db.add(note)
|
||||
@@ -191,12 +176,7 @@ async def test_a_note_keeps_its_prose_on_both_sides_of_its_list(db, owner):
|
||||
|
||||
|
||||
async def test_ticking_an_item_is_a_body_edit(db, owner):
|
||||
"""What replaced `_apply_note_items`: there is no separate thing left to apply.
|
||||
|
||||
The regression that function guarded against — a sync silently eating a checklist
|
||||
off a note that also had a body — cannot recur, because there is nothing to delete.
|
||||
A pushed body either has the lines or it does not.
|
||||
"""
|
||||
"""A pushed body carries its checklist; there is nothing separate to apply."""
|
||||
body = "packing\n\n- [ ] socks"
|
||||
note = Note(owner_id=owner.id, body=body, display_title="packing")
|
||||
db.add(note)
|
||||
@@ -213,11 +193,10 @@ async def test_ticking_an_item_is_a_body_edit(db, owner):
|
||||
|
||||
|
||||
async def test_a_standalone_tag_leaves_the_body_and_becomes_an_ordinary_label(db, owner):
|
||||
"""M311. The tag was being shown twice — as text and as a chip — so the text goes.
|
||||
"""A tag on its own line leaves the text and shows as a chip.
|
||||
|
||||
`via_tag=False` is the load-bearing half. It is what makes the chip's × appear in
|
||||
both editors (they gate it on exactly this), which matters because deleting the
|
||||
text is no longer a way to remove the tag: there is no text.
|
||||
`via_tag=False` is what makes the chip's × appear in both editors, the only way
|
||||
to remove a tag with no text.
|
||||
"""
|
||||
note = Note(owner_id=owner.id, body="#todo\nreorganize the homepage", display_title="#todo")
|
||||
db.add(note)
|
||||
@@ -226,8 +205,7 @@ async def test_a_standalone_tag_leaves_the_body_and_becomes_an_ordinary_label(db
|
||||
await db.commit()
|
||||
|
||||
assert note.body == "reorganize the homepage"
|
||||
# Re-derived by the lift itself. Every caller sets display_title BEFORE calling,
|
||||
# so if the function did not do this the note would be named after a line it had
|
||||
# Re-derived by the lift itself, or the note would be named after a line it had
|
||||
# just deleted.
|
||||
assert note.display_title == "reorganize the homepage"
|
||||
|
||||
@@ -237,11 +215,8 @@ async def test_a_standalone_tag_leaves_the_body_and_becomes_an_ordinary_label(db
|
||||
|
||||
|
||||
async def test_a_tag_moved_onto_its_own_line_graduates_instead_of_vanishing(db, owner):
|
||||
"""The bug a naive lift has, pinned.
|
||||
|
||||
A tag that is still in prose stays derived. Move it to its own line and it must
|
||||
become an ordinary label — NOT be detached for no longer appearing in the body,
|
||||
which is what happens if the row is dropped before it is graduated.
|
||||
"""A tag in prose stays derived. Moved to its own line it becomes an ordinary
|
||||
label, rather than being detached for no longer appearing in the body.
|
||||
"""
|
||||
note = Note(owner_id=owner.id, body="call #mom tomorrow", display_title="call #mom tomorrow")
|
||||
db.add(note)
|
||||
@@ -265,8 +240,7 @@ async def test_a_tag_moved_onto_its_own_line_graduates_instead_of_vanishing(db,
|
||||
|
||||
|
||||
async def test_deleting_an_inline_tag_still_detaches_it(db, owner):
|
||||
"""The old behaviour, unchanged where the text is unchanged. A tag still living in
|
||||
prose is still owned by that prose."""
|
||||
"""A tag still in prose is still owned by that prose."""
|
||||
note = Note(owner_id=owner.id, body="call #mom tomorrow", display_title="call #mom tomorrow")
|
||||
db.add(note)
|
||||
await db.flush()
|
||||
@@ -281,11 +255,7 @@ async def test_deleting_an_inline_tag_still_detaches_it(db, owner):
|
||||
|
||||
|
||||
async def test_a_note_with_only_a_list_still_has_a_name(db, owner):
|
||||
"""The hole that made removing the title unsafe, still closed — by a different
|
||||
mechanism. There is no item table to fall back to any more; the name comes from
|
||||
the first line with its marker stripped, because calling the note "- [ ] milk"
|
||||
would show someone the storage instead of the note.
|
||||
"""
|
||||
"""A checklist-only note is named by its first item, without the marker."""
|
||||
body = "- [ ] milk\n- [ ] eggs"
|
||||
note = Note(owner_id=owner.id, body=body, display_title=derive_display_title(body))
|
||||
db.add(note)
|
||||
@@ -297,10 +267,9 @@ async def test_a_note_with_only_a_list_still_has_a_name(db, owner):
|
||||
async def test_auto_unfurl_stores_a_preview_and_skips_what_is_cached(db, owner, monkeypatch):
|
||||
"""The background pass, run inline so the assertions are deterministic.
|
||||
|
||||
The network is stubbed — this is about what reaches the DATABASE, not about
|
||||
parsing someone's OpenGraph tags (unfurl.py's own tests cover that). What matters
|
||||
here is the part only a real database can show: the unique constraint holding, the
|
||||
upsert going to the right row, and a second pass not re-fetching.
|
||||
The network is stubbed; this is about what reaches the database: the unique
|
||||
constraint holding, the upsert going to the right row, and a second pass not
|
||||
re-fetching.
|
||||
"""
|
||||
note = Note(
|
||||
owner_id=owner.id,
|
||||
@@ -373,11 +342,10 @@ async def test_detection_agrees_with_what_gets_stored(db, owner, monkeypatch):
|
||||
|
||||
|
||||
async def test_registration_closes_itself_once_an_admin_exists(app_client, db):
|
||||
"""The gap this removes: registration was open between "my account exists" and
|
||||
"I remembered to turn it off", and on a public host that gap starts at DNS.
|
||||
"""Registration closes as soon as the admin account exists.
|
||||
|
||||
Runs against a real database because it is the interaction between two writes —
|
||||
the user row and the settings row — inside one transaction.
|
||||
Real database: it is two writes, the user row and the settings row, in one
|
||||
transaction.
|
||||
"""
|
||||
# The instance is empty (the fixture truncated it), so this is the first account:
|
||||
# allowed unconditionally, and it becomes the admin.
|
||||
@@ -413,11 +381,9 @@ async def test_registration_closes_itself_once_an_admin_exists(app_client, db):
|
||||
|
||||
|
||||
async def test_security_settings_are_live_and_bounded(app_client, db):
|
||||
"""The security values are settings now, not constants — so saving one has to take
|
||||
effect without a restart, and a dangerous value has to be refused.
|
||||
|
||||
Real database because the whole point is the round trip: write through the admin
|
||||
API, re-read into the cache the throttle consults, observe the new number.
|
||||
"""Saving a security setting takes effect without a restart, and a dangerous
|
||||
value is refused: write through the admin API, re-read into the throttle's cache,
|
||||
observe the new number.
|
||||
"""
|
||||
# An admin to authenticate as. First account, so it is allowed and becomes admin.
|
||||
reset_live()
|
||||
@@ -433,9 +399,7 @@ async def test_security_settings_are_live_and_bounded(app_client, db):
|
||||
assert live("trusted_proxy_hops") == 1
|
||||
assert live("signin_limit_per_account") == 10
|
||||
|
||||
# A value that would disable the protection is REFUSED, not clamped — storing a
|
||||
# different number than the one typed is how somebody ends up believing a limit
|
||||
# is set to something it is not.
|
||||
# A value that would disable the protection is refused, not clamped.
|
||||
bad = await app_client.patch("/api/settings", json={"signin_limit_per_account": 0})
|
||||
assert bad.status_code == 400
|
||||
assert "at least" in (await bad.get_json())["error"]
|
||||
@@ -498,14 +462,8 @@ async def _revision_count(db, note_id) -> int:
|
||||
|
||||
|
||||
async def test_a_session_of_edits_costs_one_revision(db, owner):
|
||||
"""The change that makes autosave affordable.
|
||||
|
||||
Version history used to snapshot on EVERY body write, so the clients saved as
|
||||
rarely as they could — only when an editor closed — and a crash mid-session lost
|
||||
everything typed. Durability was paying for history. Now a sitting earns one
|
||||
revision no matter how many times it is written, so a client can write whenever
|
||||
it likes.
|
||||
"""
|
||||
"""A sitting earns one revision however often it is written, so clients can
|
||||
autosave."""
|
||||
note = Note(owner_id=owner.id, body="one", display_title="one")
|
||||
db.add(note)
|
||||
await db.commit()
|
||||
@@ -563,13 +521,8 @@ async def test_renaming_a_tag_onto_an_existing_one_merges_into_the_older(app_cli
|
||||
"""Renaming a tag onto a name another tag holds merges them, and the OLDER row
|
||||
is the survivor — whichever side the caller happened to be renaming.
|
||||
|
||||
Runs against a real database because the whole question is about `created_at`
|
||||
ordering and the note_labels rows moving, neither of which a unit test sees.
|
||||
|
||||
Age decides, rather than "the one that already held the name", so that renaming
|
||||
A→B and renaming B→A land on the same row. If the incumbent won, the survivor
|
||||
would depend on which way round someone typed it, and two clients racing the
|
||||
same tidy-up would disagree about which id still exists.
|
||||
Age decides, so renaming A→B and B→A land on the same row and two clients racing
|
||||
the same tidy-up agree on which id survives.
|
||||
"""
|
||||
reg = await app_client.post(
|
||||
"/api/auth/register",
|
||||
@@ -626,9 +579,8 @@ async def test_the_rename_merge_survivor_does_not_depend_on_the_direction(app_cl
|
||||
|
||||
|
||||
async def test_creating_a_tag_that_differs_only_in_case_returns_the_existing_one(app_client, db):
|
||||
"""A case-sensitive match here used to mint "Groceries" beside "groceries". No
|
||||
synced client can hold both — their `labels` index is unique on `lower(name)` —
|
||||
so the pair was a pull that would fail later, on a phone, with no UI in the path.
|
||||
"""Case-insensitive: no synced client can hold both "Groceries" and "groceries",
|
||||
since their `labels` index is unique on `lower(name)`.
|
||||
"""
|
||||
reg = await app_client.post(
|
||||
"/api/auth/register",
|
||||
@@ -646,12 +598,10 @@ async def test_creating_a_tag_that_differs_only_in_case_returns_the_existing_one
|
||||
assert len(listing) == 1
|
||||
|
||||
|
||||
# --- A deleted tag is a tombstone on every path (#5382) ---------------------------
|
||||
# --- A deleted tag is a tombstone on every path ----------------------------------
|
||||
#
|
||||
# The web deleted the row outright, so the change feed never carried the delete and
|
||||
# devices kept the tag. A device's delete left a tombstone the web still listed and
|
||||
# still matched by name. One definition now: `labeling.tombstone_label`, and every
|
||||
# read of the catalog goes through `labeling.live`.
|
||||
# So the change feed tells devices. One definition, `labeling.tombstone_label`, and
|
||||
# every read of the catalog goes through `labeling.live`.
|
||||
|
||||
|
||||
async def _label_in_feed(app_client, lid: str) -> dict | None:
|
||||
@@ -666,7 +616,7 @@ async def _push_label(app_client, lid: str, op: str, edited_at: str, name: str =
|
||||
|
||||
|
||||
async def test_a_tag_listing_says_when_each_was_last_used(app_client, db):
|
||||
"""The rail keeps the most recent few (#5931): a tag is as recent as the newest
|
||||
"""The rail keeps the most recent few: a tag is as recent as the newest
|
||||
note carrying it, and a note in the trash does not count."""
|
||||
await _signed_in(app_client, "lastused")
|
||||
used = await (await app_client.post("/api/labels", json={"name": "errands"})).get_json()
|
||||
@@ -751,11 +701,10 @@ async def test_a_device_can_make_a_deleted_tags_name_again(app_client, db):
|
||||
assert [lb["id"] for lb in listing] == [new]
|
||||
|
||||
|
||||
# --- One save path for a note's text (#5164) ---------------------------------
|
||||
# --- One save path for a note's text -----------------------------------------
|
||||
#
|
||||
# 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.
|
||||
# queue link previews. These pin the sequence at each route.
|
||||
|
||||
|
||||
async def _signed_in(app_client, who: str):
|
||||
@@ -774,9 +723,7 @@ def _record_previews(monkeypatch, module: str) -> list[tuple[str, str]]:
|
||||
|
||||
|
||||
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."""
|
||||
"""A link brought back by a restore gets its preview."""
|
||||
await _signed_in(app_client, "restore")
|
||||
queued = _record_previews(monkeypatch, "inkwell.notes")
|
||||
|
||||
@@ -845,8 +792,8 @@ async def test_a_pushed_note_is_named_tagged_and_queued_like_a_typed_one(app_cli
|
||||
|
||||
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."""
|
||||
Lifting a tag rewrites the body in a second flush, where the column's onupdate
|
||||
would 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(
|
||||
@@ -858,7 +805,7 @@ async def test_a_pushed_edit_keeps_the_clients_edit_time_when_a_tag_is_lifted(ap
|
||||
assert datetime.fromisoformat(note["updated_at"]) == datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
# --- attachments as a sync entity (#5168) ---------------------------------------
|
||||
# --- attachments as a sync entity -----------------------------------------------
|
||||
|
||||
|
||||
async def _note_revision(app_client, nid: str) -> int:
|
||||
@@ -912,8 +859,7 @@ async def test_an_offline_attachment_uploads_once_under_its_own_id(app_client, d
|
||||
|
||||
|
||||
async def test_a_purged_note_and_its_files_read_as_gone(app_client, db):
|
||||
"""A purged note reads as absent (#2128). Opening it and its attachment selected
|
||||
with the ACL inline and skipped that filter, so a tombstone came back 200 (#5383)."""
|
||||
"""A purged note reads as absent, opened directly or through its attachment."""
|
||||
await _signed_in(app_client, "purged")
|
||||
nid = await _pushed_note(app_client)
|
||||
aid = str(uuid.uuid4())
|
||||
@@ -1008,7 +954,7 @@ async def test_a_preview_arriving_or_leaving_moves_its_note_in_the_feed(app_clie
|
||||
assert await _note_revision(app_client, nid) > arrived
|
||||
|
||||
|
||||
# --- the export format, pinned against the shared fixture (#5170) ---------------
|
||||
# --- the export format, pinned against the shared fixture ----------------------
|
||||
|
||||
|
||||
async def test_an_export_writes_the_keys_the_shared_fixture_names(app_client, db):
|
||||
@@ -1043,7 +989,7 @@ async def test_an_export_writes_the_keys_the_shared_fixture_names(app_client, db
|
||||
|
||||
|
||||
async def test_a_keep_note_that_is_only_a_photo_imports(app_client, db):
|
||||
"""It used to be skipped as empty. The core's importer keeps it as well."""
|
||||
"""A photo-only note is a note, as in the core's importer."""
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
@@ -1066,7 +1012,7 @@ async def test_a_keep_note_that_is_only_a_photo_imports(app_client, db):
|
||||
assert att.mime == "image/png"
|
||||
|
||||
|
||||
# ---- invites (#5172) ---------------------------------------------------------------
|
||||
# ---- invites -----------------------------------------------------------------------
|
||||
|
||||
_PASSWORD = "a-long-enough-password"
|
||||
|
||||
@@ -1082,8 +1028,8 @@ async def _admin_with_invite(app_client, **body) -> str:
|
||||
|
||||
|
||||
async def test_the_first_account_is_refused_once_the_setup_window_has_closed(db):
|
||||
"""A fresh server on a public address must not belong to whoever finds it first
|
||||
(family idea #5105, practice 8). Past the window, nobody becomes admin."""
|
||||
"""A fresh server on a public address must not belong to whoever finds it first.
|
||||
Past the window, nobody becomes admin."""
|
||||
from inkwell.auth import SETUP_WINDOW_S
|
||||
|
||||
ratelimit.reset_all()
|
||||
@@ -1217,8 +1163,7 @@ async def test_two_people_racing_one_invite_cannot_both_get_in(app_client, db):
|
||||
assert count == 2, "the admin and exactly one of the two"
|
||||
|
||||
|
||||
|
||||
# --- Admin-issued password reset links (#5173) ---------------------------------
|
||||
# --- Admin-issued password reset links -----------------------------------------
|
||||
|
||||
|
||||
async def _admin_and_guest(app_client):
|
||||
@@ -1323,7 +1268,7 @@ async def test_a_short_password_leaves_the_reset_link_usable(app_client, db):
|
||||
assert ok.status_code == 200
|
||||
|
||||
|
||||
# --- Storage per account (#2939 §4) ---------------------------------------------
|
||||
# --- Storage per account --------------------------------------------------------
|
||||
|
||||
_GB = 1024**3
|
||||
|
||||
@@ -1417,7 +1362,7 @@ async def _back_to_default(key: str) -> None:
|
||||
await fresh.commit()
|
||||
|
||||
|
||||
# --- Changing a password, and signing out everywhere else (#5105, practice 4) ---
|
||||
# --- Changing a password, and signing out everywhere else ----------------------
|
||||
|
||||
|
||||
async def _elsewhere(app_client):
|
||||
@@ -1505,14 +1450,13 @@ async def test_sign_out_elsewhere_from_a_device_keeps_that_device(app_client, db
|
||||
|
||||
async def test_revoking_this_device_from_a_web_session_is_a_bad_request(app_client, db):
|
||||
"""A session-cookie caller holds no device token, so "revoke the one I'm using"
|
||||
has nothing to name. Moved here from the unit lane when the session check began
|
||||
reading the account (#5173)."""
|
||||
has nothing to name."""
|
||||
await _signed_in(app_client, "web")
|
||||
resp = await app_client.delete("/api/auth/devices/self")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
# --- The audit log (#2939 §5) --------------------------------------------------
|
||||
# --- The audit log -------------------------------------------------------------
|
||||
|
||||
|
||||
async def _activity(client) -> list[dict]:
|
||||
@@ -1580,7 +1524,7 @@ async def test_activity_older_than_its_retention_is_swept(app_client, db):
|
||||
assert emails == {"owner@example.test"}
|
||||
|
||||
|
||||
# --- Sharing a note (#5174) ---------------------------------------------------
|
||||
# --- Sharing a note -----------------------------------------------------------
|
||||
#
|
||||
# Owner, a recipient, and a stranger who is on the instance but not shared with.
|
||||
|
||||
@@ -1656,7 +1600,7 @@ async def test_a_view_share_writes_nothing_and_an_edit_share_writes_only_text(ap
|
||||
nid = await _owners_note(app_client, "first line\n- [ ] milk")
|
||||
await _share(app_client, nid, people["recipient"], "view")
|
||||
|
||||
# View: every write is a 404, the same answer a stranger gets (#1984).
|
||||
# View: every write is a 404, the same answer a stranger gets.
|
||||
writes = [
|
||||
recipient.patch(f"/api/notes/{nid}", json={"body": "mine now"}),
|
||||
recipient.patch(f"/api/notes/{nid}/items/0", json={"checked": True}),
|
||||
@@ -1684,7 +1628,7 @@ async def test_a_view_share_writes_nothing_and_an_edit_share_writes_only_text(ap
|
||||
assert "fromguest" in owner_tags
|
||||
assert (await (await recipient.get("/api/labels")).get_json())["labels"] == []
|
||||
|
||||
# Everything else but their own pin and archive (#5176) stays the owner's.
|
||||
# Everything but their own pin and archive stays the owner's.
|
||||
assert (await recipient.patch(f"/api/notes/{nid}", json={"remind_at": "2099-01-01T00:00:00Z"})).status_code == 403
|
||||
assert (await recipient.patch(f"/api/notes/{nid}", json={"body": "x", "recurrence": "daily"})).status_code == 403
|
||||
assert (await recipient.post(f"/api/notes/{nid}/trash")).status_code == 404
|
||||
@@ -1769,7 +1713,7 @@ async def test_a_share_names_someone_else_on_this_instance(app_client, db):
|
||||
assert (await app_client.delete(f"/api/notes/{other}/shares/{sid}")).status_code == 404
|
||||
|
||||
|
||||
# --- Shared notes over sync (#5175) -------------------------------------------
|
||||
# --- Shared notes over sync ---------------------------------------------------
|
||||
|
||||
|
||||
async def _feed(client, since: int = 0, shares: bool = True) -> dict:
|
||||
@@ -1794,7 +1738,7 @@ async def test_a_share_reaches_the_recipients_feed_and_a_revoke_takes_it_back(ap
|
||||
assert held is not None, "the grant moved the note past the recipient's cursor"
|
||||
assert (held["permission"], held["shared_by"]["display_name"], held["labels"]) == ("view", "owner", [])
|
||||
assert granted["revoked"] == []
|
||||
# A client that never asked for shares gets only its own notes, as before.
|
||||
# A client that never asked for shares gets only its own notes.
|
||||
assert _feed_note(await _feed(recipient, shares=False), nid) is None
|
||||
assert "revoked" not in await _feed(recipient, shares=False)
|
||||
assert _feed_note(await _feed(stranger), nid) is None
|
||||
@@ -1902,7 +1846,7 @@ async def test_deleting_a_shared_note_reaches_the_recipients_devices(app_client,
|
||||
assert _feed_note(gone, nid) is None
|
||||
|
||||
|
||||
# --- Groups (#5177) ------------------------------------------------------------
|
||||
# --- Groups --------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _group(app_client, name: str, *member_ids: str) -> str:
|
||||
@@ -1982,7 +1926,7 @@ async def test_deleting_a_group_ends_every_share_made_to_it(app_client, db):
|
||||
assert (await (await app_client.get(f"/api/notes/{nid}")).get_json())["shared"] is False
|
||||
|
||||
|
||||
# --- Password reset by email (#5266) ------------------------------------------
|
||||
# --- Password reset by email --------------------------------------------------
|
||||
#
|
||||
# The SMTP hand-off is replaced by a list; everything up to it is real.
|
||||
|
||||
@@ -2024,7 +1968,7 @@ async def test_the_smtp_password_never_leaves_the_server(app_client, db):
|
||||
assert (await app_client.patch("/api/settings", json={"smtp_password": ""})).status_code == 200
|
||||
async with session_scope() as fresh:
|
||||
assert await get_setting(fresh, "smtp_password") == "hunter2"
|
||||
# And the row itself holds it sealed, not as text (#5927).
|
||||
# And the row itself holds it sealed, not as text.
|
||||
row = await fresh.get(Setting, "smtp_password")
|
||||
assert "hunter2" not in row.value and sealing.PREFIX in row.value
|
||||
|
||||
@@ -2035,7 +1979,7 @@ async def test_the_smtp_password_never_leaves_the_server(app_client, db):
|
||||
async def test_a_credential_stored_before_sealing_is_sealed_and_a_lost_key_asks_again(app_client, db):
|
||||
await _mail_off()
|
||||
await _admin_with_invite(app_client)
|
||||
# As an instance from before #5927 left it: the password as plain JSON.
|
||||
# A password stored in the clear, as plain JSON.
|
||||
async with session_scope() as fresh:
|
||||
fresh.add(Setting(key="smtp_password", value='"hunter2"'))
|
||||
await fresh.commit()
|
||||
@@ -2150,7 +2094,7 @@ async def test_the_test_email_goes_to_the_admin_and_reports_a_failure(app_client
|
||||
assert "bad credentials" in (await failed.get_json())["error"]
|
||||
|
||||
|
||||
# --- backups (#5918–#5921) --------------------------------------------------------------
|
||||
# --- backups ------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _backup_settings_off() -> None:
|
||||
|
||||
Reference in New Issue
Block a user