aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/MESHBAY_DESIGN.md3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js1
-rw-r--r--packages/meshbay-hub/tests/test_account_deletion.py42
-rw-r--r--packages/meshbay-hub/tests/test_admin.py56
-rw-r--r--packages/meshbay-hub/tests/test_admin_views.py22
-rw-r--r--packages/meshbay-hub/tests/test_device_auth.py46
-rw-r--r--packages/meshbay-hub/tests/test_federation.py4
-rw-r--r--packages/meshbay-hub/tests/test_group_description.py14
-rw-r--r--packages/meshbay-hub/tests/test_group_hosting.py32
-rw-r--r--packages/meshbay-hub/tests/test_group_leave_and_quota.py46
-rw-r--r--packages/meshbay-hub/tests/test_group_membership.py16
-rw-r--r--packages/meshbay-hub/tests/test_group_name_unique.py18
-rw-r--r--packages/meshbay-hub/tests/test_group_purge.py16
-rw-r--r--packages/meshbay-hub/tests/test_groups_self_service.py68
-rw-r--r--packages/meshbay-hub/tests/test_hub_api.py84
-rw-r--r--packages/meshbay-hub/tests/test_login_lockout.py84
-rw-r--r--packages/meshbay-hub/tests/test_mail_is_not_a_relay.py14
-rw-r--r--packages/meshbay-hub/tests/test_moderation.py2
-rw-r--r--packages/meshbay-hub/tests/test_node_auth.py38
-rw-r--r--packages/meshbay-hub/tests/test_node_ws_auth.py38
-rw-r--r--packages/meshbay-hub/tests/test_notification_dismissal.py10
-rw-r--r--packages/meshbay-hub/tests/test_notifications.py26
-rw-r--r--packages/meshbay-hub/tests/test_notifications_behaviour.py30
-rw-r--r--packages/meshbay-hub/tests/test_password_change.py48
-rw-r--r--packages/meshbay-hub/tests/test_password_reset.py72
-rw-r--r--packages/meshbay-hub/tests/test_public_groups_toggle.py34
-rw-r--r--packages/meshbay-hub/tests/test_recovery_email.py14
-rw-r--r--packages/meshbay-hub/tests/test_register_captcha.py2
-rw-r--r--packages/meshbay-hub/tests/test_revocation.py18
-rw-r--r--packages/meshbay-hub/tests/test_username_floor.py58
41 files changed, 531 insertions, 450 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 6053bd8..ca5ba8e 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -200,7 +200,8 @@ is by design; reading what *other* operators host is not, and does not follow
### 3.1 Accounts
An account lives on the hub: a username, an encrypted email address, a status and
-a role. The passphrase never leaves the client. It derives **two independent
+a role. A username is 8 to 64 characters, checked at registration only — accounts
+created under the older 3-character floor keep signing in. The passphrase never leaves the client. It derives **two independent
values**, both salted by the trimmed username:
| Value | Derivation | Consumer |
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 05f58cd..9150909 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -78,6 +78,10 @@ async def _verify_captcha_or_raise(token: str | None, request: Request) -> None:
# ── Models ────────────────────────────────────────────────────────────────────
+# Mirrored by USERNAME_MIN_LEN in static/auth-page.js; test_username_floor.py
+# holds the two equal.
+USERNAME_MIN_LEN = 8
+
class RegisterRequest(BaseModel):
username: str
email: str
@@ -92,9 +96,11 @@ class RegisterRequest(BaseModel):
@field_validator("username")
@classmethod
def username_valid(cls, v: str) -> str:
+ # Registration only. Login, reset and deletion take the name as stored,
+ # so accounts created under the older 3-character floor keep working.
v = v.strip()
- if len(v) < 3 or len(v) > 64:
- raise ValueError("username must be 3-64 chars")
+ if len(v) < USERNAME_MIN_LEN or len(v) > 64:
+ raise ValueError(f"username must be {USERNAME_MIN_LEN}-64 chars")
if not v.replace("_", "").replace("-", "").replace(".", "").isalnum():
raise ValueError("username: only letters, digits, -, _, .")
return v
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index d4dc5a4..04a00af 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -10,6 +10,9 @@ import { Icon } from './icon.js';
const PASSWORD_MIN_BITS = 60;
const PASSWORD_MIN_LEN = 12;
+// The hub's USERNAME_MIN_LEN (api/users.py), checked here so the refusal comes
+// before a passphrase derivation rather than after it.
+const USERNAME_MIN_LEN = 8;
// ── reCAPTCHA v2 helper ──────────────────────────────────────────────────────
@@ -284,6 +287,9 @@ export function RegisterPage() {
const onSubmit = async (e) => {
e.preventDefault();
const name = username.trim();
+ if (name.length < USERNAME_MIN_LEN) {
+ setError(t('register.err_username_len', { n: USERNAME_MIN_LEN })); return;
+ }
if (password !== confirm) { setError(t('register.err_mismatch')); return; }
if (password.length < PASSWORD_MIN_LEN) {
setError(t('register.err_min_len', { n: PASSWORD_MIN_LEN })); return;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 066994c..065e58e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -114,6 +114,7 @@ export default {
'welcome.download': 'Herunterladen (Beta)',
'welcome.legal': 'Rechtliche Hinweise',
'register.err_mismatch': 'Die Passwörter stimmen nicht überein',
+ 'register.err_username_len': 'Der Benutzername muss mindestens {n} Zeichen lang sein.',
'register.err_min_len': {
one: 'Verwenden Sie mindestens {n} Zeichen',
other: 'Verwenden Sie mindestens {n} Zeichen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 89b52a6..83a39e7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -117,6 +117,7 @@ export default {
'welcome.download': 'Download (beta)',
'welcome.legal': 'Legal information',
'register.err_mismatch': 'Passwords do not match',
+ 'register.err_username_len': 'Username must be at least {n} characters.',
'register.err_min_len': {
one: 'Use at least {n} character',
other: 'Use at least {n} characters',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index b5c8ad9..73757d0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -113,6 +113,7 @@ export default {
'welcome.download': 'Descargar (beta)',
'welcome.legal': 'Información legal',
'register.err_mismatch': 'Las contraseñas no coinciden',
+ 'register.err_username_len': 'El nombre de usuario debe tener al menos {n} caracteres.',
'register.err_min_len': {
one: 'Use al menos {n} carácter',
other: 'Use al menos {n} caracteres',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 689b24c..35bce2c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -113,6 +113,7 @@ export default {
'welcome.download': 'Télécharger (bêta)',
'welcome.legal': 'Informations légales',
'register.err_mismatch': 'Les mots de passe ne correspondent pas',
+ 'register.err_username_len': 'Le nom d\'utilisateur doit contenir au moins {n} caractères.',
'register.err_min_len': {
one: 'Utilisez au moins {n} caractère',
other: 'Utilisez au moins {n} caractères',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index fc7dbb2..edc0d97 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -114,6 +114,7 @@ export default {
'welcome.download': 'Scarica (beta)',
'welcome.legal': 'Note legali',
'register.err_mismatch': 'Le password non coincidono',
+ 'register.err_username_len': 'Il nome utente deve contenere almeno {n} caratteri.',
'register.err_min_len': {
one: 'Usi almeno {n} carattere',
other: 'Usi almeno {n} caratteri',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index da477cd..8667d20 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -114,6 +114,7 @@ export default {
'welcome.download': 'ダウンロード(ベータ版)',
'welcome.legal': '法的情報',
'register.err_mismatch': 'パスワードが一致しません',
+ 'register.err_username_len': 'ユーザー名は{n}文字以上にしてください。',
'register.err_min_len': {
other: '{n} 文字以上でご入力ください',
},
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index a8d67e5..fc2eccb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -114,6 +114,7 @@ export default {
'welcome.download': 'Downloaden (bèta)',
'welcome.legal': 'Juridische informatie',
'register.err_mismatch': 'De wachtwoorden komen niet overeen',
+ 'register.err_username_len': 'De gebruikersnaam moet minstens {n} tekens lang zijn.',
'register.err_min_len': {
one: 'Gebruik minstens {n} teken',
other: 'Gebruik minstens {n} tekens',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index ae98a65..af65489 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -117,6 +117,7 @@ export default {
'welcome.download': 'Pobierz (beta)',
'welcome.legal': 'Informacje prawne',
'register.err_mismatch': 'Hasła nie są zgodne',
+ 'register.err_username_len': 'Nazwa użytkownika musi mieć co najmniej {n} znaków.',
'register.err_min_len': {
one: 'Proszę użyć co najmniej {n} znaku',
few: 'Proszę użyć co najmniej {n} znaków',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index bd57f50..65e1ec1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -115,6 +115,7 @@ export default {
'welcome.download': 'Baixar (beta)',
'welcome.legal': 'Informações legais',
'register.err_mismatch': 'As senhas não coincidem',
+ 'register.err_username_len': 'O nome de usuário deve ter pelo menos {n} caracteres.',
'register.err_min_len': {
one: 'Use pelo menos {n} caractere',
other: 'Use pelo menos {n} caracteres',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 918b0ad..87257b4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -114,6 +114,7 @@ export default {
'welcome.download': '下载(测试版)',
'welcome.legal': '法律信息',
'register.err_mismatch': '两次输入的密码不一致',
+ 'register.err_username_len': '用户名至少需要 {n} 个字符。',
'register.err_min_len': {
other: '请至少使用 {n} 个字符',
},
diff --git a/packages/meshbay-hub/tests/test_account_deletion.py b/packages/meshbay-hub/tests/test_account_deletion.py
index 4cbca8b..3d3fee4 100644
--- a/packages/meshbay-hub/tests/test_account_deletion.py
+++ b/packages/meshbay-hub/tests/test_account_deletion.py
@@ -37,11 +37,11 @@ async def _register(client, username, password="a-long-enough-passphrase"):
@pytest.mark.asyncio
async def test_owner_can_delete_their_account(client, db_session):
- token, password = await _register(client, "leaver")
+ token, password = await _register(client, "leaver_test")
headers = {"Authorization": f"Bearer {token}"}
r = await client.request("DELETE", "/v1/users/me", headers=headers,
- json={"auth_key": _auth_key(password, "leaver")})
+ json={"auth_key": _auth_key(password, "leaver_test")})
assert r.status_code == 200, r.text
user = (await db_session.execute(
@@ -58,10 +58,10 @@ async def test_deleting_needs_the_passphrase_not_just_a_session(client):
A live token may be a borrowed laptop or a tab left open. Something
irreversible asks again.
"""
- token, _ = await _register(client, "careful")
+ token, _ = await _register(client, "careful_test")
r = await client.request("DELETE", "/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
- json={"auth_key": _auth_key("wrong one", "careful")})
+ json={"auth_key": _auth_key("wrong one", "careful_test")})
assert r.status_code == 403
me = await client.get("/v1/users/me",
@@ -89,13 +89,13 @@ async def test_owning_a_group_blocks_deletion(client):
Deleting an account that owns groups would strand their members, so it is
refused with the list rather than cascading into other people's data.
"""
- token, password = await _register(client, "owner")
+ token, password = await _register(client, "owner_test")
headers = {"Authorization": f"Bearer {token}"}
r = await client.post("/v1/groups", json={"name": "orphans"}, headers=headers)
assert r.status_code in (200, 201), r.text
r = await client.request("DELETE", "/v1/users/me", headers=headers,
- json={"auth_key": _auth_key(password, "owner")})
+ json={"auth_key": _auth_key(password, "owner_test")})
assert r.status_code == 409
assert "orphans" in r.json()["detail"]
@@ -103,20 +103,20 @@ async def test_owning_a_group_blocks_deletion(client):
@pytest.mark.asyncio
async def test_deletion_clears_memberships_notifications_and_tokens(
client, db_session):
- token, password = await _register(client, "member1")
- owner_token, _ = await _register(client, "grouper")
+ token, password = await _register(client, "member1_test")
+ owner_token, _ = await _register(client, "grouper_test")
g = await client.post("/v1/groups", json={"name": "shared"},
headers={"Authorization": f"Bearer {owner_token}"})
gid = g.json()["group_id"]
- await client.post(f"/v1/groups/{gid}/members/member1", json={},
+ await client.post(f"/v1/groups/{gid}/members/member1_test", json={},
headers={"Authorization": f"Bearer {owner_token}"})
uid = (await db_session.execute(
- select(User.id).where(User.username == "member1"))).scalar_one()
+ select(User.id).where(User.username == "member1_test"))).scalar_one()
await client.request("DELETE", "/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
- json={"auth_key": _auth_key(password, "member1")})
+ json={"auth_key": _auth_key(password, "member1_test")})
for model in (GroupMember, Notification, RefreshToken):
rows = (await db_session.execute(
@@ -142,10 +142,10 @@ async def test_deletion_clears_device_keys_and_swarm_sources(client, db_session)
"""
from meshbay_hub.db.models import SwarmSource, UserDevice
- token, password = await _register(client, "devicer")
+ token, password = await _register(client, "devicer_test")
headers = {"Authorization": f"Bearer {token}"}
uid = (await db_session.execute(
- select(User.id).where(User.username == "devicer"))).scalar_one()
+ select(User.id).where(User.username == "devicer_test"))).scalar_one()
r = await client.post("/v1/users/devices", headers=headers,
json={"pk_auth_ed25519": _device_pk(), "label": "desktop"})
@@ -161,7 +161,7 @@ async def test_deletion_clears_device_keys_and_swarm_sources(client, db_session)
select(SwarmSource).where(SwarmSource.node_id == uid))).scalars().all()
r = await client.request("DELETE", "/v1/users/me", headers=headers,
- json={"auth_key": _auth_key(password, "devicer")})
+ json={"auth_key": _auth_key(password, "devicer_test")})
assert r.status_code == 200, r.text
db_session.expire_all()
@@ -202,9 +202,9 @@ async def test_the_ip_log_survives_and_stays_attributable(client, db_session):
"""
from meshbay_hub.db.models import IPLog
- token, password = await _register(client, "logged")
+ token, password = await _register(client, "logged_test")
uid = (await db_session.execute(
- select(User.id).where(User.username == "logged"))).scalar_one()
+ select(User.id).where(User.username == "logged_test"))).scalar_one()
before = (await db_session.execute(
select(IPLog).where(IPLog.user_id == uid))).scalars().all()
@@ -212,7 +212,7 @@ async def test_the_ip_log_survives_and_stays_attributable(client, db_session):
await client.request("DELETE", "/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
- json={"auth_key": _auth_key(password, "logged")})
+ json={"auth_key": _auth_key(password, "logged_test")})
after = (await db_session.execute(
select(IPLog).where(IPLog.user_id == uid))).scalars().all()
@@ -226,10 +226,10 @@ async def test_a_deleted_account_cannot_keep_using_its_token(client):
status check refuses it straight away — a deleted account must not keep
reading groups until its token happens to expire.
"""
- token, password = await _register(client, "gone")
+ token, password = await _register(client, "gone_test")
headers = {"Authorization": f"Bearer {token}"}
r = await client.request("DELETE", "/v1/users/me", headers=headers,
- json={"auth_key": _auth_key(password, "gone")})
+ json={"auth_key": _auth_key(password, "gone_test")})
assert r.status_code == 200
after = await client.get("/v1/groups/mine", headers=headers)
@@ -239,9 +239,9 @@ async def test_a_deleted_account_cannot_keep_using_its_token(client):
@pytest.mark.asyncio
async def test_only_an_admin_may_delete_someone_else(client, db_session):
token, _ = await _register(client, "ordinary")
- victim_token, _ = await _register(client, "victim")
+ victim_token, _ = await _register(client, "victim_test")
victim_id = (await db_session.execute(
- select(User.id).where(User.username == "victim"))).scalar_one()
+ select(User.id).where(User.username == "victim_test"))).scalar_one()
r = await client.delete(f"/v1/admin/users/{victim_id}",
headers={"Authorization": f"Bearer {token}"})
diff --git a/packages/meshbay-hub/tests/test_admin.py b/packages/meshbay-hub/tests/test_admin.py
index ad48487..6f90ad0 100644
--- a/packages/meshbay-hub/tests/test_admin.py
+++ b/packages/meshbay-hub/tests/test_admin.py
@@ -33,7 +33,7 @@ async def _login(client, username, password="testpass99"):
return r.json()["access_token"]
-async def _setup_admin(client, admin_name="admin"):
+async def _setup_admin(client, admin_name="admin_test"):
user_id = await _register(client, admin_name, email=f"{admin_name}@x.com")
set_admin_usernames([admin_name])
token = await _login(client, admin_name)
@@ -44,8 +44,8 @@ async def _setup_admin(client, admin_name="admin"):
@pytest.mark.asyncio
async def test_admin_stats_requires_moderator(client):
- await _register(client, "normie")
- token = await _login(client, "normie")
+ await _register(client, "normie_test")
+ token = await _login(client, "normie_test")
r = await client.get("/v1/admin/stats", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 403
@@ -64,14 +64,14 @@ async def test_admin_stats_allowed_for_config_admin(client):
@pytest.mark.asyncio
async def test_admin_stats_allowed_for_db_moderator(client):
- _, admin_token = await _setup_admin(client, "boss")
- mod_id = await _register(client, "moduser")
+ _, admin_token = await _setup_admin(client, "boss_test")
+ mod_id = await _register(client, "moduser_test")
r = await client.patch(f"/v1/admin/users/{mod_id}",
json={"role": "moderator"},
headers={"Authorization": f"Bearer {admin_token}"})
assert r.status_code == 200
- mod_token = await _login(client, "moduser")
+ mod_token = await _login(client, "moduser_test")
r = await client.get("/v1/admin/stats", headers={"Authorization": f"Bearer {mod_token}"})
assert r.status_code == 200
@@ -81,8 +81,8 @@ async def test_admin_stats_allowed_for_db_moderator(client):
@pytest.mark.asyncio
async def test_admin_list_users(client):
_, token = await _setup_admin(client)
- await _register(client, "alice", email="a@x.com")
- await _register(client, "bob", email="b@x.com")
+ await _register(client, "alice_test", email="a@x.com")
+ await _register(client, "bob_test", email="b@x.com")
r = await client.get("/v1/admin/users", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
@@ -94,27 +94,27 @@ async def test_admin_list_users(client):
@pytest.mark.asyncio
async def test_admin_list_users_search(client):
_, token = await _setup_admin(client)
- await _register(client, "alice", email="a@x.com")
- await _register(client, "bob", email="b@x.com")
+ await _register(client, "alice_test", email="a@x.com")
+ await _register(client, "bob_test", email="b@x.com")
r = await client.get("/v1/admin/users?q=ali",
headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
data = r.json()
assert data["total"] == 1
- assert data["users"][0]["username"] == "alice"
+ assert data["users"][0]["username"] == "alice_test"
@pytest.mark.asyncio
async def test_admin_get_user_detail(client):
_, token = await _setup_admin(client)
- uid = await _register(client, "alice", email="alice@example.com")
+ uid = await _register(client, "alice_test", email="alice@example.com")
r = await client.get(f"/v1/admin/users/{uid}",
headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
data = r.json()
- assert data["username"] == "alice"
+ assert data["username"] == "alice_test"
assert data["role"] == "user"
assert data["status"] == "active"
assert data["group_count"] == 0
@@ -124,8 +124,8 @@ async def test_admin_get_user_detail(client):
@pytest.mark.asyncio
async def test_admin_suspend_unsuspend_user(client):
_, token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ uid = await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
r = await client.patch(f"/v1/admin/users/{uid}",
json={"status": "suspended"},
@@ -151,7 +151,7 @@ async def test_admin_suspend_unsuspend_user(client):
@pytest.mark.asyncio
async def test_admin_change_role(client):
_, token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
+ uid = await _register(client, "alice_test", email="a@x.com")
r = await client.patch(f"/v1/admin/users/{uid}",
json={"role": "moderator"},
@@ -165,14 +165,14 @@ async def test_moderator_cannot_change_roles_or_revoke(client):
"""A moderator suspends and restores (reversible); it cannot promote anyone
or hard-revoke, which would be a path from the moderation role to full
instance control."""
- _, admin_token = await _setup_admin(client, "boss")
- mod_id = await _register(client, "moduser")
+ _, admin_token = await _setup_admin(client, "boss_test")
+ mod_id = await _register(client, "moduser_test")
await client.patch(f"/v1/admin/users/{mod_id}", json={"role": "moderator"},
headers={"Authorization": f"Bearer {admin_token}"})
- mod_token = await _login(client, "moduser")
+ mod_token = await _login(client, "moduser_test")
mod_h = {"Authorization": f"Bearer {mod_token}"}
- victim = await _register(client, "victim", email="v@x.com")
+ victim = await _register(client, "victim_test", email="v@x.com")
# No promoting an accomplice.
r = await client.patch(f"/v1/admin/users/{victim}", json={"role": "admin"},
@@ -185,7 +185,7 @@ async def test_moderator_cannot_change_roles_or_revoke(client):
assert r.status_code == 403
# No touching an admin's account.
- admin2 = await _register(client, "admin2", email="a2@x.com")
+ admin2 = await _register(client, "admin2_test", email="a2@x.com")
await client.patch(f"/v1/admin/users/{admin2}", json={"role": "admin"},
headers={"Authorization": f"Bearer {admin_token}"})
r = await client.patch(f"/v1/admin/users/{admin2}", json={"status": "suspended"},
@@ -210,7 +210,7 @@ async def test_admin_cannot_modify_self(client):
@pytest.mark.asyncio
async def test_admin_invalid_role_rejected(client):
_, token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
+ uid = await _register(client, "alice_test", email="a@x.com")
r = await client.patch(f"/v1/admin/users/{uid}",
json={"role": "superuser"},
headers={"Authorization": f"Bearer {token}"})
@@ -256,7 +256,7 @@ async def test_admin_suspend_group(client):
@pytest.mark.asyncio
async def test_admin_logs(client):
_, token = await _setup_admin(client)
- await _register(client, "alice", email="a@x.com")
+ await _register(client, "alice_test", email="a@x.com")
r = await client.get("/v1/admin/logs",
headers={"Authorization": f"Bearer {token}"})
@@ -271,8 +271,8 @@ async def test_admin_logs(client):
@pytest.mark.asyncio
async def test_admin_logs_filter_by_event(client):
_, token = await _setup_admin(client)
- await _register(client, "alice", email="a@x.com")
- await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ await _login(client, "alice_test")
r = await client.get("/v1/admin/logs?event=login",
headers={"Authorization": f"Bearer {token}"})
@@ -285,12 +285,12 @@ async def test_admin_logs_filter_by_event(client):
@pytest.mark.asyncio
async def test_users_me(client):
- await _register(client, "alice", email="a@x.com")
- token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ token = await _login(client, "alice_test")
r = await client.get("/v1/users/me",
headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
data = r.json()
- assert data["username"] == "alice"
+ assert data["username"] == "alice_test"
assert data["role"] == "user"
assert data["status"] == "active"
diff --git a/packages/meshbay-hub/tests/test_admin_views.py b/packages/meshbay-hub/tests/test_admin_views.py
index 29f41d8..5017b76 100644
--- a/packages/meshbay-hub/tests/test_admin_views.py
+++ b/packages/meshbay-hub/tests/test_admin_views.py
@@ -31,7 +31,7 @@ async def _user(client, username, password="a-long-enough-passphrase"):
return {"Authorization": f"Bearer {r.json()['access_token']}"}
-async def _admin(client, db_session, username="root"):
+async def _admin(client, db_session, username="root_test"):
headers = await _user(client, username)
user = (await db_session.execute(
select(User).where(User.username == username))).scalar_one()
@@ -43,12 +43,12 @@ async def _admin(client, db_session, username="root"):
@pytest.mark.asyncio
async def test_a_deleted_account_stops_being_counted(client, db_session):
admin = await _admin(client, db_session)
- leaver = await _user(client, "ghost")
+ leaver = await _user(client, "ghost_test")
before = (await client.get("/v1/admin/stats", headers=admin)).json()["users"]
await client.request("DELETE", "/v1/users/me", headers=leaver,
json={"auth_key": _auth_key(
- "a-long-enough-passphrase", "ghost")})
+ "a-long-enough-passphrase", "ghost_test")})
after = (await client.get("/v1/admin/stats", headers=admin)).json()["users"]
assert after == before - 1, "a tombstone is still being counted as a user"
@@ -56,7 +56,7 @@ async def test_a_deleted_account_stops_being_counted(client, db_session):
@pytest.mark.asyncio
async def test_a_deleted_account_is_not_listed(client, db_session):
- admin = await _admin(client, db_session, "root2")
+ admin = await _admin(client, db_session, "root2_test")
leaver = await _user(client, "vanishing")
await client.request("DELETE", "/v1/users/me", headers=leaver,
@@ -73,21 +73,21 @@ async def test_a_deleted_account_is_not_listed(client, db_session):
@pytest.mark.asyncio
async def test_the_member_list_of_a_group_skips_them(client, db_session):
- admin = await _admin(client, db_session, "root3")
- owner = await _user(client, "host3")
- leaver = await _user(client, "quitter")
+ admin = await _admin(client, db_session, "root3_test")
+ owner = await _user(client, "host3_test")
+ leaver = await _user(client, "quitter_test")
g = await client.post("/v1/groups", json={"name": "party"}, headers=owner)
gid = g.json()["group_id"]
- await client.post(f"/v1/groups/{gid}/members/quitter", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/quitter_test", json={}, headers=owner)
await client.request("DELETE", "/v1/users/me", headers=leaver,
json={"auth_key": _auth_key(
- "a-long-enough-passphrase", "quitter")})
+ "a-long-enough-passphrase", "quitter_test")})
members = (await client.get(f"/v1/groups/{gid}/members",
headers=owner)).json()["members"]
- assert [m["username"] for m in members] == ["host3"]
+ assert [m["username"] for m in members] == ["host3_test"]
groups = (await client.get("/v1/admin/groups", headers=admin)).json()["groups"]
party = next(g for g in groups if g["name"] == "party")
@@ -114,7 +114,7 @@ async def test_a_node_is_recorded_at_the_address_it_announced_from(
from meshbay_hub.db.models import Node
- admin = await _admin(client, db_session, "root4")
+ admin = await _admin(client, db_session, "root4_test")
owner = await _user(client, "nodeowner")
uid = (await db_session.execute(
select(User.id).where(User.username == "nodeowner"))).scalar_one()
diff --git a/packages/meshbay-hub/tests/test_device_auth.py b/packages/meshbay-hub/tests/test_device_auth.py
index 752a0e6..e899f12 100644
--- a/packages/meshbay-hub/tests/test_device_auth.py
+++ b/packages/meshbay-hub/tests/test_device_auth.py
@@ -39,7 +39,7 @@ def _sign(sk, username: str, ts: int | None = None) -> dict:
"signature": base64.b64encode(sk.sign(message)).decode()}
-async def _account(client, username="alice") -> str:
+async def _account(client, username="alice_test") -> str:
await client.post("/v1/users/register", json={
"username": username, "auth_key": "k" * 44,
"email": f"{username}@example.invalid"})
@@ -62,7 +62,7 @@ async def test_a_registered_device_signs_in(client):
sk, pk = _device()
assert (await _register_device(client, token, pk, "laptop")).status_code == 201
- resp = await client.post("/v1/users/auth", json=_sign(sk, "alice"))
+ resp = await client.post("/v1/users/auth", json=_sign(sk, "alice_test"))
assert resp.status_code == 200, resp.text
body = resp.json()
@@ -77,12 +77,12 @@ async def test_the_session_it_returns_is_a_real_one(client):
await _register_device(client, token, pk)
device_token = (await client.post(
- "/v1/users/auth", json=_sign(sk, "alice"))).json()["access_token"]
+ "/v1/users/auth", json=_sign(sk, "alice_test"))).json()["access_token"]
me = await client.get("/v1/users/me",
headers={"Authorization": f"Bearer {device_token}"})
assert me.status_code == 200
- assert me.json()["username"] == "alice"
+ assert me.json()["username"] == "alice_test"
async def test_several_devices_on_one_account(client):
@@ -95,7 +95,7 @@ async def test_several_devices_on_one_account(client):
for sk in (sk_a, sk_b):
assert (await client.post("/v1/users/auth",
- json=_sign(sk, "alice"))).status_code == 200
+ json=_sign(sk, "alice_test"))).status_code == 200
listed = await client.get("/v1/users/devices",
headers={"Authorization": f"Bearer {token}"})
@@ -108,20 +108,20 @@ async def test_an_unregistered_key_is_refused(client):
await _account(client)
sk, _ = _device()
- resp = await client.post("/v1/users/auth", json=_sign(sk, "alice"))
+ resp = await client.post("/v1/users/auth", json=_sign(sk, "alice_test"))
assert resp.status_code == 401
async def test_another_accounts_device_cannot_sign_in_as_you(client):
- token_a = await _account(client, "alice")
- await _account(client, "bob")
+ token_a = await _account(client, "alice_test")
+ await _account(client, "bob_test")
sk, pk = _device()
await _register_device(client, token_a, pk)
# Alice's device, Bob's name. The signature covers the username, so it does
# not verify — and even if it did, the key is not on Bob's account.
- resp = await client.post("/v1/users/auth", json=_sign(sk, "bob"))
+ resp = await client.post("/v1/users/auth", json=_sign(sk, "bob_test"))
assert resp.status_code == 401
@@ -133,7 +133,7 @@ async def test_a_stale_signature_is_refused(client):
await _register_device(client, token, pk)
old = int(time.time()) - 3600
- resp = await client.post("/v1/users/auth", json=_sign(sk, "alice", ts=old))
+ resp = await client.post("/v1/users/auth", json=_sign(sk, "alice_test", ts=old))
assert resp.status_code == 401
assert "timestamp" in resp.json()["detail"].lower()
@@ -144,7 +144,7 @@ async def test_a_signature_for_a_different_timestamp_does_not_verify(client):
sk, pk = _device()
await _register_device(client, token, pk)
- signed = _sign(sk, "alice")
+ signed = _sign(sk, "alice_test")
signed["timestamp"] = signed["timestamp"] + 1 # inside the window, wrong
assert (await client.post("/v1/users/auth", json=signed)).status_code == 401
@@ -164,14 +164,14 @@ async def test_a_device_cannot_enrol_itself(client):
# what matters is that nothing was created.
assert resp.status_code in (401, 403, 422)
signed_in = await client.post("/v1/users/auth", json=_sign(
- Ed25519PrivateKey.generate(), "alice"))
+ Ed25519PrivateKey.generate(), "alice_test"))
assert signed_in.status_code == 401
async def test_one_key_belongs_to_one_account(client):
"""Sharing it would make "who signed in" a question with two answers."""
- token_a = await _account(client, "alice")
- token_b = await _account(client, "bob")
+ token_a = await _account(client, "alice_test")
+ token_b = await _account(client, "bob_test")
_, pk = _device()
await _register_device(client, token_a, pk)
@@ -189,11 +189,11 @@ async def test_a_suspended_account_cannot_sign_in_with_a_device(client):
from meshbay_hub.db.models import User
from sqlalchemy import update
async with get_session_factory()() as s:
- await s.execute(update(User).where(User.username == "alice")
+ await s.execute(update(User).where(User.username == "alice_test")
.values(status="suspended"))
await s.commit()
- resp = await client.post("/v1/users/auth", json=_sign(sk, "alice"))
+ resp = await client.post("/v1/users/auth", json=_sign(sk, "alice_test"))
assert resp.status_code == 403
@@ -212,13 +212,13 @@ async def test_the_hub_publishes_no_device_keys(client):
auth keys must stay invisible to everyone but the hub: no endpoint returns
another account's, and `/pubkeys` must not grow one.
"""
- token = await _account(client, "alice")
+ token = await _account(client, "alice_test")
_, pk = _device()
await _register_device(client, token, pk)
# `/pubkeys` is itself behind a session — it is an account lookup for
# invitations, not a public directory — so ask it as a signed-in member.
- public = await client.get("/v1/users/alice/pubkeys",
+ public = await client.get("/v1/users/alice_test/pubkeys",
headers={"Authorization": f"Bearer {token}"})
assert public.status_code == 200
body = public.text
@@ -227,8 +227,8 @@ async def test_the_hub_publishes_no_device_keys(client):
async def test_you_cannot_read_another_accounts_devices(client):
- token_a = await _account(client, "alice")
- token_b = await _account(client, "bob")
+ token_a = await _account(client, "alice_test")
+ token_b = await _account(client, "bob_test")
_, pk = _device()
await _register_device(client, token_a, pk, "alice-laptop")
@@ -250,12 +250,12 @@ async def test_removing_a_device_stops_it_signing_in(client):
assert gone.status_code == 200
assert (await client.post("/v1/users/auth",
- json=_sign(sk, "alice"))).status_code == 401
+ json=_sign(sk, "alice_test"))).status_code == 401
async def test_you_cannot_remove_someone_elses_device(client):
- token_a = await _account(client, "alice")
- token_b = await _account(client, "bob")
+ token_a = await _account(client, "alice_test")
+ token_b = await _account(client, "bob_test")
_, pk = _device()
device_id = (await _register_device(client, token_a, pk)).json()["id"]
diff --git a/packages/meshbay-hub/tests/test_federation.py b/packages/meshbay-hub/tests/test_federation.py
index 17d4161..6303799 100644
--- a/packages/meshbay-hub/tests/test_federation.py
+++ b/packages/meshbay-hub/tests/test_federation.py
@@ -65,7 +65,7 @@ def _auth_key(password: str, username: str) -> str:
hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 600_000, 32)).decode()
-async def _admin(client, username="root"):
+async def _admin(client, username="root_test"):
pw = "a-long-enough-passphrase"
await client.post("/v1/users/register", json={
"username": username, "email": f"{username}@example.com",
@@ -158,7 +158,7 @@ async def test_a_federated_id_cannot_shadow_a_local_group(client):
peer = Peer("peer-a.example")
await _register_peer(client, admin, peer)
- owner = await _admin(client, "owner")
+ owner = await _admin(client, "owner_test")
r = await client.post("/v1/groups", headers=owner, json={
"name": "mine", "visibility": "public", "join_policy": "open"})
local_id = r.json()["group_id"]
diff --git a/packages/meshbay-hub/tests/test_group_description.py b/packages/meshbay-hub/tests/test_group_description.py
index b18c90a..b41a7db 100644
--- a/packages/meshbay-hub/tests/test_group_description.py
+++ b/packages/meshbay-hub/tests/test_group_description.py
@@ -35,7 +35,7 @@ async def _group(client, headers, name="described", **kw):
@pytest.mark.asyncio
async def test_the_owner_can_write_a_description(client):
- owner = await _user(client, "writer")
+ owner = await _user(client, "writer_test")
gid = await _group(client, owner)
r = await client.patch(f"/v1/groups/{gid}",
@@ -49,10 +49,10 @@ async def test_the_owner_can_write_a_description(client):
@pytest.mark.asyncio
async def test_a_member_cannot(client):
- owner = await _user(client, "owner2")
- member = await _user(client, "member2")
+ owner = await _user(client, "owner2_test")
+ member = await _user(client, "member2_test")
gid = await _group(client, owner, name="not-yours")
- await client.post(f"/v1/groups/{gid}/members/member2", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/member2_test", json={}, headers=owner)
r = await client.patch(f"/v1/groups/{gid}",
json={"description": "mine now"}, headers=member)
@@ -61,7 +61,7 @@ async def test_a_member_cannot(client):
@pytest.mark.asyncio
async def test_an_empty_description_clears_it(client):
- owner = await _user(client, "clearer")
+ owner = await _user(client, "clearer_test")
gid = await _group(client, owner, name="clearme", description="temporary")
r = await client.patch(f"/v1/groups/{gid}", json={"description": " "},
@@ -77,7 +77,7 @@ async def test_the_terms_members_joined_on_are_not_editable(client):
members agreed to be in. Changing that needs a decision about who gets told,
so the endpoint ignores it rather than half-implementing it.
"""
- owner = await _user(client, "sneaky")
+ owner = await _user(client, "sneaky_test")
gid = await _group(client, owner, name="private-please", visibility="private")
await client.patch(f"/v1/groups/{gid}",
@@ -94,7 +94,7 @@ async def test_the_terms_members_joined_on_are_not_editable(client):
@pytest.mark.asyncio
async def test_a_long_description_is_truncated_not_refused(client):
- owner = await _user(client, "verbose")
+ owner = await _user(client, "verbose_test")
gid = await _group(client, owner, name="long")
r = await client.patch(f"/v1/groups/{gid}", json={"description": "x" * 900},
diff --git a/packages/meshbay-hub/tests/test_group_hosting.py b/packages/meshbay-hub/tests/test_group_hosting.py
index ad5f57d..c571771 100644
--- a/packages/meshbay-hub/tests/test_group_hosting.py
+++ b/packages/meshbay-hub/tests/test_group_hosting.py
@@ -58,7 +58,7 @@ async def _mark_hosted(db_session, group_id, when=None):
@pytest.mark.asyncio
async def test_the_owner_sees_their_unhosted_group(client):
"""They have to: it is the page they set the node up from."""
- owner = await _user(client, "setup1")
+ owner = await _user(client, "setup1_test")
r = await _group(client, owner, "not-yet")
assert r.status_code == 201
@@ -70,7 +70,7 @@ async def test_the_owner_sees_their_unhosted_group(client):
@pytest.mark.asyncio
async def test_a_member_does_not_see_an_unhosted_group(client):
"""A name they cannot open, with no way to say why, is worse than nothing."""
- owner = await _user(client, "setup2")
+ owner = await _user(client, "setup2_test")
member = await _user(client, "early_bird")
gid = (await _group(client, owner, "premature")).json()["group_id"]
await client.post(f"/v1/groups/{gid}/members/early_bird", json={}, headers=owner)
@@ -81,10 +81,10 @@ async def test_a_member_does_not_see_an_unhosted_group(client):
@pytest.mark.asyncio
async def test_a_member_sees_it_once_a_node_has_announced_it(client, db_session):
- owner = await _user(client, "setup3")
- member = await _user(client, "patient")
+ owner = await _user(client, "setup3_test")
+ member = await _user(client, "patient_test")
gid = (await _group(client, owner, "ready")).json()["group_id"]
- await client.post(f"/v1/groups/{gid}/members/patient", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/patient_test", json={}, headers=owner)
await _mark_hosted(db_session, gid)
@@ -95,7 +95,7 @@ async def test_a_member_sees_it_once_a_node_has_announced_it(client, db_session)
@pytest.mark.asyncio
async def test_the_public_directory_hides_unhosted_groups(client, db_session):
- owner = await _user(client, "setup4")
+ owner = await _user(client, "setup4_test")
hidden = (await _group(client, owner, "pub-unhosted", "public")).json()["group_id"]
shown = (await _group(client, owner, "pub-hosted", "public")).json()["group_id"]
await _mark_hosted(db_session, shown)
@@ -109,7 +109,7 @@ async def test_the_public_directory_hides_unhosted_groups(client, db_session):
@pytest.mark.asyncio
async def test_a_group_stays_visible_when_its_node_goes_offline(client, db_session):
"""`hosted_at` records that a node existed, not that one is answering now."""
- owner = await _user(client, "setup5")
+ owner = await _user(client, "setup5_test")
gid = (await _group(client, owner, "quiet-node", "public")).json()["group_id"]
await _mark_hosted(db_session, gid)
@@ -121,7 +121,7 @@ async def test_a_group_stays_visible_when_its_node_goes_offline(client, db_sessi
@pytest.mark.asyncio
async def test_a_fresh_unhosted_group_is_left_alone(client, db_session):
- owner = await _user(client, "reaper1")
+ owner = await _user(client, "reaper1_test")
gid = (await _group(client, owner, "brand-new")).json()["group_id"]
assert await find_unhosted_groups(db_session) == []
@@ -130,7 +130,7 @@ async def test_a_fresh_unhosted_group_is_left_alone(client, db_session):
@pytest.mark.asyncio
async def test_an_unhosted_group_past_the_grace_period_is_collected(client, db_session):
- owner = await _user(client, "reaper2")
+ owner = await _user(client, "reaper2_test")
gid = (await _group(client, owner, "abandoned")).json()["group_id"]
g = await db_session.get(Group, gid)
@@ -145,7 +145,7 @@ async def test_an_unhosted_group_past_the_grace_period_is_collected(client, db_s
@pytest.mark.asyncio
async def test_an_old_group_that_was_hosted_is_never_collected(client, db_session):
"""The whole point of the column: age alone must not condemn a group."""
- owner = await _user(client, "reaper3")
+ owner = await _user(client, "reaper3_test")
gid = (await _group(client, owner, "long-lived")).json()["group_id"]
g = await db_session.get(Group, gid)
@@ -158,7 +158,7 @@ async def test_an_old_group_that_was_hosted_is_never_collected(client, db_sessio
@pytest.mark.asyncio
async def test_dry_run_reports_without_deleting(client, db_session):
- owner = await _user(client, "reaper4")
+ owner = await _user(client, "reaper4_test")
gid = (await _group(client, owner, "still-here")).json()["group_id"]
g = await db_session.get(Group, gid)
g.created_at = datetime.now(timezone.utc) - timedelta(days=30)
@@ -172,7 +172,7 @@ async def test_dry_run_reports_without_deleting(client, db_session):
@pytest.mark.asyncio
async def test_collecting_a_group_takes_its_memberships_with_it(client, db_session):
"""Nothing cascades in the schema, and an orphan row keeps the group in /mine."""
- owner = await _user(client, "reaper5")
+ owner = await _user(client, "reaper5_test")
await _user(client, "tagalong")
gid = (await _group(client, owner, "doomed")).json()["group_id"]
await client.post(f"/v1/groups/{gid}/members/tagalong", json={}, headers=owner)
@@ -198,7 +198,7 @@ async def test_a_public_group_cannot_be_invite_only(client):
the only channel is the hub, so a one-time code would travel through the
party it exists to exclude.
"""
- owner = await _user(client, "policy1")
+ owner = await _user(client, "policy1_test")
r = await _group(client, owner, "contradiction", "public", join_policy="invite")
assert r.status_code == 422, r.text
assert "private" in r.json()["detail"]
@@ -206,14 +206,14 @@ async def test_a_public_group_cannot_be_invite_only(client):
@pytest.mark.asyncio
async def test_a_public_group_is_open(client):
- owner = await _user(client, "policy2")
+ owner = await _user(client, "policy2_test")
r = await _group(client, owner, "welcoming", "public", join_policy="open")
assert r.status_code == 201, r.text
@pytest.mark.asyncio
async def test_a_private_group_is_invite_only_by_default(client, db_session):
- owner = await _user(client, "policy3")
+ owner = await _user(client, "policy3_test")
gid = (await _group(client, owner, "closed")).json()["group_id"]
assert (await db_session.get(Group, gid)).join_policy == "invite"
@@ -230,7 +230,7 @@ async def test_node_registration_stamps_hosted_at(client, db_session):
"""
from meshbay_hub.api.revocation import _mark_hosted
- owner = await _user(client, "stamped")
+ owner = await _user(client, "stamped_test")
gid = (await _group(client, owner, "about-to-be-hosted")).json()["group_id"]
assert (await db_session.get(Group, gid)).hosted_at is None
diff --git a/packages/meshbay-hub/tests/test_group_leave_and_quota.py b/packages/meshbay-hub/tests/test_group_leave_and_quota.py
index dfa18b8..4431f53 100644
--- a/packages/meshbay-hub/tests/test_group_leave_and_quota.py
+++ b/packages/meshbay-hub/tests/test_group_leave_and_quota.py
@@ -53,10 +53,10 @@ async def _group(client, owner, name, visibility="private"):
@pytest.mark.asyncio
async def test_a_member_can_leave(client, db_session):
- owner = await _user(client, "owner1")
- member = await _user(client, "member1")
+ owner = await _user(client, "owner1_test")
+ member = await _user(client, "member1_test")
gid = await _group(client, owner, "readers")
- await client.post(f"/v1/groups/{gid}/members/member1", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/member1_test", json={}, headers=owner)
# Marked hosted, or the member would not see the group in the first place
# and the assertion below would hold whether or not leaving worked.
@@ -78,10 +78,10 @@ async def test_a_member_can_leave(client, db_session):
@pytest.mark.asyncio
async def test_leaving_removes_only_that_membership_row(client, db_session):
"""The group and everyone else in it are untouched — this is not a deletion."""
- owner = await _user(client, "owner2")
- member = await _user(client, "member2")
+ owner = await _user(client, "owner2_test")
+ member = await _user(client, "member2_test")
gid = await _group(client, owner, "still-here")
- await client.post(f"/v1/groups/{gid}/members/member2", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/member2_test", json={}, headers=owner)
await client.post(f"/v1/groups/{gid}/leave", headers=member)
@@ -96,18 +96,18 @@ async def test_leaving_removes_only_that_membership_row(client, db_session):
@pytest.mark.asyncio
async def test_leaving_does_not_touch_the_account_or_its_other_groups(client, db_session):
- owner = await _user(client, "owner3")
- member = await _user(client, "member3")
- elsewhere = await _user(client, "owner3b")
+ owner = await _user(client, "owner3_test")
+ member = await _user(client, "member3_test")
+ elsewhere = await _user(client, "owner3b_test")
gid = await _group(client, owner, "leaving")
other = await _group(client, elsewhere, "staying")
- await client.post(f"/v1/groups/{gid}/members/member3", json={}, headers=owner)
- await client.post(f"/v1/groups/{other}/members/member3", json={}, headers=elsewhere)
+ await client.post(f"/v1/groups/{gid}/members/member3_test", json={}, headers=owner)
+ await client.post(f"/v1/groups/{other}/members/member3_test", json={}, headers=elsewhere)
await client.post(f"/v1/groups/{gid}/leave", headers=member)
user = (await db_session.execute(
- select(User).where(User.username == "member3"))).scalar_one()
+ select(User).where(User.username == "member3_test"))).scalar_one()
assert user.status == "active"
assert await db_session.get(GroupMember, (other, user.id)) is not None
@@ -115,7 +115,7 @@ async def test_leaving_does_not_touch_the_account_or_its_other_groups(client, db
@pytest.mark.asyncio
async def test_the_owner_cannot_leave_their_own_group(client):
"""It would leave the group with nobody able to admit, edit or delete it."""
- owner = await _user(client, "owner4")
+ owner = await _user(client, "owner4_test")
gid = await _group(client, owner, "orphan-risk")
r = await client.post(f"/v1/groups/{gid}/leave", headers=owner)
@@ -128,10 +128,10 @@ async def test_the_owner_cannot_leave_their_own_group(client):
@pytest.mark.asyncio
async def test_leaving_twice_is_refused(client):
- owner = await _user(client, "owner5")
- member = await _user(client, "member5")
+ owner = await _user(client, "owner5_test")
+ member = await _user(client, "member5_test")
gid = await _group(client, owner, "once")
- await client.post(f"/v1/groups/{gid}/members/member5", json={}, headers=owner)
+ await client.post(f"/v1/groups/{gid}/members/member5_test", json={}, headers=owner)
assert (await client.post(f"/v1/groups/{gid}/leave",
headers=member)).status_code == 200
@@ -141,7 +141,7 @@ async def test_leaving_twice_is_refused(client):
@pytest.mark.asyncio
async def test_leaving_a_group_you_were_never_in_is_refused(client):
- owner = await _user(client, "owner6")
+ owner = await _user(client, "owner6_test")
stranger = await _user(client, "stranger6")
gid = await _group(client, owner, "not-yours")
@@ -170,7 +170,7 @@ async def test_public_groups_are_capped(client):
@pytest.mark.asyncio
async def test_private_groups_are_not_capped(client):
"""Private groups cost other people nothing — they are invisible to non-members."""
- owner = await _user(client, "hoarder")
+ owner = await _user(client, "hoarder_test")
for i in range(MAX_PUBLIC_GROUPS + 5):
await _group(client, owner, f"private-{i}")
@@ -199,12 +199,12 @@ async def test_a_suspended_public_group_does_not_hold_a_slot(client, db_session)
@pytest.mark.asyncio
async def test_the_cap_is_per_owner(client):
"""Being a member of someone else's public groups costs nothing."""
- a = await _user(client, "ownera")
- await _user(client, "ownerb")
- b = await _user(client, "ownerb2")
+ a = await _user(client, "ownera_test")
+ await _user(client, "ownerb_test")
+ b = await _user(client, "ownerb2_test")
for i in range(MAX_PUBLIC_GROUPS):
gid = await _group(client, a, f"a-pub-{i}", visibility="public")
- await client.post(f"/v1/groups/{gid}/members/ownerb2", json={}, headers=a)
+ await client.post(f"/v1/groups/{gid}/members/ownerb2_test", json={}, headers=a)
r = await client.post("/v1/groups",
json={"name": "b-first", "visibility": "public",
@@ -241,7 +241,7 @@ async def test_the_group_list_reports_node_presence(client):
dishonest hub could not fake. The client downgrades it on a connection it
tried and failed, which is the evidence that concerns the reader.
"""
- owner = await _user(client, "watcher")
+ owner = await _user(client, "watcher_test")
gid = await _group(client, owner, "quiet")
mine = await client.get("/v1/groups/mine", headers=owner)
diff --git a/packages/meshbay-hub/tests/test_group_membership.py b/packages/meshbay-hub/tests/test_group_membership.py
index 7eeaa2f..2761595 100644
--- a/packages/meshbay-hub/tests/test_group_membership.py
+++ b/packages/meshbay-hub/tests/test_group_membership.py
@@ -41,7 +41,7 @@ async def _group_with_member(client, owner, member_name, name="crew"):
@pytest.mark.asyncio
async def test_the_owner_removes_a_member(client, db_session):
- owner = await _user(client, "chief")
+ owner = await _user(client, "chief_test")
await _user(client, "hanger_on")
gid = await _group_with_member(client, owner, "hanger_on")
@@ -52,7 +52,7 @@ async def test_the_owner_removes_a_member(client, db_session):
select(GroupMember).where(GroupMember.group_id == gid))).scalars().all()
assert [m.user_id for m in rows] != [], "the owner lost their own membership"
names = {(await db_session.get(User, m.user_id)).username for m in rows}
- assert names == {"chief"}
+ assert names == {"chief_test"}
@pytest.mark.asyncio
@@ -61,7 +61,7 @@ async def test_removing_a_member_is_not_deleting_an_account(client, db_session):
The account survives untouched, with its other groups. Anything else would
make one group's owner able to erase someone from the whole hub.
"""
- owner = await _user(client, "boss")
+ owner = await _user(client, "boss_test")
member = await _user(client, "member_x")
elsewhere = await _user(client, "other_owner")
@@ -83,7 +83,7 @@ async def test_removing_a_member_is_not_deleting_an_account(client, db_session):
@pytest.mark.asyncio
async def test_a_member_cannot_remove_anyone(client):
- owner = await _user(client, "owner_y")
+ owner = await _user(client, "owner_y_test")
member = await _user(client, "member_y")
await _user(client, "victim_y")
gid = await _group_with_member(client, owner, "member_y")
@@ -96,16 +96,16 @@ async def test_a_member_cannot_remove_anyone(client):
@pytest.mark.asyncio
async def test_the_owner_cannot_be_removed_from_their_own_group(client):
"""Otherwise the group is left with nobody who can invite or remove."""
- owner = await _user(client, "owner_z")
- gid = await _group_with_member(client, owner, "owner_z")
+ owner = await _user(client, "owner_z_test")
+ gid = await _group_with_member(client, owner, "owner_z_test")
- r = await client.delete(f"/v1/groups/{gid}/members/owner_z", headers=owner)
+ r = await client.delete(f"/v1/groups/{gid}/members/owner_z_test", headers=owner)
assert r.status_code == 409
@pytest.mark.asyncio
async def test_removing_someone_who_is_not_a_member_says_so(client):
- owner = await _user(client, "owner_w")
+ owner = await _user(client, "owner_w_test")
await _user(client, "stranger")
g = await client.post("/v1/groups", json={"name": "closed"}, headers=owner)
gid = g.json()["group_id"]
diff --git a/packages/meshbay-hub/tests/test_group_name_unique.py b/packages/meshbay-hub/tests/test_group_name_unique.py
index 2bc109c..ff9d42d 100644
--- a/packages/meshbay-hub/tests/test_group_name_unique.py
+++ b/packages/meshbay-hub/tests/test_group_name_unique.py
@@ -30,10 +30,10 @@ async def _create(client, headers, name):
@pytest.mark.asyncio
async def test_same_owner_same_name_is_refused(client):
- alice = await _user(client, "alice")
+ alice = await _user(client, "alice_test")
r1 = await _create(client, alice, "photos")
assert r1.status_code == 201
- assert r1.json()["owner_username"] == "alice"
+ assert r1.json()["owner_username"] == "alice_test"
r2 = await _create(client, alice, "photos")
assert r2.status_code == 409
@@ -42,22 +42,22 @@ async def test_same_owner_same_name_is_refused(client):
@pytest.mark.asyncio
async def test_same_owner_different_case_is_refused(client):
- alice = await _user(client, "alice")
+ alice = await _user(client, "alice_test")
assert (await _create(client, alice, "Photos")).status_code == 201
assert (await _create(client, alice, " photos ")).status_code == 409
@pytest.mark.asyncio
async def test_two_owners_may_share_a_name(client):
- alice = await _user(client, "alice")
- bob = await _user(client, "bob")
+ alice = await _user(client, "alice_test")
+ bob = await _user(client, "bob_test")
assert (await _create(client, alice, "photos")).status_code == 201
assert (await _create(client, bob, "photos")).status_code == 201
@pytest.mark.asyncio
async def test_name_is_trimmed_on_create(client):
- alice = await _user(client, "alice")
+ alice = await _user(client, "alice_test")
r = await _create(client, alice, " spaced out ")
assert r.status_code == 201
assert r.json()["name"] == "spaced out"
@@ -65,14 +65,14 @@ async def test_name_is_trimmed_on_create(client):
@pytest.mark.asyncio
async def test_blank_name_is_refused(client):
- alice = await _user(client, "alice")
+ alice = await _user(client, "alice_test")
assert (await _create(client, alice, " ")).status_code == 422
@pytest.mark.asyncio
async def test_owner_username_is_reported_in_listings(client):
- alice = await _user(client, "alice")
+ alice = await _user(client, "alice_test")
await _create(client, alice, "photos")
mine = (await client.get("/v1/groups/mine", headers=alice)).json()["groups"]
- assert mine and mine[0]["owner_username"] == "alice"
+ assert mine and mine[0]["owner_username"] == "alice_test"
diff --git a/packages/meshbay-hub/tests/test_group_purge.py b/packages/meshbay-hub/tests/test_group_purge.py
index e05c374..0611dba 100644
--- a/packages/meshbay-hub/tests/test_group_purge.py
+++ b/packages/meshbay-hub/tests/test_group_purge.py
@@ -101,7 +101,7 @@ def test_the_seeding_covers_every_reference():
async def test_enforcement_is_really_on(client, db_session):
"""The control: without it, every test below would pass on SQLite's
default and prove nothing about PostgreSQL."""
- owner = await _account(client, "control")
+ owner = await _account(client, "control_test")
await _account(client, "control_member")
gid = await _group_with_everything(client, db_session, owner, "control_member", "control")
await _enforce_foreign_keys(db_session)
@@ -121,16 +121,16 @@ async def test_an_admin_erases_an_account_that_owns_groups(client, db_session, m
monkeypatch.setattr("meshbay_hub.api.revocation.broadcast_revocation", capture)
admin_token = await _account(client, "the_admin")
- owner_token = await _account(client, "owner")
- await _account(client, "member")
+ owner_token = await _account(client, "owner_test")
+ await _account(client, "member_test")
admin = await db_session.get(User, await _uid(db_session, "the_admin"))
admin.role = "admin"
await db_session.commit()
- first = await _group_with_everything(client, db_session, owner_token, "member", "first")
+ first = await _group_with_everything(client, db_session, owner_token, "member_test", "first")
r = await client.post("/v1/groups", json={"name": "second"},
headers={"Authorization": f"Bearer {owner_token}"})
second = r.json()["group_id"]
- owner_id = await _uid(db_session, "owner")
+ owner_id = await _uid(db_session, "owner_test")
await _enforce_foreign_keys(db_session)
r = await client.delete(f"/v1/admin/users/{owner_id}",
@@ -156,9 +156,9 @@ async def test_an_admin_erases_an_account_that_owns_groups(client, db_session, m
async def test_the_owner_deletes_a_group_with_everything_pointing_at_it(client, db_session):
"""The owner's route left `email_verifications` behind — an IntegrityError
on PostgreSQL the first time an invited group was deleted."""
- owner_token = await _account(client, "keeper")
- await _account(client, "guest")
- gid = await _group_with_everything(client, db_session, owner_token, "guest", "doomed")
+ owner_token = await _account(client, "keeper_test")
+ await _account(client, "guest_test")
+ gid = await _group_with_everything(client, db_session, owner_token, "guest_test", "doomed")
await _enforce_foreign_keys(db_session)
r = await client.delete(f"/v1/groups/{gid}",
diff --git a/packages/meshbay-hub/tests/test_groups_self_service.py b/packages/meshbay-hub/tests/test_groups_self_service.py
index def663d..f9346c1 100644
--- a/packages/meshbay-hub/tests/test_groups_self_service.py
+++ b/packages/meshbay-hub/tests/test_groups_self_service.py
@@ -52,8 +52,8 @@ async def _mark_hosted(db_session, *group_ids):
@pytest.mark.asyncio
async def test_create_group(client):
- await _register(client, "alice", email="a@x.com")
- token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ token = await _login(client, "alice_test")
r = await client.post("/v1/groups", json={
"name": "my-group", "visibility": "public", "join_policy": "open",
@@ -66,12 +66,12 @@ async def test_create_group(client):
@pytest.mark.asyncio
async def test_join_open_group(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
gid = await _create_group(client, alice_token, "open-group")
- await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
r = await client.post(f"/v1/groups/{gid}/join",
headers={"Authorization": f"Bearer {bob_token}"})
@@ -81,16 +81,16 @@ async def test_join_open_group(client):
@pytest.mark.asyncio
async def test_join_invite_group_rejected(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
# Private: invite-only is refused on a public group now, since a group
# everyone can find and nobody can enter is a dead end. What is under test
# here — /join refusing a group that is not open — is unchanged.
gid = await _create_group(client, alice_token, "invite-group",
visibility="private", join_policy="invite")
- await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
r = await client.post(f"/v1/groups/{gid}/join",
headers={"Authorization": f"Bearer {bob_token}"})
@@ -99,8 +99,8 @@ async def test_join_invite_group_rejected(client):
@pytest.mark.asyncio
async def test_join_already_member(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
gid = await _create_group(client, alice_token, "dup-group")
r = await client.post(f"/v1/groups/{gid}/join",
@@ -110,12 +110,12 @@ async def test_join_already_member(client):
@pytest.mark.asyncio
async def test_group_members(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
gid = await _create_group(client, alice_token, "team-group")
- await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
await client.post(f"/v1/groups/{gid}/join",
headers={"Authorization": f"Bearer {bob_token}"})
@@ -124,20 +124,20 @@ async def test_group_members(client):
assert r.status_code == 200
data = r.json()
usernames = [m["username"] for m in data["members"]]
- assert "alice" in usernames
- assert "bob" in usernames
+ assert "alice_test" in usernames
+ assert "bob_test" in usernames
assert data["admin_id"] is not None
@pytest.mark.asyncio
async def test_group_members_non_member_denied(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
gid = await _create_group(client, alice_token, "private-group",
visibility="private", join_policy="invite")
- await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
r = await client.get(f"/v1/groups/{gid}/members",
headers={"Authorization": f"Bearer {bob_token}"})
@@ -146,8 +146,8 @@ async def test_group_members_non_member_denied(client):
@pytest.mark.asyncio
async def test_group_search(client, db_session):
- await _register(client, "alice", email="a@x.com")
- token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ token = await _login(client, "alice_test")
a = await _create_group(client, token, "alpha-team")
b = await _create_group(client, token, "beta-team")
# The directory shows groups a node has announced. Marked here so this test
@@ -163,12 +163,12 @@ async def test_group_search(client, db_session):
@pytest.mark.asyncio
async def test_join_triggers_notification(client):
- await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
gid = await _create_group(client, alice_token, "notif-group")
- await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
await client.post(f"/v1/groups/{gid}/join",
headers={"Authorization": f"Bearer {bob_token}"})
@@ -187,8 +187,8 @@ async def test_a_private_group_cannot_be_open_to_everyone(client):
rather than a link, so nothing can reach it. It was accepted until now, and
the create form offered it.
"""
- await _register(client, "pat", email="pat@x.com")
- token = await _login(client, "pat")
+ await _register(client, "pat_test", email="pat@x.com")
+ token = await _login(client, "pat_test")
resp = await client.post("/v1/groups", json={
"name": "nowhere", "visibility": "private", "join_policy": "open",
}, headers={"Authorization": f"Bearer {token}"})
@@ -201,8 +201,8 @@ async def test_a_private_group_cannot_be_open_to_everyone(client):
async def test_a_public_group_cannot_be_invite_only(client):
"""The other half, which was already refused — kept so that removing one
check does not quietly remove both."""
- await _register(client, "sam", email="sam@x.com")
- token = await _login(client, "sam")
+ await _register(client, "sam_test", email="sam@x.com")
+ token = await _login(client, "sam_test")
resp = await client.post("/v1/groups", json={
"name": "deadend", "visibility": "public", "join_policy": "invite",
}, headers={"Authorization": f"Bearer {token}"})
@@ -212,8 +212,8 @@ async def test_a_public_group_cannot_be_invite_only(client):
@pytest.mark.asyncio
async def test_the_two_combinations_that_mean_something_are_accepted(client):
- await _register(client, "robin", email="robin@x.com")
- token = await _login(client, "robin")
+ await _register(client, "robin_test", email="robin@x.com")
+ token = await _login(client, "robin_test")
for name, visibility, policy in (("closed", "private", "invite"),
("open-house", "public", "open")):
resp = await client.post("/v1/groups", json={
diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py
index 73f7fea..5e9e6c2 100644
--- a/packages/meshbay-hub/tests/test_hub_api.py
+++ b/packages/meshbay-hub/tests/test_hub_api.py
@@ -88,7 +88,7 @@ async def test_hub_pubkey(client):
async def test_register_and_login(client):
pk_ed, pk_x, _ = _gen_user_keys()
r = await client.post("/v1/users/register", json={
- "username": "alice", "email": "alice@example.com",
+ "username": "alice_test", "email": "alice@example.com",
"password": "alicepass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x,
})
@@ -96,7 +96,7 @@ async def test_register_and_login(client):
assert "user_id" in r.json()
r = await client.post("/v1/users/login", json={
- "username": "alice", "password": "alicepass99"})
+ "username": "alice_test", "password": "alicepass99"})
assert r.status_code == 200
data = r.json()
assert "access_token" in data
@@ -107,7 +107,7 @@ async def test_register_and_login(client):
@pytest.mark.asyncio
async def test_register_duplicate_rejected(client):
pk_ed, pk_x, _ = _gen_user_keys()
- body = {"username": "bob", "email": "bob@example.com",
+ body = {"username": "bob_test", "email": "bob@example.com",
"password": "bobpass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}
await client.post("/v1/users/register", json=body)
@@ -119,11 +119,11 @@ async def test_register_duplicate_rejected(client):
async def test_wrong_password_rejected(client):
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "carol", "email": "carol@example.com",
+ "username": "carol_test", "email": "carol@example.com",
"password": "carolpass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
r = await client.post("/v1/users/login", json={
- "username": "carol", "password": "wrongpass"})
+ "username": "carol_test", "password": "wrongpass"})
assert r.status_code == 401
@@ -133,11 +133,11 @@ async def test_jwt_offline_verify(client, hub_key_path):
import jwt as pyjwt
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "dave", "email": "dave@example.com",
+ "username": "dave_test", "email": "dave@example.com",
"password": "davepass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
r = await client.post("/v1/users/login", json={
- "username": "dave", "password": "davepass99"})
+ "username": "dave_test", "password": "davepass99"})
token = r.json()["access_token"]
r_pk = await client.get("/v1/hub/pubkey")
@@ -155,11 +155,11 @@ async def test_jwt_offline_verify(client, hub_key_path):
async def test_token_refresh(client):
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "eve", "email": "eve@example.com",
+ "username": "eve_test", "email": "eve@example.com",
"password": "evepass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
r = await client.post("/v1/users/login", json={
- "username": "eve", "password": "evepass99"})
+ "username": "eve_test", "password": "evepass99"})
rt = r.json()["refresh_token"]
at = r.json()["access_token"]
@@ -206,17 +206,17 @@ async def test_get_user_pubkeys(client):
"""
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "frank", "email": "frank@example.com",
+ "username": "frank_test", "email": "frank@example.com",
"password": "frankpass99"})
login = await client.post("/v1/users/login", json={
- "username": "frank", "password": "frankpass99"})
+ "username": "frank_test", "password": "frankpass99"})
token = login.json()["access_token"]
- r = await client.get("/v1/users/frank/pubkeys",
+ r = await client.get("/v1/users/frank_test/pubkeys",
headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
body = r.json()
- assert body["user_id"] and body["username"] == "frank"
+ assert body["user_id"] and body["username"] == "frank_test"
assert "pk_ed25519" not in body, "user identity keys must not be published (H3)"
assert "pk_x25519" not in body, "user identity keys must not be published (H3)"
@@ -227,11 +227,11 @@ async def test_get_user_pubkeys(client):
async def test_announce_and_get_node(client):
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "node1", "email": "n@example.com",
+ "username": "node1_test", "email": "n@example.com",
"password": "nodepass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
login = await client.post("/v1/users/login", json={
- "username": "node1", "password": "nodepass99"})
+ "username": "node1_test", "password": "nodepass99"})
token = login.json()["access_token"]
hdrs = {"Authorization": f"Bearer {token}"}
@@ -253,15 +253,15 @@ async def test_group_member_add(client):
pk_ed_b, pk_x_b, sk_x_b = _gen_user_keys()
for uname, email, pwd, pk_ed, pk_x in [
- ("alice2", "a2@x.com", "alicepass99", pk_ed_a, pk_x_a),
- ("bob2", "b2@x.com", "bobpass99", pk_ed_b, pk_x_b),
+ ("alice2_test", "a2@x.com", "alicepass99", pk_ed_a, pk_x_a),
+ ("bob2_test", "b2@x.com", "bobpass99", pk_ed_b, pk_x_b),
]:
await client.post("/v1/users/register", json={
"username": uname, "email": email, "password": pwd,
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
alice_token = (await client.post("/v1/users/login",
- json={"username": "alice2", "password": "alicepass99"})).json()["access_token"]
+ json={"username": "alice2_test", "password": "alicepass99"})).json()["access_token"]
a_hdrs = {"Authorization": f"Bearer {alice_token}"}
@@ -270,19 +270,19 @@ async def test_group_member_add(client):
group_id = r.json()["group_id"]
# Add bob as member (hub handles membership only, GEK exchange is P2P)
- r = await client.post(f"/v1/groups/{group_id}/members/bob2",
+ r = await client.post(f"/v1/groups/{group_id}/members/bob2_test",
json={}, headers=a_hdrs)
assert r.status_code == 201
# Verify bob is in the group
bob_token = (await client.post("/v1/users/login",
- json={"username": "bob2", "password": "bobpass99"})).json()["access_token"]
+ json={"username": "bob2_test", "password": "bobpass99"})).json()["access_token"]
b_hdrs = {"Authorization": f"Bearer {bob_token}"}
r = await client.get(f"/v1/groups/{group_id}/members", headers=b_hdrs)
assert r.status_code == 200
members = [m["username"] for m in r.json()["members"]]
- assert "alice2" in members
- assert "bob2" in members
+ assert "alice2_test" in members
+ assert "bob2_test" in members
@pytest.mark.asyncio
@@ -291,24 +291,24 @@ async def test_non_admin_cannot_add_member(client):
pk_ed_b, pk_x_b, _ = _gen_user_keys()
for uname, email, pwd, pk_ed, pk_x in [
- ("charlie", "c@x.com", "charliepass", pk_ed_a, pk_x_a),
- ("dan", "d@x.com", "danpass1234", pk_ed_b, pk_x_b),
+ ("charlie_test", "c@x.com", "charliepass", pk_ed_a, pk_x_a),
+ ("dan_test", "d@x.com", "danpass1234", pk_ed_b, pk_x_b),
]:
await client.post("/v1/users/register", json={
"username": uname, "email": email, "password": pwd,
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
charlie_token = (await client.post("/v1/users/login",
- json={"username": "charlie", "password": "charliepass"})).json()["access_token"]
+ json={"username": "charlie_test", "password": "charliepass"})).json()["access_token"]
dan_token = (await client.post("/v1/users/login",
- json={"username": "dan", "password": "danpass1234"})).json()["access_token"]
+ json={"username": "dan_test", "password": "danpass1234"})).json()["access_token"]
r = await client.post("/v1/groups", json={"name": "charlies-group"},
headers={"Authorization": f"Bearer {charlie_token}"})
group_id = r.json()["group_id"]
# Dan (non-admin) tries to add a member → 403
- r = await client.post(f"/v1/groups/{group_id}/members/charlie",
+ r = await client.post(f"/v1/groups/{group_id}/members/charlie_test",
json={},
headers={"Authorization": f"Bearer {dan_token}"})
assert r.status_code == 403
@@ -325,12 +325,12 @@ async def test_jwt_contains_groups_claim(client):
"username": "grp_alice", "email": "ga@x.com", "password": "alicepass99",
"pk_user_ed25519": pk_ed_a, "pk_user_x25519": pk_x_a})
await client.post("/v1/users/register", json={
- "username": "grp_bob", "email": "gb@x.com", "password": "bobpass99",
+ "username": "grp_bob_test", "email": "gb@x.com", "password": "bobpass99",
"pk_user_ed25519": pk_ed_b, "pk_user_x25519": pk_x_b})
# Login before joining any group — groups should be empty
r = await client.post("/v1/users/login", json={
- "username": "grp_bob", "password": "bobpass99"})
+ "username": "grp_bob_test", "password": "bobpass99"})
token_pre = r.json()["access_token"]
r_pk = await client.get("/v1/hub/pubkey")
hub_pk = r_pk.json()["pk_hub_pem"].encode()
@@ -344,13 +344,13 @@ async def test_jwt_contains_groups_claim(client):
headers={"Authorization": f"Bearer {alice_token}"})
group_id = r.json()["group_id"]
- await client.post(f"/v1/groups/{group_id}/members/grp_bob",
+ await client.post(f"/v1/groups/{group_id}/members/grp_bob_test",
json={},
headers={"Authorization": f"Bearer {alice_token}"})
# Login again — groups should contain the new group
r = await client.post("/v1/users/login", json={
- "username": "grp_bob", "password": "bobpass99"})
+ "username": "grp_bob_test", "password": "bobpass99"})
token_post = r.json()["access_token"]
decoded_post = pyjwt.decode(token_post, hub_pk, algorithms=["EdDSA"])
assert group_id in decoded_post["groups"]
@@ -374,13 +374,13 @@ async def test_my_groups(client, db_session):
"username": "mg_alice", "email": "mga@x.com", "password": "alicepass99",
"pk_user_ed25519": pk_ed_a, "pk_user_x25519": pk_x_a})
await client.post("/v1/users/register", json={
- "username": "mg_bob", "email": "mgb@x.com", "password": "bobpass99",
+ "username": "mg_bob_test", "email": "mgb@x.com", "password": "bobpass99",
"pk_user_ed25519": pk_ed_b, "pk_user_x25519": pk_x_b})
alice_token = (await client.post("/v1/users/login",
json={"username": "mg_alice", "password": "alicepass99"})).json()["access_token"]
bob_token = (await client.post("/v1/users/login",
- json={"username": "mg_bob", "password": "bobpass99"})).json()["access_token"]
+ json={"username": "mg_bob_test", "password": "bobpass99"})).json()["access_token"]
# Bob has no groups initially
r = await client.get("/v1/groups/mine",
@@ -392,7 +392,7 @@ async def test_my_groups(client, db_session):
r = await client.post("/v1/groups", json={"name": "mg-group"},
headers={"Authorization": f"Bearer {alice_token}"})
group_id = r.json()["group_id"]
- await client.post(f"/v1/groups/{group_id}/members/mg_bob",
+ await client.post(f"/v1/groups/{group_id}/members/mg_bob_test",
json={},
headers={"Authorization": f"Bearer {alice_token}"})
@@ -406,7 +406,7 @@ async def test_my_groups(client, db_session):
# Re-login to get fresh token with group claims
bob_token = (await client.post("/v1/users/login",
- json={"username": "mg_bob", "password": "bobpass99"})).json()["access_token"]
+ json={"username": "mg_bob_test", "password": "bobpass99"})).json()["access_token"]
# Now Bob should see the group
r = await client.get("/v1/groups/mine",
@@ -439,10 +439,10 @@ async def test_group_online_nodes(client):
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "gn_user", "email": "gn@x.com", "password": "gnpass999",
+ "username": "gn_user_test", "email": "gn@x.com", "password": "gnpass999",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x})
r = await client.post("/v1/users/login",
- json={"username": "gn_user", "password": "gnpass999"})
+ json={"username": "gn_user_test", "password": "gnpass999"})
token = r.json()["access_token"]
r = await client.post("/v1/groups", json={"name": "gn-group"},
@@ -451,7 +451,7 @@ async def test_group_online_nodes(client):
# Re-login to get fresh token with group claims
token = (await client.post("/v1/users/login",
- json={"username": "gn_user", "password": "gnpass999"})).json()["access_token"]
+ json={"username": "gn_user_test", "password": "gnpass999"})).json()["access_token"]
# Announce a node
node_id, pk_node = await _announce_signed(client, token)
@@ -816,7 +816,7 @@ async def test_register_no_credentials_rejected(client):
"""Registration without auth_key or password returns 400."""
pk_ed, pk_x, _ = _gen_user_keys()
r = await client.post("/v1/users/register", json={
- "username": "nocred",
+ "username": "nocred_test",
"email": "nocred@test.com",
"pk_user_ed25519": pk_ed,
"pk_user_x25519": pk_x,
@@ -868,7 +868,7 @@ async def test_login_v3_account_password_only_rejected(client):
"""Login with raw password to a v3 (auth_key) account returns 401."""
pk_ed, pk_x, _ = _gen_user_keys()
await client.post("/v1/users/register", json={
- "username": "v3nopw",
+ "username": "v3nopw_test",
"email": "v3nopw@test.com",
"auth_key": "dGVzdGF1dGhrZXl0ZXN0YXV0aGtleXRlc3RhdXRo",
"pk_user_ed25519": pk_ed,
@@ -876,7 +876,7 @@ async def test_login_v3_account_password_only_rejected(client):
})
r = await client.post("/v1/users/login", json={
- "username": "v3nopw", "password": "somepassword"})
+ "username": "v3nopw_test", "password": "somepassword"})
assert r.status_code == 401
@@ -955,7 +955,7 @@ async def test_login_legacy_migration(client, app):
@pytest.mark.asyncio
async def test_login_no_credentials_rejected(client):
"""Login without auth_key or password returns 401."""
- r = await client.post("/v1/users/login", json={"username": "nobody"})
+ r = await client.post("/v1/users/login", json={"username": "nobody_test"})
assert r.status_code == 401
assert "No credentials" in r.json()["detail"]
diff --git a/packages/meshbay-hub/tests/test_login_lockout.py b/packages/meshbay-hub/tests/test_login_lockout.py
index 43300d0..6f17d98 100644
--- a/packages/meshbay-hub/tests/test_login_lockout.py
+++ b/packages/meshbay-hub/tests/test_login_lockout.py
@@ -51,10 +51,10 @@ async def _fail(client, username, times):
@pytest.mark.asyncio
async def test_the_fourth_failure_locks_and_the_right_passphrase_is_refused(client):
- await _register(client, "alice")
- await _fail(client, "alice", 4)
+ await _register(client, "alice_test")
+ await _fail(client, "alice_test", 4)
- r = await _login(client, "alice", RIGHT)
+ r = await _login(client, "alice_test", RIGHT)
assert r.status_code == 429, r.text
assert r.json()["detail"] == "account_locked"
# An hour, give or take the time the four failures took.
@@ -64,11 +64,11 @@ async def test_the_fourth_failure_locks_and_the_right_passphrase_is_refused(clie
@pytest.mark.asyncio
async def test_an_unknown_name_locks_exactly_like_a_real_one(client):
"""M1: the lockout must not become the enumeration oracle `login` avoids."""
- await _register(client, "bob")
- await _fail(client, "bob", 4)
+ await _register(client, "bob_test")
+ await _fail(client, "bob_test", 4)
await _fail(client, "nobody-by-this-name", 4)
- real = await _login(client, "bob", WRONG)
+ real = await _login(client, "bob_test", WRONG)
ghost = await _login(client, "nobody-by-this-name", WRONG)
assert (real.status_code, real.json()) == (ghost.status_code, ghost.json())
assert real.status_code == 429
@@ -76,42 +76,42 @@ async def test_an_unknown_name_locks_exactly_like_a_real_one(client):
@pytest.mark.asyncio
async def test_the_right_passphrase_clears_the_count(client):
- await _register(client, "carol")
- await _fail(client, "carol", 3)
- r = await _login(client, "carol", RIGHT)
+ await _register(client, "carol_test")
+ await _fail(client, "carol_test", 3)
+ r = await _login(client, "carol_test", RIGHT)
assert r.status_code == 200, r.text
# Three more would have been seven in a row without the reset.
- await _fail(client, "carol", 3)
- assert (await _login(client, "carol", RIGHT)).status_code == 200
+ await _fail(client, "carol_test", 3)
+ assert (await _login(client, "carol_test", RIGHT)).status_code == 200
@pytest.mark.asyncio
async def test_a_lockout_ends_when_its_window_does(client, db_session):
- await _register(client, "dave")
- await _fail(client, "dave", 4)
- assert (await _login(client, "dave", RIGHT)).status_code == 429
+ await _register(client, "dave_test")
+ await _fail(client, "dave_test", 4)
+ assert (await _login(client, "dave_test", RIGHT)).status_code == 429
await db_session.execute(
- update(LoginThrottle).where(LoginThrottle.key == _key("dave"))
+ update(LoginThrottle).where(LoginThrottle.key == _key("dave_test"))
.values(last_failure_at=datetime.now(timezone.utc) - timedelta(minutes=61)))
await db_session.commit()
- assert (await _login(client, "dave", RIGHT)).status_code == 200
+ assert (await _login(client, "dave_test", RIGHT)).status_code == 200
@pytest.mark.asyncio
async def test_old_failures_do_not_carry_into_a_new_window(client, db_session):
- await _register(client, "erin")
- await _fail(client, "erin", 3)
+ await _register(client, "erin_test")
+ await _fail(client, "erin_test", 3)
await db_session.execute(
- update(LoginThrottle).where(LoginThrottle.key == _key("erin"))
+ update(LoginThrottle).where(LoginThrottle.key == _key("erin_test"))
.values(last_failure_at=datetime.now(timezone.utc) - timedelta(minutes=61)))
await db_session.commit()
# One stale window of three, then one fresh failure: a count of one, not four.
- await _fail(client, "erin", 1)
- assert (await _login(client, "erin", RIGHT)).status_code == 200
+ await _fail(client, "erin_test", 1)
+ assert (await _login(client, "erin_test", RIGHT)).status_code == 200
@pytest.mark.asyncio
@@ -124,8 +124,8 @@ async def test_a_burst_of_concurrent_guesses_gets_no_more_than_the_limit(client)
the statement is an `ON CONFLICT DO UPDATE … WHERE`, which both evaluate
against the row as locked.
"""
- await _register(client, "frank")
- results = await asyncio.gather(*[_login(client, "frank", WRONG) for _ in range(10)])
+ await _register(client, "frank_test")
+ results = await asyncio.gather(*[_login(client, "frank_test", WRONG) for _ in range(10)])
codes = sorted(r.status_code for r in results)
assert codes.count(401) == 4, codes
assert codes.count(429) == 6, codes
@@ -134,8 +134,8 @@ async def test_a_burst_of_concurrent_guesses_gets_no_more_than_the_limit(client)
@pytest.mark.asyncio
async def test_change_password_counts_on_the_same_row(client):
"""It checks the same passphrase, so it is the same oracle."""
- await _register(client, "grace")
- token = (await _login(client, "grace", RIGHT)).json()["access_token"]
+ await _register(client, "grace_test")
+ token = (await _login(client, "grace_test", RIGHT)).json()["access_token"]
auth = {"Authorization": f"Bearer {token}"}
for _ in range(4):
@@ -146,19 +146,19 @@ async def test_change_password_counts_on_the_same_row(client):
r = await client.post("/v1/users/password", headers=auth, json={
"old_auth_key": RIGHT, "new_auth_key": "n" * 44})
assert r.status_code == 429, r.text
- assert (await _login(client, "grace", RIGHT)).status_code == 429
+ assert (await _login(client, "grace_test", RIGHT)).status_code == 429
@pytest.mark.asyncio
async def test_a_signed_in_session_is_told_its_own_lockout(client):
"""A passphrase change re-wraps every node's bundle before the hub accepts
it, so the client must know not to start one the hub would then refuse."""
- await _register(client, "olivia")
- token = (await _login(client, "olivia", RIGHT)).json()["access_token"]
+ await _register(client, "olivia_test")
+ token = (await _login(client, "olivia_test", RIGHT)).json()["access_token"]
auth = {"Authorization": f"Bearer {token}"}
assert (await client.get("/v1/users/me", headers=auth)).json()["passphrase_locked_for"] == 0
- await _fail(client, "olivia", 4)
+ await _fail(client, "olivia_test", 4)
left = (await client.get("/v1/users/me", headers=auth)).json()["passphrase_locked_for"]
assert 3500 <= left <= 3600
@@ -168,16 +168,16 @@ async def test_an_attempt_that_checks_no_passphrase_is_not_counted(client, db_se
"""A legacy account asked to upgrade has been told nothing about its passphrase."""
from meshbay_hub.db.models import User
- await _register(client, "heidi")
+ await _register(client, "heidi_test")
await db_session.execute(
- update(User).where(User.username == "heidi").values(pw_version=2))
+ update(User).where(User.username == "heidi_test").values(pw_version=2))
await db_session.commit()
for _ in range(6):
- r = await _login(client, "heidi", RIGHT)
+ r = await _login(client, "heidi_test", RIGHT)
assert r.status_code == 401 and r.json()["detail"] == "auth_upgrade_required"
failures = await db_session.scalar(
- select(LoginThrottle.failures).where(LoginThrottle.key == _key("heidi")))
+ select(LoginThrottle.failures).where(LoginThrottle.key == _key("heidi_test")))
assert not failures
@@ -191,7 +191,7 @@ async def test_the_table_never_holds_what_was_typed(client, db_session):
# ── The admin's two numbers ──────────────────────────────────────────────────
-async def _admin_headers(client, username="root"):
+async def _admin_headers(client, username="root_test"):
await _register(client, username)
set_admin_usernames([username])
token = (await _login(client, username, RIGHT)).json()["access_token"]
@@ -211,9 +211,9 @@ async def test_the_admin_sets_the_limit_and_the_hub_applies_it(client):
assert r.status_code == 200, r.text
assert r.json()["login"] == {"max_failures": 2, "lockout_minutes": 5}
- await _register(client, "ivan")
- await _fail(client, "ivan", 2)
- r = await _login(client, "ivan", RIGHT)
+ await _register(client, "ivan_test")
+ await _fail(client, "ivan_test", 2)
+ r = await _login(client, "ivan_test", RIGHT)
assert r.status_code == 429
assert int(r.headers["retry-after"]) <= 300
@@ -224,9 +224,9 @@ async def test_zero_failures_turns_the_lockout_off(client):
await client.patch("/v1/admin/settings", headers=admin,
json={"login": {"max_failures": 0}})
- await _register(client, "judy")
- await _fail(client, "judy", 8)
- assert (await _login(client, "judy", RIGHT)).status_code == 200
+ await _register(client, "judy_test")
+ await _fail(client, "judy_test", 8)
+ assert (await _login(client, "judy_test", RIGHT)).status_code == 200
@pytest.mark.asyncio
@@ -248,8 +248,8 @@ async def test_values_are_clamped_and_unknown_keys_refused(client):
@pytest.mark.asyncio
async def test_only_an_admin_changes_them(client):
await _admin_headers(client) # an admin exists; this is someone else
- await _register(client, "mallory")
- token = (await _login(client, "mallory", RIGHT)).json()["access_token"]
+ await _register(client, "mallory_test")
+ token = (await _login(client, "mallory_test", RIGHT)).json()["access_token"]
r = await client.patch("/v1/admin/settings",
headers={"Authorization": f"Bearer {token}"},
json={"login": {"max_failures": 0}})
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 4e5bd6d..fe6a5ee 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
@@ -298,12 +298,12 @@ async def test_the_register_resend_branch_cannot_be_hammered(client, wire):
as long as the account stayed pending.
"""
victim = "bombing-target@example.test"
- r = await _register(client, "relay_a", victim)
+ r = await _register(client, "relay_a_test", victim)
assert r.status_code == 201, r.text
assert len(wire) == 1
for _ in range(5):
- r = await _register(client, "relay_a", victim)
+ r = await _register(client, "relay_a_test", victim)
assert r.status_code == 201, r.text
assert len(wire) == 1, f"{len(wire)} mails to one address from one sign-up"
@@ -344,7 +344,7 @@ async def test_an_account_may_point_the_hub_at_one_stranger_then_wait(
indefinitely, so the delay is long — and a second, different address
inside it is refused.
"""
- headers = await _signed_in(client, db_session, "relay_b",
+ headers = await _signed_in(client, db_session, "relay_b_test",
"relay_b@example.test")
before = len(wire)
@@ -370,7 +370,7 @@ async def test_the_address_already_pending_may_be_asked_for_again(
from meshbay_hub import hub_settings
from meshbay_hub.db.models import MailQuota
- headers = await _signed_in(client, db_session, "relay_d",
+ headers = await _signed_in(client, db_session, "relay_d_test",
"relay_d@example.test")
typo = "jean@gmial.test"
@@ -399,7 +399,7 @@ async def test_the_delay_survives_the_verification_row_being_deleted(
one, so a window counted from that table would count one, always. Which is
what the first version of this counted. It comes off the IP log.
"""
- headers = await _signed_in(client, db_session, "relay_c",
+ headers = await _signed_in(client, db_session, "relay_c_test",
"relay_c@example.test")
r = await client.patch("/v1/users/me", headers=headers,
@@ -495,10 +495,10 @@ async def test_a_moderator_may_read_the_bounds_but_not_change_them(
from meshbay_hub.db.models import User
- headers = await _signed_in(client, db_session, "mailmod",
+ headers = await _signed_in(client, db_session, "mailmod_test",
"mailmod@example.test")
await db_session.execute(
- update(User).where(User.username == "mailmod").values(role="moderator"))
+ update(User).where(User.username == "mailmod_test").values(role="moderator"))
await db_session.commit()
assert (await client.get("/v1/admin/settings", headers=headers)).status_code == 200
diff --git a/packages/meshbay-hub/tests/test_moderation.py b/packages/meshbay-hub/tests/test_moderation.py
index 6848929..3109e29 100644
--- a/packages/meshbay-hub/tests/test_moderation.py
+++ b/packages/meshbay-hub/tests/test_moderation.py
@@ -77,7 +77,7 @@ async def test_same_reporter_cannot_walk_the_threshold(client, reporter):
async def test_auto_block_on_distinct_reporters(client):
h = "c" * 64
for i in range(3):
- headers = await _register_and_login(client, f"rep_{i}")
+ headers = await _register_and_login(client, f"reporter_{i}")
r = await client.post("/v1/reports",
json={"content_hash": h, "reason": "illegal"},
headers=headers)
diff --git a/packages/meshbay-hub/tests/test_node_auth.py b/packages/meshbay-hub/tests/test_node_auth.py
index a104a72..4137932 100644
--- a/packages/meshbay-hub/tests/test_node_auth.py
+++ b/packages/meshbay-hub/tests/test_node_auth.py
@@ -153,7 +153,7 @@ async def test_node_token_may_add_a_member_to_its_own_operators_group(client):
the invitee's SPA (commit 0443cf8). A node-scoped token is accepted here —
the `group.admin_id == caller` check is the guard — but only for a group the
node's operator owns."""
- sk_op, op_token = await _setup_node_user(client, "op1")
+ sk_op, op_token = await _setup_node_user(client, "op1_test")
r = await client.post("/v1/groups", json={
"name": "mygroup", "visibility": "private", "join_policy": "invite",
@@ -162,34 +162,34 @@ async def test_node_token_may_add_a_member_to_its_own_operators_group(client):
_, pk2 = _gen_ed25519()
_, px2 = _gen_x25519()
- await _register(client, "member1", pk2, px2)
+ await _register(client, "member1_test", pk2, px2)
- node_token = (await _node_auth(client, "op1", sk_op)).json()["access_token"]
+ node_token = (await _node_auth(client, "op1_test", sk_op)).json()["access_token"]
- r = await client.post(f"/v1/groups/{gid}/members/member1",
+ r = await client.post(f"/v1/groups/{gid}/members/member1_test",
headers={"Authorization": f"Bearer {node_token}"})
assert r.status_code == 201
# …but not to a group it does not own.
- sk_other, other_token = await _setup_node_user(client, "op2")
+ sk_other, other_token = await _setup_node_user(client, "op2_test")
r = await client.post("/v1/groups", json={"name": "theirs", "visibility": "private"},
headers={"Authorization": f"Bearer {other_token}"})
other_gid = r.json()["group_id"]
- r = await client.post(f"/v1/groups/{other_gid}/members/member1",
+ r = await client.post(f"/v1/groups/{other_gid}/members/member1_test",
headers={"Authorization": f"Bearer {node_token}"})
assert r.status_code == 403
@pytest.mark.asyncio
async def test_node_scope_blocks_delete_group(client):
- sk_node, user_token = await _setup_node_user(client, "op2")
+ sk_node, user_token = await _setup_node_user(client, "op2_test")
r = await client.post("/v1/groups", json={
"name": "deleteme", "visibility": "public", "join_policy": "open",
}, headers={"Authorization": f"Bearer {user_token}"})
gid = r.json()["group_id"]
- r = await _node_auth(client, "op2", sk_node)
+ r = await _node_auth(client, "op2_test", sk_node)
node_token = r.json()["access_token"]
r = await client.delete(f"/v1/groups/{gid}",
@@ -199,14 +199,14 @@ async def test_node_scope_blocks_delete_group(client):
@pytest.mark.asyncio
async def test_node_scope_allows_read_members(client):
- sk_node, user_token = await _setup_node_user(client, "op3")
+ sk_node, user_token = await _setup_node_user(client, "op3_test")
r = await client.post("/v1/groups", json={
"name": "readgroup", "visibility": "private", "join_policy": "invite",
}, headers={"Authorization": f"Bearer {user_token}"})
gid = r.json()["group_id"]
- r = await _node_auth(client, "op3", sk_node)
+ r = await _node_auth(client, "op3_test", sk_node)
node_token = r.json()["access_token"]
r = await client.get(f"/v1/groups/{gid}/members",
@@ -217,12 +217,12 @@ async def test_node_scope_allows_read_members(client):
@pytest.mark.asyncio
async def test_node_scope_allows_pubkey_lookup(client):
- sk_node, _ = await _setup_node_user(client, "op4")
+ sk_node, _ = await _setup_node_user(client, "op4_test")
- r = await _node_auth(client, "op4", sk_node)
+ r = await _node_auth(client, "op4_test", sk_node)
node_token = r.json()["access_token"]
- r = await client.get("/v1/users/op4/pubkeys",
+ r = await client.get("/v1/users/op4_test/pubkeys",
headers={"Authorization": f"Bearer {node_token}"})
assert r.status_code == 200
# An account id and the node's linking key — no user identity keys (H3).
@@ -235,9 +235,9 @@ async def test_user_scope_still_works(client):
"""Verify that user-scoped tokens (browser login) still have full access."""
_, pk_ed = _gen_ed25519()
_, pk_x = _gen_x25519()
- await _register(client, "webuser", pk_ed, pk_x)
+ await _register(client, "webuser_test", pk_ed, pk_x)
- token = await _user_login(client, "webuser")
+ token = await _user_login(client, "webuser_test")
r = await client.post("/v1/groups", json={
"name": "browser-group", "visibility": "public", "join_policy": "open",
@@ -268,9 +268,9 @@ async def test_link_node_key(client):
async def test_link_node_key_invalid_format(client):
_, pk_ed = _gen_ed25519()
_, pk_x = _gen_x25519()
- await _register(client, "badkey", pk_ed, pk_x)
+ await _register(client, "badkey_test", pk_ed, pk_x)
- token = await _user_login(client, "badkey")
+ token = await _user_login(client, "badkey_test")
r = await client.put("/v1/users/me/node_key", json={
"pk_node_ed25519": "not-valid-base64!!!",
@@ -281,9 +281,9 @@ async def test_link_node_key_invalid_format(client):
@pytest.mark.asyncio
async def test_link_node_key_blocked_for_node_scope(client):
"""Node-scoped tokens must not be able to change the node key."""
- sk_node, _ = await _setup_node_user(client, "sneaky")
+ sk_node, _ = await _setup_node_user(client, "sneaky_test")
- r = await _node_auth(client, "sneaky", sk_node)
+ r = await _node_auth(client, "sneaky_test", sk_node)
node_token = r.json()["access_token"]
_, pk_evil = _gen_ed25519()
diff --git a/packages/meshbay-hub/tests/test_node_ws_auth.py b/packages/meshbay-hub/tests/test_node_ws_auth.py
index 4ead0d7..f3ac3a2 100644
--- a/packages/meshbay-hub/tests/test_node_ws_auth.py
+++ b/packages/meshbay-hub/tests/test_node_ws_auth.py
@@ -72,7 +72,7 @@ async def test_ws_rejects_user_scoped_token(client):
"""C2: a browser token must never be able to register as a node."""
from meshbay_hub.api.revocation import _authorize_node_ws
- victim = await _make_user(client, "victim1")
+ victim = await _make_user(client, "victim1_test")
node_id = await _announce_node(client, victim)
resolved, detail = await _authorize_node_ws(victim["token"], node_id, None)
@@ -88,7 +88,7 @@ async def test_ws_rejects_foreign_node_id(client):
"""
from meshbay_hub.api.revocation import _authorize_node_ws
- victim = await _make_user(client, "victim2")
+ victim = await _make_user(client, "victim2_test")
attacker = await _make_user(client, "attacker2")
victim_node = await _announce_node(client, victim)
await _announce_node(client, attacker)
@@ -104,7 +104,7 @@ async def test_ws_rejects_unknown_node_id(client):
"""C2: an invented node_id must not register either."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "user3")
+ user = await _make_user(client, "user3_test")
resolved, _ = await _authorize_node_ws(_node_token(user), "no-such-node", None)
assert resolved is None
@@ -114,7 +114,7 @@ async def test_ws_rejects_missing_node_id(client):
"""C2: identity may not fall back to the token subject."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "user4")
+ user = await _make_user(client, "user4_test")
resolved, _ = await _authorize_node_ws(_node_token(user), "", None)
assert resolved is None
@@ -124,7 +124,7 @@ async def test_ws_accepts_own_node(client):
"""The legitimate path still works."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "owner5")
+ user = await _make_user(client, "owner5_test")
node_id = await _announce_node(client, user)
resolved, groups = await _authorize_node_ws(_node_token(user), node_id, None)
@@ -140,7 +140,7 @@ async def test_ws_group_claims_cannot_widen_beyond_membership(client):
"""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "owner6")
+ user = await _make_user(client, "owner6_test")
node_id = await _announce_node(client, user)
r = await client.post(
@@ -168,7 +168,7 @@ async def test_signaling_rejects_non_member(client):
"""
from meshbay_hub.api import revocation as rev
- owner = await _make_user(client, "owner8")
+ owner = await _make_user(client, "owner8_test")
outsider = await _make_user(client, "outsider8")
node_id = await _announce_node(client, owner)
@@ -201,7 +201,7 @@ async def test_signaling_rejects_non_member(client):
@pytest.mark.asyncio
async def test_signaling_rejects_oversized_sdp(client):
"""H6: an SDP offer is ~2 KB; unbounded input is a memory amplifier."""
- user = await _make_user(client, "user9")
+ user = await _make_user(client, "user9_test")
resp = await client.post(
"/v1/nodes/whatever/webrtc/offer",
json={"sdp": "v=0" + ("x" * 200_000), "ice_candidates": []},
@@ -219,7 +219,7 @@ async def test_incoming_rejects_foreign_peer_ip(client):
"""
from meshbay_hub.api import revocation as rev
- owner = await _make_user(client, "owner10")
+ owner = await _make_user(client, "owner10_test")
node_id = await _announce_node(client, owner)
class _FakeWS:
@@ -243,7 +243,7 @@ async def test_ws_node_may_narrow_its_group_set(client):
"""A node hosting a subset of the operator's groups may say so."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "owner7")
+ user = await _make_user(client, "owner7_test")
node_id = await _announce_node(client, user)
created = []
@@ -282,7 +282,7 @@ async def test_announce_requires_proof_of_possession(client):
the private key, so a user could announce a record carrying someone else's
node key.
"""
- user = await _make_user(client, "ann1")
+ user = await _make_user(client, "ann1_test")
r = await client.post(
"/v1/nodes/announce",
json={"pk_node": user["pk_ed"], "endpoint_hint": "test"},
@@ -294,7 +294,7 @@ async def test_announce_requires_proof_of_possession(client):
@pytest.mark.asyncio
async def test_announce_rejects_foreign_key(client):
"""M8: announcing someone else's public key must fail — no matching private key."""
- user = await _make_user(client, "ann2")
+ user = await _make_user(client, "ann2_test")
victim_sk = Ed25519PrivateKey.generate()
victim_pk = pk_to_b64(victim_sk.public_key())
@@ -312,7 +312,7 @@ async def test_announce_rejects_foreign_key(client):
async def test_announce_rejects_stale_timestamp(client):
"""M8: a captured announce must not be replayable later."""
import time as _t
- user = await _make_user(client, "ann3")
+ user = await _make_user(client, "ann3_test")
sk = Ed25519PrivateKey.generate()
payload = _announce_payload(
user["user_id"], sk, pk_to_b64(sk.public_key()), ts=int(_t.time()) - 3600)
@@ -327,7 +327,7 @@ async def test_announce_rejects_stale_timestamp(client):
@pytest.mark.asyncio
async def test_announce_with_valid_proof_succeeds_and_is_idempotent(client):
"""The legitimate path works, and re-announcing updates rather than piling up rows."""
- user = await _make_user(client, "ann4")
+ user = await _make_user(client, "ann4_test")
sk = Ed25519PrivateKey.generate()
pk_b64 = pk_to_b64(sk.public_key())
@@ -362,7 +362,7 @@ async def test_ws_absent_claim_registers_no_groups(client):
"""A node that declares nothing hosts nothing — it must not inherit the set."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "empty1")
+ user = await _make_user(client, "empty1_test")
node_id = await _announce_node(client, user)
for name in ("has-one", "has-two"):
r = await client.post(
@@ -383,7 +383,7 @@ async def test_ws_explicit_empty_claim_registers_no_groups(client):
"""And the same when the node says so out loud, which it now does."""
from meshbay_hub.api.revocation import _authorize_node_ws
- user = await _make_user(client, "empty2")
+ user = await _make_user(client, "empty2_test")
node_id = await _announce_node(client, user)
r = await client.post(
"/v1/groups",
@@ -407,8 +407,8 @@ async def test_empty_node_cannot_shadow_another_members_group(client):
from meshbay_hub.api.revocation import (
_authorize_node_ws, _node_groups, get_online_nodes_for_group)
- host = await _make_user(client, "hoster")
- guest = await _make_user(client, "guest")
+ host = await _make_user(client, "hoster_test")
+ guest = await _make_user(client, "guest_test")
host_node = await _announce_node(client, host)
guest_node = await _announce_node(client, guest)
@@ -421,7 +421,7 @@ async def test_empty_node_cannot_shadow_another_members_group(client):
group_id = r.json()["group_id"]
r = await client.post(
- f"/v1/groups/{group_id}/members/{'guest'}",
+ f"/v1/groups/{group_id}/members/{'guest_test'}",
headers={"Authorization": f"Bearer {host['token']}"},
)
assert r.status_code == 201, r.text
diff --git a/packages/meshbay-hub/tests/test_notification_dismissal.py b/packages/meshbay-hub/tests/test_notification_dismissal.py
index 07cf715..1c9c7b3 100644
--- a/packages/meshbay-hub/tests/test_notification_dismissal.py
+++ b/packages/meshbay-hub/tests/test_notification_dismissal.py
@@ -64,11 +64,11 @@ async def _login(client, username):
async def _one_notification_for_alice(client):
"""A role change is the cheapest thing that notifies somebody."""
- await _register(client, "admin", "admin@x.com")
- set_admin_usernames(["admin"])
- admin_token = await _login(client, "admin")
- uid = await _register(client, "alice", "a@x.com")
- alice_token = await _login(client, "alice")
+ await _register(client, "admin_test", "admin@x.com")
+ set_admin_usernames(["admin_test"])
+ admin_token = await _login(client, "admin_test")
+ uid = await _register(client, "alice_test", "a@x.com")
+ alice_token = await _login(client, "alice_test")
await client.patch(f"/v1/admin/users/{uid}", json={"role": "moderator"},
headers={"Authorization": f"Bearer {admin_token}"})
return alice_token
diff --git a/packages/meshbay-hub/tests/test_notifications.py b/packages/meshbay-hub/tests/test_notifications.py
index 35ed288..02aca1c 100644
--- a/packages/meshbay-hub/tests/test_notifications.py
+++ b/packages/meshbay-hub/tests/test_notifications.py
@@ -32,7 +32,7 @@ async def _login(client, username, password="testpass99"):
return r.json()["access_token"]
-async def _setup_admin(client, admin_name="admin"):
+async def _setup_admin(client, admin_name="admin_test"):
user_id = await _register(client, admin_name, email=f"{admin_name}@x.com")
set_admin_usernames([admin_name])
token = await _login(client, admin_name)
@@ -41,8 +41,8 @@ async def _setup_admin(client, admin_name="admin"):
@pytest.mark.asyncio
async def test_notifications_empty(client):
- await _register(client, "alice", email="a@x.com")
- token = await _login(client, "alice")
+ await _register(client, "alice_test", email="a@x.com")
+ token = await _login(client, "alice_test")
r = await client.get("/v1/notifications", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
data = r.json()
@@ -53,8 +53,8 @@ async def test_notifications_empty(client):
@pytest.mark.asyncio
async def test_notification_on_role_change(client):
_, admin_token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ uid = await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
await client.patch(f"/v1/admin/users/{uid}",
json={"role": "moderator"},
@@ -72,8 +72,8 @@ async def test_notification_on_role_change(client):
@pytest.mark.asyncio
async def test_notification_on_suspend(client):
_, admin_token = await _setup_admin(client)
- uid = await _register(client, "bob", email="b@x.com")
- bob_token = await _login(client, "bob")
+ uid = await _register(client, "bob_test", email="b@x.com")
+ bob_token = await _login(client, "bob_test")
await client.patch(f"/v1/admin/users/{uid}",
json={"status": "suspended"},
@@ -94,8 +94,8 @@ async def test_notification_on_suspend(client):
@pytest.mark.asyncio
async def test_dismissing_one_deletes_it(client):
_, admin_token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ uid = await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
await client.patch(f"/v1/admin/users/{uid}",
json={"role": "moderator"},
@@ -122,8 +122,8 @@ async def test_dismissing_one_deletes_it(client):
@pytest.mark.asyncio
async def test_dismissing_all_deletes_them(client):
_, admin_token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ uid = await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
await client.patch(f"/v1/admin/users/{uid}",
json={"role": "moderator"},
@@ -151,8 +151,8 @@ async def test_dismissing_all_deletes_them(client):
@pytest.mark.asyncio
async def test_notification_unread_filter(client):
_, admin_token = await _setup_admin(client)
- uid = await _register(client, "alice", email="a@x.com")
- alice_token = await _login(client, "alice")
+ uid = await _register(client, "alice_test", email="a@x.com")
+ alice_token = await _login(client, "alice_test")
await client.patch(f"/v1/admin/users/{uid}",
json={"role": "moderator"},
diff --git a/packages/meshbay-hub/tests/test_notifications_behaviour.py b/packages/meshbay-hub/tests/test_notifications_behaviour.py
index 5fe9170..a247508 100644
--- a/packages/meshbay-hub/tests/test_notifications_behaviour.py
+++ b/packages/meshbay-hub/tests/test_notifications_behaviour.py
@@ -37,7 +37,7 @@ async def test_chat_keeps_one_notification_per_group(client, db_session):
from meshbay_hub.api.notifications import create_notification
token = await _user(client, "listener")
- owner = await _user(client, "talker")
+ owner = await _user(client, "talker_test")
g = await client.post("/v1/groups", json={"name": "busy"},
headers={"Authorization": f"Bearer {owner}"})
gid = g.json()["group_id"]
@@ -68,12 +68,12 @@ async def test_muting_a_group_stops_notifications_being_created(client, db_sessi
"""
from meshbay_hub.api.notifications import create_notification
- token = await _user(client, "quiet")
- owner = await _user(client, "noisy")
+ token = await _user(client, "quiet_test")
+ owner = await _user(client, "noisy_test")
g = await client.post("/v1/groups", json={"name": "loud"},
headers={"Authorization": f"Bearer {owner}"})
gid = g.json()["group_id"]
- await client.post(f"/v1/groups/{gid}/members/quiet", json={},
+ await client.post(f"/v1/groups/{gid}/members/quiet_test", json={},
headers={"Authorization": f"Bearer {owner}"})
r = await client.post(f"/v1/groups/{gid}/mute", json={"muted": True},
@@ -81,7 +81,7 @@ async def test_muting_a_group_stops_notifications_being_created(client, db_sessi
assert r.status_code == 200, r.text
uid = (await db_session.execute(
- select(User.id).where(User.username == "quiet"))).scalar_one()
+ select(User.id).where(User.username == "quiet_test"))).scalar_one()
await db_session.execute(
select(GroupMember).where(GroupMember.user_id == uid))
@@ -138,10 +138,10 @@ async def test_purge_clears_the_list(client, db_session):
async def test_only_your_own_notifications_are_purged(client, db_session):
from meshbay_hub.api.notifications import create_notification
- mine = await _user(client, "self")
- await _user(client, "other")
+ mine = await _user(client, "self_test")
+ await _user(client, "other_test")
other_id = (await db_session.execute(
- select(User.id).where(User.username == "other"))).scalar_one()
+ select(User.id).where(User.username == "other_test"))).scalar_one()
await create_notification(db_session, other_id, "system", "not yours")
await db_session.commit()
@@ -165,17 +165,17 @@ async def test_you_are_not_notified_of_your_own_message(client, db_session):
from meshbay_hub.api.revocation import _handle_chat_notify
owner = await _user(client, "operator")
- await _user(client, "chatty")
- await _user(client, "quiet")
+ await _user(client, "chatty_test")
+ await _user(client, "quiet_test")
g = await client.post("/v1/groups", json={"name": "room"},
headers={"Authorization": f"Bearer {owner}"})
gid = g.json()["group_id"]
- for name in ("chatty", "quiet"):
+ for name in ("chatty_test", "quiet_test"):
await client.post(f"/v1/groups/{gid}/members/{name}", json={},
headers={"Authorization": f"Bearer {owner}"})
ids = {u.username: u.id for u in (await db_session.execute(
- select(User).where(User.username.in_(["operator", "chatty", "quiet"]))
+ select(User).where(User.username.in_(["operator", "chatty_test", "quiet_test"]))
)).scalars().all()}
# A node registered for this group, because a notification is now written
@@ -183,7 +183,7 @@ async def test_you_are_not_notified_of_your_own_message(client, db_session):
node_id = "notify-test-node"
rev._node_groups[node_id] = [gid]
try:
- await _handle_chat_notify(gid, "chatty", ids["chatty"], node_id=node_id)
+ await _handle_chat_notify(gid, "chatty_test", ids["chatty_test"], node_id=node_id)
finally:
rev._node_groups.pop(node_id, None)
@@ -193,7 +193,7 @@ async def test_you_are_not_notified_of_your_own_message(client, db_session):
Notification.kind == "chat_message")
)).scalars().all()
- assert await chat_rows(ids["chatty"]) == [], "notified of their own message"
- assert len(await chat_rows(ids["quiet"])) == 1
+ assert await chat_rows(ids["chatty_test"]) == [], "notified of their own message"
+ assert len(await chat_rows(ids["quiet_test"])) == 1
assert len(await chat_rows(ids["operator"])) == 1, \
"the operator reads the group too, and was the one being skipped"
diff --git a/packages/meshbay-hub/tests/test_password_change.py b/packages/meshbay-hub/tests/test_password_change.py
index aba2d9a..4d2f303 100644
--- a/packages/meshbay-hub/tests/test_password_change.py
+++ b/packages/meshbay-hub/tests/test_password_change.py
@@ -37,44 +37,44 @@ async def _register(client, username, password="the-first-passphrase"):
@pytest.mark.asyncio
async def test_change_then_sign_in_with_the_new_passphrase(client):
old, new = "the-first-passphrase", "a-second-passphrase-entirely"
- session = await _register(client, "alice", old)
+ session = await _register(client, "alice_test", old)
r = await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key(old, "alice"),
- "new_auth_key": _auth_key(new, "alice"),
+ "old_auth_key": _auth_key(old, "alice_test"),
+ "new_auth_key": _auth_key(new, "alice_test"),
}, headers={"Authorization": f"Bearer {session['access_token']}"})
assert r.status_code == 200, r.text
assert r.json()["status"] == "changed"
assert (await client.post("/v1/users/login", json={
- "username": "alice", "auth_key": _auth_key(old, "alice")})).status_code == 401
+ "username": "alice_test", "auth_key": _auth_key(old, "alice_test")})).status_code == 401
assert (await client.post("/v1/users/login", json={
- "username": "alice", "auth_key": _auth_key(new, "alice")})).status_code == 200
+ "username": "alice_test", "auth_key": _auth_key(new, "alice_test")})).status_code == 200
@pytest.mark.asyncio
async def test_wrong_current_passphrase_is_refused_and_changes_nothing(client):
old = "the-first-passphrase"
- session = await _register(client, "bob", old)
+ session = await _register(client, "bob_test", old)
r = await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key("not the passphrase", "bob"),
- "new_auth_key": _auth_key("some-new-passphrase", "bob"),
+ "old_auth_key": _auth_key("not the passphrase", "bob_test"),
+ "new_auth_key": _auth_key("some-new-passphrase", "bob_test"),
}, headers={"Authorization": f"Bearer {session['access_token']}"})
assert r.status_code == 403
assert (await client.post("/v1/users/login", json={
- "username": "bob", "auth_key": _auth_key(old, "bob")})).status_code == 200
+ "username": "bob_test", "auth_key": _auth_key(old, "bob_test")})).status_code == 200
@pytest.mark.asyncio
async def test_new_must_differ_from_old(client):
old = "the-first-passphrase"
- session = await _register(client, "carol", old)
+ session = await _register(client, "carol_test", old)
r = await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key(old, "carol"),
- "new_auth_key": _auth_key(old, "carol"),
+ "old_auth_key": _auth_key(old, "carol_test"),
+ "new_auth_key": _auth_key(old, "carol_test"),
}, headers={"Authorization": f"Bearer {session['access_token']}"})
assert r.status_code == 400
@@ -82,10 +82,10 @@ async def test_new_must_differ_from_old(client):
@pytest.mark.asyncio
async def test_unauthenticated_call_is_rejected(client):
"""A session is required — the current passphrase alone is not a credential."""
- await _register(client, "dave", "the-first-passphrase")
+ await _register(client, "dave_test", "the-first-passphrase")
r = await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key("the-first-passphrase", "dave"),
- "new_auth_key": _auth_key("a-new-one", "dave"),
+ "old_auth_key": _auth_key("the-first-passphrase", "dave_test"),
+ "new_auth_key": _auth_key("a-new-one", "dave_test"),
})
assert r.status_code in (401, 403, 422)
@@ -94,14 +94,14 @@ async def test_unauthenticated_call_is_rejected(client):
async def test_other_sessions_are_invalidated_and_the_caller_keeps_one(
client, db_session):
old, new = "the-first-passphrase", "a-second-passphrase-entirely"
- first = await _register(client, "erin", old)
+ first = await _register(client, "erin_test", old)
# A second browser signs in before the change.
second = (await client.post("/v1/users/login", json={
- "username": "erin", "auth_key": _auth_key(old, "erin")})).json()
+ "username": "erin_test", "auth_key": _auth_key(old, "erin_test")})).json()
r = await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key(old, "erin"),
- "new_auth_key": _auth_key(new, "erin"),
+ "old_auth_key": _auth_key(old, "erin_test"),
+ "new_auth_key": _auth_key(new, "erin_test"),
}, headers={"Authorization": f"Bearer {first['access_token']}"})
assert r.status_code == 200, r.text
@@ -116,7 +116,7 @@ async def test_other_sessions_are_invalidated_and_the_caller_keeps_one(
assert fresh.status_code == 200, fresh.text
uid = (await db_session.execute(
- select(User.id).where(User.username == "erin"))).scalar_one()
+ select(User.id).where(User.username == "erin_test"))).scalar_one()
live = (await db_session.execute(
select(RefreshToken).where(RefreshToken.user_id == uid,
RefreshToken.revoked.is_(False)))).scalars().all()
@@ -127,14 +127,14 @@ async def test_other_sessions_are_invalidated_and_the_caller_keeps_one(
@pytest.mark.asyncio
async def test_the_change_is_logged(client, db_session):
old, new = "the-first-passphrase", "a-second-passphrase-entirely"
- session = await _register(client, "frank", old)
+ session = await _register(client, "frank_test", old)
await client.post("/v1/users/password", json={
- "old_auth_key": _auth_key(old, "frank"),
- "new_auth_key": _auth_key(new, "frank"),
+ "old_auth_key": _auth_key(old, "frank_test"),
+ "new_auth_key": _auth_key(new, "frank_test"),
}, headers={"Authorization": f"Bearer {session['access_token']}"})
uid = (await db_session.execute(
- select(User.id).where(User.username == "frank"))).scalar_one()
+ select(User.id).where(User.username == "frank_test"))).scalar_one()
events = {e.event for e in (await db_session.execute(
select(IPLog).where(IPLog.user_id == uid))).scalars().all()}
assert "password_change" in events
diff --git a/packages/meshbay-hub/tests/test_password_reset.py b/packages/meshbay-hub/tests/test_password_reset.py
index 80977c5..1273315 100644
--- a/packages/meshbay-hub/tests/test_password_reset.py
+++ b/packages/meshbay-hub/tests/test_password_reset.py
@@ -48,22 +48,22 @@ async def _reset_code(db_session, username) -> str:
@pytest.mark.asyncio
async def test_reset_lets_the_user_sign_in_with_a_new_passphrase(client, db_session):
- await _register(client, "alice", "old" + "a" * 41)
- r = await _request_reset(client, "alice")
+ await _register(client, "alice_test", "old" + "a" * 41)
+ r = await _request_reset(client, "alice_test")
assert r.status_code == 200 and r.json()["status"] == "sent_if_exists"
- code = await _reset_code(db_session, "alice")
+ code = await _reset_code(db_session, "alice_test")
assert code
new = "new" + "b" * 41
r = await client.post("/v1/users/password/reset", json={
- "username": "alice", "code": code, "new_auth_key": new})
+ "username": "alice_test", "code": code, "new_auth_key": new})
assert r.status_code == 200, r.text
assert (await client.post("/v1/users/login", json={
- "username": "alice", "auth_key": "old" + "a" * 41})).status_code == 401
+ "username": "alice_test", "auth_key": "old" + "a" * 41})).status_code == 401
assert (await client.post("/v1/users/login", json={
- "username": "alice", "auth_key": new})).status_code == 200
+ "username": "alice_test", "auth_key": new})).status_code == 200
@pytest.mark.asyncio
@@ -78,81 +78,81 @@ async def test_reset_request_never_reveals_whether_an_account_exists(
@pytest.mark.asyncio
async def test_reset_request_needs_the_username_and_email_to_match(client, db_session):
- await _register(client, "hank")
+ await _register(client, "hank_test")
# Right username, wrong e-mail — answered exactly like an unknown account,
# and no code is created.
- r = await _request_reset(client, "hank", email="someone.else@example.com")
+ r = await _request_reset(client, "hank_test", email="someone.else@example.com")
assert r.status_code == 200
assert r.json()["status"] == "sent_if_exists"
assert (await db_session.execute(
select(EmailVerification))).scalars().all() == []
# The real pair does create one.
- await _request_reset(client, "hank")
+ await _request_reset(client, "hank_test")
assert (await db_session.execute(
select(EmailVerification))).scalars().first() is not None
@pytest.mark.asyncio
async def test_reset_request_rejects_a_malformed_email(client):
- await _register(client, "iris")
+ await _register(client, "iris_test")
r = await client.post("/v1/users/password/reset-request", json={
- "username": "iris", "email": "not-an-email"})
+ "username": "iris_test", "email": "not-an-email"})
assert r.status_code == 422
@pytest.mark.asyncio
async def test_a_wrong_code_is_rejected_and_counts_against_the_limit(
client, db_session):
- await _register(client, "bob")
- await _request_reset(client, "bob")
+ await _register(client, "bob_test")
+ await _request_reset(client, "bob_test")
for _ in range(10):
r = await client.post("/v1/users/password/reset", json={
- "username": "bob", "code": "000000", "new_auth_key": "x" * 44})
+ "username": "bob_test", "code": "000000", "new_auth_key": "x" * 44})
assert r.status_code == 400
r = await client.post("/v1/users/password/reset", json={
- "username": "bob", "code": "000000", "new_auth_key": "x" * 44})
+ "username": "bob_test", "code": "000000", "new_auth_key": "x" * 44})
assert r.status_code == 429
@pytest.mark.asyncio
async def test_an_expired_code_is_refused(client, db_session):
- await _register(client, "carol")
- await _request_reset(client, "carol")
+ await _register(client, "carol_test")
+ await _request_reset(client, "carol_test")
uid = (await db_session.execute(
- select(User.id).where(User.username == "carol"))).scalar_one()
+ select(User.id).where(User.username == "carol_test"))).scalar_one()
row = (await db_session.execute(select(EmailVerification).where(
EmailVerification.user_id == uid))).scalars().one()
row.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1)
await db_session.commit()
r = await client.post("/v1/users/password/reset", json={
- "username": "carol", "code": row.code, "new_auth_key": "y" * 44})
+ "username": "carol_test", "code": row.code, "new_auth_key": "y" * 44})
assert r.status_code == 410
@pytest.mark.asyncio
async def test_a_reset_code_works_once(client, db_session):
- await _register(client, "dave")
- await _request_reset(client, "dave")
- code = await _reset_code(db_session, "dave")
+ await _register(client, "dave_test")
+ await _request_reset(client, "dave_test")
+ code = await _reset_code(db_session, "dave_test")
first = await client.post("/v1/users/password/reset", json={
- "username": "dave", "code": code, "new_auth_key": "z" * 44})
+ "username": "dave_test", "code": code, "new_auth_key": "z" * 44})
assert first.status_code == 200
second = await client.post("/v1/users/password/reset", json={
- "username": "dave", "code": code, "new_auth_key": "z" * 44})
+ "username": "dave_test", "code": code, "new_auth_key": "z" * 44})
assert second.status_code == 404
@pytest.mark.asyncio
async def test_reset_revokes_sessions_and_wipes_devices(client, db_session):
- await _register(client, "erin", "erin" + "a" * 40)
+ await _register(client, "erin_test", "erin_test" + "a" * 40)
login = await client.post("/v1/users/login", json={
- "username": "erin", "auth_key": "erin" + "a" * 40})
+ "username": "erin_test", "auth_key": "erin_test" + "a" * 40})
refresh_token = login.json()["refresh_token"]
token = login.json()["access_token"]
@@ -163,10 +163,10 @@ async def test_reset_revokes_sessions_and_wipes_devices(client, db_session):
headers={"Authorization": f"Bearer {token}"})
assert dev.status_code == 201, dev.text
- await _request_reset(client, "erin")
- code = await _reset_code(db_session, "erin")
+ await _request_reset(client, "erin_test")
+ code = await _reset_code(db_session, "erin_test")
r = await client.post("/v1/users/password/reset", json={
- "username": "erin", "code": code, "new_auth_key": "erin-new" + "b" * 36})
+ "username": "erin_test", "code": code, "new_auth_key": "erin-new" + "b" * 36})
assert r.status_code == 200
# Old refresh token is dead.
@@ -175,7 +175,7 @@ async def test_reset_revokes_sessions_and_wipes_devices(client, db_session):
# Every device key is gone; the stored one can no longer sign in.
uid = (await db_session.execute(
- select(User.id).where(User.username == "erin"))).scalar_one()
+ select(User.id).where(User.username == "erin_test"))).scalar_one()
from meshbay_hub.db.models import UserDevice
devices = (await db_session.execute(
select(UserDevice).where(UserDevice.user_id == uid))).scalars().all()
@@ -184,21 +184,21 @@ async def test_reset_revokes_sessions_and_wipes_devices(client, db_session):
ts = int(time.time())
msg = f"meshbay:user_auth:erin:{ts}".encode()
da = await client.post("/v1/users/auth", json={
- "username": "erin", "timestamp": ts,
+ "username": "erin_test", "timestamp": ts,
"signature": base64.b64encode(sk.sign(msg)).decode()})
assert da.status_code == 401
@pytest.mark.asyncio
async def test_the_request_and_the_reset_are_logged(client, db_session):
- await _register(client, "frank")
- await _request_reset(client, "frank")
- code = await _reset_code(db_session, "frank")
+ await _register(client, "frank_test")
+ await _request_reset(client, "frank_test")
+ code = await _reset_code(db_session, "frank_test")
await client.post("/v1/users/password/reset", json={
- "username": "frank", "code": code, "new_auth_key": "f" * 44})
+ "username": "frank_test", "code": code, "new_auth_key": "f" * 44})
uid = (await db_session.execute(
- select(User.id).where(User.username == "frank"))).scalar_one()
+ select(User.id).where(User.username == "frank_test"))).scalar_one()
events = {e.event for e in (await db_session.execute(
select(IPLog).where(IPLog.user_id == uid))).scalars().all()}
assert {"password_reset_request", "password_reset"} <= events
diff --git a/packages/meshbay-hub/tests/test_public_groups_toggle.py b/packages/meshbay-hub/tests/test_public_groups_toggle.py
index 2a57429..96e3383 100644
--- a/packages/meshbay-hub/tests/test_public_groups_toggle.py
+++ b/packages/meshbay-hub/tests/test_public_groups_toggle.py
@@ -39,7 +39,7 @@ async def _user(client, username, password="a-long-enough-passphrase"):
return {"Authorization": f"Bearer {r.json()['access_token']}"}
-async def _admin(client, username="root"):
+async def _admin(client, username="root_test"):
await _user(client, username)
set_admin_usernames([username])
# re-login so the token is minted with the admin role in context
@@ -74,7 +74,7 @@ async def _set_public_groups(client, admin, allowed):
@pytest.mark.asyncio
async def test_public_groups_are_allowed_by_default(client):
- owner = await _user(client, "alice")
+ owner = await _user(client, "alice_test")
r = await client.get("/v1/hub/info")
assert r.json()["allow_public_groups"] is True
r = await client.post("/v1/groups", json=_public("open-house"), headers=owner)
@@ -83,7 +83,7 @@ async def test_public_groups_are_allowed_by_default(client):
@pytest.mark.asyncio
async def test_a_normal_member_cannot_change_the_setting(client):
- member = await _user(client, "mallory")
+ member = await _user(client, "mallory_test")
r = await client.patch("/v1/admin/settings",
json={"allow_public_groups": False}, headers=member)
assert r.status_code == 403
@@ -92,7 +92,7 @@ async def test_a_normal_member_cannot_change_the_setting(client):
@pytest.mark.asyncio
async def test_admin_disables_public_groups_end_to_end(client):
admin = await _admin(client)
- owner = await _user(client, "bob")
+ owner = await _user(client, "bob_test")
r = await client.patch("/v1/admin/settings",
json={"allow_public_groups": False}, headers=admin)
@@ -122,8 +122,8 @@ async def test_admin_disables_public_groups_end_to_end(client):
@pytest.mark.asyncio
async def test_re_enabling_restores_public_creation(client):
- admin = await _admin(client, "chief")
- owner = await _user(client, "carol")
+ admin = await _admin(client, "chief_test")
+ owner = await _user(client, "carol_test")
await client.patch("/v1/admin/settings",
json={"allow_public_groups": False}, headers=admin)
@@ -138,7 +138,7 @@ async def test_re_enabling_restores_public_creation(client):
@pytest.mark.asyncio
async def test_a_patch_without_the_field_is_a_no_op(client):
- admin = await _admin(client, "keeper")
+ admin = await _admin(client, "keeper_test")
await client.patch("/v1/admin/settings",
json={"allow_public_groups": False}, headers=admin)
r = await client.patch("/v1/admin/settings", json={}, headers=admin)
@@ -151,7 +151,7 @@ async def test_a_patch_without_the_field_is_a_no_op(client):
@pytest.mark.asyncio
async def test_disabled_empties_the_public_directory(client, db_session):
admin = await _admin(client)
- owner = await _user(client, "dora")
+ owner = await _user(client, "dora_test")
gid = await _create_public(client, owner, "town-square")
await _mark_hosted(db_session, gid)
@@ -167,8 +167,8 @@ async def test_disabled_empties_the_public_directory(client, db_session):
@pytest.mark.asyncio
async def test_disabled_refuses_open_join_of_a_public_group(client, db_session):
- admin = await _admin(client, "chief")
- owner = await _user(client, "erin")
+ admin = await _admin(client, "chief_test")
+ owner = await _user(client, "erin_test")
early = await _user(client, "early-bird")
late = await _user(client, "late-comer")
gid = await _create_public(client, owner, "commons")
@@ -188,10 +188,10 @@ async def test_disabled_refuses_open_join_of_a_public_group(client, db_session):
@pytest.mark.asyncio
async def test_disabled_hands_a_non_member_no_node(client, db_session):
- admin = await _admin(client, "chief")
- owner = await _user(client, "frank")
- member = await _user(client, "grace")
- stranger = await _user(client, "heidi")
+ admin = await _admin(client, "chief_test")
+ owner = await _user(client, "frank_test")
+ member = await _user(client, "grace_test")
+ stranger = await _user(client, "heidi_test")
gid = await _create_public(client, owner, "atrium")
await _mark_hosted(db_session, gid)
assert (await client.post(f"/v1/groups/{gid}/join", headers=member)).status_code == 200
@@ -241,7 +241,7 @@ async def test_disabled_empties_the_federation_export(client, monkeypatch):
monkeypatch.setattr(federation, "FEDERATION_ENABLED", True)
admin = await _admin(client)
- owner = await _user(client, "ivan")
+ owner = await _user(client, "ivan_test")
await _create_public(client, owner, "exported-square")
info = (await client.get("/mhp/info")).json()
@@ -262,8 +262,8 @@ async def test_disabled_empties_the_federation_export(client, monkeypatch):
@pytest.mark.asyncio
async def test_re_enabling_brings_the_directory_back(client, db_session):
- admin = await _admin(client, "chief")
- owner = await _user(client, "judy")
+ admin = await _admin(client, "chief_test")
+ owner = await _user(client, "judy_test")
gid = await _create_public(client, owner, "reopened")
await _mark_hosted(db_session, gid)
diff --git a/packages/meshbay-hub/tests/test_recovery_email.py b/packages/meshbay-hub/tests/test_recovery_email.py
index c6dab97..07880d0 100644
--- a/packages/meshbay-hub/tests/test_recovery_email.py
+++ b/packages/meshbay-hub/tests/test_recovery_email.py
@@ -44,8 +44,8 @@ def _skip_email_verification(monkeypatch):
async def test_register_appends_the_recovery_key_to_the_email(
client, _skip_email_verification):
r = await client.post("/v1/users/register", json={
- "username": "rk1", "email": "rk1@example.com",
- "auth_key": _auth_key("a-long-enough-passphrase", "rk1"),
+ "username": "rk1_test", "email": "rk1@example.com",
+ "auth_key": _auth_key("a-long-enough-passphrase", "rk1_test"),
"recovery_key": RECOVERY,
})
assert r.status_code in (200, 201), r.text
@@ -59,8 +59,8 @@ async def test_register_appends_the_recovery_key_to_the_email(
async def test_register_without_recovery_key_sends_only_the_code(
client, _skip_email_verification):
r = await client.post("/v1/users/register", json={
- "username": "rk2", "email": "rk2@example.com",
- "auth_key": _auth_key("a-long-enough-passphrase", "rk2"),
+ "username": "rk2_test", "email": "rk2@example.com",
+ "auth_key": _auth_key("a-long-enough-passphrase", "rk2_test"),
})
assert r.status_code in (200, 201), r.text
body = _skip_email_verification[0].get_content()
@@ -72,8 +72,8 @@ async def test_register_without_recovery_key_sends_only_the_code(
async def test_the_recovery_key_is_not_persisted(
client, db_session, _skip_email_verification):
await client.post("/v1/users/register", json={
- "username": "rk3", "email": "rk3@example.com",
- "auth_key": _auth_key("a-long-enough-passphrase", "rk3"),
+ "username": "rk3_test", "email": "rk3@example.com",
+ "auth_key": _auth_key("a-long-enough-passphrase", "rk3_test"),
"recovery_key": RECOVERY,
})
rows = (await db_session.execute(select(EmailVerification))).scalars().all()
@@ -82,7 +82,7 @@ async def test_the_recovery_key_is_not_persisted(
assert RECOVERY not in (row.code or "")
assert RECOVERY not in (row.email_encrypted or "")
user = (await db_session.execute(
- select(User).where(User.username == "rk3"))).scalar_one()
+ select(User).where(User.username == "rk3_test"))).scalar_one()
assert RECOVERY not in repr(vars(user))
diff --git a/packages/meshbay-hub/tests/test_register_captcha.py b/packages/meshbay-hub/tests/test_register_captcha.py
index 32663fe..5f94b9c 100644
--- a/packages/meshbay-hub/tests/test_register_captcha.py
+++ b/packages/meshbay-hub/tests/test_register_captcha.py
@@ -25,7 +25,7 @@ def captcha_on(client, monkeypatch):
def _body(**over):
- b = {"username": "newbie", "email": "newbie@t.com", "auth_key": "a" * 44}
+ b = {"username": "newbie_test", "email": "newbie@t.com", "auth_key": "a" * 44}
b.update(over)
return b
diff --git a/packages/meshbay-hub/tests/test_revocation.py b/packages/meshbay-hub/tests/test_revocation.py
index d1147e1..ad9caf4 100644
--- a/packages/meshbay-hub/tests/test_revocation.py
+++ b/packages/meshbay-hub/tests/test_revocation.py
@@ -33,7 +33,7 @@ async def test_revoke_user_marks_db(client):
pk_x = pk_to_b64(sk_x.public_key())
r = await client.post("/v1/users/register", json={
- "username": "vic1", "email": "v@t.com", "password": "vicpass99",
+ "username": "vic1_test", "email": "v@t.com", "password": "vicpass99",
"pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x,
})
victim_id = r.json()["user_id"]
@@ -42,11 +42,11 @@ async def test_revoke_user_marks_db(client):
sk_admin_ed = Ed25519PrivateKey.generate()
sk_admin_x = X25519PrivateKey.generate()
admin_token, _ = await _register_and_login(
- client, "admin1",
+ client, "admin1_test",
pk_to_b64(sk_admin_ed.public_key()),
pk_to_b64(sk_admin_x.public_key()),
)
- set_admin_usernames(["admin1"])
+ set_admin_usernames(["admin1_test"])
r = await client.post("/v1/admin/revoke", json={
"target": "user", "target_id": victim_id, "reason": "spam",
@@ -58,7 +58,7 @@ async def test_revoke_user_marks_db(client):
# Victim can no longer login
r = await client.post("/v1/users/login", json={
- "username": "vic1", "password": "vicpass99"})
+ "username": "vic1_test", "password": "vicpass99"})
assert r.status_code == 403
@@ -69,7 +69,7 @@ async def test_revocation_token_verifiable_offline(client):
sk_x = X25519PrivateKey.generate()
r = await client.post("/v1/users/register", json={
- "username": "vic2", "email": "v2@t.com", "password": "vicpass99",
+ "username": "vic2_test", "email": "v2@t.com", "password": "vicpass99",
"pk_user_ed25519": pk_to_b64(sk_ed.public_key()),
"pk_user_x25519": pk_to_b64(sk_x.public_key()),
})
@@ -78,11 +78,11 @@ async def test_revocation_token_verifiable_offline(client):
sk_admin_ed = Ed25519PrivateKey.generate()
sk_admin_x = X25519PrivateKey.generate()
admin_token, _ = await _register_and_login(
- client, "admin2",
+ client, "admin2_test",
pk_to_b64(sk_admin_ed.public_key()),
pk_to_b64(sk_admin_x.public_key()),
)
- set_admin_usernames(["admin2"])
+ set_admin_usernames(["admin2_test"])
r = await client.post("/v1/admin/revoke", json={
"target": "user", "target_id": victim_id, "reason": "test",
@@ -106,11 +106,11 @@ async def test_revoke_group(client):
sk_ed = Ed25519PrivateKey.generate()
sk_x = X25519PrivateKey.generate()
admin_token, _ = await _register_and_login(
- client, "admin3",
+ client, "admin3_test",
pk_to_b64(sk_ed.public_key()),
pk_to_b64(sk_x.public_key()),
)
- set_admin_usernames(["admin3"])
+ set_admin_usernames(["admin3_test"])
hdrs = {"Authorization": f"Bearer {admin_token}"}
r = await client.post("/v1/groups", json={"name": "grp-to-revoke"}, headers=hdrs)
diff --git a/packages/meshbay-hub/tests/test_username_floor.py b/packages/meshbay-hub/tests/test_username_floor.py
new file mode 100644
index 0000000..607aacc
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_username_floor.py
@@ -0,0 +1,58 @@
+"""
+A username is at least 8 characters — at registration, and nowhere else.
+
+Accounts created under the older 3-character floor must keep signing in, so
+the check lives on `RegisterRequest` alone. The client checks the same number
+before deriving anything; the two constants are held equal here.
+"""
+
+import re
+from pathlib import Path
+
+import pytest
+from meshbay_hub.api import users
+
+AUTH_PAGE = (Path(__file__).resolve().parents[1]
+ / "src" / "meshbay_hub" / "static" / "auth-page.js")
+AUTH_KEY = "k" * 44
+
+
+async def _register(client, username):
+ return await client.post("/v1/users/register", json={
+ "username": username, "email": "floor@example.com", "auth_key": AUTH_KEY})
+
+
+@pytest.mark.asyncio
+async def test_seven_characters_is_refused(client):
+ r = await _register(client, "sevenc7")
+ assert r.status_code == 422, r.text
+
+
+@pytest.mark.asyncio
+async def test_surrounding_spaces_do_not_count(client):
+ r = await _register(client, " sevenc7 ")
+ assert r.status_code == 422, r.text
+
+
+@pytest.mark.asyncio
+async def test_eight_characters_is_accepted(client):
+ r = await _register(client, "eightch8")
+ assert r.status_code == 201, r.text
+
+
+@pytest.mark.asyncio
+async def test_an_existing_short_account_still_signs_in(client, monkeypatch):
+ """An account made under the old floor is not locked out by the new one."""
+ monkeypatch.setattr(users, "USERNAME_MIN_LEN", 3)
+ assert (await _register(client, "bob")).status_code == 201
+ monkeypatch.setattr(users, "USERNAME_MIN_LEN", 8)
+
+ r = await client.post("/v1/users/login", json={"username": "bob", "auth_key": AUTH_KEY})
+ assert r.status_code == 200, r.text
+
+
+def test_the_client_checks_the_same_floor():
+ m = re.search(r"^const USERNAME_MIN_LEN = (\d+);", AUTH_PAGE.read_text(encoding="utf-8"),
+ re.M)
+ assert m, "auth-page.js no longer declares USERNAME_MIN_LEN"
+ assert int(m.group(1)) == users.USERNAME_MIN_LEN