mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 06:57:47 +01:00
feat(docs): codebase refactor - added src directory
This commit is contained in:
210
7project/src/backend/tests/test_e2e.py
Normal file
210
7project/src/backend/tests/test_e2e.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import pytest
|
||||
import uuid
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from fastapi import status
|
||||
|
||||
|
||||
def test_e2e(client):
|
||||
# 1) Service is alive
|
||||
alive = client.get("/")
|
||||
assert alive.status_code == status.HTTP_200_OK
|
||||
|
||||
# 2) Attempt to login without payload should fail fast (validation error)
|
||||
login = client.post("/auth/jwt/login")
|
||||
assert login.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_422_UNPROCESSABLE_CONTENT)
|
||||
|
||||
# 3) Protected endpoint should not be accessible without token
|
||||
me = client.get("/users/me")
|
||||
assert me.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_full_user_lifecycle(fastapi_app, test_user):
|
||||
# Use an AsyncClient with ASGITransport for async tests
|
||||
transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True)
|
||||
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
|
||||
login_payload = test_user
|
||||
|
||||
# 1. Log in with the new credentials
|
||||
login_resp = await ac.post("/auth/jwt/login", data=login_payload)
|
||||
assert login_resp.status_code == status.HTTP_200_OK
|
||||
token = login_resp.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 2. Access a protected endpoint
|
||||
me_resp = await ac.get("/users/me", headers=headers)
|
||||
assert me_resp.status_code == status.HTTP_200_OK
|
||||
assert me_resp.json()["email"] == test_user["username"]
|
||||
|
||||
# 3. Update the user's profile
|
||||
update_payload = {"first_name": "Test"}
|
||||
patch_resp = await ac.patch("/users/me", json=update_payload, headers=headers)
|
||||
assert patch_resp.status_code == status.HTTP_200_OK
|
||||
assert patch_resp.json()["first_name"] == "Test"
|
||||
|
||||
# 4. Log out
|
||||
logout_resp = await ac.post("/auth/jwt/logout", headers=headers)
|
||||
assert logout_resp.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# 5. Verify token is invalid
|
||||
me_again_resp = await ac.get("/users/me", headers=headers)
|
||||
assert me_again_resp.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_transaction_workflow(fastapi_app, test_user):
|
||||
transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True)
|
||||
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
|
||||
# 1. Log in to get the token
|
||||
login_resp = await ac.post("/auth/jwt/login", data=test_user)
|
||||
token = login_resp.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# NEW STEP: Create a category first to get a valid ID
|
||||
category_payload = {"name": "Test Category for E2E"}
|
||||
create_category_resp = await ac.post("/categories/create", json=category_payload, headers=headers)
|
||||
assert create_category_resp.status_code == status.HTTP_201_CREATED
|
||||
category_id = create_category_resp.json()["id"]
|
||||
|
||||
# 2. Create a new transaction
|
||||
tx_payload = {"amount": -55.40, "description": "Milk and eggs"}
|
||||
tx_resp = await ac.post("/transactions/create", json=tx_payload, headers=headers)
|
||||
assert tx_resp.status_code == status.HTTP_201_CREATED
|
||||
tx_id = tx_resp.json()["id"]
|
||||
|
||||
# 3. Assign the category
|
||||
assign_resp = await ac.post(f"/transactions/{tx_id}/categories/{category_id}", headers=headers)
|
||||
assert assign_resp.status_code == status.HTTP_200_OK
|
||||
|
||||
# 4. Verify assignment
|
||||
get_tx_resp = await ac.get(f"/transactions/{tx_id}", headers=headers)
|
||||
assert category_id in get_tx_resp.json()["category_ids"]
|
||||
|
||||
# 5. Unassign the category
|
||||
unassign_resp = await ac.delete(f"/transactions/{tx_id}/categories/{category_id}", headers=headers)
|
||||
assert unassign_resp.status_code == status.HTTP_200_OK
|
||||
|
||||
# 6. Get the transaction again and verify the category is gone
|
||||
get_tx_again_resp = await ac.get(f"/transactions/{tx_id}", headers=headers)
|
||||
final_tx_data = get_tx_again_resp.json()
|
||||
assert category_id not in final_tx_data["category_ids"]
|
||||
|
||||
# 7. Delete the transaction for cleanup
|
||||
delete_resp = await ac.delete(f"/transactions/{tx_id}/delete", headers=headers)
|
||||
assert delete_resp.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# 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)
|
||||
|
||||
@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:
|
||||
# Use unique email to avoid duplicates across runs
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
email = f"newuser_{suffix}@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"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
try:
|
||||
me = await ac.get("/users/me", headers=headers)
|
||||
assert me.status_code == status.HTTP_200_OK
|
||||
assert me.json()["email"] == email
|
||||
finally:
|
||||
# Cleanup: delete the created user so future runs won’t conflict
|
||||
d = await ac.delete("/users/me", headers=headers)
|
||||
assert d.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
@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:
|
||||
# Generate unique emails for both users
|
||||
sfx = uuid.uuid4().hex[:8]
|
||||
u1 = {"email": f"u1_{sfx}@example.com", "password": "Aaaaaa1!"}
|
||||
u2 = {"email": f"u2_{sfx}@example.com", "password": "Aaaaaa1!"}
|
||||
|
||||
# user1
|
||||
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"]
|
||||
h1 = {"Authorization": f"Bearer {t1}"}
|
||||
|
||||
# user1 creates a category
|
||||
c = (await ac.post("/categories/create", json={"name": "Private"}, headers=h1)).json()
|
||||
cat_id = c["id"]
|
||||
|
||||
# user2
|
||||
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"]
|
||||
h2 = {"Authorization": f"Bearer {t2}"}
|
||||
|
||||
try:
|
||||
# user2 cannot read/delete user1's category
|
||||
g = await ac.get(f"/categories/{cat_id}", headers=h2)
|
||||
assert g.status_code == status.HTTP_404_NOT_FOUND
|
||||
d = await ac.delete(f"/categories/{cat_id}", headers=h2)
|
||||
assert d.status_code == status.HTTP_404_NOT_FOUND
|
||||
finally:
|
||||
# Cleanup: remove the created category as its owner
|
||||
try:
|
||||
_ = await ac.delete(f"/categories/{cat_id}", headers=h1)
|
||||
except Exception:
|
||||
pass
|
||||
# Cleanup: delete both users to avoid email conflicts later
|
||||
try:
|
||||
_ = await ac.delete("/users/me", headers=h1)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_ = await ac.delete("/users/me", headers=h2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user