from fastapi import status import pytest from httpx import AsyncClient, ASGITransport def test_root_ok(client): resp = client.get("/") assert resp.status_code == status.HTTP_200_OK assert resp.json() == {"status": "ok"} def test_authenticated_route_requires_auth(client): resp = client.get("/authenticated-route") assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN) @pytest.mark.asyncio async def test_create_and_get_category(fastapi_app, test_user): # Use AsyncClient for async tests transport = ASGITransport(app=fastapi_app) async with AsyncClient(transport=transport, base_url="http://testserver") as ac: # 1. Log in to get an auth token login_resp = await ac.post("/auth/jwt/login", data=test_user) token = login_resp.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} # 2. Define and create the new category category_name = "Async Integration Test" category_payload = {"name": category_name} create_resp = await ac.post("/categories/create", json=category_payload, headers=headers) # 3. Assert creation was successful assert create_resp.status_code == status.HTTP_201_CREATED created_data = create_resp.json() category_id = created_data["id"] assert created_data["name"] == category_name # 4. GET the list of categories to verify list_resp = await ac.get("/categories/", headers=headers) assert list_resp.status_code == status.HTTP_200_OK # 5. Check that our new category is in the list categories_list = list_resp.json() assert any(cat["name"] == category_name for cat in categories_list) delete_resp = await ac.delete(f"/categories/{category_id}", headers=headers) assert delete_resp.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT) @pytest.mark.asyncio async def test_create_transaction_missing_amount_fails(fastapi_app, test_user): transport = ASGITransport(app=fastapi_app) async with AsyncClient(transport=transport, base_url="http://testserver") as ac: # 1. Log in to get an auth token login_resp = await ac.post("/auth/jwt/login", data=test_user) token = login_resp.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} # 2. Define an invalid payload invalid_payload = {"description": "This should fail"} # 3. Attempt to create the transaction 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_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