test: add pytest suite covering auth, invites, admin and OIDC flows (59 tests)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user