diff options
Diffstat (limited to 'packages')
40 files changed, 529 insertions, 449 deletions
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 |