diff --git a/7project/backend/tests/test_e2e_auth_flow.py b/7project/backend/tests/test_e2e.py similarity index 51% rename from 7project/backend/tests/test_e2e_auth_flow.py rename to 7project/backend/tests/test_e2e.py index 7ad18d2..49a57e3 100644 --- a/7project/backend/tests/test_e2e_auth_flow.py +++ b/7project/backend/tests/test_e2e.py @@ -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) \ No newline at end of file + 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 + diff --git a/7project/backend/tests/test_integration_app.py b/7project/backend/tests/test_integration_app.py index c924e25..5806452 100644 --- a/7project/backend/tests/test_integration_app.py +++ b/7project/backend/tests/test_integration_app.py @@ -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 +