feat: backend core — models, auth, CRUD, tests
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user