41ab900c33
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
4.5 KiB
Python
99 lines
4.5 KiB
Python
# tests/test_operator_login.py
|
|
import uuid
|
|
from tests.conftest import wire_operator_auth
|
|
from backend.operator_auth import (
|
|
create_operator, make_operator_cookie, COOKIE_NAME, MAX_LOGIN_FAILURES,
|
|
)
|
|
|
|
|
|
def test_login_page_renders(client, db_session, monkeypatch):
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.get("/login")
|
|
assert r.status_code == 200
|
|
assert "password" in r.text.lower()
|
|
|
|
|
|
def test_login_success_sets_cookie_and_redirects(client, db_session, monkeypatch):
|
|
create_operator(db_session, "ok@x.com", "Ok", "admin", password="rightpw-9")
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.post("/login", data={"email": "ok@x.com", "password": "rightpw-9"},
|
|
follow_redirects=False)
|
|
assert r.status_code == 303
|
|
assert r.headers["location"] == "/"
|
|
assert f"{COOKIE_NAME}=" in r.headers.get("set-cookie", "")
|
|
|
|
|
|
def test_login_honors_next(client, db_session, monkeypatch):
|
|
create_operator(db_session, "ok@x.com", "Ok", "admin", password="rightpw-9")
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.post("/login?next=/settings", data={"email": "ok@x.com", "password": "rightpw-9"},
|
|
follow_redirects=False)
|
|
assert r.headers["location"] == "/settings"
|
|
|
|
|
|
def test_login_wrong_password_no_cookie_generic_error(client, db_session, monkeypatch):
|
|
create_operator(db_session, "ok@x.com", "Ok", "admin", password="rightpw-9")
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.post("/login", data={"email": "ok@x.com", "password": "nope"},
|
|
follow_redirects=False)
|
|
assert r.status_code == 200
|
|
assert f"{COOKIE_NAME}=" not in r.headers.get("set-cookie", "")
|
|
assert "invalid" in r.text.lower()
|
|
|
|
|
|
def test_login_must_change_redirects_to_change_password(client, db_session, monkeypatch):
|
|
create_operator(db_session, "new@x.com", "New", "admin") # generated temp → must_change
|
|
from backend.models import OperatorUser
|
|
user = db_session.query(OperatorUser).filter_by(email="new@x.com").first()
|
|
from backend.auth_passwords import hash_password
|
|
user.password_hash = hash_password("temp-pw-1")
|
|
db_session.commit()
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.post("/login", data={"email": "new@x.com", "password": "temp-pw-1"},
|
|
follow_redirects=False)
|
|
assert r.status_code == 303
|
|
assert r.headers["location"] == "/change-password"
|
|
|
|
|
|
def test_login_lockout_message_after_five(client, db_session, monkeypatch):
|
|
create_operator(db_session, "lk@x.com", "Lk", "admin", password="rightpw-9")
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
for _ in range(MAX_LOGIN_FAILURES):
|
|
client.post("/login", data={"email": "lk@x.com", "password": "nope"}, follow_redirects=False)
|
|
r = client.post("/login", data={"email": "lk@x.com", "password": "rightpw-9"}, follow_redirects=False)
|
|
assert r.status_code == 200
|
|
assert "too many" in r.text.lower()
|
|
|
|
|
|
def test_logout_clears_cookie(client, db_session, monkeypatch):
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
r = client.get("/logout", follow_redirects=False)
|
|
assert r.status_code == 303
|
|
assert r.headers["location"] == "/login"
|
|
set_cookie = r.headers.get("set-cookie", "").lower()
|
|
assert COOKIE_NAME.lower() in set_cookie
|
|
assert 'max-age=0' in set_cookie or 'expires=thu, 01 jan 1970' in set_cookie
|
|
|
|
|
|
def test_change_password_self_service(client, db_session, monkeypatch):
|
|
user, _ = create_operator(db_session, "c@x.com", "C", "admin", password="orig-pw-1")
|
|
wire_operator_auth(monkeypatch, db_session, enabled=True)
|
|
client.cookies.set(COOKIE_NAME, make_operator_cookie(user.id))
|
|
r = client.post("/change-password",
|
|
data={"current_password": "orig-pw-1", "new_password": "brand-new-2",
|
|
"confirm_password": "brand-new-2"}, follow_redirects=False)
|
|
assert r.status_code == 303
|
|
from backend.auth_passwords import verify_password
|
|
db_session.refresh(user)
|
|
assert verify_password("brand-new-2", user.password_hash)
|
|
assert user.must_change_password is False
|
|
|
|
|
|
def test_safe_next_blocks_open_redirect():
|
|
from backend.routers.operator_auth_routes import _safe_next
|
|
assert _safe_next("//evil.com") == "/"
|
|
assert _safe_next("/\\evil.com") == "/" # backslash authority bypass
|
|
assert _safe_next("https://evil.com") == "/"
|
|
assert _safe_next("") == "/"
|
|
assert _safe_next("/settings") == "/settings"
|