test: add pytest suite covering auth, invites, admin and OIDC flows (59 tests)

This commit is contained in:
Davide Scaini
2026-06-03 22:10:40 +02:00
parent b61aa39b3b
commit 1d3c25f855
6 changed files with 934 additions and 2 deletions
+235
View File
@@ -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"