diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py new file mode 100644 index 0000000..142f3ce --- /dev/null +++ b/tests/integration/test_auth.py @@ -0,0 +1,132 @@ +import pytest +from httpx import ASGITransport, AsyncClient + +from proxy_pool.app import create_app + + +@pytest.fixture +async def client(): + app = create_app() + async with app.router.lifespan_context(app): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + yield client + + +async def register_user(client, email=None): + """Helper to register and return (user_data, raw_api_key).""" + import uuid + email = email or f"test-{uuid.uuid4().hex[:8]}@example.com" + resp = await client.post( + "/auth/register", + json={"email": email, "display_name": "Test User"}, + ) + data = resp.json() + return data, data["api_key"]["key"] + + +def auth_header(api_key: str) -> dict: + return {"Authorization": f"Bearer {api_key}"} + + +class TestRegistration: + async def test_register_returns_user_and_key(self, client): + data, raw_key = await register_user(client) + + assert data["user"]["email"].endswith("@example.com") + assert data["user"]["is_active"] is True + assert raw_key.startswith("pp_") + assert data["api_key"]["label"] == "default" + + async def test_register_duplicate_email(self, client): + email = "dupe@example.com" + await register_user(client, email=email) + + resp = await client.post( + "/auth/register", + json={"email": email}, + ) + assert resp.status_code == 409 + + +class TestAuthentication: + async def test_authenticated_request(self, client): + _, api_key = await register_user(client) + + resp = await client.get("/account", headers=auth_header(api_key)) + + assert resp.status_code == 200 + assert resp.json()["is_active"] is True + + async def test_invalid_key_rejected(self, client): + resp = await client.get( + "/account", + headers=auth_header("pp_bogus_key_that_does_not_exist"), + ) + + assert resp.status_code == 401 + + async def test_missing_key_rejected(self, client): + resp = await client.get("/account") + + assert resp.status_code == 401 + + +class TestCredits: + async def test_initial_credits(self, client): + _, api_key = await register_user(client) + + resp = await client.get( + "/account/credits", + headers=auth_header(api_key), + ) + + assert resp.status_code == 200 + data = resp.json() + assert data["balance"] == 100 + assert len(data["recent_transactions"]) == 1 + assert data["recent_transactions"][0]["tx_type"] == "purchase" + + +class TestApiKeyManagement: + async def test_create_additional_key(self, client): + _, api_key = await register_user(client) + + resp = await client.post( + "/auth/keys", + headers=auth_header(api_key), + json={"label": "ci-server"}, + ) + + assert resp.status_code == 201 + assert resp.json()["label"] == "ci-server" + assert resp.json()["key"].startswith("pp_") + + async def test_list_keys(self, client): + _, api_key = await register_user(client) + + resp = await client.get( + "/auth/keys", + headers=auth_header(api_key), + ) + + assert resp.status_code == 200 + assert len(resp.json()) >= 1 + + async def test_revoke_key(self, client): + _, api_key = await register_user(client) + + # Create a second key so we can revoke it + create_resp = await client.post( + "/auth/keys", + headers=auth_header(api_key), + json={"label": "throwaway"}, + ) + key_id = create_resp.json()["id"] + + # Revoke it + del_resp = await client.delete( + f"/auth/keys/{key_id}", + headers=auth_header(api_key), + ) + assert del_resp.status_code == 204 \ No newline at end of file