236 lines
9.0 KiB
Python
236 lines
9.0 KiB
Python
"""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"
|