178 lines
5.8 KiB
Python
178 lines
5.8 KiB
Python
"""Tests for auth endpoints: register, login, refresh, logout."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from tests.conftest import create_user_and_login
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_success(client: AsyncClient):
|
|
resp = await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "alice@example.com", "password": "s3cr3t", "full_name": "Alice"},
|
|
)
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["email"] == "alice@example.com"
|
|
assert data["full_name"] == "Alice"
|
|
assert "id" in data
|
|
assert "hashed_password" not in data
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_duplicate_email(client: AsyncClient):
|
|
payload = {"email": "dup@example.com", "password": "pass", "full_name": "Dup"}
|
|
await client.post("/api/v1/auth/register", json=payload)
|
|
resp = await client.post("/api/v1/auth/register", json=payload)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_creates_default_categories(client: AsyncClient):
|
|
_, token = await create_user_and_login(client)
|
|
resp = await client.get(
|
|
"/api/v1/categories",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert resp.status_code == 200
|
|
cats = resp.json()
|
|
assert len(cats) >= 6
|
|
names = {c["name"] for c in cats}
|
|
assert "Alimentation" in names
|
|
assert "Salaire" in names
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_success(client: AsyncClient):
|
|
await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "bob@example.com", "password": "mypass", "full_name": "Bob"},
|
|
)
|
|
resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "bob@example.com", "password": "mypass"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_wrong_password(client: AsyncClient):
|
|
await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "carol@example.com", "password": "correct", "full_name": "Carol"},
|
|
)
|
|
resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "carol@example.com", "password": "wrong"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_unknown_email(client: AsyncClient):
|
|
resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "nobody@example.com", "password": "x"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_access_protected_route_with_valid_token(client: AsyncClient):
|
|
_, token = await create_user_and_login(client)
|
|
resp = await client.get(
|
|
"/api/v1/categories",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_access_protected_route_without_token(client: AsyncClient):
|
|
resp = await client.get("/api/v1/categories")
|
|
assert resp.status_code == 403 # HTTPBearer returns 403 when no credentials
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_access_protected_route_invalid_token(client: AsyncClient):
|
|
resp = await client.get(
|
|
"/api/v1/categories",
|
|
headers={"Authorization": "Bearer invalidtoken"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_refresh_token(client: AsyncClient):
|
|
await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "dave@example.com", "password": "pass", "full_name": "Dave"},
|
|
)
|
|
login_resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "dave@example.com", "password": "pass"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
refresh_token = login_resp.json()["refresh_token"]
|
|
|
|
resp = await client.post(
|
|
"/api/v1/auth/refresh",
|
|
json={"refresh_token": refresh_token},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "access_token" in resp.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_refresh_token_cannot_be_reused(client: AsyncClient):
|
|
"""Refresh token is revoked after use (rotation)."""
|
|
await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "eve@example.com", "password": "pass", "full_name": "Eve"},
|
|
)
|
|
login_resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "eve@example.com", "password": "pass"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
refresh_token = login_resp.json()["refresh_token"]
|
|
|
|
# First use
|
|
r1 = await client.post("/api/v1/auth/refresh", json={"refresh_token": refresh_token})
|
|
assert r1.status_code == 200
|
|
|
|
# Second use of the same token should fail
|
|
r2 = await client.post("/api/v1/auth/refresh", json={"refresh_token": refresh_token})
|
|
assert r2.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_logout(client: AsyncClient):
|
|
_, token = await create_user_and_login(client, email="frank@example.com")
|
|
|
|
login_resp = await client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "frank@example.com", "password": "password123"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
refresh_token = login_resp.json()["refresh_token"]
|
|
|
|
resp = await client.post(
|
|
"/api/v1/auth/logout",
|
|
json={"refresh_token": refresh_token},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert resp.status_code == 204
|
|
|
|
# Token should be revoked now
|
|
r = await client.post("/api/v1/auth/refresh", json={"refresh_token": refresh_token})
|
|
assert r.status_code == 401
|