015c622016
Add 162 integration tests covering authentication flows, TOTP MFA, SSH key/certificate management, organization workflows, multi-org access, self-service features, admin operations, authorization, security edge cases, department/principal management, CA management, policy compliance, WebAuthn passkeys, and ZeroTier network access. Includes: - Reusable API client library with session management - Test fixtures for users, organizations, memberships, and CAs - Helper functions for SSH key generation and verification - Documentation for running and writing tests Also update test configuration to disable conflicting maas plugins and configure WebAuthn/session settings for localhost testing.
591 lines
26 KiB
Python
591 lines
26 KiB
Python
"""Authentication flow integration tests.
|
|
|
|
Covers user registration, login, logout, sessions, and password
|
|
recovery. Every test prints a clear description of WHAT is being
|
|
tested, WHY it matters, and the EXPECTED result so failures are
|
|
actionable.
|
|
"""
|
|
import pytest
|
|
import uuid
|
|
|
|
from tests.integration.client.base import ApiError
|
|
|
|
|
|
# =============================================================================
|
|
# Helper assertions
|
|
# =============================================================================
|
|
|
|
def assert_success(response: dict, message_contains: str = "") -> dict:
|
|
"""Assert that an api_response-wrapped payload succeeded."""
|
|
data = response.get("data", {})
|
|
assert response.get("success") is not False, (
|
|
f"Expected success but got error: {response.get('message')}"
|
|
)
|
|
if message_contains:
|
|
assert message_contains.lower() in response.get("message", "").lower(), (
|
|
f"Expected message to contain '{message_contains}' but got: {response.get('message')}"
|
|
)
|
|
return data
|
|
|
|
|
|
def assert_error(response_or_exc, expected_status: int, expected_error_type: str | None = None):
|
|
"""Assert that an ApiError carries the expected status (and optionally error_type).
|
|
|
|
Because our client raises on >=400, we catch ApiError and inspect it.
|
|
"""
|
|
assert isinstance(response_or_exc, ApiError), (
|
|
f"Expected ApiError but got: {type(response_or_exc).__name__} — {response_or_exc}"
|
|
)
|
|
assert response_or_exc.status_code == expected_status, (
|
|
f"Expected status {expected_status} but got {response_or_exc.status_code}\n"
|
|
f"URL: {response_or_exc.method} {response_or_exc.url}\n"
|
|
f"Response: {response_or_exc.response_data}"
|
|
)
|
|
if expected_error_type:
|
|
assert response_or_exc.error_type == expected_error_type, (
|
|
f"Expected error_type '{expected_error_type}' but got '{response_or_exc.error_type}'"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Tier 2 — E. User Registration & Login
|
|
# =============================================================================
|
|
|
|
class TestRegistration:
|
|
"""Test user registration at POST /auth/register.
|
|
|
|
Registration is the front door of the application. These tests
|
|
ensure that valid users can sign up, duplicate accounts are
|
|
rejected, and weak passwords are blocked.
|
|
"""
|
|
|
|
def test_register_user_positive(self, integration_client):
|
|
"""TEST: AUTH-01 — Register a new user with valid data.
|
|
|
|
WHAT: Call POST /auth/register with a unique email, strong
|
|
password, and full name.
|
|
WHY: This is the primary on-ramp for every user. It must
|
|
create the user, return a session token, and flag the
|
|
account as the first user when appropriate.
|
|
EXPECTED: 201 Created, response contains user object, token,
|
|
expires_at, and is_first_user=True (since this is
|
|
the first user in the fresh test DB).
|
|
"""
|
|
email = f"auth01_{uuid.uuid4().hex[:8]}@example.com"
|
|
result = integration_client.auth.register(
|
|
email=email,
|
|
password="StrongPass123!",
|
|
full_name="Auth One",
|
|
)
|
|
data = assert_success(result, "registration successful")
|
|
|
|
assert "user" in data, "Response missing 'user' object"
|
|
assert data["user"]["email"] == email
|
|
assert "token" in data, "Response missing 'token' — session not created"
|
|
assert "expires_at" in data, "Response missing 'expires_at'"
|
|
assert data.get("is_first_user") is True, "First user should have is_first_user=True"
|
|
|
|
def test_register_duplicate_email_negative(self, integration_client):
|
|
"""TEST: AUTH-02 — Reject registration with a duplicate email.
|
|
|
|
WHAT: Register a user, then attempt to register again with
|
|
the same email address.
|
|
WHY: Duplicate accounts would break email-based lookups,
|
|
password reset flows, and invite acceptance.
|
|
EXPECTED: 400 Bad Request, error_type="VALIDATION_ERROR".
|
|
"""
|
|
email = f"auth02_{uuid.uuid4().hex[:8]}@example.com"
|
|
integration_client.auth.register(email=email, password="StrongPass123!", full_name="First")
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.register(email=email, password="DifferentPass123!", full_name="Second")
|
|
|
|
assert_error(exc_info.value, 409, "CONFLICT")
|
|
|
|
def test_register_weak_password_negative(self, integration_client):
|
|
"""TEST: AUTH-03 — Reject registration with a weak password.
|
|
|
|
WHAT: Attempt to register with a password shorter than 8
|
|
characters.
|
|
WHY: Weak passwords are the #1 cause of account takeovers.
|
|
The API must enforce a minimum length.
|
|
EXPECTED: 400 Bad Request, error_type="VALIDATION_ERROR".
|
|
"""
|
|
email = f"auth03_{uuid.uuid4().hex[:8]}@example.com"
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.register(email=email, password="short", full_name="Weak")
|
|
|
|
assert_error(exc_info.value, 400, "VALIDATION_ERROR")
|
|
|
|
def test_register_missing_fields_negative(self, integration_client):
|
|
"""TEST: AUTH-04 — Reject registration with missing required fields.
|
|
|
|
WHAT: Send a POST /auth/register payload without the email
|
|
and password fields.
|
|
WHY: The schema must validate presence of required fields
|
|
before touching the database.
|
|
EXPECTED: 400 Bad Request, error_type="VALIDATION_ERROR".
|
|
"""
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.post("/auth/register", data={})
|
|
|
|
assert_error(exc_info.value, 400, "VALIDATION_ERROR")
|
|
|
|
|
|
class TestLogin:
|
|
"""Test user login at POST /auth/login.
|
|
|
|
Login is the most frequently used endpoint. These tests verify
|
|
that valid credentials issue a session, invalid credentials are
|
|
rejected without leaking existence, and suspended accounts are
|
|
blocked.
|
|
"""
|
|
|
|
def test_login_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-05 — Login with valid credentials.
|
|
|
|
WHAT: Create a user via factory, then call POST /auth/login
|
|
with the correct email and password.
|
|
WHY: This is the core authentication flow. A successful
|
|
login must issue a session token and return user data.
|
|
EXPECTED: 200 OK, response contains user object, token, and
|
|
expires_at. Subsequent GET /auth/me must succeed.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
result = integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
data = assert_success(result, "login successful")
|
|
|
|
assert "token" in data, "Login response missing token"
|
|
assert data["user"]["email"] == user["email"]
|
|
|
|
# Verify the token actually works
|
|
me_result = integration_client.auth.me()
|
|
me_data = assert_success(me_result)
|
|
assert me_data["user"]["email"] == user["email"]
|
|
|
|
def test_login_wrong_password_negative(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-06 — Reject login with wrong password.
|
|
|
|
WHAT: Create a user, then attempt login with an incorrect
|
|
password.
|
|
WHY: We must not leak whether the email exists. The
|
|
response for wrong-password and non-existent-user
|
|
should be identical.
|
|
EXPECTED: 400 Bad Request (or 401) with a generic failure
|
|
message.
|
|
"""
|
|
user = create_test_user(password="CorrectPass123!")
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(email=user["email"], password="wrongpassword")
|
|
|
|
assert exc_info.value.status_code in (400, 401), (
|
|
f"Expected 400 or 401 for wrong password, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
def test_login_nonexistent_user_negative(self, integration_client):
|
|
"""TEST: AUTH-07 — Reject login for non-existent user.
|
|
|
|
WHAT: Attempt to login with an email that has never been
|
|
registered.
|
|
WHY: Same as AUTH-06 — user enumeration must be prevented.
|
|
EXPECTED: Identical response to wrong-password (400/401).
|
|
"""
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(
|
|
email=f"doesnotexist_{uuid.uuid4().hex[:8]}@example.com",
|
|
password="SomePassword123!",
|
|
)
|
|
|
|
assert exc_info.value.status_code in (400, 401), (
|
|
f"Expected 400 or 401 for non-existent user, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
def test_login_suspended_user_negative(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-08 — Reject login for suspended account.
|
|
|
|
WHAT: Create a user, suspend the account by setting
|
|
user.status = SUSPENDED, then attempt login.
|
|
WHY: Admin suspension is a critical security tool. A
|
|
suspended user must not be able to obtain a session.
|
|
EXPECTED: 403 Forbidden, error_type="ACCOUNT_SUSPENDED".
|
|
"""
|
|
from gatehouse_app.utils.constants import UserStatus
|
|
|
|
user_info = create_test_user(password="MyPassword123!")
|
|
|
|
# Suspend the user directly in the DB
|
|
with integration_app.app_context():
|
|
from gatehouse_app.models.user.user import User
|
|
user = User.query.get(user_info["id"])
|
|
user.status = UserStatus.SUSPENDED
|
|
from gatehouse_app.extensions import db
|
|
db.session.commit()
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(email=user_info["email"], password="MyPassword123!")
|
|
|
|
assert_error(exc_info.value, 403, "AUTHORIZATION_ERROR")
|
|
|
|
|
|
class TestLogoutAndSessions:
|
|
"""Test logout and session management.
|
|
|
|
Sessions are the mechanism that keeps users authenticated across
|
|
requests. These tests verify that logout destroys the session and
|
|
that users can list and revoke their active sessions.
|
|
"""
|
|
|
|
def test_logout_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-09 — Logout an authenticated user.
|
|
|
|
WHAT: Login, verify /auth/me works, call /auth/logout, then
|
|
verify /auth/me returns 401.
|
|
WHY: Logout must invalidate the token so it cannot be reused
|
|
for protected endpoints.
|
|
EXPECTED: 200 OK on logout, then 401 on subsequent me call.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
# Confirm we're authenticated
|
|
me = integration_client.auth.me()
|
|
assert_success(me)
|
|
|
|
# Logout
|
|
result = integration_client.auth.logout()
|
|
assert_success(result, "logout successful")
|
|
|
|
# Token should no longer work
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.me()
|
|
|
|
assert exc_info.value.status_code == 401, (
|
|
f"Expected 401 after logout, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
def test_logout_without_auth_negative(self, integration_client):
|
|
"""TEST: AUTH-10 — Reject logout when not authenticated.
|
|
|
|
WHAT: Call POST /auth/logout without a Bearer token.
|
|
WHY: The endpoint is protected by @login_required; an
|
|
unauthenticated request must be rejected.
|
|
EXPECTED: 401 Unauthorized.
|
|
"""
|
|
integration_client.clear_token()
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.logout()
|
|
|
|
assert exc_info.value.status_code == 401, (
|
|
f"Expected 401 for unauthenticated logout, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
def test_list_sessions_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-11 — List active sessions.
|
|
|
|
WHAT: Login and request GET /auth/sessions.
|
|
WHY: Users need visibility into where they are logged in for
|
|
security hygiene.
|
|
EXPECTED: 200 OK with a list containing at least the current
|
|
session.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
result = integration_client.auth.list_sessions()
|
|
data = assert_success(result, "sessions retrieved")
|
|
|
|
sessions = data.get("sessions", [])
|
|
assert len(sessions) >= 1, "Expected at least one active session"
|
|
assert "id" in sessions[0], "Session object missing 'id'"
|
|
|
|
def test_revoke_session_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-12 — Revoke an active session.
|
|
|
|
WHAT: Login, list sessions, revoke the first session, then
|
|
verify the token no longer works.
|
|
WHY: Session revocation allows users to remotely sign out
|
|
devices they no longer control.
|
|
EXPECTED: 200 OK on revocation, then 401 on subsequent me call.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
sessions = integration_client.auth.list_sessions()
|
|
session_id = sessions["data"]["sessions"][0]["id"]
|
|
|
|
result = integration_client.auth.revoke_session(session_id)
|
|
assert_success(result, "session revoked")
|
|
|
|
# Token should be invalid now
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.me()
|
|
|
|
assert exc_info.value.status_code == 401, (
|
|
f"Expected 401 after revoking session, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
def test_revoke_nonexistent_session_negative(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-13 — Reject revoking a non-existent session.
|
|
|
|
WHAT: Login and attempt to DELETE /auth/sessions/<fake-id>.
|
|
WHY: The API must distinguish between "not found" and
|
|
"forbidden" so clients can show correct error states.
|
|
EXPECTED: 404 Not Found.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.revoke_session("00000000-0000-0000-0000-000000000000")
|
|
|
|
assert exc_info.value.status_code == 404, (
|
|
f"Expected 404 for non-existent session, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
|
|
class TestCurrentUser:
|
|
"""Test the /auth/me endpoint."""
|
|
|
|
def test_get_current_user_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-14 — Get current user when authenticated.
|
|
|
|
WHAT: Login and call GET /auth/me.
|
|
WHY: The frontend uses this endpoint on every page load to
|
|
determine login state and populate the user menu.
|
|
EXPECTED: 200 OK with user object and organizations list.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
result = integration_client.auth.me()
|
|
data = assert_success(result, "user retrieved")
|
|
|
|
assert data["user"]["email"] == user["email"]
|
|
assert "organizations" in data, "Response missing 'organizations' list"
|
|
|
|
def test_get_current_user_without_auth_negative(self, integration_client):
|
|
"""TEST: AUTH-15 — Reject /auth/me without authentication.
|
|
|
|
WHAT: Call GET /auth/me with no Bearer token.
|
|
WHY: Protected endpoints must reject unauthenticated requests
|
|
to prevent data leakage.
|
|
EXPECTED: 401 Unauthorized.
|
|
"""
|
|
integration_client.clear_token()
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.me()
|
|
|
|
assert exc_info.value.status_code == 401, (
|
|
f"Expected 401 for unauthenticated /auth/me, got {exc_info.value.status_code}"
|
|
)
|
|
|
|
|
|
class TestPasswordRecovery:
|
|
"""Test password reset flow at POST /auth/forgot-password and
|
|
POST /auth/reset-password.
|
|
|
|
These endpoints allow users to regain access when they forget their
|
|
password. Security requirements: the forgot-password endpoint must
|
|
not leak whether an email exists, and tokens must be single-use.
|
|
"""
|
|
|
|
def test_forgot_password_positive(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-20 — Request password reset for existing email.
|
|
|
|
WHAT: Create a user, then POST /auth/forgot-password with
|
|
the user's email.
|
|
WHY: This is the entry point for password recovery. It must
|
|
succeed silently and generate a token in the DB.
|
|
EXPECTED: 200 OK with a generic success message. A
|
|
PasswordResetToken should exist in the database.
|
|
"""
|
|
user = create_test_user(password="OldPass123!")
|
|
result = integration_client.auth.forgot_password(user["email"])
|
|
data = assert_success(result, "you will receive")
|
|
|
|
# Verify token was created in DB
|
|
from gatehouse_app.models.auth.password_reset_token import PasswordResetToken
|
|
from gatehouse_app.extensions import db
|
|
from gatehouse_app.models.user.user import User
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
token = PasswordResetToken.query.filter_by(user_id=db_user.id, used_at=None).first()
|
|
assert token is not None, "Password reset token was not created"
|
|
|
|
def test_forgot_password_nonexistent_email_positive(self, integration_client):
|
|
"""TEST: AUTH-21 — Request password reset for non-existent email.
|
|
|
|
WHAT: POST /auth/forgot-password with an email that has never
|
|
been registered.
|
|
WHY: User enumeration must be prevented. The response for
|
|
non-existent and existing emails must be identical.
|
|
EXPECTED: 200 OK with the exact same message as AUTH-20.
|
|
"""
|
|
result = integration_client.auth.forgot_password("doesnotexist@example.com")
|
|
data = assert_success(result, "you will receive")
|
|
|
|
def test_reset_password_positive(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-22 — Reset password with a valid token.
|
|
|
|
WHAT: Create a user, generate a PasswordResetToken directly in
|
|
the DB, then POST /auth/reset-password with the token
|
|
and a new password.
|
|
WHY: This is the actual password change step. It must update
|
|
the auth method hash and invalidate the token.
|
|
EXPECTED: 200 OK. Subsequent login with the NEW password must
|
|
succeed; login with the OLD password must fail.
|
|
"""
|
|
from gatehouse_app.models.auth.password_reset_token import PasswordResetToken
|
|
from gatehouse_app.extensions import db
|
|
from gatehouse_app.models.user.user import User
|
|
|
|
user = create_test_user(password="OldPass123!")
|
|
|
|
# Generate token directly in DB
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
reset_token = PasswordResetToken.generate(user_id=db_user.id)
|
|
token_value = reset_token.token
|
|
|
|
result = integration_client.auth.reset_password(
|
|
token=token_value,
|
|
new_password="NewPass456!",
|
|
new_password_confirm="NewPass456!",
|
|
)
|
|
assert_success(result, "reset")
|
|
|
|
# Verify old password no longer works
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(email=user["email"], password="OldPass123!")
|
|
assert exc_info.value.status_code in (400, 401)
|
|
|
|
# Verify new password works
|
|
login_result = integration_client.auth.login(email=user["email"], password="NewPass456!")
|
|
assert_success(login_result, "login successful")
|
|
|
|
def test_reset_password_invalid_token_negative(self, integration_client):
|
|
"""TEST: AUTH-23 — Reject password reset with invalid/expired token.
|
|
|
|
WHAT: POST /auth/reset-password with a made-up token string.
|
|
WHY: Expired or forged tokens must not allow password changes.
|
|
EXPECTED: 400 Bad Request, error_type="INVALID_TOKEN".
|
|
"""
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.reset_password(
|
|
token="invalid-token-12345",
|
|
new_password="NewPass456!",
|
|
new_password_confirm="NewPass456!",
|
|
)
|
|
assert_error(exc_info.value, 400, "INVALID_TOKEN")
|
|
|
|
def test_reset_password_mismatched_passwords_negative(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-24 — Reject password reset with mismatched passwords.
|
|
|
|
WHAT: Generate a valid reset token, then submit mismatched
|
|
new_password and new_password_confirm.
|
|
WHY: Typo protection — ensures the user knows what they typed.
|
|
EXPECTED: 400 Bad Request, error_type="VALIDATION_ERROR".
|
|
"""
|
|
from gatehouse_app.models.auth.password_reset_token import PasswordResetToken
|
|
from gatehouse_app.models.user.user import User
|
|
|
|
user = create_test_user(password="OldPass123!")
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
reset_token = PasswordResetToken.generate(user_id=db_user.id)
|
|
token_value = reset_token.token
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.reset_password(
|
|
token=token_value,
|
|
new_password="NewPass456!",
|
|
new_password_confirm="DifferentPass789!",
|
|
)
|
|
assert_error(exc_info.value, 400, "VALIDATION_ERROR")
|
|
|
|
def test_reset_password_weak_password_negative(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-25 — Reject password reset with weak password.
|
|
|
|
WHAT: Generate a valid reset token, then submit a password
|
|
shorter than 8 characters.
|
|
WHY: Weak passwords must be blocked even during reset.
|
|
EXPECTED: 400 Bad Request, error_type="VALIDATION_ERROR".
|
|
"""
|
|
from gatehouse_app.models.auth.password_reset_token import PasswordResetToken
|
|
from gatehouse_app.models.user.user import User
|
|
|
|
user = create_test_user(password="OldPass123!")
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
reset_token = PasswordResetToken.generate(user_id=db_user.id)
|
|
token_value = reset_token.token
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.reset_password(
|
|
token=token_value,
|
|
new_password="short",
|
|
new_password_confirm="short",
|
|
)
|
|
assert_error(exc_info.value, 400, "VALIDATION_ERROR")
|
|
|
|
|
|
class TestEmailVerification:
|
|
"""Test email verification at POST /auth/verify-email and
|
|
POST /auth/resend-verification.
|
|
"""
|
|
|
|
def test_verify_email_positive(self, integration_app, integration_client, create_test_user):
|
|
"""TEST: AUTH-26 — Verify email with valid token.
|
|
|
|
WHAT: Create a user with email_verified=False, generate an
|
|
EmailVerificationToken in the DB, then POST
|
|
/auth/verify-email.
|
|
WHY: Email verification is required for some features. The
|
|
token must mark the user as verified.
|
|
EXPECTED: 200 OK. User.email_verified becomes True.
|
|
"""
|
|
from gatehouse_app.models.auth.email_verification_token import EmailVerificationToken
|
|
from gatehouse_app.extensions import db
|
|
from gatehouse_app.models.user.user import User
|
|
|
|
user = create_test_user(password="MyPassword123!", email_verified=False)
|
|
assert user["email"]
|
|
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
verify_token = EmailVerificationToken.generate(user_id=db_user.id)
|
|
token_value = verify_token.token
|
|
|
|
result = integration_client.auth.verify_email(token=token_value)
|
|
assert_success(result, "verified")
|
|
|
|
with integration_app.app_context():
|
|
db_user = User.query.filter_by(email=user["email"]).first()
|
|
assert db_user.email_verified is True
|
|
|
|
def test_verify_email_invalid_token_negative(self, integration_client):
|
|
"""TEST: AUTH-27 — Reject email verification with invalid token.
|
|
|
|
WHAT: POST /auth/verify-email with a fabricated token.
|
|
WHY: Invalid or expired tokens must not verify emails.
|
|
EXPECTED: 400 Bad Request, error_type="INVALID_TOKEN".
|
|
"""
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.verify_email(token="invalid-token-12345")
|
|
assert_error(exc_info.value, 400, "INVALID_TOKEN")
|
|
|
|
def test_resend_verification_positive(self, integration_client, create_test_user):
|
|
"""TEST: AUTH-28 — Resend verification email.
|
|
|
|
WHAT: Create a user with email_verified=False, then POST
|
|
/auth/resend-verification.
|
|
WHY: Users may lose the original verification email. The
|
|
endpoint must generate a new token.
|
|
EXPECTED: 200 OK with generic success message.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!", email_verified=False)
|
|
result = integration_client.auth.resend_verification(email=user["email"])
|
|
assert_success(result, "you will receive")
|