182 lines
6.2 KiB
Python
182 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.main import app
|
|
from tests.integration.helpers import (
|
|
api_bootstrap_csrf_headers,
|
|
insert_user,
|
|
login_user,
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.db
|
|
@pytest.mark.asyncio
|
|
async def test_api_me_requires_authentication() -> None:
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/api/v1/auth/me")
|
|
|
|
assert response.status_code == 401
|
|
payload = response.json()
|
|
assert payload["error"]["code"] == "auth_required"
|
|
assert payload["error"]["message"] == "Authentication required."
|
|
assert "request_id" in payload["meta"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.db
|
|
@pytest.mark.asyncio
|
|
async def test_api_envelope_contract_for_success_and_csrf_error() -> None:
|
|
client = TestClient(app)
|
|
|
|
success_response = client.get("/api/v1/auth/csrf")
|
|
assert success_response.status_code == 200
|
|
success_payload = success_response.json()
|
|
assert set(success_payload.keys()) == {"data", "meta"}
|
|
assert isinstance(success_payload["data"]["csrf_token"], str)
|
|
assert set(success_payload["meta"].keys()) == {"request_id"}
|
|
|
|
error_response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "missing-csrf@example.com", "password": "irrelevant"},
|
|
)
|
|
assert error_response.status_code == 403
|
|
error_payload = error_response.json()
|
|
assert set(error_payload.keys()) == {"error", "meta"}
|
|
assert set(error_payload["error"].keys()) == {"code", "message", "details"}
|
|
assert error_payload["error"]["code"] == "csrf_invalid"
|
|
assert error_payload["error"]["details"] is None
|
|
assert set(error_payload["meta"].keys()) == {"request_id"}
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.db
|
|
@pytest.mark.asyncio
|
|
async def test_api_csrf_bootstrap_returns_token_for_anonymous_and_authenticated(
|
|
db_session: AsyncSession,
|
|
) -> None:
|
|
await insert_user(
|
|
db_session,
|
|
email="api-bootstrap@example.com",
|
|
password="api-password",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
anonymous_response = client.get("/api/v1/auth/csrf")
|
|
assert anonymous_response.status_code == 200
|
|
anonymous_payload = anonymous_response.json()["data"]
|
|
assert isinstance(anonymous_payload["csrf_token"], str)
|
|
assert anonymous_payload["authenticated"] is False
|
|
|
|
login_user(client, email="api-bootstrap@example.com", password="api-password")
|
|
authenticated_response = client.get("/api/v1/auth/csrf")
|
|
assert authenticated_response.status_code == 200
|
|
authenticated_payload = authenticated_response.json()["data"]
|
|
assert isinstance(authenticated_payload["csrf_token"], str)
|
|
assert authenticated_payload["authenticated"] is True
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.db
|
|
@pytest.mark.asyncio
|
|
async def test_api_me_returns_user_and_csrf_token(db_session: AsyncSession) -> None:
|
|
await insert_user(
|
|
db_session,
|
|
email="api-me@example.com",
|
|
password="api-password",
|
|
)
|
|
|
|
client = TestClient(app)
|
|
login_user(client, email="api-me@example.com", password="api-password")
|
|
|
|
response = client.get("/api/v1/auth/me")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["data"]["authenticated"] is True
|
|
assert payload["data"]["user"]["email"] == "api-me@example.com"
|
|
assert isinstance(payload["data"]["csrf_token"], str)
|
|
assert payload["meta"]["request_id"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.db
|
|
@pytest.mark.asyncio
|
|
async def test_api_login_and_change_password_flow(db_session: AsyncSession) -> None:
|
|
await insert_user(
|
|
db_session,
|
|
email="api-login@example.com",
|
|
password="old-password",
|
|
)
|
|
client = TestClient(app)
|
|
|
|
missing_csrf = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "api-login@example.com", "password": "old-password"},
|
|
)
|
|
assert missing_csrf.status_code == 403
|
|
assert missing_csrf.json()["error"]["code"] == "csrf_missing"
|
|
|
|
login_headers = api_bootstrap_csrf_headers(client)
|
|
login_response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "api-login@example.com", "password": "old-password"},
|
|
headers=login_headers,
|
|
)
|
|
assert login_response.status_code == 200
|
|
login_payload = login_response.json()["data"]
|
|
assert login_payload["authenticated"] is True
|
|
assert login_payload["user"]["email"] == "api-login@example.com"
|
|
assert isinstance(login_payload["csrf_token"], str)
|
|
|
|
bad_change_response = client.post(
|
|
"/api/v1/auth/change-password",
|
|
json={
|
|
"current_password": "not-correct",
|
|
"new_password": "new-password",
|
|
"confirm_password": "new-password",
|
|
},
|
|
headers={"X-CSRF-Token": login_payload["csrf_token"]},
|
|
)
|
|
assert bad_change_response.status_code == 400
|
|
assert bad_change_response.json()["error"]["code"] == "invalid_current_password"
|
|
|
|
change_response = client.post(
|
|
"/api/v1/auth/change-password",
|
|
json={
|
|
"current_password": "old-password",
|
|
"new_password": "new-password",
|
|
"confirm_password": "new-password",
|
|
},
|
|
headers={"X-CSRF-Token": login_payload["csrf_token"]},
|
|
)
|
|
assert change_response.status_code == 200
|
|
assert change_response.json()["data"]["message"] == "Password updated successfully."
|
|
|
|
logout_response = client.post(
|
|
"/api/v1/auth/logout",
|
|
headers={"X-CSRF-Token": login_payload["csrf_token"]},
|
|
)
|
|
assert logout_response.status_code == 200
|
|
|
|
relogin_headers = api_bootstrap_csrf_headers(client)
|
|
old_password_login = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "api-login@example.com", "password": "old-password"},
|
|
headers=relogin_headers,
|
|
)
|
|
assert old_password_login.status_code == 401
|
|
assert old_password_login.json()["error"]["code"] == "invalid_credentials"
|
|
|
|
fresh_headers = api_bootstrap_csrf_headers(client)
|
|
new_password_login = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "api-login@example.com", "password": "new-password"},
|
|
headers=fresh_headers,
|
|
)
|
|
assert new_password_login.status_code == 200
|
|
assert new_password_login.json()["data"]["authenticated"] is True
|