Merge pull request #45 from dat515-2025/add_more_tests

feat(test): added more tests
This commit is contained in:
Dejan Ribarovski
2025-11-06 11:31:07 +01:00
committed by GitHub
2 changed files with 188 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ from httpx import AsyncClient, ASGITransport
from fastapi import status
def test_e2e_minimal_auth_flow(client):
def test_e2e(client):
# 1) Service is alive
alive = client.get("/")
assert alive.status_code == status.HTTP_200_OK
@@ -95,4 +95,85 @@ async def test_e2e_transaction_workflow(fastapi_app, test_user):
# NEW STEP: Clean up the created category
delete_category_resp = await ac.delete(f"/categories/{category_id}", headers=headers)
assert delete_category_resp.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT)
assert delete_category_resp.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT)
@pytest.mark.asyncio
async def test_register_then_login_and_fetch_me(fastapi_app):
transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
email = "newuser@example.com"
password = "StrongPassw0rd!"
reg = await ac.post("/auth/register", json={"email": email, "password": password})
assert reg.status_code in (status.HTTP_201_CREATED, status.HTTP_200_OK)
login = await ac.post("/auth/jwt/login", data={"username": email, "password": password})
assert login.status_code == status.HTTP_200_OK
token = login.json()["access_token"]
me = await ac.get("/users/me", headers={"Authorization": f"Bearer {token}"})
assert me.status_code == status.HTTP_200_OK
assert me.json()["email"] == email
@pytest.mark.asyncio
async def test_delete_current_user_revokes_access(fastapi_app):
transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
email = "todelete@example.com"
password = "Passw0rd!"
reg = await ac.post("/auth/register", json={"email": email, "password": password})
assert reg.status_code in (status.HTTP_200_OK, status.HTTP_201_CREATED)
login = await ac.post("/auth/jwt/login", data={"username": email, "password": password})
token = login.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
# Delete self
d = await ac.delete("/users/me", headers=headers)
assert d.status_code == status.HTTP_204_NO_CONTENT
# Access should now fail
me = await ac.get("/users/me", headers=headers)
assert me.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
@pytest.mark.asyncio
async def test_update_category_conflict_and_404(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
a = (await ac.post("/categories/create", json={"name": "A"}, headers=h)).json()
b = (await ac.post("/categories/create", json={"name": "B"}, headers=h)).json()
# Attempt to rename A -> B should conflict
conflict = await ac.patch(f"/categories/{a['id']}", json={"name": "B"}, headers=h)
assert conflict.status_code == status.HTTP_409_CONFLICT
# Update non-existent
missing = await ac.patch("/categories/999999", json={"name": "Z"}, headers=h)
assert missing.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_category_cross_user_isolation(fastapi_app):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
# user1
u1 = {"email": "u1@example.com", "password": "Aaaaaa1!"}
assert (await ac.post("/auth/register", json=u1)).status_code in (200, 201)
t1 = (await ac.post("/auth/jwt/login", data={"username": u1["email"], "password": u1["password"]})).json()["access_token"]
# user1 creates a category
c = (await ac.post("/categories/create", json={"name": "Private"}, headers={"Authorization": f"Bearer {t1}"})).json()
# user2
u2 = {"email": "u2@example.com", "password": "Aaaaaa1!"}
assert (await ac.post("/auth/register", json=u2)).status_code in (200, 201)
t2 = (await ac.post("/auth/jwt/login", data={"username": u2["email"], "password": u2["password"]})).json()["access_token"]
# user2 cannot read/delete user1's category
g = await ac.get(f"/categories/{c['id']}", headers={"Authorization": f"Bearer {t2}"})
assert g.status_code == status.HTTP_404_NOT_FOUND
d = await ac.delete(f"/categories/{c['id']}", headers={"Authorization": f"Bearer {t2}"})
assert d.status_code == status.HTTP_404_NOT_FOUND

View File

@@ -63,4 +63,108 @@ async def test_create_transaction_missing_amount_fails(fastapi_app, test_user):
resp = await ac.post("/transactions/create", json=invalid_payload, headers=headers)
# 4. Assert the expected validation error
assert resp.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert resp.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
@pytest.mark.asyncio
async def test_login_invalid_credentials(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
bad = await ac.post("/auth/jwt/login", data={"username": test_user["username"], "password": "nope"})
assert bad.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_400_BAD_REQUEST)
unknown = await ac.post("/auth/jwt/login", data={"username": "nouser@example.com", "password": "x"})
assert unknown.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_400_BAD_REQUEST)
@pytest.mark.asyncio
async def test_category_duplicate_name_conflict(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
p = {"name": "Food"}
r1 = await ac.post("/categories/create", json=p, headers=h)
assert r1.status_code == status.HTTP_201_CREATED
r2 = await ac.post("/categories/create", json=p, headers=h)
assert r2.status_code == status.HTTP_409_CONFLICT
@pytest.mark.asyncio
async def test_create_transaction_invalid_date_format(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
bad = await ac.post("/transactions/create", json={"amount": 10, "description": "x", "date": "31-12-2024"}, headers=h)
assert bad.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.asyncio
async def test_update_transaction_rejects_duplicate_category_ids(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
tx = (await ac.post("/transactions/create", json={"amount": 5, "description": "x"}, headers=h)).json()
dup = await ac.patch(f"/transactions/{tx['id']}/edit", json={"category_ids": [1, 1]}, headers=h)
assert dup.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.asyncio
async def test_assign_unassign_category_not_found_cases(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
# Create tx and category
tx = (await ac.post("/transactions/create", json={"amount": 1, "description": "a"}, headers=h)).json()
cat = (await ac.post("/categories/create", json={"name": "X"}, headers=h)).json()
# Missing transaction
r1 = await ac.post(f"/transactions/999999/categories/{cat['id']}", headers=h)
assert r1.status_code == status.HTTP_404_NOT_FOUND
# Missing category
r2 = await ac.post(f"/transactions/{tx['id']}/categories/999999", headers=h)
assert r2.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_transactions_date_filter_and_balance_series(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
# Seed transactions spanning days
data = [
{"amount": 100, "description": "day1", "date": "2024-01-01"},
{"amount": -25, "description": "day2", "date": "2024-01-02"},
{"amount": 50, "description": "day3", "date": "2024-01-03"},
]
for p in data:
r = await ac.post("/transactions/create", json=p, headers=h)
assert r.status_code == status.HTTP_201_CREATED
# Filtered list (2nd and 3rd only)
lst = await ac.get("/transactions/", params={"start_date": "2024-01-02", "end_date": "2024-01-03"}, headers=h)
assert lst.status_code == status.HTTP_200_OK
assert len(lst.json()) == 2
# Balance series should be cumulative per date
series = await ac.get("/transactions/balance_series", headers=h)
assert series.status_code == status.HTTP_200_OK
s = series.json()
assert s == [
{"date": "2024-01-01", "balance": 100.0},
{"date": "2024-01-02", "balance": 75.0},
{"date": "2024-01-03", "balance": 125.0},
]
@pytest.mark.asyncio
async def test_delete_transaction_not_found(fastapi_app, test_user):
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
r = await ac.delete("/transactions/999999/delete", headers=h)
assert r.status_code == status.HTTP_404_NOT_FOUND