from datetime import UTC, datetime import pytest from httpx import AsyncClient from porchlight.authn.password import PasswordHasher, PasswordService from porchlight.models import PasswordCredential, User from tests.conftest import get_csrf_token async def _create_inactive_user_with_password(client: AsyncClient) -> None: """Create an inactive user with a password credential.""" app = client._transport.app user_repo = app.state.user_repo cred_repo = app.state.credential_repo user = User( userid="inactive-01", username="inactive", active=False, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) await user_repo.create(user) svc = PasswordService(hasher=PasswordHasher(time_cost=1, memory_cost=8192)) await cred_repo.create_password( PasswordCredential(user_id=user.userid, password_hash=svc.hash("password123!Secure")) ) @pytest.mark.asyncio async def test_inactive_user_cannot_login_password(client: AsyncClient) -> None: await _create_inactive_user_with_password(client) token = await get_csrf_token(client) response = await client.post( "/login/password", data={"username": "inactive", "password": "password123!Secure"}, headers={"HX-Request": "true", "X-CSRF-Token": token}, ) assert "Invalid username or password" in response.text @pytest.mark.asyncio async def test_inactive_user_cannot_register_magic_link(client: AsyncClient) -> None: """If an inactive user exists, magic link registration should reject them.""" app = client._transport.app user_repo = app.state.user_repo magic_link_service = app.state.magic_link_service user = User( userid="inactive-02", username="deactivated", active=False, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) await user_repo.create(user) link = await magic_link_service.create( username="deactivated", created_by="admin", note="test" ) response = await client.get(f"/register/{link.token}", follow_redirects=False) assert response.status_code == 400 or "deactivated" in response.text.lower() or "Invalid" in response.text