docs: round-3 copy corrections, and pin the two behaviours nothing was guarding

Round 3's correctness/security pass came back clean; what was left said more than
the code does.

- emailOff promised the access-request list is where sign-ups show up. A sign-up
  on an address that is already approved or already pending adds nothing to it
  (roadmap UX-25), and "only" was falsified by this release's own inbox notice.
- deleteConfirm claimed the admin was dropping "an approval you already gave" —
  it is the confirmation for pending and denied rows too.
- sendFailed blamed the mail server for rejecting a message; the reason covers
  connect, DNS, TLS and login failures alike, so it now describes the symptom and
  leaves the diagnosis to the log.
- ADMIN_EMAILS re-promotion happens in the Google callback only, not on any
  sign-in.

Tests: the decided-list copy of the delete affordance was never rendered (both
new cases fail if that branch is dropped), and the throttle release — a
deliberate behaviour the notes advertise — was asserted by nothing; a drop with
no admin yet, or with a failing write, must not silence the following hour.
This commit is contained in:
2026-09-02 00:06:01 +02:00
parent 8d7aaad9a8
commit 2cba6b9fca
6 changed files with 66 additions and 8 deletions
+34
View File
@@ -102,6 +102,40 @@ def test_no_admins_means_no_notification_and_no_error(db, make_user):
assert _notifications(db, plain.id) == []
def test_a_drop_with_nobody_to_tell_does_not_burn_the_hour(db, make_user):
# The throttle window must only be spent on a notice that was actually written. Otherwise a drop
# during setup — before any admin exists — silences the next hour for the admin created a minute
# later, which is exactly the blind spot this alert exists to remove.
email_mod.send_test("someone@example.com") # no admins yet
admin = make_user(email="admin@example.com", role="admin")
assert email_mod.send_test("someone@example.com") is False
db.expire_all()
assert len(_notifications(db, admin.id)) == 1
def test_a_failed_alert_does_not_burn_the_hour_either(db, make_user, monkeypatch):
admin = make_user(email="admin@example.com", role="admin")
boom = {"on": True}
def _maybe_boom(*a, **kw):
if boom["on"]:
raise RuntimeError("notification store is down")
from app.notifications import create_notification as real
return real(*a, **kw)
monkeypatch.setattr(email_mod, "create_notification", _maybe_boom)
email_mod.send_test("someone@example.com") # the write blows up; nothing recorded
boom["on"] = False
assert email_mod.send_test("someone@example.com") is False
db.expire_all()
assert len(_notifications(db, admin.id)) == 1
def test_a_broken_alert_never_breaks_the_send(db, make_user, monkeypatch):
# The alert is the fallback path; if IT fails, the caller must still get the ordinary fail-soft
# False rather than an exception surfacing from a background task.
@@ -122,6 +122,30 @@ describe("Access requests — the delete affordance", () => {
expect(screen.queryByLabelText(/deleteLabel/)).toBeNull();
});
it("keeps the same control on the previously-decided list, where the case is commonest", async () => {
// The affordance exists twice — the pending rows and the collapsed "decided" list — and an
// approved address that has since registered lands in the second one. Both must behave alike.
listMock.mockResolvedValue([invite({ status: "approved", has_account: true })]);
renderPanel();
await openAccessTab();
fireEvent.click(await screen.findByText(/settings\.invites\.decided/));
const btn = await screen.findByLabelText("settings.invites.deleteHasAccount");
expect(btn.getAttribute("aria-disabled")).toBe("true");
expect((btn as HTMLButtonElement).disabled).toBe(false);
});
it("offers a working delete on a decided entry with no account", async () => {
listMock.mockResolvedValue([invite({ status: "denied" })]);
renderPanel();
await openAccessTab();
fireEvent.click(await screen.findByText(/settings\.invites\.decided/));
fireEvent.click(await screen.findByLabelText("settings.invites.deleteLabel:asker@example.com"));
await waitFor(() => expect(deleteMock).toHaveBeenCalledWith(1));
});
it("warns in the confirmation — before the click is irreversible — when the env still allows it", async () => {
listMock.mockResolvedValue([invite({ still_allowed_by_env: true })]);
renderPanel();
+1 -1
View File
@@ -32,6 +32,6 @@
"emailUndelivered": {
"title": "Email is not going out",
"notConfigured": "SMTP is not configured, so nothing this instance sends is being delivered — verification links, password resets and access requests alike. One of them was “{{subject}}”. Set it up under Configuration → Email / SMTP.",
"sendFailed": "The mail server rejected a message, so deliveries are failing. One of them was “{{subject}}”. Check Configuration → Email / SMTP and send a test."
"sendFailed": "Sending failed, so messages are not being delivered. One of them was “{{subject}}”. Check Configuration → Email / SMTP and send a test; the server log has the reason."
}
}
+3 -3
View File
@@ -169,12 +169,12 @@
"deleteLabel": "Delete {{email}} from the whitelist",
"deleteHint": "Forget this address. A later request from it comes back as a new one.",
"deleteTitle": "Delete whitelist entry?",
"deleteConfirm": "Forget {{email}}? Siftlode drops the record, including an approval you already gave, so anyone at that address would have to ask again.",
"deleteConfirm": "Forget {{email}}? Siftlode drops the record, so anyone at that address would have to ask again.",
"deleteHasAccount": "Someone has registered on this address — manage it under Users & roles, where deleting the account removes this entry too.",
"deleted": "Whitelist entry removed.",
"deleteFailed": "Could not remove the entry.",
"emailOff": "Email sending is not configured, so nobody gets a verification link and no request reaches you by email — you will only see sign-ups in this list. Fix it under Configuration → Email / SMTP.",
"emailOff": "Email sending is not configured, so nobody gets a verification link and no request reaches you by email. Watch this list and your inbox instead, and fix it under Configuration → Email / SMTP.",
"deletedStillAllowed": "Entry removed, but this address is still allow-listed by the server's configuration, so it keeps access. Remove it from ALLOWED_EMAILS / ADMIN_EMAILS to change that.",
"deleteConfirmStillAllowed": "Forget {{email}}? Careful: this address is also allow-listed in the server's configuration (ALLOWED_EMAILS / ADMIN_EMAILS), so removing this row will NOT take its access away — and an ADMIN_EMAILS address is made an admin again on its next sign-in."
"deleteConfirmStillAllowed": "Forget {{email}}? Careful: this address is also allow-listed in the server's configuration (ALLOWED_EMAILS / ADMIN_EMAILS), so removing this row will NOT take its access away — and an ADMIN_EMAILS address is made an admin again on its next Google sign-in."
}
}
+1 -1
View File
@@ -32,6 +32,6 @@
"emailUndelivered": {
"title": "Nem megy ki e-mail",
"notConfigured": "Az SMTP nincs beállítva, így semmi nem kézbesíthető, amit ez a példány küldene — megerősítő linkek, jelszó-visszaállítások és hozzáférés-kérelmek egyaránt. Az egyik ilyen: „{{subject}}”. Állítsd be a Konfiguráció → E-mail / SMTP fülön.",
"sendFailed": "A levelezőszerver visszautasított egy üzenetet, tehát a kézbesítés hibára fut. Az egyik ilyen: „{{subject}}”. Nézd meg a Konfiguráció → E-mail / SMTP fület, és küldj tesztet."
"sendFailed": "A küldés nem sikerült, így az üzenetek nem kézbesíthetők. Az egyik ilyen: „{{subject}}”. Nézd meg a Konfiguráció → E-mail / SMTP fület, és küldj tesztet; az okot a szerver naplója tartalmazza."
}
}
+3 -3
View File
@@ -169,12 +169,12 @@
"deleteLabel": "{{email}} törlése a whitelistáról",
"deleteHint": "A cím elfelejtése. Egy későbbi kérelme új kérelemként jelenik meg.",
"deleteTitle": "Törlöd a whitelista-bejegyzést?",
"deleteConfirm": "Elfelejted {{email}} címet? A Siftlode eldobja a bejegyzést, a korábban megadott jóváhagyással együtt, így az adott címről újra kérelmezni kell.",
"deleteConfirm": "Elfelejted {{email}} címet? A Siftlode eldobja a bejegyzést, így az adott címről újra kérelmezni kell.",
"deleteHasAccount": "Erre a címre már regisztrált valaki — a Felhasználók és szerepek fülön kezelhető, ahol a fiók törlése ezt a bejegyzést is eltávolítja.",
"deleted": "Whitelista-bejegyzés eltávolítva.",
"deleteFailed": "A bejegyzést nem sikerült eltávolítani.",
"emailOff": "Az e-mail-küldés nincs beállítva, így senki nem kap megerősítő linket, és e-mailben egy kérelem sem jut el hozzád — a regisztrációkat csak ebben a listában látod. A Konfiguráció → E-mail / SMTP fülön javítható.",
"emailOff": "Az e-mail-küldés nincs beállítva, így senki nem kap megerősítő linket, és e-mailben egy kérelem sem jut el hozzád. Ezt a listát és az értesítéseidet figyeld helyette, a javítás pedig a Konfiguráció → E-mail / SMTP fülön van.",
"deletedStillAllowed": "A bejegyzés törölve, de ezt a címet a szerver konfigurációja továbbra is engedélyezi, tehát megmarad a hozzáférése. Vedd ki az ALLOWED_EMAILS / ADMIN_EMAILS listából, ha ezen változtatnál.",
"deleteConfirmStillAllowed": "Elfelejted {{email}} címet? Figyelem: ezt a címet a szerver konfigurációja is engedélyezi (ALLOWED_EMAILS / ADMIN_EMAILS), így a bejegyzés törlése NEM veszi el a hozzáférését — sőt egy ADMIN_EMAILS cím a következő belépéskor újra adminná válik."
"deleteConfirmStillAllowed": "Elfelejted {{email}} címet? Figyelem: ezt a címet a szerver konfigurációja is engedélyezi (ALLOWED_EMAILS / ADMIN_EMAILS), így a bejegyzés törlése NEM veszi el a hozzáférését — sőt egy ADMIN_EMAILS cím a következő Google-belépéskor újra adminná válik."
}
}