mirror of
https://github.com/langgenius/dify.git
synced 2026-09-01 15:09:21 +08:00
test: centralize app and account config overrides (#40860)
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import builtins
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from importlib import util
|
||||
from pathlib import Path
|
||||
@@ -17,7 +18,6 @@ from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
from configs import dify_config
|
||||
from models.model import App, AppMode, IconType
|
||||
from models.workflow import Workflow, WorkflowType
|
||||
|
||||
@@ -594,7 +594,10 @@ def test_app_list_uses_injected_session_for_draft_workflows(
|
||||
assert response["data"][0]["permission_keys"] == ["app.acl.edit"]
|
||||
|
||||
|
||||
def test_app_create_api_attaches_permission_keys(app, app_module, unbound_session: Session):
|
||||
def test_app_create_api_attaches_permission_keys(
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppListApi.post
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -611,7 +614,6 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
|
||||
|
||||
with app.test_request_context("/apps", method="POST", json={}):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
app_module.console_ns.payload = {
|
||||
"name": "Created App",
|
||||
"description": "Summary",
|
||||
@@ -658,7 +660,10 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
|
||||
initialize_rbac_task.delay.assert_called_once_with("tenant-1", "acct-1", app_id="app-new")
|
||||
|
||||
|
||||
def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session: Session):
|
||||
def test_app_list_api_attaches_permission_keys(
|
||||
app, app_module, sqlite_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -678,7 +683,6 @@ def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session:
|
||||
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.AppService,
|
||||
"get_paginate_apps",
|
||||
@@ -719,7 +723,13 @@ def test_app_list_api_attaches_permission_keys(app, app_module, sqlite_session:
|
||||
assert resp["data"][0]["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
|
||||
|
||||
def test_recent_app_list_api_returns_only_home_card_fields(app, app_module, unbound_session: Session):
|
||||
def test_recent_app_list_api_returns_only_home_card_fields(
|
||||
app,
|
||||
app_module,
|
||||
unbound_session: Session,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=False)
|
||||
method = app_module.RecentAppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -739,7 +749,6 @@ def test_recent_app_list_api_returns_only_home_card_fields(app, app_module, unbo
|
||||
|
||||
with app.test_request_context("/apps/recent?limit=8"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", False)
|
||||
monkeypatch.setattr(app_module.AppService, "get_recent_apps", get_recent_apps)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
@@ -797,7 +806,10 @@ def test_recent_app_response_rejects_non_home_app_modes(app_module, mode: str) -
|
||||
)
|
||||
|
||||
|
||||
def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unbound_session: Session):
|
||||
def test_recent_app_list_api_applies_rbac_visibility_filter(
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.RecentAppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -805,7 +817,6 @@ def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unb
|
||||
get_recent_apps = MagicMock(return_value=[])
|
||||
with app.test_request_context("/apps/recent"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(app_module.AppService, "get_recent_apps", get_recent_apps)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
@@ -835,8 +846,9 @@ def test_recent_app_list_api_applies_rbac_visibility_filter(app, app_module, unb
|
||||
|
||||
|
||||
def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permission(
|
||||
app, app_module, unbound_session: Session
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -847,7 +859,6 @@ def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permis
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
@@ -879,8 +890,9 @@ def test_app_list_api_limits_to_apps_created_by_current_user_without_view_permis
|
||||
|
||||
|
||||
def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
|
||||
app, app_module, unbound_session: Session
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -891,7 +903,6 @@ def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
@@ -936,8 +947,9 @@ def test_app_list_api_limits_to_preview_overrides_without_manage_own_permission(
|
||||
|
||||
|
||||
def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permission(
|
||||
app, app_module, unbound_session: Session
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppListApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -948,7 +960,6 @@ def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permiss
|
||||
with app.test_request_context("/apps"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(app_module.AppService, "get_paginate_apps", get_paginate_apps)
|
||||
monkeypatch.setattr(app_module.dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module.enterprise_rbac_service.RBACService.MyPermissions,
|
||||
"get",
|
||||
@@ -973,7 +984,10 @@ def test_app_list_api_returns_no_apps_without_workspace_or_resource_view_permiss
|
||||
assert params.is_created_by_me is None
|
||||
|
||||
|
||||
def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, unbound_session: Session):
|
||||
def test_app_detail_api_attaches_current_user_permission_keys(
|
||||
app, app_module, unbound_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppApi.get
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -990,7 +1004,6 @@ def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, u
|
||||
|
||||
with app.test_request_context("/apps/app-1"):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
get_app = MagicMock(return_value=app_obj)
|
||||
monkeypatch.setattr(app_module, "AppService", lambda: SimpleNamespace(get_app=get_app))
|
||||
monkeypatch.setattr(
|
||||
@@ -1039,7 +1052,14 @@ def test_app_detail_api_attaches_current_user_permission_keys(app, app_module, u
|
||||
]
|
||||
|
||||
|
||||
def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session: Session, sqlite_engine: Engine):
|
||||
def test_app_copy_api_attaches_permission_keys(
|
||||
app,
|
||||
app_module,
|
||||
sqlite_session: Session,
|
||||
sqlite_engine: Engine,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
method = app_module.AppCopyApi.post
|
||||
while hasattr(method, "__wrapped__"):
|
||||
method = method.__wrapped__
|
||||
@@ -1063,7 +1083,6 @@ def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session:
|
||||
|
||||
with app.test_request_context("/apps/app-original/copy", method="POST", json={}):
|
||||
with pytest.MonkeyPatch.context() as monkeypatch:
|
||||
monkeypatch.setattr(dify_config, "RBAC_ENABLED", True)
|
||||
monkeypatch.setattr(
|
||||
app_module,
|
||||
"AppDslService",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -24,6 +25,11 @@ from services.errors.account import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _cloud_edition(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
|
||||
|
||||
class TestEmailRegisterSendEmailApi:
|
||||
@patch("controllers.console.auth.email_register.AccountService.get_account_by_email_with_case_fallback")
|
||||
@patch("controllers.console.auth.email_register.AccountService.send_email_register_email")
|
||||
@@ -50,8 +56,6 @@ class TestEmailRegisterSendEmailApi:
|
||||
is_allow_register=True,
|
||||
)
|
||||
with (
|
||||
patch("controllers.console.auth.email_register.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
@@ -138,7 +142,6 @@ class TestEmailRegisterCheckApi:
|
||||
is_allow_register=True,
|
||||
)
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
@@ -212,7 +215,6 @@ class TestEmailRegisterResetApi:
|
||||
is_allow_register=True,
|
||||
)
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
@@ -265,7 +267,6 @@ class TestEmailRegisterResetApi:
|
||||
is_allow_register=True,
|
||||
)
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
@@ -323,7 +324,6 @@ class TestEmailRegisterResetApi:
|
||||
is_allow_register=True,
|
||||
)
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=feature_flags),
|
||||
):
|
||||
with app.test_request_context(
|
||||
|
||||
@@ -9,6 +9,7 @@ This module tests the email code login mechanism including:
|
||||
"""
|
||||
|
||||
import base64
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -56,6 +57,11 @@ from services.turnstile_service import TurnstileChallengeRejectedError, Turnstil
|
||||
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _default_edition(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||
|
||||
|
||||
def encode_code(code: str) -> str:
|
||||
"""Helper to encode verification code as Base64 for testing."""
|
||||
return base64.b64encode(code.encode("utf-8")).decode()
|
||||
@@ -197,7 +203,14 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
|
||||
@patch("controllers.console.auth.login.TurnstileService.verify")
|
||||
def test_send_email_code_ip_rate_limited(self, mock_verify, mock_is_ip_limit, mock_db, app: Flask):
|
||||
def test_send_email_code_ip_rate_limited(
|
||||
self,
|
||||
mock_verify,
|
||||
mock_is_ip_limit,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
"""
|
||||
Test email code sending blocked by IP rate limit.
|
||||
|
||||
@@ -205,12 +218,11 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
- EmailSendIpLimitError is raised when IP limit exceeded
|
||||
- Prevents spam and abuse
|
||||
"""
|
||||
# Arrange
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
mock_is_ip_limit.return_value = True
|
||||
|
||||
# Act & Assert
|
||||
with (
|
||||
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
app.test_request_context("/email-code-login", method="POST", json={"email": "test@example.com"}),
|
||||
):
|
||||
with pytest.raises(EmailSendIpLimitError):
|
||||
@@ -232,11 +244,12 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
mock_db,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
mock_get_user.return_value = mock_account
|
||||
|
||||
with (
|
||||
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
app.test_request_context(
|
||||
"/email-code-login",
|
||||
method="POST",
|
||||
@@ -268,9 +281,10 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
app: Flask,
|
||||
service_error: Exception,
|
||||
http_error: type[Exception],
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
with (
|
||||
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.auth.login.TurnstileService.verify", side_effect=service_error),
|
||||
app.test_request_context(
|
||||
"/email-code-login",
|
||||
@@ -297,11 +311,12 @@ class TestEmailCodeLoginSendEmailApi:
|
||||
mock_db,
|
||||
app: Flask,
|
||||
mock_account,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||
mock_get_user.return_value = mock_account
|
||||
|
||||
with (
|
||||
patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
app.test_request_context("/email-code-login", method="POST", json={"email": "test@example.com"}),
|
||||
):
|
||||
response = EmailCodeLoginSendEmailApi().post()
|
||||
|
||||
@@ -10,6 +10,7 @@ This module tests the core authentication endpoints including:
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import ANY, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -47,6 +48,11 @@ from services.errors.account import (
|
||||
TEST_TOKEN = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _login_config(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||
|
||||
|
||||
def encode_password(password: str) -> str:
|
||||
"""Helper to encode password as Base64 for testing."""
|
||||
return base64.b64encode(password.encode("utf-8")).decode()
|
||||
@@ -98,7 +104,6 @@ class TestLoginApi:
|
||||
return token_pair
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
@@ -149,7 +154,6 @@ class TestLoginApi:
|
||||
assert response.json["result"] == "success"
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
@@ -202,7 +206,6 @@ class TestLoginApi:
|
||||
assert response.json["result"] == "success"
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_login_fails_when_rate_limited(
|
||||
@@ -235,10 +238,14 @@ class TestLoginApi:
|
||||
assert warn_records[0].args[1] == LoginFailureReason.LOGIN_RATE_LIMITED
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
|
||||
def test_login_fails_when_account_frozen(
|
||||
self, mock_is_frozen, mock_db, app: Flask, caplog: pytest.LogCaptureFixture
|
||||
self,
|
||||
mock_get_freeze_type,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
"""
|
||||
Test login rejection for frozen accounts.
|
||||
@@ -248,7 +255,8 @@ class TestLoginApi:
|
||||
- AccountInFreezeError is raised for frozen accounts
|
||||
"""
|
||||
# Arrange
|
||||
mock_is_frozen.return_value = "freeze"
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
mock_get_freeze_type.return_value = "freeze"
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(
|
||||
@@ -266,9 +274,15 @@ class TestLoginApi:
|
||||
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_IN_FREEZE
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
@patch("controllers.console.auth.login.BillingService.get_email_freeze_type")
|
||||
def test_login_fails_when_email_domain_is_suspended(self, mock_get_freeze_type, mock_db, app: Flask):
|
||||
def test_login_fails_when_email_domain_is_suspended(
|
||||
self,
|
||||
mock_get_freeze_type,
|
||||
mock_db,
|
||||
app: Flask,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
mock_get_freeze_type.return_value = "email_domain_suspended"
|
||||
|
||||
with app.test_request_context(
|
||||
@@ -376,7 +390,6 @@ class TestLoginApi:
|
||||
ResetPasswordSendEmailApi().post()
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
@@ -423,7 +436,6 @@ class TestLoginApi:
|
||||
assert warn_records[0].args[1] == LoginFailureReason.INVALID_CREDENTIALS
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
@@ -460,7 +472,6 @@ class TestLoginApi:
|
||||
assert warn_records[0].args[1] == LoginFailureReason.ACCOUNT_BANNED
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
@@ -504,7 +515,6 @@ class TestLoginApi:
|
||||
login_api.post()
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
def test_login_invitation_email_mismatch(self, mock_get_invitation, mock_is_rate_limit, mock_db, app: Flask):
|
||||
@@ -534,7 +544,6 @@ class TestLoginApi:
|
||||
login_api.post()
|
||||
|
||||
@patch("controllers.console.wraps.db")
|
||||
@patch("controllers.console.auth.login.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_with_case_fallback")
|
||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
from typing import override
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -52,6 +53,15 @@ def reset_setup_required_cache():
|
||||
_is_setup_completed.reset_success()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _wraps_config(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(
|
||||
RBAC_ENABLED=True,
|
||||
DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY,
|
||||
INIT_PASSWORD="",
|
||||
)
|
||||
|
||||
|
||||
class MockUser(UserMixin):
|
||||
"""Simple User class for testing."""
|
||||
|
||||
@@ -399,7 +409,6 @@ class TestRbacPermissionRequired:
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-1")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="app-123") as mock_extract,
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
|
||||
@@ -427,7 +436,6 @@ class TestRbacPermissionRequired:
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-2")),
|
||||
patch("controllers.common.wraps._extract_resource_id") as mock_extract,
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=False) as mock_owned,
|
||||
@@ -455,7 +463,6 @@ class TestRbacPermissionRequired:
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-3")),
|
||||
patch("controllers.common.wraps.RBACService.CheckAccess.check", return_value=True) as mock_check,
|
||||
):
|
||||
@@ -477,7 +484,6 @@ class TestRbacPermissionRequired:
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-4")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="app-123"),
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
|
||||
@@ -496,7 +502,6 @@ class TestRbacPermissionRequired:
|
||||
return "ok"
|
||||
|
||||
with (
|
||||
patch("controllers.common.wraps.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.common.wraps.current_account_with_tenant", return_value=(current_user, "tenant-5")),
|
||||
patch("controllers.common.wraps._extract_resource_id", return_value="dataset-123"),
|
||||
patch("controllers.common.wraps._is_resource_owned_by_current_user", return_value=True) as mock_owned,
|
||||
@@ -610,8 +615,7 @@ class TestRbacPermissionRequired:
|
||||
def protected_view():
|
||||
return "ok"
|
||||
|
||||
with patch("controllers.console.wraps.dify_config.RBAC_ENABLED", True):
|
||||
assert protected_view() == "ok"
|
||||
assert protected_view() == "ok"
|
||||
|
||||
|
||||
class TestModelValidationInjection:
|
||||
@@ -668,7 +672,7 @@ class TestModelValidationInjection:
|
||||
class TestEditionChecks:
|
||||
"""Test edition-specific decorators"""
|
||||
|
||||
def test_only_edition_cloud_allows_cloud_edition(self):
|
||||
def test_only_edition_cloud_allows_cloud_edition(self, config_overrides: Callable[..., None]):
|
||||
"""Test cloud edition decorator allows CLOUD edition"""
|
||||
|
||||
# Arrange
|
||||
@@ -676,9 +680,8 @@ class TestEditionChecks:
|
||||
def cloud_view():
|
||||
return "cloud_success"
|
||||
|
||||
# Act
|
||||
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD):
|
||||
result = cloud_view()
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
result = cloud_view()
|
||||
|
||||
# Assert
|
||||
assert result == "cloud_success"
|
||||
@@ -694,12 +697,11 @@ class TestEditionChecks:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context():
|
||||
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
cloud_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
cloud_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
def test_only_edition_enterprise_allows_enterprise_edition(self):
|
||||
def test_only_edition_enterprise_allows_enterprise_edition(self, config_overrides: Callable[..., None]):
|
||||
"""Test enterprise edition decorator allows the ENTERPRISE edition."""
|
||||
|
||||
# Arrange
|
||||
@@ -707,9 +709,8 @@ class TestEditionChecks:
|
||||
def enterprise_view():
|
||||
return "enterprise_success"
|
||||
|
||||
# Act
|
||||
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.ENTERPRISE):
|
||||
result = enterprise_view()
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
|
||||
result = enterprise_view()
|
||||
|
||||
# Assert
|
||||
assert result == "enterprise_success"
|
||||
@@ -722,9 +723,7 @@ class TestEditionChecks:
|
||||
def self_hosted_view():
|
||||
return "self_hosted_success"
|
||||
|
||||
# Act
|
||||
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
|
||||
result = self_hosted_view()
|
||||
result = self_hosted_view()
|
||||
|
||||
# Assert
|
||||
assert result == "self_hosted_success"
|
||||
@@ -805,8 +804,9 @@ class TestBillingResourceLimits:
|
||||
assert result == "member_added"
|
||||
get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
|
||||
|
||||
def test_should_load_vector_space_from_dedicated_quota_api(self):
|
||||
def test_should_load_vector_space_from_dedicated_quota_api(self, config_overrides: Callable[..., None]):
|
||||
"""Test vector-space limit checks avoid loading the full feature payload."""
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
# Arrange
|
||||
mock_vector_space = MagicMock()
|
||||
mock_vector_space.limit = 10
|
||||
@@ -821,7 +821,6 @@ class TestBillingResourceLimits:
|
||||
"controllers.console.wraps.current_account_with_tenant", return_value=(MockUser("test_user"), "tenant123")
|
||||
):
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch(
|
||||
"controllers.console.wraps.FeatureService.get_vector_space", return_value=mock_vector_space
|
||||
) as get_vector_space,
|
||||
@@ -984,8 +983,9 @@ class TestRateLimiting:
|
||||
class TestCloudUtmRecord:
|
||||
"""Test cloud UTM recording decorator."""
|
||||
|
||||
def test_should_record_utm_for_cloud_edition_and_cookie(self):
|
||||
def test_should_record_utm_for_cloud_edition_and_cookie(self, config_overrides: Callable[..., None]):
|
||||
"""Test Cloud UTM recording without loading tenant features."""
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
app = create_app_with_login()
|
||||
|
||||
@cloud_utm_record
|
||||
@@ -994,7 +994,6 @@ class TestCloudUtmRecord:
|
||||
|
||||
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.CLOUD),
|
||||
patch("controllers.console.wraps.current_account_with_tenant", return_value=(MockUser("u1"), "t1")),
|
||||
patch("controllers.console.wraps.OperationService.record_utm") as record_utm,
|
||||
patch("controllers.console.wraps.FeatureService.get_features") as get_features,
|
||||
@@ -1015,7 +1014,6 @@ class TestCloudUtmRecord:
|
||||
|
||||
with app.test_request_context("/", headers={"Cookie": "utm_info={}"}):
|
||||
with (
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
patch("controllers.console.wraps.current_account_with_tenant") as current_account,
|
||||
patch("controllers.console.wraps.OperationService.record_utm") as record_utm,
|
||||
patch("controllers.console.wraps.FeatureService.get_features") as get_features,
|
||||
@@ -1047,10 +1045,7 @@ class TestSystemSetup:
|
||||
return "admin_success"
|
||||
|
||||
# Act
|
||||
with (
|
||||
patch("controllers.console.wraps.db.session", sqlite_session),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
):
|
||||
with patch("controllers.console.wraps.db.session", sqlite_session):
|
||||
result = admin_view()
|
||||
|
||||
# Assert
|
||||
@@ -1064,10 +1059,7 @@ class TestSystemSetup:
|
||||
def admin_view():
|
||||
return "admin_success"
|
||||
|
||||
with (
|
||||
patch("controllers.console.wraps.db.session", sqlite_session),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
):
|
||||
with patch("controllers.console.wraps.db.session", sqlite_session):
|
||||
assert admin_view() == "admin_success"
|
||||
sqlite_session.delete(setup)
|
||||
sqlite_session.commit()
|
||||
@@ -1084,27 +1076,23 @@ class TestSystemSetup:
|
||||
|
||||
with (
|
||||
patch("controllers.console.wraps.db.session", sqlite_session),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
|
||||
):
|
||||
with pytest.raises(NotSetupError):
|
||||
admin_view()
|
||||
self._complete_setup(sqlite_session)
|
||||
assert admin_view() == "admin_success"
|
||||
|
||||
def test_should_raise_not_init_validate_error_with_init_password(self, sqlite_session: Session):
|
||||
def test_should_raise_not_init_validate_error_with_init_password(
|
||||
self, sqlite_session: Session, config_overrides: Callable[..., None]
|
||||
):
|
||||
"""Test NotInitValidateError when INIT_PASSWORD is set but setup not complete"""
|
||||
|
||||
@setup_required
|
||||
def admin_view():
|
||||
return "admin_success"
|
||||
|
||||
# Act & Assert
|
||||
with (
|
||||
patch("controllers.console.wraps.db.session", sqlite_session),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", "some_password"),
|
||||
):
|
||||
config_overrides(INIT_PASSWORD="some_password")
|
||||
with patch("controllers.console.wraps.db.session", sqlite_session):
|
||||
with pytest.raises(NotInitValidateError):
|
||||
admin_view()
|
||||
|
||||
@@ -1116,11 +1104,7 @@ class TestSystemSetup:
|
||||
return "admin_success"
|
||||
|
||||
# Act & Assert
|
||||
with (
|
||||
patch("controllers.console.wraps.db.session", sqlite_session),
|
||||
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
|
||||
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
|
||||
):
|
||||
with patch("controllers.console.wraps.db.session", sqlite_session):
|
||||
with pytest.raises(NotSetupError):
|
||||
admin_view()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -24,6 +25,11 @@ from models.model import App
|
||||
from services.enterprise.enterprise_service import WebAppAccessMode
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _rbac_config(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
|
||||
|
||||
def _data(**kwargs) -> AuthData:
|
||||
defaults: dict = {"token_type": TokenType.OAUTH_ACCOUNT, "token_hash": "hash", "scopes": frozenset({Scope.FULL})}
|
||||
defaults.update(kwargs)
|
||||
@@ -88,18 +94,15 @@ def test_check_rbac_noop_when_no_requirement():
|
||||
mock_enforce.assert_not_called()
|
||||
|
||||
|
||||
def test_check_rbac_noop_when_rbac_disabled():
|
||||
with (
|
||||
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", False),
|
||||
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
|
||||
):
|
||||
def test_check_rbac_noop_when_rbac_disabled(config_overrides: Callable[..., None]):
|
||||
config_overrides(RBAC_ENABLED=False)
|
||||
with patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce:
|
||||
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account"))
|
||||
mock_enforce.assert_not_called()
|
||||
|
||||
|
||||
def test_check_rbac_skips_end_user_caller():
|
||||
with (
|
||||
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
|
||||
):
|
||||
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="end_user"))
|
||||
@@ -107,9 +110,8 @@ def test_check_rbac_skips_end_user_caller():
|
||||
|
||||
|
||||
def test_check_rbac_raises_when_context_missing():
|
||||
with patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True):
|
||||
with pytest.raises(Forbidden, match="rbac context missing"):
|
||||
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account", account_id=None, tenant=None))
|
||||
with pytest.raises(Forbidden, match="rbac context missing"):
|
||||
check_rbac_permission(_data(rbac=_RBAC_REQ, caller_kind="account", account_id=None, tenant=None))
|
||||
|
||||
|
||||
def test_check_rbac_enforces_for_account_caller():
|
||||
@@ -124,7 +126,6 @@ def test_check_rbac_enforces_for_account_caller():
|
||||
path_params={"app_id": "app-1"},
|
||||
)
|
||||
with (
|
||||
patch("controllers.openapi.auth.verify.dify_config.RBAC_ENABLED", True),
|
||||
patch("controllers.openapi.auth.verify.enforce_rbac_access") as mock_enforce,
|
||||
):
|
||||
check_rbac_permission(data)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Callable, Iterator
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -140,10 +140,11 @@ def test_resolve_file_url_requires_extension_for_tool_files() -> None:
|
||||
|
||||
def test_resolve_file_url_uses_tool_signatures_for_tool_and_datasource_files(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
config_overrides: Callable[..., None],
|
||||
) -> None:
|
||||
sign_tool_file_uri = MagicMock(return_value="/files/signed")
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file_uri", sign_tool_file_uri)
|
||||
monkeypatch.setattr(file_runtime.dify_config, "FILES_URL", "https://files.example.com")
|
||||
config_overrides(FILES_URL="https://files.example.com")
|
||||
runtime = _build_runtime()
|
||||
|
||||
tool_file = _build_file(
|
||||
@@ -175,9 +176,11 @@ def test_resolve_file_uri_keeps_dify_owned_file_origin_free(monkeypatch: pytest.
|
||||
assert runtime.resolve_file_uri(file=file) == "/files/tools/tool-file-id.png?sign=1"
|
||||
|
||||
|
||||
def test_resolve_file_url_returns_relative_uri_when_files_url_is_empty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_resolve_file_url_returns_relative_uri_when_files_url_is_empty(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file_uri", lambda **_: "/files/tools/tool-file-id.png?sign=1")
|
||||
monkeypatch.setattr(file_runtime.dify_config, "FILES_URL", "")
|
||||
config_overrides(FILES_URL="")
|
||||
runtime = _build_runtime()
|
||||
file = _build_file(
|
||||
transfer_method=FileTransferMethod.TOOL_FILE,
|
||||
@@ -190,15 +193,15 @@ def test_resolve_file_url_returns_relative_uri_when_files_url_is_empty(monkeypat
|
||||
|
||||
def test_resolve_upload_file_url_signs_internal_urls_and_supports_attachments(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
config_overrides: Callable[..., None],
|
||||
) -> None:
|
||||
config_overrides(
|
||||
SECRET_KEY="unit-secret",
|
||||
FILES_URL="https://files.example.com",
|
||||
INTERNAL_FILES_URL="https://internal.example.com",
|
||||
)
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.os.urandom", lambda _: b"\x01" * 16)
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr(
|
||||
"core.app.workflow.file_runtime.dify_config.INTERNAL_FILES_URL",
|
||||
"https://internal.example.com",
|
||||
)
|
||||
|
||||
runtime = _build_runtime()
|
||||
url = runtime.resolve_upload_file_url(
|
||||
@@ -215,10 +218,11 @@ def test_resolve_upload_file_url_signs_internal_urls_and_supports_attachments(
|
||||
assert query["timestamp"] == ["1700000000"]
|
||||
|
||||
|
||||
def test_verify_preview_signature_validates_signature_and_expiration(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_verify_preview_signature_validates_signature_and_expiration(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(SECRET_KEY="unit-secret", FILES_ACCESS_TIMEOUT=60)
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.dify_config.FILES_ACCESS_TIMEOUT", 60)
|
||||
runtime = _build_runtime()
|
||||
payload = "file-preview|upload-file-id|1700000000|nonce"
|
||||
sign = base64.urlsafe_b64encode(hmac.new(b"unit-secret", payload.encode(), hashlib.sha256).digest()).decode()
|
||||
@@ -372,8 +376,10 @@ def test_resolve_storage_key_raises_when_records_are_missing(
|
||||
runtime._resolve_storage_key(file=file)
|
||||
|
||||
|
||||
def test_runtime_helper_wrappers_delegate_to_config_and_io(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.app.workflow.file_runtime.dify_config.MULTIMODAL_SEND_FORMAT", "url")
|
||||
def test_runtime_helper_wrappers_delegate_to_config_and_io(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(MULTIMODAL_SEND_FORMAT="url")
|
||||
runtime = _build_runtime()
|
||||
|
||||
assert runtime.multimodal_send_format == "url"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -39,6 +40,22 @@ class DummyDocumentExtractorNode(DummyNode):
|
||||
|
||||
|
||||
class TestDifyNodeFactory:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _node_config(self, config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(
|
||||
CODE_MAX_STRING_LENGTH=10,
|
||||
CODE_MAX_NUMBER=10,
|
||||
CODE_MIN_NUMBER=-10,
|
||||
CODE_MAX_PRECISION=4,
|
||||
CODE_MAX_DEPTH=2,
|
||||
CODE_MAX_NUMBER_ARRAY_LENGTH=2,
|
||||
CODE_MAX_STRING_ARRAY_LENGTH=2,
|
||||
CODE_MAX_OBJECT_ARRAY_LENGTH=2,
|
||||
TEMPLATE_TRANSFORM_MAX_LENGTH=100,
|
||||
UNSTRUCTURED_API_URL="http://u",
|
||||
UNSTRUCTURED_API_KEY="key",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _stub_node_resolution(monkeypatch: pytest.MonkeyPatch, node_class):
|
||||
monkeypatch.setattr(
|
||||
@@ -46,19 +63,7 @@ class TestDifyNodeFactory:
|
||||
lambda **_kwargs: node_class,
|
||||
)
|
||||
|
||||
def _factory(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_STRING_LENGTH", 10)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_NUMBER", 10)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MIN_NUMBER", -10)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_PRECISION", 4)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_DEPTH", 2)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH", 2)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_STRING_ARRAY_LENGTH", 2)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH", 2)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH", 100)
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.UNSTRUCTURED_API_URL", "http://u")
|
||||
monkeypatch.setattr("core.workflow.node_factory.dify_config.UNSTRUCTURED_API_KEY", "key")
|
||||
|
||||
def _factory(self):
|
||||
run_context = build_dify_run_context(
|
||||
tenant_id="tenant",
|
||||
app_id="app",
|
||||
@@ -72,21 +77,21 @@ class TestDifyNodeFactory:
|
||||
graph_runtime_state=SimpleNamespace(),
|
||||
)
|
||||
|
||||
def test_create_node_unknown_type(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
def test_create_node_unknown_type(self):
|
||||
factory = self._factory()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
factory.create_node({"id": "node-1", "data": {"type": "unknown"}})
|
||||
|
||||
def test_create_node_missing_mapping(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
monkeypatch.setattr("core.workflow.node_factory.get_node_type_classes_mapping", lambda: {})
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.START}})
|
||||
|
||||
def test_create_node_missing_latest_class(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.node_factory.get_node_type_classes_mapping",
|
||||
lambda: {BuiltinNodeTypes.START: {"1": None}},
|
||||
@@ -97,7 +102,7 @@ class TestDifyNodeFactory:
|
||||
factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.START}})
|
||||
|
||||
def test_create_node_selects_versioned_class(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
selected_versions: list[tuple[str, str]] = []
|
||||
|
||||
class DummyNodeV2(DummyNode):
|
||||
@@ -116,7 +121,7 @@ class TestDifyNodeFactory:
|
||||
assert selected_versions == [("snapshot", "called")]
|
||||
|
||||
def test_create_node_code_branch(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
self._stub_node_resolution(monkeypatch, DummyCodeNode)
|
||||
|
||||
node = factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.CODE}})
|
||||
@@ -125,7 +130,7 @@ class TestDifyNodeFactory:
|
||||
assert node.id == "node-1"
|
||||
|
||||
def test_create_node_template_transform_branch(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
self._stub_node_resolution(monkeypatch, DummyTemplateTransformNode)
|
||||
|
||||
node = factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.TEMPLATE_TRANSFORM}})
|
||||
@@ -134,7 +139,7 @@ class TestDifyNodeFactory:
|
||||
assert "jinja2_template_renderer" in node.kwargs
|
||||
|
||||
def test_create_node_http_request_branch(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
self._stub_node_resolution(monkeypatch, DummyHttpRequestNode)
|
||||
|
||||
node = factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.HTTP_REQUEST}})
|
||||
@@ -143,7 +148,7 @@ class TestDifyNodeFactory:
|
||||
assert "http_request_config" in node.kwargs
|
||||
|
||||
def test_create_node_knowledge_retrieval_branch(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
self._stub_node_resolution(monkeypatch, DummyKnowledgeRetrievalNode)
|
||||
|
||||
node = factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL}})
|
||||
@@ -152,7 +157,7 @@ class TestDifyNodeFactory:
|
||||
assert node.kwargs == {}
|
||||
|
||||
def test_create_node_document_extractor_branch(self, monkeypatch: pytest.MonkeyPatch):
|
||||
factory = self._factory(monkeypatch)
|
||||
factory = self._factory()
|
||||
self._stub_node_resolution(monkeypatch, DummyDocumentExtractorNode)
|
||||
|
||||
node = factory.create_node({"id": "node-1", "data": {"type": BuiltinNodeTypes.DOCUMENT_EXTRACTOR}})
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import urllib.parse
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock
|
||||
@@ -120,11 +120,14 @@ def _signed_url(*, base_url: str, path: str, payload: str, secret: str = "test-s
|
||||
return f"{base_url}{path}?{query}"
|
||||
|
||||
|
||||
def _patch_file_fetcher_config(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "FILES_URL", "http://localhost:5001")
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "INTERNAL_FILES_URL", "http://api:5001")
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "SECRET_KEY", "test-secret")
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "FILES_ACCESS_TIMEOUT", 3600)
|
||||
@pytest.fixture(autouse=True)
|
||||
def _file_fetcher_config(monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(
|
||||
FILES_URL="http://localhost:5001",
|
||||
INTERNAL_FILES_URL="http://api:5001",
|
||||
SECRET_KEY="test-secret",
|
||||
FILES_ACCESS_TIMEOUT=3600,
|
||||
)
|
||||
monkeypatch.setattr(remote_fetcher.time, "time", lambda: 1700000100)
|
||||
|
||||
|
||||
@@ -141,7 +144,6 @@ def _patch_signer_times(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
|
||||
def test_get_signed_upload_file_url_reads_storage_without_ssrf(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"hello")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -165,7 +167,6 @@ def test_get_signed_upload_file_url_reads_storage_without_ssrf(monkeypatch: pyte
|
||||
def test_make_request_resolves_upload_preview_url_generated_by_signer(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
_patch_signer_times(monkeypatch)
|
||||
file_database.upload_file.key = "upload_files/tenant/image.png"
|
||||
file_database.upload_file.name = "image.png"
|
||||
@@ -191,7 +192,6 @@ def test_make_request_resolves_upload_preview_url_generated_by_signer(
|
||||
def test_make_request_resolves_sign_tool_file_url_with_empty_extension(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
_patch_signer_times(monkeypatch)
|
||||
file_database.tool_file.file_key = "tools/tenant/no-extension"
|
||||
file_database.tool_file.name = "no-extension"
|
||||
@@ -216,7 +216,6 @@ def test_make_request_resolves_sign_tool_file_url_with_empty_extension(
|
||||
def test_make_request_resolves_tool_manager_url_with_empty_extension(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
_patch_signer_times(monkeypatch)
|
||||
file_database.tool_file.file_key = "tools/tenant/manager-file"
|
||||
file_database.tool_file.name = "manager-file"
|
||||
@@ -240,7 +239,6 @@ def test_make_request_resolves_tool_manager_url_with_empty_extension(
|
||||
def test_make_request_resolves_datasource_manager_url_with_empty_extension(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
_patch_signer_times(monkeypatch)
|
||||
file_database.datasource_upload_file.key = "datasources/tenant/no-extension"
|
||||
file_database.datasource_upload_file.name = "no-extension"
|
||||
@@ -264,7 +262,6 @@ def test_make_request_resolves_datasource_manager_url_with_empty_extension(
|
||||
|
||||
|
||||
def test_head_signed_upload_file_url_returns_metadata_without_storage_content(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"hello")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -286,7 +283,6 @@ def test_head_signed_upload_file_url_returns_metadata_without_storage_content(mo
|
||||
|
||||
|
||||
def test_make_request_get_signed_upload_file_url_reads_storage_without_ssrf(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"hello")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -306,7 +302,6 @@ def test_make_request_get_signed_upload_file_url_reads_storage_without_ssrf(monk
|
||||
|
||||
|
||||
def test_make_request_head_signed_upload_file_url_returns_metadata_without_ssrf(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"hello")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -330,7 +325,6 @@ def test_make_request_head_signed_upload_file_url_returns_metadata_without_ssrf(
|
||||
def test_make_request_get_unsigned_dify_url_delegates_to_ssrf_proxy(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = f"http://localhost:5001/files/{UPLOAD_FILE_ID}/file-preview?timestamp=1700000000&nonce=nonce"
|
||||
proxy_response = httpx.Response(403, request=httpx.Request("GET", url))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
@@ -350,7 +344,6 @@ def test_make_request_get_unsigned_dify_url_delegates_to_ssrf_proxy(
|
||||
def test_make_request_post_signed_upload_file_url_delegates_to_ssrf_proxy(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
proxy_response = httpx.Response(201, request=httpx.Request("POST", f"http://localhost:5001/files/{UPLOAD_FILE_ID}"))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
url = _signed_url(
|
||||
@@ -374,7 +367,6 @@ def test_make_request_post_signed_upload_file_url_delegates_to_ssrf_proxy(
|
||||
def test_get_signed_image_preview_url_uses_image_preview_signature(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
file_database.upload_file.key = "upload_files/tenant/image.png"
|
||||
file_database.upload_file.name = "image.png"
|
||||
file_database.upload_file.mime_type = "image/png"
|
||||
@@ -400,7 +392,6 @@ def test_get_signed_image_preview_url_uses_image_preview_signature(
|
||||
|
||||
|
||||
def test_image_preview_url_with_file_preview_signature_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
proxy_response = httpx.Response(403, request=httpx.Request("GET", "http://localhost:5001/bad"))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
url = _signed_url(
|
||||
@@ -420,7 +411,6 @@ def test_image_preview_url_with_file_preview_signature_delegates_to_ssrf_proxy(m
|
||||
|
||||
|
||||
def test_duplicate_signature_query_value_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = (
|
||||
_signed_url(
|
||||
base_url="http://localhost:5001",
|
||||
@@ -443,7 +433,6 @@ def test_duplicate_signature_query_value_delegates_to_ssrf_proxy(monkeypatch: py
|
||||
|
||||
|
||||
def test_malformed_timestamp_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = _signed_url(
|
||||
base_url="http://localhost:5001",
|
||||
path=f"/files/{UPLOAD_FILE_ID}/file-preview",
|
||||
@@ -463,7 +452,6 @@ def test_malformed_timestamp_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyP
|
||||
|
||||
|
||||
def test_expired_signature_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
monkeypatch.setattr(remote_fetcher.time, "time", lambda: 1700004001)
|
||||
url = _signed_url(
|
||||
base_url="http://localhost:5001",
|
||||
@@ -484,7 +472,6 @@ def test_expired_signature_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPat
|
||||
|
||||
|
||||
def test_invalid_signature_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
proxy_response = httpx.Response(403, request=httpx.Request("GET", "http://localhost:5001/bad"))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
url = f"http://localhost:5001/files/{UPLOAD_FILE_ID}/file-preview?timestamp=1700000000&nonce=nonce&sign=bad"
|
||||
@@ -501,7 +488,6 @@ def test_invalid_signature_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPat
|
||||
|
||||
|
||||
def test_host_mismatch_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = _signed_url(
|
||||
base_url="http://example.com",
|
||||
path=f"/files/{UPLOAD_FILE_ID}/file-preview",
|
||||
@@ -521,7 +507,6 @@ def test_host_mismatch_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
|
||||
def test_unsupported_dify_path_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = _signed_url(
|
||||
base_url="http://localhost:5001",
|
||||
path=f"/files/{UPLOAD_FILE_ID}/not-preview",
|
||||
@@ -542,7 +527,6 @@ def test_unsupported_dify_path_delegates_to_ssrf_proxy(monkeypatch: pytest.Monke
|
||||
|
||||
|
||||
def test_invalid_url_scheme_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = f"file:///tmp/files/{UPLOAD_FILE_ID}/file-preview?timestamp=1700000000&nonce=nonce&sign=ignored"
|
||||
proxy_response = httpx.Response(403, request=httpx.Request("GET", url))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
@@ -558,7 +542,6 @@ def test_invalid_url_scheme_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPa
|
||||
|
||||
|
||||
def test_invalid_url_port_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
url = f"http://localhost:invalid/files/{UPLOAD_FILE_ID}/file-preview?timestamp=1700000000&nonce=nonce&sign=ignored"
|
||||
proxy_response = httpx.Response(403, request=httpx.Request("GET", "http://proxy.example/fallback"))
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch, proxy_response)
|
||||
@@ -573,10 +556,10 @@ def test_invalid_url_port_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatc
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_configured_file_origin_delegates_to_ssrf_proxy(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "FILES_URL", "")
|
||||
monkeypatch.setattr(remote_fetcher.dify_config, "INTERNAL_FILES_URL", "file:///tmp/files")
|
||||
def test_invalid_configured_file_origin_delegates_to_ssrf_proxy(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(FILES_URL="", INTERNAL_FILES_URL="file:///tmp/files")
|
||||
url = _signed_url(
|
||||
base_url="http://localhost:5001",
|
||||
path=f"/files/{UPLOAD_FILE_ID}/file-preview",
|
||||
@@ -598,7 +581,6 @@ def test_invalid_configured_file_origin_delegates_to_ssrf_proxy(monkeypatch: pyt
|
||||
def test_signed_upload_file_url_returns_404_when_record_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
file_database.session.delete(file_database.upload_file)
|
||||
file_database.session.commit()
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -616,7 +598,6 @@ def test_signed_upload_file_url_returns_404_when_record_missing(
|
||||
|
||||
|
||||
def test_get_signed_tool_file_url_reads_storage_without_ssrf(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"result")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -638,7 +619,6 @@ def test_get_signed_tool_file_url_reads_storage_without_ssrf(monkeypatch: pytest
|
||||
def test_signed_tool_file_url_returns_404_when_record_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
file_database.session.delete(file_database.tool_file)
|
||||
file_database.session.commit()
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -656,7 +636,6 @@ def test_signed_tool_file_url_returns_404_when_record_missing(
|
||||
|
||||
|
||||
def test_get_signed_datasource_file_url_reads_upload_storage_without_ssrf(monkeypatch: pytest.MonkeyPatch):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
load_once = MagicMock(return_value=b"data")
|
||||
monkeypatch.setattr(remote_fetcher.storage, "load_once", load_once)
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
@@ -677,7 +656,6 @@ def test_get_signed_datasource_file_url_reads_upload_storage_without_ssrf(monkey
|
||||
def test_get_signed_datasource_file_url_reads_tool_storage_when_upload_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
file_database.session.delete(file_database.datasource_upload_file)
|
||||
datasource_tool_file = ToolFile(
|
||||
user_id=USER_ID,
|
||||
@@ -713,7 +691,6 @@ def test_get_signed_datasource_file_url_reads_tool_storage_when_upload_missing(
|
||||
def test_signed_datasource_file_url_returns_404_when_records_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, file_database: FileDatabase
|
||||
):
|
||||
_patch_file_fetcher_config(monkeypatch)
|
||||
file_database.session.delete(file_database.datasource_upload_file)
|
||||
file_database.session.commit()
|
||||
ssrf_make_request = _patch_ssrf_make_request(monkeypatch)
|
||||
|
||||
@@ -11,8 +11,8 @@ This test suite covers:
|
||||
|
||||
import base64
|
||||
import secrets
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -401,6 +401,10 @@ class TestTenantRelationshipIntegrity:
|
||||
|
||||
|
||||
class TestAccountRolePermissions:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _rbac_disabled(self, config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(RBAC_ENABLED=False)
|
||||
|
||||
"""Test suite for account role permissions."""
|
||||
|
||||
def test_is_admin_or_owner_with_admin_role(self):
|
||||
@@ -412,16 +416,14 @@ class TestAccountRolePermissions:
|
||||
)
|
||||
account.role = TenantAccountRole.ADMIN
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert account.is_admin_or_owner
|
||||
assert account.is_admin_or_owner
|
||||
|
||||
def test_is_admin_or_owner_with_rbac_enabled(self):
|
||||
def test_is_admin_or_owner_with_rbac_enabled(self, config_overrides: Callable[..., None]):
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.role = TenantAccountRole.NORMAL
|
||||
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", True):
|
||||
assert account.is_admin_or_owner
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
assert account.is_admin_or_owner
|
||||
|
||||
def test_is_admin_or_owner_with_owner_role(self):
|
||||
"""Test is_admin_or_owner property with owner role."""
|
||||
@@ -456,17 +458,15 @@ class TestAccountRolePermissions:
|
||||
owner_account = Account(name="Owner", email="owner@example.com")
|
||||
owner_account.role = TenantAccountRole.OWNER
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert admin_account.is_admin
|
||||
assert not owner_account.is_admin
|
||||
assert admin_account.is_admin
|
||||
assert not owner_account.is_admin
|
||||
|
||||
def test_is_admin_with_rbac_enabled(self):
|
||||
def test_is_admin_with_rbac_enabled(self, config_overrides: Callable[..., None]):
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.role = TenantAccountRole.NORMAL
|
||||
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", True):
|
||||
assert account.is_admin
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
assert account.is_admin
|
||||
|
||||
def test_has_edit_permission_with_editing_roles(self):
|
||||
"""Test has_edit_permission property with roles that have edit permission."""
|
||||
@@ -481,16 +481,14 @@ class TestAccountRolePermissions:
|
||||
account = Account(name="Test User", email=f"test_{role}@example.com")
|
||||
account.role = role
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert account.has_edit_permission, f"Role {role} should have edit permission"
|
||||
assert account.has_edit_permission, f"Role {role} should have edit permission"
|
||||
|
||||
def test_has_edit_permission_with_rbac_enabled(self):
|
||||
def test_has_edit_permission_with_rbac_enabled(self, config_overrides: Callable[..., None]):
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.role = TenantAccountRole.NORMAL
|
||||
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", True):
|
||||
assert account.has_edit_permission
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
assert account.has_edit_permission
|
||||
|
||||
def test_has_edit_permission_without_editing_roles(self):
|
||||
"""Test has_edit_permission property with roles that don't have edit permission."""
|
||||
@@ -504,9 +502,7 @@ class TestAccountRolePermissions:
|
||||
account = Account(name="Test User", email=f"test_{role}@example.com")
|
||||
account.role = role
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert not account.has_edit_permission, f"Role {role} should not have edit permission"
|
||||
assert not account.has_edit_permission, f"Role {role} should not have edit permission"
|
||||
|
||||
def test_is_dataset_editor_property(self):
|
||||
"""Test is_dataset_editor property."""
|
||||
@@ -522,22 +518,19 @@ class TestAccountRolePermissions:
|
||||
account = Account(name="Test User", email=f"test_{role}@example.com")
|
||||
account.role = role
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert account.is_dataset_editor, f"Role {role} should have dataset edit permission"
|
||||
assert account.is_dataset_editor, f"Role {role} should have dataset edit permission"
|
||||
|
||||
# Test normal role doesn't have dataset edit permission
|
||||
normal_account = Account(name="Normal User", email="normal@example.com")
|
||||
normal_account.role = TenantAccountRole.NORMAL
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert not normal_account.is_dataset_editor
|
||||
assert not normal_account.is_dataset_editor
|
||||
|
||||
def test_is_dataset_editor_with_rbac_enabled(self):
|
||||
def test_is_dataset_editor_with_rbac_enabled(self, config_overrides: Callable[..., None]):
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.role = TenantAccountRole.NORMAL
|
||||
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", True):
|
||||
assert account.is_dataset_editor
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
assert account.is_dataset_editor
|
||||
|
||||
def test_is_dataset_operator_property(self):
|
||||
"""Test is_dataset_operator property."""
|
||||
@@ -548,17 +541,15 @@ class TestAccountRolePermissions:
|
||||
normal_account = Account(name="Normal User", email="normal@example.com")
|
||||
normal_account.role = TenantAccountRole.NORMAL
|
||||
|
||||
# Act & Assert
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", False):
|
||||
assert dataset_operator.is_dataset_operator
|
||||
assert not normal_account.is_dataset_operator
|
||||
assert dataset_operator.is_dataset_operator
|
||||
assert not normal_account.is_dataset_operator
|
||||
|
||||
def test_is_dataset_operator_with_rbac_enabled(self):
|
||||
def test_is_dataset_operator_with_rbac_enabled(self, config_overrides: Callable[..., None]):
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.role = TenantAccountRole.NORMAL
|
||||
|
||||
with patch("models.account.dify_config.RBAC_ENABLED", True):
|
||||
assert account.is_dataset_operator
|
||||
config_overrides(RBAC_ENABLED=True)
|
||||
assert account.is_dataset_operator
|
||||
|
||||
def test_current_role_property(self):
|
||||
"""Test current_role property."""
|
||||
|
||||
@@ -115,8 +115,10 @@ def _unexpected_timer(interval: float, function: Callable[[], bool]) -> _FakeTim
|
||||
|
||||
|
||||
class TestBuildStreamingTaskOnSubscribe:
|
||||
def test_streams_starts_only_when_hook_is_invoked_without_creating_timer(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "streams")
|
||||
def test_streams_starts_only_when_hook_is_invoked_without_creating_timer(
|
||||
self, monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams")
|
||||
|
||||
monkeypatch.setattr(ags_module.threading, "Timer", _unexpected_timer)
|
||||
called: list[int] = []
|
||||
@@ -133,8 +135,9 @@ class TestBuildStreamingTaskOnSubscribe:
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
channel_type: str,
|
||||
config_overrides: Callable[..., None],
|
||||
):
|
||||
monkeypatch.setattr(ags_module.dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", channel_type)
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE=channel_type)
|
||||
timers: list[_FakeTimer] = []
|
||||
|
||||
def build_timer(interval: float, function: Callable[[], bool]) -> _FakeTimer:
|
||||
@@ -157,8 +160,10 @@ class TestBuildStreamingTaskOnSubscribe:
|
||||
assert called == [1]
|
||||
assert timers[0].cancelled is True
|
||||
|
||||
def test_pubsub_fallback_starts_task_if_hook_is_never_invoked(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "pubsub")
|
||||
def test_pubsub_fallback_starts_task_if_hook_is_never_invoked(
|
||||
self, monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="pubsub")
|
||||
timers: list[_FakeTimer] = []
|
||||
|
||||
def build_timer(interval: float, function: Callable[[], bool]) -> _FakeTimer:
|
||||
@@ -174,8 +179,10 @@ class TestBuildStreamingTaskOnSubscribe:
|
||||
on_subscribe()
|
||||
assert called == [1]
|
||||
|
||||
def test_streams_retries_after_enqueue_failure(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "streams")
|
||||
def test_streams_retries_after_enqueue_failure(
|
||||
self, monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams")
|
||||
monkeypatch.setattr(ags_module.threading, "Timer", _unexpected_timer)
|
||||
call_count = 0
|
||||
|
||||
@@ -191,8 +198,10 @@ class TestBuildStreamingTaskOnSubscribe:
|
||||
on_subscribe()
|
||||
assert call_count == 2
|
||||
|
||||
def test_concurrent_subscribe_only_starts_once(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "streams")
|
||||
def test_concurrent_subscribe_only_starts_once(
|
||||
self, monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams")
|
||||
monkeypatch.setattr(ags_module.threading, "Timer", _unexpected_timer)
|
||||
call_count = 0
|
||||
|
||||
@@ -213,33 +222,28 @@ class TestBuildStreamingTaskOnSubscribe:
|
||||
# _get_max_active_requests
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestGetMaxActiveRequests:
|
||||
def test_both_zero_returns_zero(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_MAX_ACTIVE_REQUESTS", 0)
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_DEFAULT_ACTIVE_REQUESTS", 0)
|
||||
def test_both_zero_returns_zero(self, config_overrides: Callable[..., None]):
|
||||
config_overrides(APP_MAX_ACTIVE_REQUESTS=0, APP_DEFAULT_ACTIVE_REQUESTS=0)
|
||||
app = _make_app(AppMode.CHAT, max_active_requests=0)
|
||||
assert AppGenerateService._get_max_active_requests(app) == 0
|
||||
|
||||
def test_app_limit_only(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_MAX_ACTIVE_REQUESTS", 0)
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_DEFAULT_ACTIVE_REQUESTS", 0)
|
||||
def test_app_limit_only(self, config_overrides: Callable[..., None]):
|
||||
config_overrides(APP_MAX_ACTIVE_REQUESTS=0, APP_DEFAULT_ACTIVE_REQUESTS=0)
|
||||
app = _make_app(AppMode.CHAT, max_active_requests=5)
|
||||
assert AppGenerateService._get_max_active_requests(app) == 5
|
||||
|
||||
def test_config_limit_only(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_MAX_ACTIVE_REQUESTS", 10)
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_DEFAULT_ACTIVE_REQUESTS", 0)
|
||||
def test_config_limit_only(self, config_overrides: Callable[..., None]):
|
||||
config_overrides(APP_MAX_ACTIVE_REQUESTS=10, APP_DEFAULT_ACTIVE_REQUESTS=0)
|
||||
app = _make_app(AppMode.CHAT, max_active_requests=0)
|
||||
assert AppGenerateService._get_max_active_requests(app) == 10
|
||||
|
||||
def test_both_non_zero_returns_min(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_MAX_ACTIVE_REQUESTS", 20)
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_DEFAULT_ACTIVE_REQUESTS", 0)
|
||||
def test_both_non_zero_returns_min(self, config_overrides: Callable[..., None]):
|
||||
config_overrides(APP_MAX_ACTIVE_REQUESTS=20, APP_DEFAULT_ACTIVE_REQUESTS=0)
|
||||
app = _make_app(AppMode.CHAT, max_active_requests=5)
|
||||
assert AppGenerateService._get_max_active_requests(app) == 5
|
||||
|
||||
def test_default_active_requests_used_when_app_has_none(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_MAX_ACTIVE_REQUESTS", 0)
|
||||
monkeypatch.setattr(ags_module.dify_config, "APP_DEFAULT_ACTIVE_REQUESTS", 15)
|
||||
def test_default_active_requests_used_when_app_has_none(self, config_overrides: Callable[..., None]):
|
||||
config_overrides(APP_MAX_ACTIVE_REQUESTS=0, APP_DEFAULT_ACTIVE_REQUESTS=15)
|
||||
app = _make_app(AppMode.CHAT, max_active_requests=0)
|
||||
assert AppGenerateService._get_max_active_requests(app) == 15
|
||||
|
||||
@@ -251,8 +255,8 @@ class TestGenerate:
|
||||
"""Tests for AppGenerateService.generate covering each mode."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _common(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
def _common(self, mocker: MockerFixture, config_overrides: Callable[..., None]):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||
mocker.patch("services.app_generate_service.RateLimit", _DummyRateLimit)
|
||||
# Prevent AppExecutionParams.new from touching real models via isinstance
|
||||
mocker.patch(
|
||||
@@ -403,7 +407,8 @@ class TestGenerate:
|
||||
retrieve_spy.assert_not_called()
|
||||
|
||||
# -- ADVANCED_CHAT streaming --------------------------------------------
|
||||
def test_advanced_chat_streaming(self, mocker: MockerFixture):
|
||||
def test_advanced_chat_streaming(self, mocker: MockerFixture, config_overrides: Callable[..., None]):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams")
|
||||
workflow = _make_workflow()
|
||||
mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow)
|
||||
mocker.patch(
|
||||
@@ -463,7 +468,8 @@ class TestGenerate:
|
||||
assert call_kwargs["pause_state_config"].state_owner_user_id == "owner-id"
|
||||
|
||||
# -- WORKFLOW streaming -------------------------------------------------
|
||||
def test_workflow_streaming(self, mocker: MockerFixture):
|
||||
def test_workflow_streaming(self, mocker: MockerFixture, config_overrides: Callable[..., None]):
|
||||
config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams")
|
||||
workflow = _make_workflow()
|
||||
mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow)
|
||||
mocker.patch(
|
||||
@@ -514,15 +520,15 @@ class TestGenerate:
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestGenerateBilling:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _common(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
def _common(self, mocker: MockerFixture):
|
||||
mocker.patch("services.app_generate_service.RateLimit", _DummyRateLimit)
|
||||
mocker.patch(
|
||||
"services.app_generate_service.rate_limit_context",
|
||||
_noop_rate_limit_context,
|
||||
)
|
||||
|
||||
def test_cloud_edition_consumes_quota(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
def test_cloud_edition_consumes_quota(self, mocker: MockerFixture, config_overrides: Callable[..., None]):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
quota_charge = MagicMock()
|
||||
reserve_mock = mocker.patch(
|
||||
"services.app_generate_service.QuotaService.reserve",
|
||||
@@ -549,12 +555,12 @@ class TestGenerateBilling:
|
||||
quota_charge.commit.assert_called_once()
|
||||
|
||||
def test_billing_quota_exceeded_raises_rate_limit_error(
|
||||
self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
|
||||
self, mocker: MockerFixture, config_overrides: Callable[..., None]
|
||||
):
|
||||
from services.errors.app import QuotaExceededError
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
mocker.patch(
|
||||
"services.app_generate_service.QuotaService.reserve",
|
||||
side_effect=QuotaExceededError(feature="workflow", tenant_id="t", required=1),
|
||||
@@ -570,8 +576,10 @@ class TestGenerateBilling:
|
||||
session=MagicMock(),
|
||||
)
|
||||
|
||||
def test_exception_refunds_quota_and_exits_rate_limit(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
def test_exception_refunds_quota_and_exits_rate_limit(
|
||||
self, mocker: MockerFixture, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
quota_charge = MagicMock()
|
||||
mocker.patch(
|
||||
"services.app_generate_service.QuotaService.reserve",
|
||||
@@ -598,10 +606,10 @@ class TestGenerateBilling:
|
||||
quota_charge.refund.assert_called_once()
|
||||
|
||||
def test_rate_limit_exit_called_in_finally_for_blocking(
|
||||
self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
|
||||
self, mocker: MockerFixture, config_overrides: Callable[..., None]
|
||||
):
|
||||
"""For non-streaming (blocking) calls, rate_limit.exit should be called in finally."""
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||
|
||||
exit_calls: list[str] = []
|
||||
|
||||
@@ -630,8 +638,8 @@ class TestGenerateBilling:
|
||||
# exit is called in finally block for non-streaming
|
||||
assert exit_calls == ["dummy-request-id"]
|
||||
|
||||
def test_blocking_failure_exits_rate_limit_once(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
def test_blocking_failure_exits_rate_limit_once(self, mocker: MockerFixture, config_overrides: Callable[..., None]):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
quota_charge = MagicMock()
|
||||
mocker.patch(
|
||||
"services.app_generate_service.QuotaService.reserve",
|
||||
@@ -662,8 +670,10 @@ class TestGenerateBilling:
|
||||
quota_charge.refund.assert_called_once()
|
||||
assert exit_calls == ["dummy-request-id"]
|
||||
|
||||
def test_streaming_failure_exits_rate_limit_once(self, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(ags_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
|
||||
def test_streaming_failure_exits_rate_limit_once(
|
||||
self, mocker: MockerFixture, config_overrides: Callable[..., None]
|
||||
):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
quota_charge = MagicMock()
|
||||
mocker.patch(
|
||||
"services.app_generate_service.QuotaService.reserve",
|
||||
|
||||
Reference in New Issue
Block a user