import uuid from datetime import UTC, datetime, timedelta from httpx import AsyncClient from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.auth.passwords import hash_password, verify_password from app.auth.sessions import COOKIE_NAME from app.models import AuthSession, User def test_password_hash_roundtrip() -> None: hashed = hash_password("secret123") assert hashed != "secret123" assert verify_password(hashed, "secret123") assert not verify_password(hashed, "wrong") assert not verify_password("not-a-hash", "secret123") async def _session_count(db: AsyncSession) -> int: return (await db.execute(select(func.count(AuthSession.id)))).scalar_one() async def test_login_success_sets_cookie_and_session( client: AsyncClient, db: AsyncSession, seeded_user: User ) -> None: response = await client.post( "/api/auth/login", json={"email": "pablo@test.dev", "password": "secret123"}, ) assert response.status_code == 200 body = response.json() assert body["email"] == "pablo@test.dev" assert body["role"] == "member" assert COOKIE_NAME in response.cookies assert await _session_count(db) == 1 async def test_login_normalizes_email(client: AsyncClient, seeded_user: User) -> None: response = await client.post( "/api/auth/login", json={"email": " PABLO@test.dev ", "password": "secret123"}, ) assert response.status_code == 200 async def test_login_wrong_password( client: AsyncClient, db: AsyncSession, seeded_user: User ) -> None: response = await client.post( "/api/auth/login", json={"email": "pablo@test.dev", "password": "wrong"}, ) assert response.status_code == 401 assert response.json() == { "detail": "Invalid email or password.", "code": "invalid_credentials", } assert await _session_count(db) == 0 async def test_login_unknown_email_same_error(client: AsyncClient) -> None: response = await client.post( "/api/auth/login", json={"email": "ghost@test.dev", "password": "secret123"}, ) assert response.status_code == 401 assert response.json()["code"] == "invalid_credentials" async def test_me_without_cookie(client: AsyncClient) -> None: response = await client.get("/api/auth/me") assert response.status_code == 401 assert response.json()["code"] == "not_authenticated" async def test_me_with_garbage_cookie(client: AsyncClient) -> None: client.cookies.set(COOKIE_NAME, "not-a-uuid") response = await client.get("/api/auth/me") assert response.status_code == 401 async def test_me_after_login(client: AsyncClient, seeded_user: User) -> None: await client.post( "/api/auth/login", json={"email": "pablo@test.dev", "password": "secret123"}, ) response = await client.get("/api/auth/me") assert response.status_code == 200 assert response.json()["id"] == str(seeded_user.id) async def test_logout_deletes_session( client: AsyncClient, db: AsyncSession, seeded_user: User ) -> None: await client.post( "/api/auth/login", json={"email": "pablo@test.dev", "password": "secret123"}, ) assert await _session_count(db) == 1 response = await client.post("/api/auth/logout") assert response.status_code == 204 assert await _session_count(db) == 0 response = await client.get("/api/auth/me") assert response.status_code == 401 async def test_expired_session_rejected( client: AsyncClient, db: AsyncSession, seeded_user: User ) -> None: session = AuthSession( id=uuid.uuid4(), user_id=seeded_user.id, expires_at=datetime.now(UTC) - timedelta(minutes=1), ) db.add(session) await db.commit() client.cookies.set(COOKIE_NAME, str(session.id)) response = await client.get("/api/auth/me") assert response.status_code == 401 assert response.json()["code"] == "not_authenticated"