mirror of
https://github.com/langgenius/dify.git
synced 2026-04-23 08:26:28 +08:00
fix: fix orm_exc.DetachedInstanceError (#34904)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
488fcd4f83
commit
bcd738d2e6
@ -2,7 +2,6 @@ import base64
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -25,30 +24,31 @@ def reset_password(email, new_password, password_confirm):
|
|||||||
return
|
return
|
||||||
normalized_email = email.strip().lower()
|
normalized_email = email.strip().lower()
|
||||||
|
|
||||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email.strip())
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email.strip(), session=session)
|
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
|
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
valid_password(new_password)
|
valid_password(new_password)
|
||||||
except:
|
except:
|
||||||
click.echo(click.style(f"Invalid password. Must match {password_pattern}", fg="red"))
|
click.echo(click.style(f"Invalid password. Must match {password_pattern}", fg="red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# generate password salt
|
# generate password salt
|
||||||
salt = secrets.token_bytes(16)
|
salt = secrets.token_bytes(16)
|
||||||
base64_salt = base64.b64encode(salt).decode()
|
base64_salt = base64.b64encode(salt).decode()
|
||||||
|
|
||||||
# encrypt password with salt
|
# encrypt password with salt
|
||||||
password_hashed = hash_password(new_password, salt)
|
password_hashed = hash_password(new_password, salt)
|
||||||
base64_password_hashed = base64.b64encode(password_hashed).decode()
|
base64_password_hashed = base64.b64encode(password_hashed).decode()
|
||||||
account.password = base64_password_hashed
|
account = db.session.merge(account)
|
||||||
account.password_salt = base64_salt
|
account.password = base64_password_hashed
|
||||||
AccountService.reset_login_error_rate_limit(normalized_email)
|
account.password_salt = base64_salt
|
||||||
click.echo(click.style("Password reset successfully.", fg="green"))
|
db.session.commit()
|
||||||
|
AccountService.reset_login_error_rate_limit(normalized_email)
|
||||||
|
click.echo(click.style("Password reset successfully.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
@click.command("reset-email", help="Reset the account email.")
|
@click.command("reset-email", help="Reset the account email.")
|
||||||
@ -65,21 +65,22 @@ def reset_email(email, new_email, email_confirm):
|
|||||||
return
|
return
|
||||||
normalized_new_email = new_email.strip().lower()
|
normalized_new_email = new_email.strip().lower()
|
||||||
|
|
||||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email.strip())
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email.strip(), session=session)
|
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
|
click.echo(click.style(f"Account not found for email: {email}", fg="red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email_validate(normalized_new_email)
|
email_validate(normalized_new_email)
|
||||||
except:
|
except:
|
||||||
click.echo(click.style(f"Invalid email: {new_email}", fg="red"))
|
click.echo(click.style(f"Invalid email: {new_email}", fg="red"))
|
||||||
return
|
return
|
||||||
|
|
||||||
account.email = normalized_new_email
|
account = db.session.merge(account)
|
||||||
click.echo(click.style("Email updated successfully.", fg="green"))
|
account.email = normalized_new_email
|
||||||
|
db.session.commit()
|
||||||
|
click.echo(click.style("Email updated successfully.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
@click.command("create-tenant", help="Create account and tenant.")
|
@click.command("create-tenant", help="Create account and tenant.")
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
@ -14,7 +13,6 @@ from controllers.console.auth.error import (
|
|||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
PasswordMismatchError,
|
PasswordMismatchError,
|
||||||
)
|
)
|
||||||
from extensions.ext_database import db
|
|
||||||
from libs.helper import EmailStr, extract_remote_ip
|
from libs.helper import EmailStr, extract_remote_ip
|
||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
from models import Account
|
from models import Account
|
||||||
@ -73,8 +71,7 @@ class EmailRegisterSendEmailApi(Resource):
|
|||||||
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
|
||||||
raise AccountInFreezeError()
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(args.email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session)
|
|
||||||
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
|
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
@ -145,17 +142,16 @@ class EmailRegisterResetApi(Resource):
|
|||||||
email = register_data.get("email", "")
|
email = register_data.get("email", "")
|
||||||
normalized_email = email.lower()
|
normalized_email = email.lower()
|
||||||
|
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email, session=session)
|
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
raise EmailAlreadyInUseError()
|
raise EmailAlreadyInUseError()
|
||||||
else:
|
else:
|
||||||
account = self._create_new_account(normalized_email, args.password_confirm)
|
account = self._create_new_account(normalized_email, args.password_confirm)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountNotFoundError()
|
raise AccountNotFoundError()
|
||||||
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
|
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
|
||||||
AccountService.reset_login_error_rate_limit(normalized_email)
|
AccountService.reset_login_error_rate_limit(normalized_email)
|
||||||
|
|
||||||
return {"result": "success", "data": token_pair.model_dump()}
|
return {"result": "success", "data": token_pair.model_dump()}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import secrets
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from controllers.common.schema import register_schema_models
|
from controllers.common.schema import register_schema_models
|
||||||
from controllers.console import console_ns
|
from controllers.console import console_ns
|
||||||
@ -85,8 +84,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
else:
|
else:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
|
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(args.email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session)
|
|
||||||
|
|
||||||
token = AccountService.send_reset_password_email(
|
token = AccountService.send_reset_password_email(
|
||||||
account=account,
|
account=account,
|
||||||
@ -184,17 +182,18 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
password_hashed = hash_password(args.new_password, salt)
|
password_hashed = hash_password(args.new_password, salt)
|
||||||
|
|
||||||
email = reset_data.get("email", "")
|
email = reset_data.get("email", "")
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email, session=session)
|
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
self._update_existing_account(account, password_hashed, salt, session)
|
account = db.session.merge(account)
|
||||||
else:
|
self._update_existing_account(account, password_hashed, salt)
|
||||||
raise AccountNotFound()
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
raise AccountNotFound()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
def _update_existing_account(self, account, password_hashed, salt, session):
|
def _update_existing_account(self, account, password_hashed, salt):
|
||||||
# Update existing account credentials
|
# Update existing account credentials
|
||||||
account.password = base64.b64encode(password_hashed).decode()
|
account.password = base64.b64encode(password_hashed).decode()
|
||||||
account.password_salt = base64.b64encode(salt).decode()
|
account.password_salt = base64.b64encode(salt).decode()
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import urllib.parse
|
|||||||
import httpx
|
import httpx
|
||||||
from flask import current_app, redirect, request
|
from flask import current_app, redirect, request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from werkzeug.exceptions import Unauthorized
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -180,8 +179,7 @@ def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) ->
|
|||||||
account: Account | None = Account.get_by_openid(provider, user_info.id)
|
account: Account | None = Account.get_by_openid(provider, user_info.id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(user_info.email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(user_info.email, session=session)
|
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ from flask import request
|
|||||||
from flask_restx import Resource, fields, marshal_with
|
from flask_restx import Resource, fields, marshal_with
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
@ -562,8 +561,7 @@ class ChangeEmailSendEmailApi(Resource):
|
|||||||
|
|
||||||
user_email = current_user.email
|
user_email = current_user.email
|
||||||
else:
|
else:
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(args.email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session)
|
|
||||||
if account is None:
|
if account is None:
|
||||||
raise AccountNotFound()
|
raise AccountNotFound()
|
||||||
email_for_sending = account.email
|
email_for_sending = account.email
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import secrets
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from controllers.common.schema import register_schema_models
|
from controllers.common.schema import register_schema_models
|
||||||
from controllers.console.auth.error import (
|
from controllers.console.auth.error import (
|
||||||
@ -62,9 +61,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
else:
|
else:
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
|
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(request_email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(request_email, session=session)
|
|
||||||
token = None
|
|
||||||
if account is None:
|
if account is None:
|
||||||
raise AuthenticationFailedError()
|
raise AuthenticationFailedError()
|
||||||
else:
|
else:
|
||||||
@ -161,13 +158,14 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
|
|
||||||
email = reset_data.get("email", "")
|
email = reset_data.get("email", "")
|
||||||
|
|
||||||
with sessionmaker(db.engine).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email, session=session)
|
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
self._update_existing_account(account, password_hashed, salt)
|
account = db.session.merge(account)
|
||||||
else:
|
self._update_existing_account(account, password_hashed, salt)
|
||||||
raise AuthenticationFailedError()
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
raise AuthenticationFailedError()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,8 @@ from typing import Any, TypedDict, cast
|
|||||||
|
|
||||||
from pydantic import BaseModel, TypeAdapter
|
from pydantic import BaseModel, TypeAdapter
|
||||||
from sqlalchemy import delete, func, select, update
|
from sqlalchemy import delete, func, select, update
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
|
||||||
|
from core.db.session_factory import session_factory
|
||||||
|
|
||||||
|
|
||||||
class InvitationData(TypedDict):
|
class InvitationData(TypedDict):
|
||||||
@ -800,19 +801,19 @@ class AccountService:
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_account_by_email_with_case_fallback(email: str, session: Session | None = None) -> Account | None:
|
def get_account_by_email_with_case_fallback(email: str) -> Account | None:
|
||||||
"""
|
"""
|
||||||
Retrieve an account by email and fall back to the lowercase email if the original lookup fails.
|
Retrieve an account by email and fall back to the lowercase email if the original lookup fails.
|
||||||
|
|
||||||
This keeps backward compatibility for older records that stored uppercase emails while the
|
This keeps backward compatibility for older records that stored uppercase emails while the
|
||||||
rest of the system gradually normalizes new inputs.
|
rest of the system gradually normalizes new inputs.
|
||||||
"""
|
"""
|
||||||
query_session = session or db.session
|
with session_factory.create_session() as session:
|
||||||
account = query_session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
|
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
|
||||||
if account or email == email.lower():
|
if account or email == email.lower():
|
||||||
return account
|
return account
|
||||||
|
|
||||||
return query_session.execute(select(Account).filter_by(email=email.lower())).scalar_one_or_none()
|
return session.execute(select(Account).filter_by(email=email.lower())).scalar_one_or_none()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_email_code_login_data(cls, token: str) -> dict[str, Any] | None:
|
def get_email_code_login_data(cls, token: str) -> dict[str, Any] | None:
|
||||||
@ -1516,8 +1517,7 @@ class RegisterService:
|
|||||||
|
|
||||||
check_workspace_member_invite_permission(tenant.id)
|
check_workspace_member_invite_permission(tenant.id)
|
||||||
|
|
||||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
account = AccountService.get_account_by_email_with_case_fallback(email)
|
||||||
account = AccountService.get_account_by_email_with_case_fallback(email, session=session)
|
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
TenantService.check_member_permission(tenant, inviter, None, "add")
|
TenantService.check_member_permission(tenant, inviter, None, "add")
|
||||||
|
|||||||
@ -158,7 +158,10 @@ def test_get_account_by_email_with_case_fallback_falls_back_to_lowercase():
|
|||||||
second_result.scalar_one_or_none.return_value = expected_account
|
second_result.scalar_one_or_none.return_value = expected_account
|
||||||
mock_session.execute.side_effect = [first_result, second_result]
|
mock_session.execute.side_effect = [first_result, second_result]
|
||||||
|
|
||||||
result = AccountService.get_account_by_email_with_case_fallback("Case@Test.com", session=mock_session)
|
with patch("services.account_service.session_factory") as mock_factory:
|
||||||
|
mock_factory.create_session.return_value.__enter__ = MagicMock(return_value=mock_session)
|
||||||
|
mock_factory.create_session.return_value.__exit__ = MagicMock(return_value=False)
|
||||||
|
result = AccountService.get_account_by_email_with_case_fallback("Case@Test.com")
|
||||||
|
|
||||||
assert result is expected_account
|
assert result is expected_account
|
||||||
assert mock_session.execute.call_count == 2
|
assert mock_session.execute.call_count == 2
|
||||||
|
|||||||
@ -113,12 +113,14 @@ class TestForgotPasswordCheckApi:
|
|||||||
class TestForgotPasswordResetApi:
|
class TestForgotPasswordResetApi:
|
||||||
@patch("controllers.console.auth.forgot_password.ForgotPasswordResetApi._update_existing_account")
|
@patch("controllers.console.auth.forgot_password.ForgotPasswordResetApi._update_existing_account")
|
||||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
@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.AccountService.revoke_reset_password_token")
|
@patch("controllers.console.auth.forgot_password.AccountService.revoke_reset_password_token")
|
||||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
||||||
def test_reset_fetches_account_with_original_email(
|
def test_reset_fetches_account_with_original_email(
|
||||||
self,
|
self,
|
||||||
mock_get_reset_data,
|
mock_get_reset_data,
|
||||||
mock_revoke_token,
|
mock_revoke_token,
|
||||||
|
mock_db,
|
||||||
mock_get_account,
|
mock_get_account,
|
||||||
mock_update_account,
|
mock_update_account,
|
||||||
app,
|
app,
|
||||||
@ -126,6 +128,7 @@ class TestForgotPasswordResetApi:
|
|||||||
mock_get_reset_data.return_value = {"phase": "reset", "email": "User@Example.com"}
|
mock_get_reset_data.return_value = {"phase": "reset", "email": "User@Example.com"}
|
||||||
mock_account = MagicMock()
|
mock_account = MagicMock()
|
||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
|
mock_db.session.merge.return_value = mock_account
|
||||||
|
|
||||||
wraps_features = SimpleNamespace(enable_email_password_login=True)
|
wraps_features = SimpleNamespace(enable_email_password_login=True)
|
||||||
with (
|
with (
|
||||||
@ -161,7 +164,10 @@ def test_get_account_by_email_with_case_fallback_falls_back_to_lowercase():
|
|||||||
second_result.scalar_one_or_none.return_value = expected_account
|
second_result.scalar_one_or_none.return_value = expected_account
|
||||||
mock_session.execute.side_effect = [first_result, second_result]
|
mock_session.execute.side_effect = [first_result, second_result]
|
||||||
|
|
||||||
result = AccountService.get_account_by_email_with_case_fallback("Mixed@Test.com", session=mock_session)
|
with patch("services.account_service.session_factory") as mock_factory:
|
||||||
|
mock_factory.create_session.return_value.__enter__ = MagicMock(return_value=mock_session)
|
||||||
|
mock_factory.create_session.return_value.__exit__ = MagicMock(return_value=False)
|
||||||
|
result = AccountService.get_account_by_email_with_case_fallback("Mixed@Test.com")
|
||||||
|
|
||||||
assert result is expected_account
|
assert result is expected_account
|
||||||
assert mock_session.execute.call_count == 2
|
assert mock_session.execute.call_count == 2
|
||||||
|
|||||||
@ -437,7 +437,10 @@ class TestAccountGeneration:
|
|||||||
second_result.scalar_one_or_none.return_value = expected_account
|
second_result.scalar_one_or_none.return_value = expected_account
|
||||||
mock_session.execute.side_effect = [first_result, second_result]
|
mock_session.execute.side_effect = [first_result, second_result]
|
||||||
|
|
||||||
result = AccountService.get_account_by_email_with_case_fallback("Case@Test.com", session=mock_session)
|
with patch("services.account_service.session_factory") as mock_factory:
|
||||||
|
mock_factory.create_session.return_value.__enter__ = MagicMock(return_value=mock_session)
|
||||||
|
mock_factory.create_session.return_value.__exit__ = MagicMock(return_value=False)
|
||||||
|
result = AccountService.get_account_by_email_with_case_fallback("Case@Test.com")
|
||||||
|
|
||||||
assert result is expected_account
|
assert result is expected_account
|
||||||
assert mock_session.execute.call_count == 2
|
assert mock_session.execute.call_count == 2
|
||||||
|
|||||||
@ -335,10 +335,12 @@ class TestForgotPasswordResetApi:
|
|||||||
@patch("controllers.console.auth.forgot_password.AccountService.get_reset_password_data")
|
@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.revoke_reset_password_token")
|
||||||
@patch("controllers.console.auth.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
@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")
|
@patch("controllers.console.auth.forgot_password.TenantService.get_join_tenants")
|
||||||
def test_reset_password_success(
|
def test_reset_password_success(
|
||||||
self,
|
self,
|
||||||
mock_get_tenants,
|
mock_get_tenants,
|
||||||
|
mock_db,
|
||||||
mock_get_account,
|
mock_get_account,
|
||||||
mock_revoke_token,
|
mock_revoke_token,
|
||||||
mock_get_data,
|
mock_get_data,
|
||||||
@ -356,6 +358,7 @@ class TestForgotPasswordResetApi:
|
|||||||
# Arrange
|
# Arrange
|
||||||
mock_get_data.return_value = {"email": "test@example.com", "phase": "reset"}
|
mock_get_data.return_value = {"email": "test@example.com", "phase": "reset"}
|
||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
|
mock_db.session.merge.return_value = mock_account
|
||||||
mock_get_tenants.return_value = [MagicMock()]
|
mock_get_tenants.return_value = [MagicMock()]
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
|||||||
@ -37,10 +37,8 @@ class TestForgotPasswordSendEmailApi:
|
|||||||
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||||
@patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False)
|
@patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False)
|
||||||
@patch("controllers.web.forgot_password.extract_remote_ip", return_value="127.0.0.1")
|
@patch("controllers.web.forgot_password.extract_remote_ip", return_value="127.0.0.1")
|
||||||
@patch("controllers.web.forgot_password.sessionmaker")
|
|
||||||
def test_should_normalize_email_before_sending(
|
def test_should_normalize_email_before_sending(
|
||||||
self,
|
self,
|
||||||
mock_session_cls,
|
|
||||||
mock_extract_ip,
|
mock_extract_ip,
|
||||||
mock_rate_limit,
|
mock_rate_limit,
|
||||||
mock_get_account,
|
mock_get_account,
|
||||||
@ -50,19 +48,16 @@ class TestForgotPasswordSendEmailApi:
|
|||||||
mock_account = MagicMock()
|
mock_account = MagicMock()
|
||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
mock_send_mail.return_value = "token-123"
|
mock_send_mail.return_value = "token-123"
|
||||||
mock_session = MagicMock()
|
|
||||||
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
|
|
||||||
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
|
with app.test_request_context(
|
||||||
with app.test_request_context(
|
"/web/forgot-password",
|
||||||
"/web/forgot-password",
|
method="POST",
|
||||||
method="POST",
|
json={"email": "User@Example.com", "language": "zh-Hans"},
|
||||||
json={"email": "User@Example.com", "language": "zh-Hans"},
|
):
|
||||||
):
|
response = ForgotPasswordSendEmailApi().post()
|
||||||
response = ForgotPasswordSendEmailApi().post()
|
|
||||||
|
|
||||||
assert response == {"result": "success", "data": "token-123"}
|
assert response == {"result": "success", "data": "token-123"}
|
||||||
mock_get_account.assert_called_once_with("User@Example.com", session=mock_session)
|
mock_get_account.assert_called_once_with("User@Example.com")
|
||||||
mock_send_mail.assert_called_once_with(account=mock_account, email="user@example.com", language="zh-Hans")
|
mock_send_mail.assert_called_once_with(account=mock_account, email="user@example.com", language="zh-Hans")
|
||||||
mock_extract_ip.assert_called_once()
|
mock_extract_ip.assert_called_once()
|
||||||
mock_rate_limit.assert_called_once_with("127.0.0.1")
|
mock_rate_limit.assert_called_once_with("127.0.0.1")
|
||||||
@ -153,14 +148,14 @@ class TestForgotPasswordResetApi:
|
|||||||
|
|
||||||
@patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account")
|
@patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account")
|
||||||
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||||
@patch("controllers.web.forgot_password.sessionmaker")
|
@patch("controllers.web.forgot_password.db")
|
||||||
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
|
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
|
||||||
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
|
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
|
||||||
def test_should_fetch_account_with_fallback(
|
def test_should_fetch_account_with_fallback(
|
||||||
self,
|
self,
|
||||||
mock_get_reset_data,
|
mock_get_reset_data,
|
||||||
mock_revoke_token,
|
mock_revoke_token,
|
||||||
mock_session_cls,
|
mock_db,
|
||||||
mock_get_account,
|
mock_get_account,
|
||||||
mock_update_account,
|
mock_update_account,
|
||||||
app,
|
app,
|
||||||
@ -168,29 +163,27 @@ class TestForgotPasswordResetApi:
|
|||||||
mock_get_reset_data.return_value = {"phase": "reset", "email": "User@Example.com", "code": "1234"}
|
mock_get_reset_data.return_value = {"phase": "reset", "email": "User@Example.com", "code": "1234"}
|
||||||
mock_account = MagicMock()
|
mock_account = MagicMock()
|
||||||
mock_get_account.return_value = mock_account
|
mock_get_account.return_value = mock_account
|
||||||
mock_session = MagicMock()
|
mock_db.session.merge.return_value = mock_account
|
||||||
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
|
|
||||||
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
|
with app.test_request_context(
|
||||||
with app.test_request_context(
|
"/web/forgot-password/resets",
|
||||||
"/web/forgot-password/resets",
|
method="POST",
|
||||||
method="POST",
|
json={
|
||||||
json={
|
"token": "token-123",
|
||||||
"token": "token-123",
|
"new_password": "ValidPass123!",
|
||||||
"new_password": "ValidPass123!",
|
"password_confirm": "ValidPass123!",
|
||||||
"password_confirm": "ValidPass123!",
|
},
|
||||||
},
|
):
|
||||||
):
|
response = ForgotPasswordResetApi().post()
|
||||||
response = ForgotPasswordResetApi().post()
|
|
||||||
|
|
||||||
assert response == {"result": "success"}
|
assert response == {"result": "success"}
|
||||||
mock_get_account.assert_called_once_with("User@Example.com", session=mock_session)
|
mock_get_account.assert_called_once_with("User@Example.com")
|
||||||
mock_update_account.assert_called_once()
|
mock_update_account.assert_called_once()
|
||||||
mock_revoke_token.assert_called_once_with("token-123")
|
mock_revoke_token.assert_called_once_with("token-123")
|
||||||
|
|
||||||
@patch("controllers.web.forgot_password.hash_password", return_value=b"hashed-value")
|
@patch("controllers.web.forgot_password.hash_password", return_value=b"hashed-value")
|
||||||
@patch("controllers.web.forgot_password.secrets.token_bytes", return_value=b"0123456789abcdef")
|
@patch("controllers.web.forgot_password.secrets.token_bytes", return_value=b"0123456789abcdef")
|
||||||
@patch("controllers.web.forgot_password.sessionmaker")
|
@patch("controllers.web.forgot_password.db")
|
||||||
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
|
@patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token")
|
||||||
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
|
@patch("controllers.web.forgot_password.AccountService.get_reset_password_data")
|
||||||
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
@patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback")
|
||||||
@ -199,7 +192,7 @@ class TestForgotPasswordResetApi:
|
|||||||
mock_get_account,
|
mock_get_account,
|
||||||
mock_get_reset_data,
|
mock_get_reset_data,
|
||||||
mock_revoke_token,
|
mock_revoke_token,
|
||||||
mock_session_cls,
|
mock_db,
|
||||||
mock_token_bytes,
|
mock_token_bytes,
|
||||||
mock_hash_password,
|
mock_hash_password,
|
||||||
app,
|
app,
|
||||||
@ -207,20 +200,18 @@ class TestForgotPasswordResetApi:
|
|||||||
mock_get_reset_data.return_value = {"phase": "reset", "email": "user@example.com"}
|
mock_get_reset_data.return_value = {"phase": "reset", "email": "user@example.com"}
|
||||||
account = MagicMock()
|
account = MagicMock()
|
||||||
mock_get_account.return_value = account
|
mock_get_account.return_value = account
|
||||||
mock_session = MagicMock()
|
mock_db.session.merge.return_value = account
|
||||||
mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
|
|
||||||
with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")):
|
with app.test_request_context(
|
||||||
with app.test_request_context(
|
"/web/forgot-password/resets",
|
||||||
"/web/forgot-password/resets",
|
method="POST",
|
||||||
method="POST",
|
json={
|
||||||
json={
|
"token": "reset-token",
|
||||||
"token": "reset-token",
|
"new_password": "StrongPass123!",
|
||||||
"new_password": "StrongPass123!",
|
"password_confirm": "StrongPass123!",
|
||||||
"password_confirm": "StrongPass123!",
|
},
|
||||||
},
|
):
|
||||||
):
|
response = ForgotPasswordResetApi().post()
|
||||||
response = ForgotPasswordResetApi().post()
|
|
||||||
|
|
||||||
assert response == {"result": "success"}
|
assert response == {"result": "success"}
|
||||||
mock_get_reset_data.assert_called_once_with("reset-token")
|
mock_get_reset_data.assert_called_once_with("reset-token")
|
||||||
|
|||||||
@ -233,11 +233,10 @@ class TestWebAppAuthService:
|
|||||||
assert result.status == AccountStatus.ACTIVE
|
assert result.status == AccountStatus.ACTIVE
|
||||||
|
|
||||||
# Verify database state
|
# Verify database state
|
||||||
|
refreshed = db_session_with_containers.get(Account, result.id)
|
||||||
db_session_with_containers.refresh(result)
|
assert refreshed is not None
|
||||||
assert result.id is not None
|
assert refreshed.password is not None
|
||||||
assert result.password is not None
|
assert refreshed.password_salt is not None
|
||||||
assert result.password_salt is not None
|
|
||||||
|
|
||||||
def test_authenticate_account_not_found(
|
def test_authenticate_account_not_found(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
@ -414,9 +413,8 @@ class TestWebAppAuthService:
|
|||||||
assert result.status == AccountStatus.ACTIVE
|
assert result.status == AccountStatus.ACTIVE
|
||||||
|
|
||||||
# Verify database state
|
# Verify database state
|
||||||
|
refreshed = db_session_with_containers.get(Account, result.id)
|
||||||
db_session_with_containers.refresh(result)
|
assert refreshed is not None
|
||||||
assert result.id is not None
|
|
||||||
|
|
||||||
def test_get_user_through_email_not_found(
|
def test_get_user_through_email_not_found(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
|
|||||||
@ -233,15 +233,20 @@ class TestCheckEmailUnique:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_account_by_email_with_case_fallback_uses_lowercase_lookup():
|
def test_get_account_by_email_with_case_fallback_uses_lowercase_lookup():
|
||||||
session = MagicMock()
|
mock_session = MagicMock()
|
||||||
first = MagicMock()
|
first = MagicMock()
|
||||||
first.scalar_one_or_none.return_value = None
|
first.scalar_one_or_none.return_value = None
|
||||||
second = MagicMock()
|
second = MagicMock()
|
||||||
expected_account = MagicMock()
|
expected_account = MagicMock()
|
||||||
second.scalar_one_or_none.return_value = expected_account
|
second.scalar_one_or_none.return_value = expected_account
|
||||||
session.execute.side_effect = [first, second]
|
mock_session.execute.side_effect = [first, second]
|
||||||
|
|
||||||
result = AccountService.get_account_by_email_with_case_fallback("Mixed@Test.com", session=session)
|
mock_factory = MagicMock()
|
||||||
|
mock_factory.create_session.return_value.__enter__ = MagicMock(return_value=mock_session)
|
||||||
|
mock_factory.create_session.return_value.__exit__ = MagicMock(return_value=False)
|
||||||
|
|
||||||
|
with patch("services.account_service.session_factory", mock_factory):
|
||||||
|
result = AccountService.get_account_by_email_with_case_fallback("Mixed@Test.com")
|
||||||
|
|
||||||
assert result is expected_account
|
assert result is expected_account
|
||||||
assert session.execute.call_count == 2
|
assert mock_session.execute.call_count == 2
|
||||||
|
|||||||
@ -1427,16 +1427,7 @@ class TestRegisterService:
|
|||||||
mock_tenant.name = "Test Workspace"
|
mock_tenant.name = "Test Workspace"
|
||||||
mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter")
|
mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter")
|
||||||
|
|
||||||
# Mock database queries - need to mock the sessionmaker query
|
|
||||||
mock_session = MagicMock()
|
|
||||||
mock_session.query.return_value.filter_by.return_value.first.return_value = None # No existing account
|
|
||||||
|
|
||||||
mock_sessionmaker = MagicMock()
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None
|
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.account_service.sessionmaker", mock_sessionmaker),
|
|
||||||
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
||||||
):
|
):
|
||||||
mock_lookup.return_value = None
|
mock_lookup.return_value = None
|
||||||
@ -1475,7 +1466,7 @@ class TestRegisterService:
|
|||||||
status=AccountStatus.PENDING,
|
status=AccountStatus.PENDING,
|
||||||
is_setup=True,
|
is_setup=True,
|
||||||
)
|
)
|
||||||
mock_lookup.assert_called_once_with("newuser@example.com", session=mock_session)
|
mock_lookup.assert_called_once_with("newuser@example.com")
|
||||||
|
|
||||||
def test_invite_new_member_normalizes_new_account_email(
|
def test_invite_new_member_normalizes_new_account_email(
|
||||||
self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies
|
self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies
|
||||||
@ -1486,13 +1477,7 @@ class TestRegisterService:
|
|||||||
mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter")
|
mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter")
|
||||||
mixed_email = "Invitee@Example.com"
|
mixed_email = "Invitee@Example.com"
|
||||||
|
|
||||||
mock_session = MagicMock()
|
|
||||||
mock_sessionmaker = MagicMock()
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None
|
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.account_service.sessionmaker", mock_sessionmaker),
|
|
||||||
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
||||||
):
|
):
|
||||||
mock_lookup.return_value = None
|
mock_lookup.return_value = None
|
||||||
@ -1525,7 +1510,7 @@ class TestRegisterService:
|
|||||||
status=AccountStatus.PENDING,
|
status=AccountStatus.PENDING,
|
||||||
is_setup=True,
|
is_setup=True,
|
||||||
)
|
)
|
||||||
mock_lookup.assert_called_once_with(mixed_email, session=mock_session)
|
mock_lookup.assert_called_once_with(mixed_email)
|
||||||
mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, None, "add")
|
mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, None, "add")
|
||||||
mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal")
|
mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal")
|
||||||
mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id)
|
mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id)
|
||||||
@ -1545,16 +1530,7 @@ class TestRegisterService:
|
|||||||
account_id="existing-user-456", email="existing@example.com", status="pending"
|
account_id="existing-user-456", email="existing@example.com", status="pending"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mock database queries - need to mock the sessionmaker query
|
|
||||||
mock_session = MagicMock()
|
|
||||||
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing_account
|
|
||||||
|
|
||||||
mock_sessionmaker = MagicMock()
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session
|
|
||||||
mock_sessionmaker.return_value.begin.return_value.__exit__.return_value = None
|
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.account_service.sessionmaker", mock_sessionmaker),
|
|
||||||
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup,
|
||||||
):
|
):
|
||||||
mock_lookup.return_value = mock_existing_account
|
mock_lookup.return_value = mock_existing_account
|
||||||
@ -1584,7 +1560,7 @@ class TestRegisterService:
|
|||||||
mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal")
|
mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal")
|
||||||
mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account)
|
mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account)
|
||||||
mock_task_dependencies.delay.assert_called_once()
|
mock_task_dependencies.delay.assert_called_once()
|
||||||
mock_lookup.assert_called_once_with("existing@example.com", session=mock_session)
|
mock_lookup.assert_called_once_with("existing@example.com")
|
||||||
|
|
||||||
def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies):
|
def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies):
|
||||||
"""Test inviting a member who is already in the tenant."""
|
"""Test inviting a member who is already in the tenant."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user