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 _login_admin(client: AsyncClient) -> str: """Create and login as admin user, return CSRF token.""" app = client._transport.app user_repo = app.state.user_repo cred_repo = app.state.credential_repo user = User( userid="admin-01", username="admin", groups=["admin", "users"], 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("AdminPass123!")) ) token = await get_csrf_token(client) await client.post( "/login/password", data={"username": "admin", "password": "AdminPass123!"}, headers={"HX-Request": "true", "X-CSRF-Token": token}, ) return token @pytest.mark.asyncio async def test_invite_valid_username(client: AsyncClient) -> None: token = await _login_admin(client) response = await client.post( "/admin/invite", data={"username": "newuser@example.com"}, headers={"X-CSRF-Token": token}, ) assert response.status_code == 200 assert "Invite created" in response.text @pytest.mark.asyncio async def test_invite_empty_username_rejected(client: AsyncClient) -> None: token = await _login_admin(client) response = await client.post( "/admin/invite", data={"username": ""}, headers={"X-CSRF-Token": token}, ) # Empty username is rejected — either by FastAPI (422) or validation (alert) assert response.status_code == 422 or "alert" in response.text @pytest.mark.asyncio async def test_invite_invalid_username_rejected(client: AsyncClient) -> None: token = await _login_admin(client) response = await client.post( "/admin/invite", data={"username": "bad user