# 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"