147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
"""Integration tests for registration/login (section 17, additive) --
|
|||
|
|
the first authenticated-user flow in Aurora Core. Exercised at the HTTP
|
||
|
|
layer (register -> login -> /me with the returned bearer token) rather
|
||
|
|
than just the service layer, since that's the actual contract a frontend
|
||
|
|
integrates against."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from httpx import AsyncClient
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_returns_a_usable_access_token(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register",
|
||
|
|
json={"email": "[email protected]", "password": "wake-up-neo", "display_name": "Neo"},
|
||
|
|
)
|
||
|
|
assert response.status_code == 201
|
||
|
|
token = response.json()["access_token"]
|
||
|
|
assert response.json()["token_type"] == "bearer"
|
||
|
|
|
||
|
|
me = await client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {token}"})
|
||
|
|
assert me.status_code == 200
|
||
|
|
body = me.json()
|
||
|
|
assert body["email"] == "[email protected]"
|
||
|
|
assert body["display_name"] == "Neo"
|
||
|
|
assert body["id"].startswith("aur_user_")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_normalizes_email_case(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register",
|
||
|
|
json={"email": " [email protected] ", "password": "follow-the-white-rabbit"},
|
||
|
|
)
|
||
|
|
assert response.status_code == 201
|
||
|
|
token = response.json()["access_token"]
|
||
|
|
|
||
|
|
me = await client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {token}"})
|
||
|
|
assert me.json()["email"] == "[email protected]"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_rejects_duplicate_email(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "red-pill-please"}
|
||
|
|
)
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register",
|
||
|
|
json={"email": "[email protected]", "password": "a-different-password"},
|
||
|
|
)
|
||
|
|
assert response.status_code == 409
|
||
|
|
assert response.json()["error"]["code"] == "email_already_registered"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_rejects_duplicate_email_case_insensitively(
|
||
|
|
client: AsyncClient, db_session: AsyncSession
|
||
|
|
):
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "i-want-steak"}
|
||
|
|
)
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "something-else"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 409
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_rejects_short_password(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "short"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 422
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_register_rejects_malformed_email(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "not-an-email", "password": "whatever-password"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 422
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_login_succeeds_with_correct_credentials(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "operator-standing-by"}
|
||
|
|
)
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/login", json={"email": "[email protected]", "password": "operator-standing-by"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 200
|
||
|
|
assert "access_token" in response.json()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_login_rejects_wrong_password(client: AsyncClient, db_session: AsyncSession):
|
||
|
|
await client.post(
|
||
|
|
"/api/v1/auth/register", json={"email": "[email protected]", "password": "the-real-password"}
|
||
|
|
)
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/login", json={"email": "[email protected]", "password": "wrong-password-here"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 401
|
||
|
|
assert response.json()["error"]["code"] == "invalid_credentials"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_login_rejects_unknown_email_with_the_same_error_as_wrong_password(
|
||
|
|
client: AsyncClient, db_session: AsyncSession
|
||
|
|
):
|
||
|
|
# Same status code and error code as a wrong password -- see
|
||
|
|
# app/services/user_service.py:authenticate_user. Distinguishing "no
|
||
|
|
# such account" from "wrong password" would let a client enumerate
|
||
|
|
# registered emails.
|
||
|
|
response = await client.post(
|
||
|
|
"/api/v1/auth/login", json={"email": "[email protected]", "password": "does-not-matter"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 401
|
||
|
|
assert response.json()["error"]["code"] == "invalid_credentials"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_me_rejects_missing_token(client: AsyncClient):
|
||
|
|
response = await client.get("/api/v1/auth/me")
|
||
|
|
assert response.status_code == 401
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_me_rejects_garbage_token(client: AsyncClient):
|
||
|
|
response = await client.get(
|
||
|
|
"/api/v1/auth/me", headers={"Authorization": "Bearer not-a-real-token"}
|
||
|
|
)
|
||
|
|
assert response.status_code == 401
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_me_rejects_a_token_for_a_different_secret(client: AsyncClient):
|
||
|
|
import jwt
|
||
|
|
|
||
|
|
forged = jwt.encode({"sub": "00000000-0000-0000-0000-000000000000"}, "wrong-secret", algorithm="HS256")
|
||
|
|
response = await client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {forged}"})
|
||
|
|
assert response.status_code == 401
|