feat: backend core — models, auth, CRUD, tests
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""Tests for category CRUD endpoints."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import create_user_and_login
|
||||
|
||||
|
||||
async def _get_first_category(client: AsyncClient, token: str) -> dict:
|
||||
resp = await client.get(
|
||||
"/api/v1/categories",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
return resp.json()[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_categories_after_register(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) == 9 # 6 expense + 3 income defaults
|
||||
for c in cats:
|
||||
assert c["is_default"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_category(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
resp = await client.post(
|
||||
"/api/v1/categories",
|
||||
json={"name": "Épargne", "type": "expense", "color": "#123456", "icon": "piggy-bank"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
assert data["name"] == "Épargne"
|
||||
assert data["color"] == "#123456"
|
||||
assert data["is_default"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_category(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
create_resp = await client.post(
|
||||
"/api/v1/categories",
|
||||
json={"name": "Old Name", "type": "income"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
cat_id = create_resp.json()["id"]
|
||||
|
||||
resp = await client.put(
|
||||
f"/api/v1/categories/{cat_id}",
|
||||
json={"name": "New Name", "color": "#aabbcc"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "New Name"
|
||||
assert resp.json()["color"] == "#aabbcc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_category_no_transactions(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
create_resp = await client.post(
|
||||
"/api/v1/categories",
|
||||
json={"name": "To Delete", "type": "expense"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
cat_id = create_resp.json()["id"]
|
||||
|
||||
resp = await client.delete(
|
||||
f"/api/v1/categories/{cat_id}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
|
||||
# Category should no longer appear in listing
|
||||
list_resp = await client.get(
|
||||
"/api/v1/categories",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
ids = [c["id"] for c in list_resp.json()]
|
||||
assert cat_id not in ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_category_with_transactions_returns_409(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
|
||||
# Get an existing default category
|
||||
cat = await _get_first_category(client, token)
|
||||
cat_id = cat["id"]
|
||||
|
||||
# Add a transaction linked to it
|
||||
await client.post(
|
||||
"/api/v1/transactions",
|
||||
json={
|
||||
"amount_cents": 1000,
|
||||
"type": "expense",
|
||||
"category_id": cat_id,
|
||||
"transaction_date": "2026-03-15",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
resp = await client.delete(
|
||||
f"/api/v1/categories/{cat_id}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_categories_by_type(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
|
||||
resp = await client.get(
|
||||
"/api/v1/categories?type=income",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
for c in resp.json():
|
||||
assert c["type"] == "income"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_category_isolation_between_users(client: AsyncClient):
|
||||
"""User A cannot see or modify User B's categories."""
|
||||
_, token_a = await create_user_and_login(client, email="a@example.com")
|
||||
_, token_b = await create_user_and_login(client, email="b@example.com")
|
||||
|
||||
# User A creates a category
|
||||
resp = await client.post(
|
||||
"/api/v1/categories",
|
||||
json={"name": "A's private", "type": "expense"},
|
||||
headers={"Authorization": f"Bearer {token_a}"},
|
||||
)
|
||||
cat_id = resp.json()["id"]
|
||||
|
||||
# User B cannot delete it
|
||||
del_resp = await client.delete(
|
||||
f"/api/v1/categories/{cat_id}",
|
||||
headers={"Authorization": f"Bearer {token_b}"},
|
||||
)
|
||||
assert del_resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_category_returns_404(client: AsyncClient):
|
||||
_, token = await create_user_and_login(client)
|
||||
fake_id = "00000000-0000-0000-0000-000000000000"
|
||||
resp = await client.delete(
|
||||
f"/api/v1/categories/{fake_id}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
Reference in New Issue
Block a user