mirror of
https://github.com/langgenius/dify.git
synced 2026-07-25 05:28:35 +08:00
test: move password reset coverage to unit tests (#38936)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
da2177c7b2
commit
0ad985515b
@ -1,12 +1,14 @@
|
||||
"""Testcontainers integration tests for password reset authentication flows."""
|
||||
"""Unit tests for password reset controller flows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
|
||||
from controllers.console.auth.error import (
|
||||
EmailCodeError,
|
||||
@ -21,47 +23,58 @@ from controllers.console.auth.forgot_password import (
|
||||
ForgotPasswordSendEmailApi,
|
||||
)
|
||||
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
|
||||
from tests.test_containers_integration_tests.controllers.console.helpers import ensure_dify_setup
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from services.feature_service import SystemFeatureModel
|
||||
|
||||
SQLITE_MODELS = (Account, Tenant, TenantAccountJoin)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _bind_database_session(session: Session) -> Generator[scoped_session[Session]]:
|
||||
"""Bind the controller's session proxy to the SQLite test engine."""
|
||||
|
||||
database_session = scoped_session(sessionmaker(bind=session.get_bind(), expire_on_commit=False))
|
||||
try:
|
||||
with patch("controllers.console.auth.forgot_password.db.session", database_session):
|
||||
yield database_session
|
||||
finally:
|
||||
database_session.remove()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def enable_password_login_wrappers(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep endpoint decorators deterministic without requiring the configured app database."""
|
||||
|
||||
monkeypatch.setattr("controllers.console.wraps.dify_config.EDITION", "CLOUD")
|
||||
monkeypatch.setattr(
|
||||
"controllers.console.wraps.FeatureService.get_system_features",
|
||||
lambda: SystemFeatureModel(enable_email_password_login=True),
|
||||
)
|
||||
|
||||
|
||||
class TestForgotPasswordSendEmailApi:
|
||||
"""Test cases for sending password reset emails."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self, flask_app_with_containers: Flask, db_session_with_containers: Session):
|
||||
ensure_dify_setup(db_session_with_containers)
|
||||
return flask_app_with_containers
|
||||
|
||||
@pytest.fixture
|
||||
def mock_account(self):
|
||||
"""Create mock account object."""
|
||||
account = MagicMock()
|
||||
account.email = "test@example.com"
|
||||
account.name = "Test User"
|
||||
return account
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [SQLITE_MODELS], indirect=True)
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.is_email_send_ip_limit")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.send_reset_password_email")
|
||||
@patch("controllers.console.auth.forgot_password.FeatureService.get_system_features")
|
||||
def test_send_reset_email_success(
|
||||
self,
|
||||
mock_get_features,
|
||||
mock_send_email,
|
||||
mock_get_account,
|
||||
mock_is_ip_limit,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
# Arrange
|
||||
mock_is_ip_limit.return_value = False
|
||||
mock_get_account.return_value = mock_account
|
||||
mock_send_email.return_value = "reset_token_123"
|
||||
mock_get_features.return_value.is_allow_register = True
|
||||
|
||||
# Act
|
||||
with app.test_request_context(
|
||||
"/forgot-password", method="POST", json={"email": "test@example.com", "language": "en-US"}
|
||||
with (
|
||||
_bind_database_session(sqlite_session),
|
||||
app.test_request_context(
|
||||
"/forgot-password", method="POST", json={"email": "test@example.com", "language": "en-US"}
|
||||
),
|
||||
):
|
||||
api = ForgotPasswordSendEmailApi()
|
||||
response = api.post()
|
||||
@ -98,20 +111,17 @@ class TestForgotPasswordSendEmailApi:
|
||||
(None, "en-US"), # Defaults to en-US when not provided
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("sqlite_session", [SQLITE_MODELS], indirect=True)
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.is_email_send_ip_limit")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.send_reset_password_email")
|
||||
@patch("controllers.console.auth.forgot_password.FeatureService.get_system_features")
|
||||
def test_send_reset_email_language_handling(
|
||||
self,
|
||||
mock_get_features,
|
||||
mock_send_email,
|
||||
mock_get_account,
|
||||
mock_is_ip_limit,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
language_input,
|
||||
expected_language,
|
||||
app: Flask,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""
|
||||
Test password reset email with different language preferences.
|
||||
@ -122,13 +132,14 @@ class TestForgotPasswordSendEmailApi:
|
||||
"""
|
||||
# Arrange
|
||||
mock_is_ip_limit.return_value = False
|
||||
mock_get_account.return_value = mock_account
|
||||
mock_send_email.return_value = "token"
|
||||
mock_get_features.return_value.is_allow_register = True
|
||||
|
||||
# Act
|
||||
with app.test_request_context(
|
||||
"/forgot-password", method="POST", json={"email": "test@example.com", "language": language_input}
|
||||
with (
|
||||
_bind_database_session(sqlite_session),
|
||||
app.test_request_context(
|
||||
"/forgot-password", method="POST", json={"email": "test@example.com", "language": language_input}
|
||||
),
|
||||
):
|
||||
api = ForgotPasswordSendEmailApi()
|
||||
api.post()
|
||||
@ -141,11 +152,6 @@ class TestForgotPasswordSendEmailApi:
|
||||
class TestForgotPasswordCheckApi:
|
||||
"""Test cases for verifying password reset codes."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self, flask_app_with_containers: Flask, db_session_with_containers: Session):
|
||||
ensure_dify_setup(db_session_with_containers)
|
||||
return flask_app_with_containers
|
||||
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.is_forgot_password_error_rate_limit")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.revoke_reset_password_token")
|
||||
@ -153,10 +159,10 @@ class TestForgotPasswordCheckApi:
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.reset_forgot_password_error_rate_limit")
|
||||
def test_verify_code_success(
|
||||
self,
|
||||
mock_reset_rate_limit,
|
||||
mock_generate_token,
|
||||
mock_revoke_token,
|
||||
mock_get_data,
|
||||
mock_reset_rate_limit: MagicMock,
|
||||
mock_generate_token: MagicMock,
|
||||
mock_revoke_token: MagicMock,
|
||||
mock_get_data: MagicMock,
|
||||
mock_is_rate_limit,
|
||||
app: Flask,
|
||||
):
|
||||
@ -200,10 +206,10 @@ class TestForgotPasswordCheckApi:
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.reset_forgot_password_error_rate_limit")
|
||||
def test_verify_code_preserves_token_email_case(
|
||||
self,
|
||||
mock_reset_rate_limit,
|
||||
mock_generate_token,
|
||||
mock_revoke_token,
|
||||
mock_get_data,
|
||||
mock_reset_rate_limit: MagicMock,
|
||||
mock_generate_token: MagicMock,
|
||||
mock_revoke_token: MagicMock,
|
||||
mock_get_data: MagicMock,
|
||||
mock_is_rate_limit,
|
||||
app: Flask,
|
||||
):
|
||||
@ -325,33 +331,15 @@ class TestForgotPasswordCheckApi:
|
||||
class TestForgotPasswordResetApi:
|
||||
"""Test cases for resetting password with verified token."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self, flask_app_with_containers: Flask, db_session_with_containers: Session):
|
||||
ensure_dify_setup(db_session_with_containers)
|
||||
return flask_app_with_containers
|
||||
|
||||
@pytest.fixture
|
||||
def mock_account(self):
|
||||
"""Create mock account object."""
|
||||
account = MagicMock()
|
||||
account.email = "test@example.com"
|
||||
account.name = "Test User"
|
||||
return account
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [SQLITE_MODELS], indirect=True)
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.revoke_reset_password_token")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||
@patch("controllers.console.auth.forgot_password.db")
|
||||
@patch("controllers.console.auth.forgot_password.TenantService.get_join_tenants")
|
||||
def test_reset_password_success(
|
||||
self,
|
||||
mock_get_tenants,
|
||||
mock_db,
|
||||
mock_get_account,
|
||||
mock_revoke_token,
|
||||
mock_get_data,
|
||||
mock_revoke_token: MagicMock,
|
||||
mock_get_data: MagicMock,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""
|
||||
Test successful password reset.
|
||||
@ -363,25 +351,39 @@ class TestForgotPasswordResetApi:
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_data.return_value = {"email": "test@example.com", "phase": "reset"}
|
||||
mock_get_account.return_value = mock_account
|
||||
mock_db.session.merge.return_value = mock_account
|
||||
mock_get_tenants.return_value = [MagicMock()]
|
||||
|
||||
# Act
|
||||
with app.test_request_context(
|
||||
"/forgot-password/resets",
|
||||
method="POST",
|
||||
json={"token": "valid_token", "new_password": "NewPass123!", "password_confirm": "NewPass123!"},
|
||||
):
|
||||
api = ForgotPasswordResetApi()
|
||||
response = api.post()
|
||||
with _bind_database_session(sqlite_session) as database_session:
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
tenant = Tenant(name="Test Workspace")
|
||||
database_session.add_all([account, tenant])
|
||||
database_session.flush()
|
||||
database_session.add(TenantAccountJoin(tenant_id=tenant.id, account_id=account.id))
|
||||
database_session.commit()
|
||||
account_id = account.id
|
||||
|
||||
with app.test_request_context(
|
||||
"/forgot-password/resets",
|
||||
method="POST",
|
||||
json={
|
||||
"token": "valid_token",
|
||||
"new_password": "NewPass123!",
|
||||
"password_confirm": "NewPass123!",
|
||||
},
|
||||
):
|
||||
api = ForgotPasswordResetApi()
|
||||
response = api.post()
|
||||
|
||||
updated_account = database_session.get(Account, account_id)
|
||||
|
||||
# Assert
|
||||
assert response["result"] == "success"
|
||||
mock_revoke_token.assert_called_once_with("valid_token")
|
||||
assert updated_account is not None
|
||||
assert updated_account.password is not None
|
||||
assert updated_account.password_salt is not None
|
||||
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
||||
def test_reset_password_mismatch(self, mock_get_data, app: Flask):
|
||||
def test_reset_password_mismatch(self, app: Flask):
|
||||
"""
|
||||
Test password reset with mismatched passwords.
|
||||
|
||||
@ -389,9 +391,6 @@ class TestForgotPasswordResetApi:
|
||||
- PasswordMismatchError is raised when passwords don't match
|
||||
- No password update occurs
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_data.return_value = {"email": "test@example.com", "phase": "reset"}
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(
|
||||
"/forgot-password/resets",
|
||||
@ -445,10 +444,12 @@ class TestForgotPasswordResetApi:
|
||||
with pytest.raises(InvalidTokenError):
|
||||
api.post()
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [SQLITE_MODELS], indirect=True)
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.revoke_reset_password_token")
|
||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||
def test_reset_password_account_not_found(self, mock_get_account, mock_revoke_token, mock_get_data, app: Flask):
|
||||
def test_reset_password_account_not_found(
|
||||
self, mock_revoke_token, mock_get_data, app: Flask, sqlite_session: Session
|
||||
):
|
||||
"""
|
||||
Test password reset for non-existent account.
|
||||
|
||||
@ -457,13 +458,15 @@ class TestForgotPasswordResetApi:
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_data.return_value = {"email": "nonexistent@example.com", "phase": "reset"}
|
||||
mock_get_account.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(
|
||||
"/forgot-password/resets",
|
||||
method="POST",
|
||||
json={"token": "token", "new_password": "NewPass123!", "password_confirm": "NewPass123!"},
|
||||
with (
|
||||
_bind_database_session(sqlite_session),
|
||||
app.test_request_context(
|
||||
"/forgot-password/resets",
|
||||
method="POST",
|
||||
json={"token": "token", "new_password": "NewPass123!", "password_confirm": "NewPass123!"},
|
||||
),
|
||||
):
|
||||
api = ForgotPasswordResetApi()
|
||||
with pytest.raises(AccountNotFound):
|
||||
Loading…
Reference in New Issue
Block a user