aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 14:21:12 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commit2114a54eb6335f97b0c276c4f1f224d45f46fd1a (patch)
treedbb2aa53f1a82d652f7aeb03e29dee80b6e8e0a5 /packages/meshbay-hub/tests/test_mail_is_not_a_relay.py
parentef4842644a207c3d1b6d6f06c1ad1055270ae283 (diff)
downloadmeshbay-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.py75
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"]