diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 14:21:12 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 2114a54eb6335f97b0c276c4f1f224d45f46fd1a (patch) | |
| tree | dbb2aa53f1a82d652f7aeb03e29dee80b6e8e0a5 /packages/meshbay-hub/tests/test_mail_is_not_a_relay.py | |
| parent | ef4842644a207c3d1b6d6f06c1ad1055270ae283 (diff) | |
| download | meshbay-2114a54eb6335f97b0c276c4f1f224d45f46fd1a.tar.gz | |
feat(hub): the mail state is a panel section, and a ceiling falling is an event
The figure was a line beside the settings form, which is where it is changed
and not where it is watched. It sits with the other live figures under
Statistics now — four cards and, above them, a banner saying which of the two
ceilings has fallen. The two states are not the same to whoever is reading:
one means newcomers are turned away, the other means somebody locked out of
their account cannot get back in. The settings block keeps a line pointing at
it.
And an operator no longer has to be looking. When a global ceiling is reached
the administrators are notified — in `mail.py`, in its own session, never
raising, because this runs while a request is being refused and an alert that
fails must not turn a refusal into a 500. Once per hour, keyed on a row
rather than a flag in memory: a flood is what spends the budget, so one alert
per refusal would bury the message under its own cause, and a hub that is
refusing mail is a hub somebody is about to restart.
`/v1/admin/mail` gains `general_exhausted` and `all_exhausted` rather than
leaving the panel to compare two numbers.
Labels in all ten catalogues; `.warn-msg` for the middle state, on the
`--warn` token both themes already define.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'packages/meshbay-hub/tests/test_mail_is_not_a_relay.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_mail_is_not_a_relay.py | 75 |
1 files changed, 75 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py index 7c1a9f4..4e5bd6d 100644 --- a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py +++ b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py @@ -542,3 +542,78 @@ def test_the_hub_refuses_to_start_with_more_than_one_worker(): with pytest.raises(SystemExit) as exit_: single_worker_or_exit(workers) assert exit_.value.code == 2 + + +# ── The operator is told, not left to notice ───────────────────────────────── + +@pytest.mark.asyncio +async def test_spending_the_sign_up_share_notifies_the_administrators( + client, db_session): + """ + A refusal is otherwise a line in the journal. A hub that has stopped + sending sign-up codes looks, from every screen anyone opens, exactly like + one nobody is signing up to. + """ + from meshbay_hub.config import MailConfig + + headers = await _admin(client, db_session, "mailwatcher") + await client.delete("/v1/notifications", headers=headers) + + cfg = MailConfig() + general = cfg.hourly_budget - cfg.hourly_reserved_for_recovery + for i in range(general): + await _charge(db_session, "registration", f"ceiling{i}@example.test") + assert not await _charge(db_session, "registration", "over@example.test") + + r = await client.get("/v1/notifications", headers=headers) + assert r.status_code == 200, r.text + kinds = [n["kind"] for n in r.json()["notifications"]] + assert "mail_budget_general" in kinds, ( + "the sign-up ceiling fell and nobody was told") + assert "mail_budget_all" not in kinds, ( + "recovery still has its share; saying otherwise would be alarming and " + "wrong") + + +@pytest.mark.asyncio +async def test_the_administrators_are_told_once_an_hour_not_once_a_refusal( + client, db_session): + """A flood is what spends the budget, so a notification per refusal would + bury the one that matters under the thing that caused it.""" + from meshbay_hub.config import MailConfig + + headers = await _admin(client, db_session, "mailwatcher2") + await client.delete("/v1/notifications", headers=headers) + + cfg = MailConfig() + general = cfg.hourly_budget - cfg.hourly_reserved_for_recovery + for i in range(general): + await _charge(db_session, "registration", f"burst{i}@example.test") + for i in range(5): + assert not await _charge(db_session, "registration", f"over{i}@example.test") + + r = await client.get("/v1/notifications", headers=headers) + general_alerts = [n for n in r.json()["notifications"] + if n["kind"] == "mail_budget_general"] + assert len(general_alerts) == 1, f"{len(general_alerts)} alerts for one hour" + + +@pytest.mark.asyncio +async def test_the_panel_says_which_ceiling_has_fallen(client, db_session): + """Two states, and the difference matters to whoever is reading: one means + newcomers are turned away, the other means somebody locked out of their + account cannot get back in.""" + from meshbay_hub.config import MailConfig + + headers = await _admin(client, db_session, "mailwatcher3") + cfg = MailConfig() + + for i in range(cfg.hourly_budget - cfg.hourly_reserved_for_recovery): + await _charge(db_session, "registration", f"state{i}@example.test") + body = (await client.get("/v1/admin/mail", headers=headers)).json() + assert body["general_exhausted"] and not body["all_exhausted"] + + for i in range(cfg.hourly_reserved_for_recovery): + await _charge(db_session, "password_reset", f"rec{i}@example.test") + body = (await client.get("/v1/admin/mail", headers=headers)).json() + assert body["all_exhausted"] |