Spaces:
Sleeping
Sleeping
| """Full register → verify → login → /me flow against the live backend + Postgres.""" | |
| import uuid | |
| import httpx | |
| import pytest | |
| from tests.integration.conftest import STRONG_PASSWORD, mark_user_verified | |
| pytestmark = pytest.mark.integration | |
| BASE_URL = "http://127.0.0.1:8000/api/v1" | |
| def unique_email() -> str: | |
| return f"user-{uuid.uuid4().hex[:12]}@example.com" | |
| def http(): | |
| with httpx.Client(base_url=BASE_URL, timeout=10.0) as client: | |
| yield client | |
| def test_register_creates_user(http): | |
| email = unique_email() | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 201 | |
| body = r.json() | |
| # /register now returns a verification-pending response, not the user row. | |
| assert body["email"] == email | |
| assert "verification_sent" in body | |
| assert isinstance(body["verification_sent"], bool) | |
| assert "message" in body | |
| assert "password" not in body | |
| assert "hashed_password" not in body | |
| def test_register_rejects_duplicate_email(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 409 | |
| def test_register_rejects_short_password(http): | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": unique_email(), | |
| "password": "short", | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 422 | |
| def test_register_rejects_weak_password(http): | |
| # All-lowercase, missing classes — should fail the validator. | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": unique_email(), | |
| "password": "abcdefghij", | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 422 | |
| def test_register_rejects_disposable_domain(http): | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": f"foo-{uuid.uuid4().hex[:6]}@mailinator.com", | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 400 | |
| def test_register_rejects_bad_email(http): | |
| r = http.post( | |
| "/auth/register", | |
| json={ | |
| "email": "not-an-email", | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 422 | |
| def test_login_rejects_unverified(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| # Without the verify step the account stays is_verified=false → 403. | |
| r = http.post( | |
| "/auth/login", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 403 | |
| def test_login_returns_bearer_token(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| mark_user_verified(email) | |
| r = http.post( | |
| "/auth/login", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 200 | |
| body = r.json() | |
| assert "access_token" in body | |
| assert body["token_type"] == "bearer" | |
| assert isinstance(body["access_token"], str) and len(body["access_token"]) > 20 | |
| def test_login_rejects_wrong_password(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| mark_user_verified(email) | |
| r = http.post( | |
| "/auth/login", | |
| json={"email": email, "password": "WRONGPASS", "first_name": "Test", "last_name": "User"}, | |
| ) | |
| assert r.status_code == 401 | |
| def test_login_rejects_unknown_email(http): | |
| r = http.post( | |
| "/auth/login", | |
| json={ | |
| "email": unique_email(), | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| assert r.status_code == 401 | |
| def test_me_returns_current_user(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| mark_user_verified(email) | |
| login = http.post( | |
| "/auth/login", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ).json() | |
| token = login["access_token"] | |
| r = http.get("/auth/me", headers={"Authorization": f"Bearer {token}"}) | |
| assert r.status_code == 200 | |
| body = r.json() | |
| assert body["email"] == email | |
| assert body["is_verified"] is True | |
| assert "id" in body | |
| assert "hashed_password" not in body | |
| def test_me_rejects_missing_token(http): | |
| r = http.get("/auth/me") | |
| # FastAPI's HTTPBearer returns 403 by default; we use auto_error=True which | |
| # returns 403 in older versions and 401 in newer FastAPI — accept either. | |
| assert r.status_code in (401, 403) | |
| def test_me_rejects_invalid_token(http): | |
| r = http.get("/auth/me", headers={"Authorization": "Bearer not-a-real-token"}) | |
| assert r.status_code == 401 | |
| def test_me_rejects_token_signed_with_wrong_secret(http): | |
| import jwt | |
| bad_token = jwt.encode( | |
| {"sub": "00000000-0000-0000-0000-000000000000"}, | |
| "wrong-secret", | |
| algorithm="HS256", | |
| ) | |
| r = http.get("/auth/me", headers={"Authorization": f"Bearer {bad_token}"}) | |
| assert r.status_code == 401 | |
| def test_forgot_password_returns_generic_message(http): | |
| # Even for an unknown email, response must be the same generic shape — | |
| # otherwise the endpoint leaks which accounts exist. | |
| r = http.post("/auth/forgot-password", json={"email": unique_email()}) | |
| assert r.status_code == 200 | |
| assert "message" in r.json() | |
| def test_resend_verification_returns_generic_message(http): | |
| r = http.post("/auth/resend-verification", json={"email": unique_email()}) | |
| assert r.status_code == 200 | |
| assert "message" in r.json() | |
| def test_verify_email_rejects_bad_code(http): | |
| email = unique_email() | |
| http.post( | |
| "/auth/register", | |
| json={ | |
| "email": email, | |
| "password": STRONG_PASSWORD, | |
| "first_name": "Test", | |
| "last_name": "User", | |
| }, | |
| ) | |
| r = http.post( | |
| "/auth/verify-email", | |
| json={"email": email, "code": "BADCODE1"}, | |
| ) | |
| assert r.status_code == 400 | |