"""Tests for authentication, registration and password reset endpoints.""" from __future__ import annotations import time from bincio.auth import deps from bincio.auth.db import ( change_password, create_email_reset_token, create_invite, create_reset_code, set_suspended, set_user_email, ) from .conftest import auth_cookies, login # ── Login ───────────────────────────────────────────────────────────────────── def test_login_success(client, admin): r = client.post("/api/auth/login", json={"handle": "admin", "password": "adminpass1"}) assert r.status_code == 200 data = r.json() assert data["ok"] is True assert data["handle"] == "admin" assert data["is_admin"] is True assert "bincio_session" in r.cookies def test_login_wrong_password(client, admin): r = client.post("/api/auth/login", json={"handle": "admin", "password": "wrong"}) assert r.status_code == 401 def test_login_unknown_handle(client): r = client.post("/api/auth/login", json={"handle": "nobody", "password": "x"}) assert r.status_code == 401 def test_login_suspended(client, db, admin): set_suspended(db, "admin", True) r = client.post("/api/auth/login", json={"handle": "admin", "password": "adminpass1"}) assert r.status_code == 401 def test_login_handle_case_insensitive(client, admin): r = client.post("/api/auth/login", json={"handle": "ADMIN", "password": "adminpass1"}) assert r.status_code == 200 # ── Logout ──────────────────────────────────────────────────────────────────── def test_logout_clears_cookie(client, admin): cookies = auth_cookies("admin", "adminpass1", client) r = client.post("/api/auth/logout", cookies=cookies) assert r.status_code == 200 assert r.cookies.get("bincio_session") == "" or "bincio_session" not in r.cookies # ── /me ─────────────────────────────────────────────────────────────────────── def test_me_authenticated(client, admin): cookies = auth_cookies("admin", "adminpass1", client) r = client.get("/api/me", cookies=cookies) assert r.status_code == 200 assert r.json()["handle"] == "admin" def test_me_unauthenticated(client): r = client.get("/api/me") assert r.status_code == 401 def test_me_suspended_after_login(client, db, admin): cookies = auth_cookies("admin", "adminpass1", client) set_suspended(db, "admin", True) r = client.get("/api/me", cookies=cookies) assert r.status_code == 401 # ── Registration ────────────────────────────────────────────────────────────── def test_register_success(client, invite): r = client.post("/api/register", json={ "code": invite, "handle": "newuser", "password": "newpass99", "display_name": "New User", }) assert r.status_code == 200 assert r.json()["handle"] == "newuser" assert "bincio_session" in r.cookies def test_register_invalid_invite(client): r = client.post("/api/register", json={ "code": "BADCODE", "handle": "newuser", "password": "newpass99", }) assert r.status_code == 400 def test_register_used_invite(client, db, admin, invite): # Use the invite first client.post("/api/register", json={"code": invite, "handle": "first", "password": "firstpass1"}) r = client.post("/api/register", json={"code": invite, "handle": "second", "password": "secondpass1"}) assert r.status_code == 400 def test_register_duplicate_handle(client, db, admin, invite): client.post("/api/register", json={"code": invite, "handle": "alice", "password": "alicepass1"}) invite2 = create_invite(db, admin.handle) r = client.post("/api/register", json={"code": invite2, "handle": "alice", "password": "alicepass1"}) assert r.status_code == 409 def test_register_invalid_handle(client, invite): # Handles must start with a letter/digit; underscore prefix is invalid r = client.post("/api/register", json={ "code": invite, "handle": "_invalid", "password": "password99", }) assert r.status_code == 400 def test_register_short_password(client, invite): r = client.post("/api/register", json={ "code": invite, "handle": "valid", "password": "short", }) assert r.status_code == 400 def test_register_grants_activity(client, db, admin): code = create_invite(db, admin.handle, grants_activity=True) r = client.post("/api/register", json={"code": code, "handle": "actuser", "password": "actpass99"}) assert r.status_code == 200 cookies = {"bincio_session": r.cookies["bincio_session"]} me = client.get("/api/me", cookies=cookies).json() assert me["activity_access"] is True # ── Admin-issued password reset ─────────────────────────────────────────────── def test_reset_password_admin_code(client, db, admin, user): cookies = auth_cookies("admin", "adminpass1", client) r = client.post(f"/api/admin/users/alice/reset-password-code", cookies=cookies) assert r.status_code == 200 code = r.json()["code"] r2 = client.post("/api/auth/reset-password", json={ "handle": "alice", "code": code, "password": "newpassword1", }) assert r2.status_code == 200 # Old password should no longer work r3 = client.post("/api/auth/login", json={"handle": "alice", "password": "alicepass1"}) assert r3.status_code == 401 # New password works r4 = client.post("/api/auth/login", json={"handle": "alice", "password": "newpassword1"}) assert r4.status_code == 200 def test_reset_password_wrong_handle(client, db, admin, user): cookies = auth_cookies("admin", "adminpass1", client) r = client.post("/api/admin/users/alice/reset-password-code", cookies=cookies) code = r.json()["code"] r2 = client.post("/api/auth/reset-password", json={ "handle": "admin", # wrong handle for this code "code": code, "password": "newpassword1", }) assert r2.status_code == 400 def test_reset_password_reuse_code(client, db, admin, user): cookies = auth_cookies("admin", "adminpass1", client) code = client.post("/api/admin/users/alice/reset-password-code", cookies=cookies).json()["code"] client.post("/api/auth/reset-password", json={"handle": "alice", "code": code, "password": "newpass111"}) r = client.post("/api/auth/reset-password", json={"handle": "alice", "code": code, "password": "anotherpass1"}) assert r.status_code == 400 # ── Self-service email password reset ──────────────────────────────────────── def test_request_reset_always_200(client, db, user): # Should never leak whether email is registered r = client.post("/api/auth/request-reset", json={"email": "notregistered@example.com"}) assert r.status_code == 200 set_user_email(db, "alice", "alice@example.com") r2 = client.post("/api/auth/request-reset", json={"email": "alice@example.com"}) assert r2.status_code == 200 def test_reset_via_email_token(client, db, user): set_user_email(db, "alice", "alice@example.com") token = create_email_reset_token(db, "alice") r = client.post("/api/auth/reset-password-token", json={"token": token, "password": "brandnewpass1"}) assert r.status_code == 200 r2 = client.post("/api/auth/login", json={"handle": "alice", "password": "brandnewpass1"}) assert r2.status_code == 200 def test_reset_via_email_token_reuse(client, db, user): set_user_email(db, "alice", "alice@example.com") token = create_email_reset_token(db, "alice") client.post("/api/auth/reset-password-token", json={"token": token, "password": "brandnewpass1"}) r = client.post("/api/auth/reset-password-token", json={"token": token, "password": "anotherpass1"}) assert r.status_code == 400 def test_reset_via_email_token_invalid(client, db, user): r = client.post("/api/auth/reset-password-token", json={"token": "bogus", "password": "somepass123"}) assert r.status_code == 400 # ── Email preference ────────────────────────────────────────────────────────── def test_set_and_get_email(client, user): cookies = auth_cookies("alice", "alicepass1", client) client.post("/api/me/email", json={"email": "alice@example.com"}, cookies=cookies) r = client.get("/api/me/email", cookies=cookies) assert r.status_code == 200 assert r.json()["email"] == "alice@example.com"