"""Instance settings (registration toggle) and changing your own password.""" from __future__ import annotations from fastapi.testclient import TestClient from sqlalchemy import select from lembas.db.models import Session as SessionRow from lembas.db.models import User from lembas.services import settings_store # --- Registration toggle ----------------------------------------------------- def test_registration_is_open_by_default(client: TestClient, db, registered): assert settings_store.signup_allowed(db) is True def test_closing_registration_blocks_new_accounts(client: TestClient, db, registered): client.post("/admin/general", data={"instance_name": "LLeMbas"}, follow_redirects=False) assert settings_store.signup_allowed(db) is False response = client.post( "/auth/register", data={"name": "Uninvited", "email": "no@thanks.test", "password": "let-me-in-please"}, follow_redirects=False, ) assert response.status_code == 403 assert "Registration is closed" in response.text assert db.scalar(select(User).where(User.email == "no@thanks.test")) is None def test_closed_registration_hides_the_create_account_link( client: TestClient, db, registered ): client.post("/admin/general", data={"instance_name": "LLeMbas"}, follow_redirects=False) client.post("/auth/logout", follow_redirects=False) page = client.get("/auth/login") assert "/auth/register" not in page.text def test_open_registration_shows_the_link(client: TestClient, db, registered): client.post( "/admin/general", data={"instance_name": "LLeMbas", "allow_signup": "true"}, follow_redirects=False, ) client.post("/auth/logout", follow_redirects=False) assert "/auth/register" in client.get("/auth/login").text def test_existing_users_can_still_sign_in_when_registration_is_closed( client: TestClient, db, registered ): client.post("/admin/general", data={"instance_name": "LLeMbas"}, follow_redirects=False) client.post("/auth/logout", follow_redirects=False) response = client.post( "/auth/login", data={"email": registered["email"], "password": registered["password"]}, follow_redirects=False, ) assert response.status_code == 303 def test_stored_setting_beats_the_environment_default(client: TestClient, db, registered): """A toggle that silently reverted on restart would be worse than none.""" from lembas.config import settings as env_settings assert env_settings.allow_signup is True settings_store.update(db, {"allow_signup": False}) assert settings_store.signup_allowed(db) is False def test_ordinary_users_cannot_change_instance_settings(client: TestClient, db, registered): client.post("/auth/logout", follow_redirects=False) client.post( "/auth/register", data={"name": "Sam", "email": "sam@shire.test", "password": "potatoes-po-ta-toes"}, follow_redirects=False, ) assert client.post("/admin/general", data={"instance_name": "Pwned"}).status_code == 403 assert settings_store.get(db, "instance_name") == "LLeMbas" # --- Password change --------------------------------------------------------- def test_password_change_works(client: TestClient, db, registered): response = client.post( "/api/preferences/password", data={ "current_password": registered["password"], "new_password": "a-much-better-password", "confirm_password": "a-much-better-password", }, follow_redirects=False, ) assert response.status_code == 303 assert "saved=" in response.headers["location"] client.post("/auth/logout", follow_redirects=False) assert ( client.post( "/auth/login", data={"email": registered["email"], "password": "a-much-better-password"}, follow_redirects=False, ).status_code == 303 ) def test_old_password_stops_working(client: TestClient, db, registered): client.post( "/api/preferences/password", data={ "current_password": registered["password"], "new_password": "a-much-better-password", "confirm_password": "a-much-better-password", }, follow_redirects=False, ) client.post("/auth/logout", follow_redirects=False) assert ( client.post( "/auth/login", data={"email": registered["email"], "password": registered["password"]}, follow_redirects=False, ).status_code == 401 ) def test_wrong_current_password_is_refused(client: TestClient, db, registered): response = client.post( "/api/preferences/password", data={ "current_password": "not-my-password", "new_password": "a-much-better-password", "confirm_password": "a-much-better-password", }, follow_redirects=False, ) assert "error=" in response.headers["location"] user = db.scalar(select(User).where(User.email == registered["email"])) db.refresh(user) from lembas.security.passwords import verify_password assert verify_password(registered["password"], user.password_hash) def test_mismatched_confirmation_is_refused(client: TestClient, db, registered): response = client.post( "/api/preferences/password", data={ "current_password": registered["password"], "new_password": "a-much-better-password", "confirm_password": "a-different-password", }, follow_redirects=False, ) assert "do%20not%20match" in response.headers["location"] def test_short_new_password_is_refused(client: TestClient, db, registered): response = client.post( "/api/preferences/password", data={ "current_password": registered["password"], "new_password": "short", "confirm_password": "short", }, follow_redirects=False, ) assert "8%20characters" in response.headers["location"] def test_other_sessions_are_revoked_but_this_one_survives(client: TestClient, db, registered): """If the reason for changing a password is that someone else knows it, leaving their session alive defeats the point.""" other = TestClient(client.app) other.post( "/auth/login", data={"email": registered["email"], "password": registered["password"]}, follow_redirects=False, ) assert other.get("/chat", follow_redirects=False).status_code == 200 assert db.scalar(select(SessionRow)) is not None client.post( "/api/preferences/password", data={ "current_password": registered["password"], "new_password": "a-much-better-password", "confirm_password": "a-much-better-password", }, follow_redirects=False, ) # The other browser is out... assert other.get("/chat", follow_redirects=False).status_code == 303 # ...and the tab that made the change is still in. assert client.get("/chat", follow_redirects=False).status_code == 200