mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
refactor(api): remove legacy billing enabled state (#41917)
This commit is contained in:
parent
fc0136647f
commit
eba589db89
@ -163,7 +163,7 @@ def cloud_edition_billing_paid_plan_required[**P, R](view: Callable[P, R]) -> Ca
|
|||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
_, current_tenant_id = current_account_with_tenant()
|
_, current_tenant_id = current_account_with_tenant()
|
||||||
billing_info = BillingService.get_info(current_tenant_id, exclude_vector_space=True)
|
billing_info = BillingService.get_info(current_tenant_id, exclude_vector_space=True)
|
||||||
if not billing_info["enabled"] or billing_info["subscription"]["plan"] not in (
|
if billing_info["subscription"]["plan"] not in (
|
||||||
CloudPlan.PROFESSIONAL,
|
CloudPlan.PROFESSIONAL,
|
||||||
CloudPlan.TEAM,
|
CloudPlan.TEAM,
|
||||||
):
|
):
|
||||||
@ -178,10 +178,10 @@ def cloud_edition_billing_resource_check[**P, R](resource: str) -> Callable[[Cal
|
|||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
_, current_tenant_id = current_account_with_tenant()
|
_, current_tenant_id = current_account_with_tenant()
|
||||||
if resource == "vector_space":
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
return view(*args, **kwargs)
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
|
if resource == "vector_space":
|
||||||
vector_space = application_services().feature_queries.get_workspace_vector_space(current_tenant_id)
|
vector_space = application_services().feature_queries.get_workspace_vector_space(current_tenant_id)
|
||||||
if 0 < vector_space.limit <= vector_space.size:
|
if 0 < vector_space.limit <= vector_space.size:
|
||||||
abort(
|
abort(
|
||||||
@ -191,30 +191,26 @@ def cloud_edition_billing_resource_check[**P, R](resource: str) -> Callable[[Cal
|
|||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled:
|
members = features.members
|
||||||
members = features.members
|
apps = features.apps
|
||||||
apps = features.apps
|
documents_upload_quota = features.documents_upload_quota
|
||||||
documents_upload_quota = features.documents_upload_quota
|
annotation_quota_limit = features.annotation_quota_limit
|
||||||
annotation_quota_limit = features.annotation_quota_limit
|
if resource == "members" and 0 < members.limit <= members.size:
|
||||||
if resource == "members" and 0 < members.limit <= members.size:
|
abort(403, "The number of members has reached the limit of your subscription.")
|
||||||
abort(403, "The number of members has reached the limit of your subscription.")
|
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
||||||
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
abort(403, "The number of apps has reached the limit of your subscription.")
|
||||||
abort(403, "The number of apps has reached the limit of your subscription.")
|
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
||||||
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
# The api of file upload is used in the multiple places,
|
||||||
# The api of file upload is used in the multiple places,
|
# so we need to check the source of the request from datasets
|
||||||
# so we need to check the source of the request from datasets
|
source = request.args.get("source") or request.form.get("source")
|
||||||
source = request.args.get("source") or request.form.get("source")
|
if source == "datasets":
|
||||||
if source == "datasets":
|
abort(403, "The number of documents has reached the limit of your subscription.")
|
||||||
abort(403, "The number of documents has reached the limit of your subscription.")
|
|
||||||
else:
|
|
||||||
return view(*args, **kwargs)
|
|
||||||
elif resource == "workspace_custom" and not features.can_replace_logo:
|
|
||||||
abort(403, "The workspace custom feature has reached the limit of your subscription.")
|
|
||||||
elif resource == "annotation" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size:
|
|
||||||
abort(403, "The annotation quota has reached the limit of your subscription.")
|
|
||||||
else:
|
else:
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
elif resource == "workspace_custom" and not features.can_replace_logo:
|
||||||
|
abort(403, "The workspace custom feature has reached the limit of your subscription.")
|
||||||
|
elif resource == "annotation" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size:
|
||||||
|
abort(403, "The annotation quota has reached the limit of your subscription.")
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
@ -229,16 +225,15 @@ def cloud_edition_billing_knowledge_limit_check[**P, R](
|
|||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
_, current_tenant_id = current_account_with_tenant()
|
_, current_tenant_id = current_account_with_tenant()
|
||||||
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD or resource != "add_segment":
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled:
|
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
if resource == "add_segment":
|
abort(
|
||||||
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
403,
|
||||||
abort(
|
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
|
||||||
403,
|
)
|
||||||
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ from controllers.openapi._models import (
|
|||||||
)
|
)
|
||||||
from controllers.openapi.auth.composition import auth_router
|
from controllers.openapi.auth.composition import auth_router
|
||||||
from controllers.openapi.auth.data import AuthData
|
from controllers.openapi.auth.data import AuthData
|
||||||
|
from enums import DeploymentEdition
|
||||||
from libs.oauth_bearer import Scope, TokenType
|
from libs.oauth_bearer import Scope, TokenType
|
||||||
from models import Account, Tenant, TenantAccountJoin
|
from models import Account, Tenant, TenantAccountJoin
|
||||||
from models.account import TenantAccountRole, TenantStatus
|
from models.account import TenantAccountRole, TenantStatus
|
||||||
@ -82,7 +83,7 @@ def _load_account(session: Session, account_id: object) -> Account:
|
|||||||
def _check_member_invite_quota(tenant_id: str) -> None:
|
def _check_member_invite_quota(tenant_id: str) -> None:
|
||||||
features = FeatureService.get_features(tenant_id)
|
features = FeatureService.get_features(tenant_id)
|
||||||
|
|
||||||
if features.billing.enabled:
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
members = features.members
|
members = features.members
|
||||||
if 0 < members.limit <= members.size:
|
if 0 < members.limit <= members.size:
|
||||||
raise MemberLimitExceeded()
|
raise MemberLimitExceeded()
|
||||||
|
|||||||
@ -399,7 +399,7 @@ class ChatApi(Resource):
|
|||||||
and dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD
|
and dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD
|
||||||
):
|
):
|
||||||
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
|
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
|
||||||
if billing_info["enabled"] and billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
|
if billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
|
||||||
raise WorkflowVersionExecutionNotAllowedError()
|
raise WorkflowVersionExecutionNotAllowedError()
|
||||||
|
|
||||||
external_trace_id = get_external_trace_id(request)
|
external_trace_id = get_external_trace_id(request)
|
||||||
|
|||||||
@ -476,7 +476,7 @@ class WorkflowRunByIdApi(Resource):
|
|||||||
|
|
||||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
|
billing_info = BillingService.get_info(app_model.tenant_id, exclude_vector_space=True)
|
||||||
if billing_info["enabled"] and billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
|
if billing_info["subscription"]["plan"] == CloudPlan.SANDBOX:
|
||||||
raise WorkflowVersionExecutionNotAllowedError()
|
raise WorkflowVersionExecutionNotAllowedError()
|
||||||
|
|
||||||
payload = WorkflowRunPayload.model_validate(omit_trace_session_id_from_payload(service_api_ns.payload) or {})
|
payload = WorkflowRunPayload.model_validate(omit_trace_session_id_from_payload(service_api_ns.payload) or {})
|
||||||
|
|||||||
@ -194,14 +194,14 @@ def cloud_edition_billing_resource_check[**P, R](
|
|||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
api_token = validate_and_get_api_token(api_token_type)
|
api_token = validate_and_get_api_token(api_token_type)
|
||||||
if resource == "vector_space":
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
return view(*args, **kwargs)
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
|
if resource == "vector_space":
|
||||||
vector_space = application_services().feature_queries.get_workspace_vector_space(api_token.tenant_id)
|
vector_space = application_services().feature_queries.get_workspace_vector_space(api_token.tenant_id)
|
||||||
if vector_space.usage_unknown:
|
if vector_space.usage_unknown:
|
||||||
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled and features.billing.subscription.plan == CloudPlan.SANDBOX:
|
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
raise ServiceUnavailable(
|
raise ServiceUnavailable(
|
||||||
"Unable to verify vector space usage right now. Please try again later."
|
"Unable to verify vector space usage right now. Please try again later."
|
||||||
)
|
)
|
||||||
@ -211,20 +211,16 @@ def cloud_edition_billing_resource_check[**P, R](
|
|||||||
|
|
||||||
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
||||||
|
|
||||||
if features.billing.enabled:
|
members = features.members
|
||||||
members = features.members
|
apps = features.apps
|
||||||
apps = features.apps
|
documents_upload_quota = features.documents_upload_quota
|
||||||
documents_upload_quota = features.documents_upload_quota
|
|
||||||
|
|
||||||
if resource == "members" and 0 < members.limit <= members.size:
|
|
||||||
raise Forbidden("The number of members has reached the limit of your subscription.")
|
|
||||||
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
|
||||||
raise Forbidden("The number of apps has reached the limit of your subscription.")
|
|
||||||
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
|
||||||
raise Forbidden("The number of documents has reached the limit of your subscription.")
|
|
||||||
else:
|
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
|
if resource == "members" and 0 < members.limit <= members.size:
|
||||||
|
raise Forbidden("The number of members has reached the limit of your subscription.")
|
||||||
|
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
||||||
|
raise Forbidden("The number of apps has reached the limit of your subscription.")
|
||||||
|
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
||||||
|
raise Forbidden("The number of documents has reached the limit of your subscription.")
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
if resource == "vector_space":
|
if resource == "vector_space":
|
||||||
@ -245,15 +241,14 @@ def cloud_edition_billing_knowledge_limit_check[**P, R](
|
|||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
api_token = validate_and_get_api_token(api_token_type)
|
api_token = validate_and_get_api_token(api_token_type)
|
||||||
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD or resource != "add_segment":
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled:
|
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
if resource == "add_segment":
|
raise Forbidden(
|
||||||
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."
|
||||||
raise Forbidden(
|
)
|
||||||
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from configs import dify_config
|
|||||||
from controllers.common.schema import register_response_schema_models
|
from controllers.common.schema import register_response_schema_models
|
||||||
from controllers.web import web_ns
|
from controllers.web import web_ns
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
|
from enums import DeploymentEdition
|
||||||
from extensions.ext_application_services import application_services
|
from extensions.ext_application_services import application_services
|
||||||
from fields.base import ResponseModel
|
from fields.base import ResponseModel
|
||||||
from libs.helper import build_icon_url, dump_response
|
from libs.helper import build_icon_url, dump_response
|
||||||
@ -102,7 +103,7 @@ class WebAppSiteResponse(ResponseModel):
|
|||||||
|
|
||||||
site_response = WebSiteResponse.model_validate(site, from_attributes=True)
|
site_response = WebSiteResponse.model_validate(site, from_attributes=True)
|
||||||
site_response.icon_url = icon_url if icon_url is not None else build_icon_url(site.icon_type, site.icon)
|
site_response.icon_url = icon_url if icon_url is not None else build_icon_url(site.icon_type, site.icon)
|
||||||
if features.billing.enabled and not features.webapp_copyright_enabled:
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and not features.webapp_copyright_enabled:
|
||||||
site_response.copyright = None
|
site_response.copyright = None
|
||||||
site_response.input_placeholder = None
|
site_response.input_placeholder = None
|
||||||
|
|
||||||
|
|||||||
@ -635,6 +635,7 @@ def build_application_services(
|
|||||||
file_service=file_service,
|
file_service=file_service,
|
||||||
workspace_features=feature_gateway.get_workspace_features,
|
workspace_features=feature_gateway.get_workspace_features,
|
||||||
files_url=dify_config.FILES_URL,
|
files_url=dify_config.FILES_URL,
|
||||||
|
deployment_edition=deployment_edition,
|
||||||
),
|
),
|
||||||
explore_banner_queries=ExploreBannerQueryService(
|
explore_banner_queries=ExploreBannerQueryService(
|
||||||
banners=ExploreBannerQueryRepository(session_factory=database_client),
|
banners=ExploreBannerQueryRepository(session_factory=database_client),
|
||||||
|
|||||||
@ -16118,7 +16118,6 @@ ExporleBanner status
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
| ---- | ---- | ----------- | -------- |
|
| ---- | ---- | ----------- | -------- |
|
||||||
| enabled | boolean | Deprecated. Use system features deployment_edition to determine the product edition. | Yes |
|
|
||||||
| subscription | [SubscriptionModel](#subscriptionmodel) | | Yes |
|
| subscription | [SubscriptionModel](#subscriptionmodel) | | Yes |
|
||||||
|
|
||||||
#### BillingOperationFailedErrorResponse
|
#### BillingOperationFailedErrorResponse
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from werkzeug.datastructures import FileStorage
|
|||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from core.helper.csv_sanitizer import CSVSanitizer
|
from core.helper.csv_sanitizer import CSVSanitizer
|
||||||
|
from enums import DeploymentEdition
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.login import current_account_with_tenant
|
from libs.login import current_account_with_tenant
|
||||||
@ -532,8 +533,8 @@ class AppAnnotationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check annotation quota limit
|
# Check annotation quota limit
|
||||||
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
if features.billing.enabled:
|
features = FeatureService.get_features(current_tenant_id, exclude_vector_space=True)
|
||||||
annotation_quota_limit = features.annotation_quota_limit
|
annotation_quota_limit = features.annotation_quota_limit
|
||||||
if annotation_quota_limit.limit < len(result) + annotation_quota_limit.size:
|
if annotation_quota_limit.limit < len(result) + annotation_quota_limit.size:
|
||||||
raise ValueError("The number of annotations exceeds the limit of your subscription.")
|
raise ValueError("The number of annotations exceeds the limit of your subscription.")
|
||||||
|
|||||||
@ -173,7 +173,6 @@ class BillingInfo(TypedDict):
|
|||||||
3. To preserve compatibility, always keep non-strict mode here and avoid strict mode
|
3. To preserve compatibility, always keep non-strict mode here and avoid strict mode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
enabled: bool
|
|
||||||
subscription: _BillingSubscription
|
subscription: _BillingSubscription
|
||||||
members: _BillingQuota
|
members: _BillingQuota
|
||||||
apps: _BillingQuota
|
apps: _BillingQuota
|
||||||
|
|||||||
@ -23,7 +23,7 @@ from core.model_manager import ModelManager
|
|||||||
from core.rag.index_processor.constant.built_in_field import BuiltInField
|
from core.rag.index_processor.constant.built_in_field import BuiltInField
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||||
from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from events.dataset_event import dataset_was_deleted
|
from events.dataset_event import dataset_was_deleted
|
||||||
from events.document_event import document_was_deleted
|
from events.document_event import document_was_deleted
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
@ -1455,8 +1455,11 @@ class DatasetService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_dataset_auto_disable_logs(dataset_ref: DatasetRef, session: Session) -> AutoDisableLogsDict:
|
def get_dataset_auto_disable_logs(dataset_ref: DatasetRef, session: Session) -> AutoDisableLogsDict:
|
||||||
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||||
|
return {"document_ids": [], "count": 0}
|
||||||
|
|
||||||
features = FeatureService.get_features(dataset_ref.tenant_id, exclude_vector_space=True)
|
features = FeatureService.get_features(dataset_ref.tenant_id, exclude_vector_space=True)
|
||||||
if not features.billing.enabled or features.billing.subscription.plan == CloudPlan.SANDBOX:
|
if features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
return {
|
return {
|
||||||
"document_ids": [],
|
"document_ids": [],
|
||||||
"count": 0,
|
"count": 0,
|
||||||
@ -2208,9 +2211,8 @@ class DocumentService:
|
|||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
assert current_user.current_tenant_id is not None
|
assert current_user.current_tenant_id is not None
|
||||||
|
|
||||||
features = FeatureService.get_features(current_user.current_tenant_id, exclude_vector_space=True)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
|
features = FeatureService.get_features(current_user.current_tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled:
|
|
||||||
if not knowledge_config.original_document_id:
|
if not knowledge_config.original_document_id:
|
||||||
count = 0
|
count = 0
|
||||||
if knowledge_config.data_source:
|
if knowledge_config.data_source:
|
||||||
@ -2520,7 +2522,7 @@ class DocumentService:
|
|||||||
# # check document limit
|
# # check document limit
|
||||||
# features = FeatureService.get_features(current_user.current_tenant_id)
|
# features = FeatureService.get_features(current_user.current_tenant_id)
|
||||||
|
|
||||||
# if features.billing.enabled:
|
# if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
# if not knowledge_config.original_document_id:
|
# if not knowledge_config.original_document_id:
|
||||||
# count = 0
|
# count = 0
|
||||||
# if knowledge_config.data_source:
|
# if knowledge_config.data_source:
|
||||||
@ -2797,7 +2799,7 @@ class DocumentService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_document_creation_limits(count: int, features: FeatureModel):
|
def check_document_creation_limits(count: int, features: FeatureModel):
|
||||||
"""Validate billing-backed document creation limits before document rows are created."""
|
"""Validate billing-backed document creation limits before document rows are created."""
|
||||||
if not features.billing.enabled:
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||||
return
|
return
|
||||||
|
|
||||||
if features.billing.subscription.plan == CloudPlan.SANDBOX and count > 1:
|
if features.billing.subscription.plan == CloudPlan.SANDBOX and count > 1:
|
||||||
@ -3014,9 +3016,8 @@ class DocumentService:
|
|||||||
assert current_user.current_tenant_id is not None
|
assert current_user.current_tenant_id is not None
|
||||||
assert knowledge_config.data_source
|
assert knowledge_config.data_source
|
||||||
|
|
||||||
features = FeatureService.get_features(current_user.current_tenant_id, exclude_vector_space=True)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
|
features = FeatureService.get_features(current_user.current_tenant_id, exclude_vector_space=True)
|
||||||
if features.billing.enabled:
|
|
||||||
count = 0
|
count = 0
|
||||||
if knowledge_config.data_source.info_list.data_source_type == "upload_file":
|
if knowledge_config.data_source.info_list.data_source_type == "upload_file":
|
||||||
upload_file_list = (
|
upload_file_list = (
|
||||||
|
|||||||
@ -4,7 +4,8 @@ from collections.abc import Callable
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from enums import CloudPlan
|
from configs import dify_config
|
||||||
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -88,14 +89,10 @@ class DocumentTaskProxyBase(ABC):
|
|||||||
- Paid plans → priority queue + tenant isolation
|
- Paid plans → priority queue + tenant isolation
|
||||||
- Self-hosted → priority queue, no isolation
|
- Self-hosted → priority queue, no isolation
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info("Dispatching tenant %s in %s", self._tenant_id, dify_config.DEPLOYMENT_EDITION)
|
||||||
"dispatch args: %s - %s - %s",
|
|
||||||
self._tenant_id,
|
# Cloud queues isolate tenants and prioritize paid plans.
|
||||||
self.features.billing.enabled,
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
self.features.billing.subscription.plan,
|
|
||||||
)
|
|
||||||
# dispatch to different indexing queue with tenant isolation when billing enabled
|
|
||||||
if self.features.billing.enabled:
|
|
||||||
if self.features.billing.subscription.plan == CloudPlan.SANDBOX:
|
if self.features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
# dispatch to normal pipeline queue with tenant self sub queue for sandbox plan
|
# dispatch to normal pipeline queue with tenant self sub queue for sandbox plan
|
||||||
self._send_to_default_tenant_queue()
|
self._send_to_default_tenant_queue()
|
||||||
|
|||||||
@ -17,13 +17,6 @@ class SubscriptionModel(FeatureResponseModel):
|
|||||||
|
|
||||||
|
|
||||||
class BillingModel(FeatureResponseModel):
|
class BillingModel(FeatureResponseModel):
|
||||||
# Deprecated compatibility field. Deployment edition is the only source of truth for product edition.
|
|
||||||
# TODO: Remove after clients migrate to `SystemFeatureModel.deployment_edition`.
|
|
||||||
enabled: bool = Field(
|
|
||||||
default=False,
|
|
||||||
deprecated=True,
|
|
||||||
description="Deprecated. Use system features deployment_edition to determine the product edition.",
|
|
||||||
)
|
|
||||||
subscription: SubscriptionModel = SubscriptionModel()
|
subscription: SubscriptionModel = SubscriptionModel()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,6 @@ class FeatureService:
|
|||||||
return CloudPlan.SANDBOX
|
return CloudPlan.SANDBOX
|
||||||
|
|
||||||
billing_info = BillingService.get_info(tenant_id, exclude_vector_space=True)
|
billing_info = BillingService.get_info(tenant_id, exclude_vector_space=True)
|
||||||
if not billing_info["enabled"]:
|
|
||||||
return CloudPlan.SANDBOX
|
|
||||||
return CloudPlan(billing_info["subscription"]["plan"])
|
return CloudPlan(billing_info["subscription"]["plan"])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -86,7 +84,7 @@ class FeatureService:
|
|||||||
return True
|
return True
|
||||||
if not tenant_id:
|
if not tenant_id:
|
||||||
return False
|
return False
|
||||||
return features.billing.enabled and features.billing.subscription.plan.is_paid
|
return features.billing.subscription.plan.is_paid
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _fulfill_trial_models_from_env(cls, quota_types: tuple[str, ...] | None = None) -> list[str]:
|
def _fulfill_trial_models_from_env(cls, quota_types: tuple[str, ...] | None = None) -> list[str]:
|
||||||
@ -140,7 +138,6 @@ class FeatureService:
|
|||||||
|
|
||||||
features_usage_info = BillingService.get_quota_info(tenant_id)
|
features_usage_info = BillingService.get_quota_info(tenant_id)
|
||||||
|
|
||||||
features.billing.enabled = billing_info["enabled"]
|
|
||||||
features.billing.subscription.plan = CloudPlan(billing_info["subscription"]["plan"])
|
features.billing.subscription.plan = CloudPlan(billing_info["subscription"]["plan"])
|
||||||
features.billing.subscription.interval = billing_info["subscription"]["interval"]
|
features.billing.subscription.interval = billing_info["subscription"]["interval"]
|
||||||
features.education.activated = billing_info["subscription"].get("education", False)
|
features.education.activated = billing_info["subscription"].get("education", False)
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import logging
|
|||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
|
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
@ -79,15 +80,10 @@ class RagPipelineTaskProxy:
|
|||||||
if not upload_file_id:
|
if not upload_file_id:
|
||||||
raise ValueError("upload_file_id is empty")
|
raise ValueError("upload_file_id is empty")
|
||||||
|
|
||||||
logger.info(
|
logger.info("Dispatching tenant %s in %s", self._dataset_tenant_id, dify_config.DEPLOYMENT_EDITION)
|
||||||
"dispatch args: %s - %s - %s",
|
|
||||||
self._dataset_tenant_id,
|
|
||||||
self.features.billing.enabled,
|
|
||||||
self.features.billing.subscription.plan,
|
|
||||||
)
|
|
||||||
|
|
||||||
# dispatch to different pipeline queue with tenant isolation when billing enabled
|
# Cloud queues isolate tenants and prioritize paid plans.
|
||||||
if self.features.billing.enabled:
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
if self.features.billing.subscription.plan == CloudPlan.SANDBOX:
|
if self.features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||||
# dispatch to normal pipeline queue with tenant isolation for sandbox plan
|
# dispatch to normal pipeline queue with tenant isolation for sandbox plan
|
||||||
self._send_to_default_tenant_queue(upload_file_id)
|
self._send_to_default_tenant_queue(upload_file_id)
|
||||||
|
|||||||
@ -395,15 +395,13 @@ class VectorSpaceAdmissionService:
|
|||||||
) from error
|
) from error
|
||||||
|
|
||||||
plan = None
|
plan = None
|
||||||
if billing_info["enabled"]:
|
try:
|
||||||
try:
|
plan = CloudPlan(billing_info["subscription"]["plan"])
|
||||||
plan = CloudPlan(billing_info["subscription"]["plan"])
|
except ValueError:
|
||||||
except ValueError:
|
logger.warning(
|
||||||
logger.warning(
|
"Skipping TiDB vector-space admission for unknown plan tenant_id=%s",
|
||||||
"Skipping TiDB vector-space admission for unknown plan tenant_id=%s plan=%s",
|
tenant_id,
|
||||||
tenant_id,
|
)
|
||||||
billing_info["subscription"]["plan"],
|
|
||||||
)
|
|
||||||
self._plan_by_tenant[tenant_id] = plan
|
self._plan_by_tenant[tenant_id] = plan
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import json
|
|||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from typing import NamedTuple, Protocol, cast
|
from typing import NamedTuple, Protocol, cast
|
||||||
|
|
||||||
|
from enums import DeploymentEdition
|
||||||
from services.app_definition_query_service import AppSiteConfiguration
|
from services.app_definition_query_service import AppSiteConfiguration
|
||||||
from services.entities.feature_entities import FeatureModel
|
from services.entities.feature_entities import FeatureModel
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
@ -50,11 +51,13 @@ class WebAppRuntimeQueryService:
|
|||||||
file_service: FileService,
|
file_service: FileService,
|
||||||
workspace_features: Callable[[str], FeatureModel],
|
workspace_features: Callable[[str], FeatureModel],
|
||||||
files_url: str,
|
files_url: str,
|
||||||
|
deployment_edition: DeploymentEdition,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._runtime = runtime
|
self._runtime = runtime
|
||||||
self._file_service = file_service
|
self._file_service = file_service
|
||||||
self._workspace_features = workspace_features
|
self._workspace_features = workspace_features
|
||||||
self._files_url = files_url
|
self._files_url = files_url
|
||||||
|
self._deployment_edition = deployment_edition
|
||||||
|
|
||||||
def get_bootstrap(self, app_id: str) -> WebAppBootstrap:
|
def get_bootstrap(self, app_id: str) -> WebAppBootstrap:
|
||||||
record = self._runtime.get_runtime_record(app_id)
|
record = self._runtime.get_runtime_record(app_id)
|
||||||
@ -70,7 +73,7 @@ class WebAppRuntimeQueryService:
|
|||||||
|
|
||||||
site = cast(dict[str, str | bool | None], record.site._asdict())
|
site = cast(dict[str, str | bool | None], record.site._asdict())
|
||||||
site["icon_url"] = site_icon_url
|
site["icon_url"] = site_icon_url
|
||||||
if features.billing.enabled and not features.webapp_copyright_enabled:
|
if self._deployment_edition == DeploymentEdition.CLOUD and not features.webapp_copyright_enabled:
|
||||||
site["copyright"] = None
|
site["copyright"] = None
|
||||||
site["input_placeholder"] = None
|
site["input_placeholder"] = None
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ class WorkspaceService:
|
|||||||
|
|
||||||
if effective_pool is None:
|
if effective_pool is None:
|
||||||
return EffectiveCreditPool(
|
return EffectiveCreditPool(
|
||||||
plan=subscription_plan if billing_info["enabled"] else None,
|
plan=subscription_plan,
|
||||||
next_credit_reset_date=billing_info.get("next_credit_reset_date"),
|
next_credit_reset_date=billing_info.get("next_credit_reset_date"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ class WorkspaceService:
|
|||||||
exhausted_at = None
|
exhausted_at = None
|
||||||
|
|
||||||
return EffectiveCreditPool(
|
return EffectiveCreditPool(
|
||||||
plan=subscription_plan if billing_info["enabled"] else None,
|
plan=subscription_plan,
|
||||||
pool_type=effective_pool_type,
|
pool_type=effective_pool_type,
|
||||||
quota_limit=effective_pool.quota_limit,
|
quota_limit=effective_pool.quota_limit,
|
||||||
quota_used=effective_pool.quota_used,
|
quota_used=effective_pool.quota_used,
|
||||||
@ -137,7 +137,9 @@ class WorkspaceService:
|
|||||||
tenant_info["role"] = tenant_account_join.role
|
tenant_info["role"] = tenant_account_join.role
|
||||||
|
|
||||||
feature = FeatureService.get_features(tenant.id, exclude_vector_space=True)
|
feature = FeatureService.get_features(tenant.id, exclude_vector_space=True)
|
||||||
tenant_info["plan"] = feature.billing.subscription.plan if feature.billing.enabled else None
|
tenant_info["plan"] = (
|
||||||
|
feature.billing.subscription.plan if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD else None
|
||||||
|
)
|
||||||
can_replace_logo = feature.can_replace_logo
|
can_replace_logo = feature.can_replace_logo
|
||||||
|
|
||||||
if can_replace_logo and TenantService.has_roles(
|
if can_replace_logo and TenantService.has_roles(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from core.entities.document_task import DocumentTask
|
|||||||
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
|
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models.dataset import Dataset, Document
|
from models.dataset import Dataset, Document
|
||||||
from models.enums import IndexingStatus
|
from models.enums import IndexingStatus
|
||||||
@ -62,9 +62,9 @@ def _document_indexing(dataset_id: str, document_ids: Sequence[str]):
|
|||||||
logger.info(click.style(f"Dataset is not found: {dataset_id}", fg="yellow"))
|
logger.info(click.style(f"Dataset is not found: {dataset_id}", fg="yellow"))
|
||||||
return
|
return
|
||||||
# check document limit
|
# check document limit
|
||||||
features = FeatureService.get_features(dataset.tenant_id)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
try:
|
features = FeatureService.get_features(dataset.tenant_id)
|
||||||
if features.billing.enabled:
|
try:
|
||||||
vector_space = features.vector_space
|
vector_space = features.vector_space
|
||||||
assert vector_space is not None
|
assert vector_space is not None
|
||||||
count = len(document_ids)
|
count = len(document_ids)
|
||||||
@ -78,18 +78,18 @@ def _document_indexing(dataset_id: str, document_ids: Sequence[str]):
|
|||||||
"Your total number of documents plus the number of uploads have over the limit of "
|
"Your total number of documents plus the number of uploads have over the limit of "
|
||||||
"your subscription."
|
"your subscription."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
for document_id in document_ids:
|
for document_id in document_ids:
|
||||||
document = session.scalar(
|
document = session.scalar(
|
||||||
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
|
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
|
||||||
)
|
)
|
||||||
if document:
|
if document:
|
||||||
document.indexing_status = IndexingStatus.ERROR
|
document.indexing_status = IndexingStatus.ERROR
|
||||||
document.error = str(e)
|
document.error = str(e)
|
||||||
document.stopped_at = naive_utc_now()
|
document.stopped_at = naive_utc_now()
|
||||||
session.add(document)
|
session.add(document)
|
||||||
session.commit()
|
session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Phase 1: Persist parsing status before slow extraction and vector operations.
|
# Phase 1: Persist parsing status before slow extraction and vector operations.
|
||||||
with session_factory.create_session() as session, session.begin():
|
with session_factory.create_session() as session, session.begin():
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from core.entities.document_task import DocumentTask
|
|||||||
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
|
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
|
||||||
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models.dataset import Dataset, Document, DocumentSegment
|
from models.dataset import Dataset, Document, DocumentSegment
|
||||||
from models.enums import IndexingStatus
|
from models.enums import IndexingStatus
|
||||||
@ -88,9 +88,9 @@ def _duplicate_document_indexing_task(dataset_id: str, document_ids: Sequence[st
|
|||||||
return
|
return
|
||||||
|
|
||||||
# check document limit
|
# check document limit
|
||||||
features = FeatureService.get_features(dataset.tenant_id)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
try:
|
features = FeatureService.get_features(dataset.tenant_id)
|
||||||
if features.billing.enabled:
|
try:
|
||||||
vector_space = features.vector_space
|
vector_space = features.vector_space
|
||||||
assert vector_space is not None
|
assert vector_space is not None
|
||||||
count = len(document_ids)
|
count = len(document_ids)
|
||||||
@ -106,20 +106,20 @@ def _duplicate_document_indexing_task(dataset_id: str, document_ids: Sequence[st
|
|||||||
"Your total number of documents plus the number of uploads have exceeded the limit of "
|
"Your total number of documents plus the number of uploads have exceeded the limit of "
|
||||||
"your subscription."
|
"your subscription."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
documents = list(
|
documents = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
select(Document).where(Document.id.in_(document_ids), Document.dataset_id == dataset_id)
|
select(Document).where(Document.id.in_(document_ids), Document.dataset_id == dataset_id)
|
||||||
).all()
|
).all()
|
||||||
)
|
)
|
||||||
for document in documents:
|
for document in documents:
|
||||||
if document is not None:
|
if document is not None:
|
||||||
document.indexing_status = IndexingStatus.ERROR
|
document.indexing_status = IndexingStatus.ERROR
|
||||||
document.error = str(e)
|
document.error = str(e)
|
||||||
document.stopped_at = naive_utc_now()
|
document.stopped_at = naive_utc_now()
|
||||||
session.add(document)
|
session.add(document)
|
||||||
session.commit()
|
session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
documents = list(
|
documents = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import click
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from core.db.session_factory import session_factory
|
from core.db.session_factory import session_factory
|
||||||
from core.indexing_runner import IndexingRunner
|
from core.indexing_runner import IndexingRunner
|
||||||
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
||||||
|
from enums import DeploymentEdition
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models import Account, Tenant
|
from models import Account, Tenant
|
||||||
@ -48,9 +50,9 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_
|
|||||||
for document_id in document_ids:
|
for document_id in document_ids:
|
||||||
retry_indexing_cache_key = f"document_{document_id}_is_retried"
|
retry_indexing_cache_key = f"document_{document_id}_is_retried"
|
||||||
# check document limit
|
# check document limit
|
||||||
features = FeatureService.get_features(tenant.id)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
try:
|
features = FeatureService.get_features(tenant.id)
|
||||||
if features.billing.enabled:
|
try:
|
||||||
vector_space = features.vector_space
|
vector_space = features.vector_space
|
||||||
assert vector_space is not None
|
assert vector_space is not None
|
||||||
if 0 < vector_space.limit <= vector_space.size:
|
if 0 < vector_space.limit <= vector_space.size:
|
||||||
@ -58,18 +60,20 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str], user_
|
|||||||
"Your total number of documents plus the number of uploads have over the limit of "
|
"Your total number of documents plus the number of uploads have over the limit of "
|
||||||
"your subscription."
|
"your subscription."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
document = session.scalar(
|
document = session.scalar(
|
||||||
select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1)
|
select(Document)
|
||||||
)
|
.where(Document.id == document_id, Document.dataset_id == dataset_id)
|
||||||
if document:
|
.limit(1)
|
||||||
document.indexing_status = IndexingStatus.ERROR
|
)
|
||||||
document.error = str(e)
|
if document:
|
||||||
document.stopped_at = naive_utc_now()
|
document.indexing_status = IndexingStatus.ERROR
|
||||||
session.add(document)
|
document.error = str(e)
|
||||||
session.commit()
|
document.stopped_at = naive_utc_now()
|
||||||
redis_client.delete(retry_indexing_cache_key)
|
session.add(document)
|
||||||
return
|
session.commit()
|
||||||
|
redis_client.delete(retry_indexing_cache_key)
|
||||||
|
return
|
||||||
|
|
||||||
logger.info(click.style(f"Start retry document: {document_id}", fg="green"))
|
logger.info(click.style(f"Start retry document: {document_id}", fg="green"))
|
||||||
document = session.scalar(
|
document = session.scalar(
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import click
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from core.db.session_factory import session_factory
|
from core.db.session_factory import session_factory
|
||||||
from core.indexing_runner import IndexingRunner
|
from core.indexing_runner import IndexingRunner
|
||||||
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
||||||
|
from enums import DeploymentEdition
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models.dataset import Dataset, DocumentSegment
|
from models.dataset import Dataset, DocumentSegment
|
||||||
@ -45,9 +47,9 @@ def sync_website_document_indexing_task(dataset_id: str, document_id: str):
|
|||||||
|
|
||||||
sync_indexing_cache_key = f"document_{document_id}_is_sync"
|
sync_indexing_cache_key = f"document_{document_id}_is_sync"
|
||||||
# check document limit
|
# check document limit
|
||||||
features = FeatureService.get_features(dataset.tenant_id)
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||||
try:
|
features = FeatureService.get_features(dataset.tenant_id)
|
||||||
if features.billing.enabled:
|
try:
|
||||||
vector_space = features.vector_space
|
vector_space = features.vector_space
|
||||||
assert vector_space is not None
|
assert vector_space is not None
|
||||||
if 0 < vector_space.limit <= vector_space.size:
|
if 0 < vector_space.limit <= vector_space.size:
|
||||||
@ -55,14 +57,14 @@ def sync_website_document_indexing_task(dataset_id: str, document_id: str):
|
|||||||
"Your total number of documents plus the number of uploads have over the limit of "
|
"Your total number of documents plus the number of uploads have over the limit of "
|
||||||
"your subscription."
|
"your subscription."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
document.indexing_status = IndexingStatus.ERROR
|
document.indexing_status = IndexingStatus.ERROR
|
||||||
document.error = str(e)
|
document.error = str(e)
|
||||||
document.stopped_at = naive_utc_now()
|
document.stopped_at = naive_utc_now()
|
||||||
session.add(document)
|
session.add(document)
|
||||||
session.commit()
|
session.commit()
|
||||||
redis_client.delete(sync_indexing_cache_key)
|
redis_client.delete(sync_indexing_cache_key)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(click.style(f"Start sync website document: {document_id}", fg="green"))
|
logger.info(click.style(f"Start sync website document: {document_id}", fg="green"))
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from faker import Faker
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models import Account
|
from models import Account
|
||||||
from models.enums import ConversationFromSource, InvokeFrom
|
from models.enums import ConversationFromSource, InvokeFrom
|
||||||
from models.model import MessageAnnotation
|
from models.model import MessageAnnotation
|
||||||
@ -12,6 +13,7 @@ from services.annotation_service import AppAnnotationService
|
|||||||
from services.app_ref_service import AnnotationRef, AppRef
|
from services.app_ref_service import AnnotationRef, AppRef
|
||||||
from services.app_service import AppService, CreateAppParams
|
from services.app_service import AppService, CreateAppParams
|
||||||
from tests.test_containers_integration_tests.helpers import generate_valid_password
|
from tests.test_containers_integration_tests.helpers import generate_valid_password
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class TestAnnotationService:
|
class TestAnnotationService:
|
||||||
@ -32,7 +34,6 @@ class TestAnnotationService:
|
|||||||
patch("services.annotation_service.current_account_with_tenant") as mock_current_account_with_tenant,
|
patch("services.annotation_service.current_account_with_tenant") as mock_current_account_with_tenant,
|
||||||
):
|
):
|
||||||
# Setup default mock returns
|
# Setup default mock returns
|
||||||
mock_account_feature_service.get_features.return_value.billing.enabled = False
|
|
||||||
mock_add_task.delay.return_value = None
|
mock_add_task.delay.return_value = None
|
||||||
mock_update_task.delay.return_value = None
|
mock_update_task.delay.return_value = None
|
||||||
mock_delete_task.delay.return_value = None
|
mock_delete_task.delay.return_value = None
|
||||||
@ -879,6 +880,7 @@ class TestAnnotationService:
|
|||||||
assert retrieved_annotation.content == annotation_args["answer"]
|
assert retrieved_annotation.content == annotation_args["answer"]
|
||||||
assert retrieved_annotation.account_id == account.id
|
assert retrieved_annotation.account_id == account.id
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_batch_import_app_annotations_success(
|
def test_batch_import_app_annotations_success(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
@ -900,8 +902,6 @@ class TestAnnotationService:
|
|||||||
stream=BytesIO(csv_content.encode("utf-8")), filename="annotations.csv", content_type="text/csv"
|
stream=BytesIO(csv_content.encode("utf-8")), filename="annotations.csv", content_type="text/csv"
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_external_service_dependencies["feature_service"].get_features.return_value.billing.enabled = False
|
|
||||||
|
|
||||||
# Mock pandas to return expected DataFrame
|
# Mock pandas to return expected DataFrame
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -958,6 +958,7 @@ class TestAnnotationService:
|
|||||||
assert "error_msg" in result
|
assert "error_msg" in result
|
||||||
assert "empty" in result["error_msg"].lower()
|
assert "empty" in result["error_msg"].lower()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_batch_import_app_annotations_quota_exceeded(
|
def test_batch_import_app_annotations_quota_exceeded(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
@ -989,7 +990,6 @@ class TestAnnotationService:
|
|||||||
mock_pd.read_csv.return_value = mock_df
|
mock_pd.read_csv.return_value = mock_df
|
||||||
|
|
||||||
# Mock FeatureService to return billing enabled with quota exceeded
|
# Mock FeatureService to return billing enabled with quota exceeded
|
||||||
mock_external_service_dependencies["feature_service"].get_features.return_value.billing.enabled = True
|
|
||||||
mock_external_service_dependencies[
|
mock_external_service_dependencies[
|
||||||
"feature_service"
|
"feature_service"
|
||||||
].get_features.return_value.annotation_quota_limit.limit = 1
|
].get_features.return_value.annotation_quota_limit.limit = 1
|
||||||
|
|||||||
@ -20,9 +20,6 @@ class TestAPIBasedExtensionService:
|
|||||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||||
patch("services.api_based_extension_service.APIBasedExtensionRequestor") as mock_requestor,
|
patch("services.api_based_extension_service.APIBasedExtensionRequestor") as mock_requestor,
|
||||||
):
|
):
|
||||||
# Setup default mock returns
|
|
||||||
mock_account_feature_service.get_features.return_value.billing.enabled = False
|
|
||||||
|
|
||||||
# Mock successful ping response
|
# Mock successful ping response
|
||||||
mock_requestor_instance = mock_requestor.return_value
|
mock_requestor_instance = mock_requestor.return_value
|
||||||
mock_requestor_instance.request.return_value = {"result": "pong"}
|
mock_requestor_instance.request.return_value = {"result": "pong"}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from flask import Flask
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||||
from models.dataset import (
|
from models.dataset import (
|
||||||
AppDatasetJoin,
|
AppDatasetJoin,
|
||||||
@ -23,6 +24,7 @@ from models.enums import DataSourceType
|
|||||||
from services.dataset_ref_service import DatasetRef, DatasetRefService
|
from services.dataset_ref_service import DatasetRef, DatasetRefService
|
||||||
from services.dataset_service import DatasetCollectionBindingService, DatasetPermissionService, DatasetService
|
from services.dataset_service import DatasetCollectionBindingService, DatasetPermissionService, DatasetService
|
||||||
from services.errors.account import NoPermissionError
|
from services.errors.account import NoPermissionError
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class DatasetPermissionIntegrationFactory:
|
class DatasetPermissionIntegrationFactory:
|
||||||
@ -406,13 +408,10 @@ class TestDatasetServicePermissionsAndLifecycle:
|
|||||||
assert dataset.updated_by == owner.id
|
assert dataset.updated_by == owner.id
|
||||||
assert dataset.updated_at == now
|
assert dataset.updated_at == now
|
||||||
|
|
||||||
def test_get_dataset_auto_disable_logs_returns_empty_when_billing_is_disabled(
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
self, db_session_with_containers: Session
|
def test_get_dataset_auto_disable_logs_returns_empty_outside_cloud(self, db_session_with_containers: Session):
|
||||||
):
|
|
||||||
owner, tenant = DatasetPermissionIntegrationFactory.create_account_with_tenant(db_session_with_containers)
|
owner, tenant = DatasetPermissionIntegrationFactory.create_account_with_tenant(db_session_with_containers)
|
||||||
features = SimpleNamespace(
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan="professional")))
|
||||||
billing=SimpleNamespace(enabled=False, subscription=SimpleNamespace(plan="professional"))
|
|
||||||
)
|
|
||||||
dataset_ref = DatasetRef(tenant_id=tenant.id, dataset_id=str(uuid4()))
|
dataset_ref = DatasetRef(tenant_id=tenant.id, dataset_id=str(uuid4()))
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=features):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=features):
|
||||||
@ -420,6 +419,7 @@ class TestDatasetServicePermissionsAndLifecycle:
|
|||||||
|
|
||||||
assert result == {"document_ids": [], "count": 0}
|
assert result == {"document_ids": [], "count": 0}
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_get_dataset_auto_disable_logs_returns_recent_document_ids(self, db_session_with_containers: Session):
|
def test_get_dataset_auto_disable_logs_returns_recent_document_ids(self, db_session_with_containers: Session):
|
||||||
owner, tenant = DatasetPermissionIntegrationFactory.create_account_with_tenant(db_session_with_containers)
|
owner, tenant = DatasetPermissionIntegrationFactory.create_account_with_tenant(db_session_with_containers)
|
||||||
dataset = DatasetPermissionIntegrationFactory.create_dataset(
|
dataset = DatasetPermissionIntegrationFactory.create_dataset(
|
||||||
@ -439,9 +439,7 @@ class TestDatasetServicePermissionsAndLifecycle:
|
|||||||
dataset_id=dataset.id,
|
dataset_id=dataset.id,
|
||||||
document_id=str(uuid4()),
|
document_id=str(uuid4()),
|
||||||
)
|
)
|
||||||
features = SimpleNamespace(
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan="professional")))
|
||||||
billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan="professional"))
|
|
||||||
)
|
|
||||||
dataset_ref = DatasetRefService.create_dataset_ref(dataset)
|
dataset_ref = DatasetRefService.create_dataset_ref(dataset)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=features):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=features):
|
||||||
|
|||||||
@ -30,7 +30,6 @@ class TestFeatureService:
|
|||||||
):
|
):
|
||||||
# Setup default mock returns for BillingService
|
# Setup default mock returns for BillingService
|
||||||
mock_billing_service.get_info.return_value = {
|
mock_billing_service.get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": True},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": True},
|
||||||
"members": {"size": 5, "limit": 10},
|
"members": {"size": 5, "limit": 10},
|
||||||
"apps": {"size": 3, "limit": 20},
|
"apps": {"size": 3, "limit": 20},
|
||||||
@ -118,7 +117,6 @@ class TestFeatureService:
|
|||||||
assert isinstance(result, FeatureModel)
|
assert isinstance(result, FeatureModel)
|
||||||
|
|
||||||
# Verify billing features
|
# Verify billing features
|
||||||
assert result.billing.enabled is True
|
|
||||||
assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL
|
assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL
|
||||||
assert result.billing.subscription.interval == "monthly"
|
assert result.billing.subscription.interval == "monthly"
|
||||||
assert result.education.activated is True
|
assert result.education.activated is True
|
||||||
@ -184,7 +182,6 @@ class TestFeatureService:
|
|||||||
|
|
||||||
# Set mock return value inside the patch context
|
# Set mock return value inside the patch context
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.SANDBOX, "interval": "monthly", "education": False},
|
"subscription": {"plan": CloudPlan.SANDBOX, "interval": "monthly", "education": False},
|
||||||
"members": {"size": 1, "limit": 3},
|
"members": {"size": 1, "limit": 3},
|
||||||
"apps": {"size": 1, "limit": 5},
|
"apps": {"size": 1, "limit": 5},
|
||||||
@ -510,9 +507,6 @@ class TestFeatureService:
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, FeatureModel)
|
assert isinstance(result, FeatureModel)
|
||||||
|
|
||||||
# Verify billing is disabled
|
|
||||||
assert result.billing.enabled is False
|
|
||||||
|
|
||||||
# Verify environment-based features
|
# Verify environment-based features
|
||||||
assert result.can_replace_logo is True
|
assert result.can_replace_logo is True
|
||||||
assert result.model_load_balancing_enabled is True
|
assert result.model_load_balancing_enabled is True
|
||||||
@ -598,9 +592,6 @@ class TestFeatureService:
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, FeatureModel)
|
assert isinstance(result, FeatureModel)
|
||||||
|
|
||||||
# Cloud billing is not loaded in the Enterprise edition.
|
|
||||||
assert result.billing.enabled is False
|
|
||||||
|
|
||||||
# Verify enterprise features
|
# Verify enterprise features
|
||||||
assert result.webapp_copyright_enabled is True
|
assert result.webapp_copyright_enabled is True
|
||||||
|
|
||||||
@ -710,9 +701,6 @@ class TestFeatureService:
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, FeatureModel)
|
assert isinstance(result, FeatureModel)
|
||||||
|
|
||||||
# Billing data is not loaded without a tenant ID.
|
|
||||||
assert result.billing.enabled is False
|
|
||||||
|
|
||||||
# Verify environment-based features
|
# Verify environment-based features
|
||||||
assert result.can_replace_logo is True
|
assert result.can_replace_logo is True
|
||||||
assert result.model_load_balancing_enabled is False
|
assert result.model_load_balancing_enabled is False
|
||||||
@ -753,7 +741,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"},
|
||||||
# Missing members, apps, vector_space, etc.
|
# Missing members, apps, vector_space, etc.
|
||||||
}
|
}
|
||||||
@ -766,7 +753,6 @@ class TestFeatureService:
|
|||||||
assert isinstance(result, FeatureModel)
|
assert isinstance(result, FeatureModel)
|
||||||
|
|
||||||
# Verify billing features
|
# Verify billing features
|
||||||
assert result.billing.enabled is True
|
|
||||||
assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL
|
assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL
|
||||||
assert result.billing.subscription.interval == "yearly"
|
assert result.billing.subscription.interval == "yearly"
|
||||||
|
|
||||||
@ -814,7 +800,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"},
|
||||||
"vector_space": {"size": 0, "limit": 0},
|
"vector_space": {"size": 0, "limit": 0},
|
||||||
"apps": {"size": 5, "limit": 10},
|
"apps": {"size": 5, "limit": 10},
|
||||||
@ -931,7 +916,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"},
|
||||||
"members": {"size": 10, "limit": 10},
|
"members": {"size": 10, "limit": 10},
|
||||||
"vector_space": {"size": 3, "limit": 5},
|
"vector_space": {"size": 3, "limit": 5},
|
||||||
@ -1251,7 +1235,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"},
|
"subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"},
|
||||||
"members": {"size": 0, "limit": 0},
|
"members": {"size": 0, "limit": 0},
|
||||||
"apps": {"size": 0, "limit": -1},
|
"apps": {"size": 0, "limit": -1},
|
||||||
@ -1355,7 +1338,6 @@ class TestFeatureService:
|
|||||||
# Arrange: Setup edge case education mock
|
# Arrange: Setup edge case education mock
|
||||||
tenant_id = self._create_test_tenant_id()
|
tenant_id = self._create_test_tenant_id()
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "semester", "education": True},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "semester", "education": True},
|
||||||
"members": {"size": 100, "limit": 200},
|
"members": {"size": 100, "limit": 200},
|
||||||
"apps": {"size": 50, "limit": 100},
|
"apps": {"size": 50, "limit": 100},
|
||||||
@ -1509,7 +1491,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM, "interval": "monthly"},
|
"subscription": {"plan": CloudPlan.TEAM, "interval": "monthly"},
|
||||||
"docs_processing": "advanced",
|
"docs_processing": "advanced",
|
||||||
"can_replace_logo": True,
|
"can_replace_logo": True,
|
||||||
@ -1627,7 +1608,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"},
|
"subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"},
|
||||||
"annotation_quota_limit": {"size": 999, "limit": 1000},
|
"annotation_quota_limit": {"size": 999, "limit": 1000},
|
||||||
"knowledge_rate_limit": {"limit": 500},
|
"knowledge_rate_limit": {"limit": 500},
|
||||||
@ -1688,7 +1668,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"},
|
||||||
"documents_upload_quota": {
|
"documents_upload_quota": {
|
||||||
"size": 0, # Edge case: zero current size
|
"size": 0, # Edge case: zero current size
|
||||||
@ -1803,7 +1782,6 @@ class TestFeatureService:
|
|||||||
mock_config.EDUCATION_ENABLED = False
|
mock_config.EDUCATION_ENABLED = False
|
||||||
|
|
||||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"plan": CloudPlan.PROFESSIONAL,
|
"plan": CloudPlan.PROFESSIONAL,
|
||||||
"interval": "monthly",
|
"interval": "monthly",
|
||||||
|
|||||||
@ -33,7 +33,6 @@ class TestMessageService:
|
|||||||
patch("services.message_service.TokenBufferMemory") as mock_token_buffer_memory,
|
patch("services.message_service.TokenBufferMemory") as mock_token_buffer_memory,
|
||||||
):
|
):
|
||||||
# Setup default mock returns
|
# Setup default mock returns
|
||||||
mock_account_feature_service.get_features.return_value.billing.enabled = False
|
|
||||||
|
|
||||||
# Mock ModelManager
|
# Mock ModelManager
|
||||||
mock_model_instance = mock_model_manager.return_value.get_default_model_instance.return_value
|
mock_model_instance = mock_model_manager.return_value.get_default_model_instance.return_value
|
||||||
|
|||||||
@ -27,9 +27,9 @@ class TestWorkspaceService:
|
|||||||
# Setup default mock returns
|
# Setup default mock returns
|
||||||
feature = mock_feature_service.get_features.return_value
|
feature = mock_feature_service.get_features.return_value
|
||||||
feature.can_replace_logo = True
|
feature.can_replace_logo = True
|
||||||
feature.billing.enabled = True
|
|
||||||
feature.billing.subscription.plan = "professional"
|
feature.billing.subscription.plan = "professional"
|
||||||
mock_tenant_service.has_roles.return_value = True
|
mock_tenant_service.has_roles.return_value = True
|
||||||
|
mock_dify_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||||
mock_dify_config.FILES_URL = "https://example.com/files"
|
mock_dify_config.FILES_URL = "https://example.com/files"
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
@ -611,7 +611,6 @@ class TestWorkspaceService:
|
|||||||
mock_external_service_dependencies["dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
mock_external_service_dependencies["dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||||
feature = mock_external_service_dependencies["feature_service"].get_features.return_value
|
feature = mock_external_service_dependencies["feature_service"].get_features.return_value
|
||||||
feature.can_replace_logo = False
|
feature.can_replace_logo = False
|
||||||
feature.billing.enabled = False
|
|
||||||
mock_external_service_dependencies["tenant_service"].has_roles.return_value = False
|
mock_external_service_dependencies["tenant_service"].has_roles.return_value = False
|
||||||
|
|
||||||
with patch("services.workspace_service.current_user", account):
|
with patch("services.workspace_service.current_user", account):
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from core.indexing_runner import DocumentIsPausedError
|
from core.indexing_runner import DocumentIsPausedError
|
||||||
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
||||||
from models.dataset import Dataset, Document
|
from models.dataset import Dataset, Document
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||||
@ -22,6 +22,7 @@ from tasks.document_indexing_task import (
|
|||||||
normal_document_indexing_task,
|
normal_document_indexing_task,
|
||||||
priority_document_indexing_task,
|
priority_document_indexing_task,
|
||||||
)
|
)
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class _TrackedSessionContext:
|
class _TrackedSessionContext:
|
||||||
@ -89,7 +90,6 @@ def patched_external_dependencies():
|
|||||||
):
|
):
|
||||||
mock_runner_instance = mock_indexing_runner.return_value
|
mock_runner_instance = mock_indexing_runner.return_value
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
mock_features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
mock_features.vector_space.limit = 100
|
mock_features.vector_space.limit = 100
|
||||||
mock_features.vector_space.size = 0
|
mock_features.vector_space.size = 0
|
||||||
@ -249,6 +249,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
assert len(run_args) == len(document_ids)
|
assert len(run_args) == len(document_ids)
|
||||||
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_batch_processing_with_limit_check(
|
def test_batch_processing_with_limit_check(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -260,7 +261,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
features.vector_space.limit = 100
|
features.vector_space.limit = 100
|
||||||
features.vector_space.size = 50
|
features.vector_space.size = 50
|
||||||
@ -273,6 +273,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
patched_external_dependencies["indexing_runner_instance"].run.assert_not_called()
|
patched_external_dependencies["indexing_runner_instance"].run.assert_not_called()
|
||||||
self._assert_documents_error_contains(db_session_with_containers, document_ids, "batch upload limit")
|
self._assert_documents_error_contains(db_session_with_containers, document_ids, "batch upload limit")
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_batch_processing_sandbox_plan_single_document_only(
|
def test_batch_processing_sandbox_plan_single_document_only(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -281,7 +282,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=2)
|
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=2)
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.SANDBOX
|
features.billing.subscription.plan = CloudPlan.SANDBOX
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
@ -375,6 +375,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
task_dispatch_spy.apply_async.assert_not_called()
|
task_dispatch_spy.apply_async.assert_not_called()
|
||||||
delete_key_spy.assert_called_once()
|
delete_key_spy.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_validation_failure_sets_error_status_when_vector_space_at_limit(
|
def test_validation_failure_sets_error_status_when_vector_space_at_limit(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -383,7 +384,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
features.vector_space.limit = 100
|
features.vector_space.limit = 100
|
||||||
features.vector_space.size = 100
|
features.vector_space.size = 100
|
||||||
@ -590,6 +590,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
call_kwargs = task_dispatch_spy.apply_async.call_args_list[index].kwargs.get("kwargs", {})
|
call_kwargs = task_dispatch_spy.apply_async.call_args_list[index].kwargs.get("kwargs", {})
|
||||||
assert call_kwargs.get("document_ids") == expected_task["document_ids"]
|
assert call_kwargs.get("document_ids") == expected_task["document_ids"]
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_billing_disabled_skips_limit_checks(
|
def test_billing_disabled_skips_limit_checks(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -601,7 +602,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
document_ids=large_document_ids,
|
document_ids=large_document_ids,
|
||||||
)
|
)
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = False
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
_document_indexing(dataset.id, large_document_ids)
|
_document_indexing(dataset.id, large_document_ids)
|
||||||
@ -688,6 +688,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
# Assert
|
# Assert
|
||||||
self._assert_documents_parsing(db_session_with_containers, [special_document_id])
|
self._assert_documents_parsing(db_session_with_containers, [special_document_id])
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_zero_vector_space_limit_allows_unlimited(
|
def test_zero_vector_space_limit_allows_unlimited(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -696,7 +697,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
features.vector_space.limit = 0
|
features.vector_space.limit = 0
|
||||||
features.vector_space.size = 1000
|
features.vector_space.size = 1000
|
||||||
@ -708,6 +708,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
patched_external_dependencies["indexing_runner_instance"].run.assert_called_once()
|
patched_external_dependencies["indexing_runner_instance"].run.assert_called_once()
|
||||||
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_negative_vector_space_values_handled_gracefully(
|
def test_negative_vector_space_values_handled_gracefully(
|
||||||
self, db_session_with_containers: Session, patched_external_dependencies
|
self, db_session_with_containers: Session, patched_external_dependencies
|
||||||
):
|
):
|
||||||
@ -716,7 +717,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
dataset, documents = self._create_test_dataset_and_documents(db_session_with_containers, document_count=3)
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
features.vector_space.limit = -1
|
features.vector_space.limit = -1
|
||||||
features.vector_space.size = 100
|
features.vector_space.size = 100
|
||||||
@ -728,6 +728,7 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
patched_external_dependencies["indexing_runner_instance"].run.assert_called_once()
|
patched_external_dependencies["indexing_runner_instance"].run.assert_called_once()
|
||||||
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
self._assert_documents_parsing(db_session_with_containers, document_ids)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_large_document_batch_processing(self, db_session_with_containers: Session, patched_external_dependencies):
|
def test_large_document_batch_processing(self, db_session_with_containers: Session, patched_external_dependencies):
|
||||||
"""Process a batch exactly at configured upload limit.
|
"""Process a batch exactly at configured upload limit.
|
||||||
|
|
||||||
@ -741,7 +742,6 @@ class TestDatasetIndexingTaskIntegration:
|
|||||||
document_ids=document_ids,
|
document_ids=document_ids,
|
||||||
)
|
)
|
||||||
features = patched_external_dependencies["features"]
|
features = patched_external_dependencies["features"]
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
features.vector_space.limit = 10000
|
features.vector_space.limit = 10000
|
||||||
features.vector_space.size = 0
|
features.vector_space.size = 0
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from core.entities.document_task import DocumentTask
|
from core.entities.document_task import DocumentTask
|
||||||
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexTechniqueType
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
||||||
from models.dataset import Dataset, Document
|
from models.dataset import Dataset, Document
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||||
@ -18,6 +18,7 @@ from tasks.document_indexing_task import (
|
|||||||
normal_document_indexing_task, # New normal task
|
normal_document_indexing_task, # New normal task
|
||||||
priority_document_indexing_task, # New priority task
|
priority_document_indexing_task, # New priority task
|
||||||
)
|
)
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class TestDocumentIndexingTasks:
|
class TestDocumentIndexingTasks:
|
||||||
@ -41,7 +42,6 @@ class TestDocumentIndexingTasks:
|
|||||||
# Setup mock indexing runner
|
# Setup mock indexing runner
|
||||||
mock_runner_instance = mock_indexing_runner.return_value # Setup mock feature service
|
mock_runner_instance = mock_indexing_runner.return_value # Setup mock feature service
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
@ -147,7 +147,9 @@ class TestDocumentIndexingTasks:
|
|||||||
return dataset, documents
|
return dataset, documents
|
||||||
|
|
||||||
def _create_test_dataset_with_billing_features(
|
def _create_test_dataset_with_billing_features(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies, billing_enabled=True
|
self,
|
||||||
|
db_session_with_containers: Session,
|
||||||
|
mock_external_service_dependencies,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Helper method to create a test dataset with billing features configured.
|
Helper method to create a test dataset with billing features configured.
|
||||||
@ -155,7 +157,6 @@ class TestDocumentIndexingTasks:
|
|||||||
Args:
|
Args:
|
||||||
db_session_with_containers: Database session from testcontainers infrastructure
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
mock_external_service_dependencies: Mock dependencies
|
mock_external_service_dependencies: Mock dependencies
|
||||||
billing_enabled: Whether billing is enabled
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (dataset, documents) - Created dataset and document instances
|
tuple: (dataset, documents) - Created dataset and document instances
|
||||||
@ -224,11 +225,9 @@ class TestDocumentIndexingTasks:
|
|||||||
db_session_with_containers.commit()
|
db_session_with_containers.commit()
|
||||||
|
|
||||||
# Configure billing features
|
# Configure billing features
|
||||||
mock_external_service_dependencies["features"].billing.enabled = billing_enabled
|
mock_external_service_dependencies["features"].billing.subscription.plan = CloudPlan.SANDBOX
|
||||||
if billing_enabled:
|
mock_external_service_dependencies["features"].vector_space.limit = 100
|
||||||
mock_external_service_dependencies["features"].billing.subscription.plan = CloudPlan.SANDBOX
|
mock_external_service_dependencies["features"].vector_space.size = 50
|
||||||
mock_external_service_dependencies["features"].vector_space.limit = 100
|
|
||||||
mock_external_service_dependencies["features"].vector_space.size = 50
|
|
||||||
|
|
||||||
# Refresh dataset to ensure it's properly loaded
|
# Refresh dataset to ensure it's properly loaded
|
||||||
db_session_with_containers.refresh(dataset)
|
db_session_with_containers.refresh(dataset)
|
||||||
@ -467,6 +466,7 @@ class TestDocumentIndexingTasks:
|
|||||||
processed_documents = self._runner_documents_arg(mock_external_service_dependencies)
|
processed_documents = self._runner_documents_arg(mock_external_service_dependencies)
|
||||||
assert len(processed_documents) == 4
|
assert len(processed_documents) == 4
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_document_indexing_task_billing_sandbox_plan_batch_limit(
|
def test_document_indexing_task_billing_sandbox_plan_batch_limit(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
@ -481,7 +481,8 @@ class TestDocumentIndexingTasks:
|
|||||||
"""
|
"""
|
||||||
# Arrange: Create test data with billing enabled
|
# Arrange: Create test data with billing enabled
|
||||||
dataset, documents = self._create_test_dataset_with_billing_features(
|
dataset, documents = self._create_test_dataset_with_billing_features(
|
||||||
db_session_with_containers, mock_external_service_dependencies, billing_enabled=True
|
db_session_with_containers,
|
||||||
|
mock_external_service_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure sandbox plan with batch limit
|
# Configure sandbox plan with batch limit
|
||||||
@ -529,21 +530,23 @@ class TestDocumentIndexingTasks:
|
|||||||
# Verify no indexing runner was called
|
# Verify no indexing runner was called
|
||||||
mock_external_service_dependencies["indexing_runner"].assert_not_called()
|
mock_external_service_dependencies["indexing_runner"].assert_not_called()
|
||||||
|
|
||||||
def test_document_indexing_task_billing_disabled_success(
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
|
def test_document_indexing_task_community_success(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Test successful processing when billing is disabled.
|
Test successful processing outside Cloud.
|
||||||
|
|
||||||
This test verifies:
|
This test verifies:
|
||||||
- Processing continues normally when billing is disabled
|
- Processing continues normally outside Cloud
|
||||||
- No billing validation occurs
|
- No billing validation occurs
|
||||||
- Documents are processed successfully
|
- Documents are processed successfully
|
||||||
- IndexingRunner is called correctly
|
- IndexingRunner is called correctly
|
||||||
"""
|
"""
|
||||||
# Arrange: Create test data with billing disabled
|
# Arrange: Create test data with billing disabled
|
||||||
dataset, documents = self._create_test_dataset_with_billing_features(
|
dataset, documents = self._create_test_dataset_with_billing_features(
|
||||||
db_session_with_containers, mock_external_service_dependencies, billing_enabled=False
|
db_session_with_containers,
|
||||||
|
mock_external_service_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
document_ids = [doc.id for doc in documents]
|
document_ids = [doc.id for doc in documents]
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from core.indexing_runner import DocumentIsPausedError
|
from core.indexing_runner import DocumentIsPausedError
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||||
from models.dataset import Dataset, Document, DocumentSegment
|
from models.dataset import Dataset, Document, DocumentSegment
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
|
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
|
||||||
@ -18,6 +18,7 @@ from tasks.duplicate_document_indexing_task import (
|
|||||||
normal_duplicate_document_indexing_task, # New normal task
|
normal_duplicate_document_indexing_task, # New normal task
|
||||||
priority_duplicate_document_indexing_task, # New priority task
|
priority_duplicate_document_indexing_task, # New priority task
|
||||||
)
|
)
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class TestDuplicateDocumentIndexingTasks:
|
class TestDuplicateDocumentIndexingTasks:
|
||||||
@ -45,7 +46,6 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
# Setup mock indexing runner
|
# Setup mock indexing runner
|
||||||
mock_runner_instance = mock_indexing_runner.return_value # Setup mock feature service
|
mock_runner_instance = mock_indexing_runner.return_value # Setup mock feature service
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
|
|
||||||
# Setup mock index processor factory
|
# Setup mock index processor factory
|
||||||
@ -214,7 +214,9 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
return dataset, documents, segments
|
return dataset, documents, segments
|
||||||
|
|
||||||
def _create_test_dataset_with_billing_features(
|
def _create_test_dataset_with_billing_features(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies, billing_enabled=True
|
self,
|
||||||
|
db_session_with_containers: Session,
|
||||||
|
mock_external_service_dependencies,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Helper method to create a test dataset with billing features configured.
|
Helper method to create a test dataset with billing features configured.
|
||||||
@ -222,7 +224,6 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
Args:
|
Args:
|
||||||
db_session_with_containers: Database session from testcontainers infrastructure
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
mock_external_service_dependencies: Mock dependencies
|
mock_external_service_dependencies: Mock dependencies
|
||||||
billing_enabled: Whether billing is enabled
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (dataset, documents) - Created dataset and document instances
|
tuple: (dataset, documents) - Created dataset and document instances
|
||||||
@ -292,11 +293,9 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
db_session_with_containers.commit()
|
db_session_with_containers.commit()
|
||||||
|
|
||||||
# Configure billing features
|
# Configure billing features
|
||||||
mock_external_service_dependencies["features"].billing.enabled = billing_enabled
|
mock_external_service_dependencies["features"].billing.subscription.plan = CloudPlan.SANDBOX
|
||||||
if billing_enabled:
|
mock_external_service_dependencies["features"].vector_space.limit = 100
|
||||||
mock_external_service_dependencies["features"].billing.subscription.plan = CloudPlan.SANDBOX
|
mock_external_service_dependencies["features"].vector_space.size = 50
|
||||||
mock_external_service_dependencies["features"].vector_space.limit = 100
|
|
||||||
mock_external_service_dependencies["features"].vector_space.size = 50
|
|
||||||
|
|
||||||
# Refresh dataset to ensure it's properly loaded
|
# Refresh dataset to ensure it's properly loaded
|
||||||
db_session_with_containers.refresh(dataset)
|
db_session_with_containers.refresh(dataset)
|
||||||
@ -517,7 +516,8 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
"""
|
"""
|
||||||
# Arrange: Create test data with billing enabled
|
# Arrange: Create test data with billing enabled
|
||||||
dataset, documents = self._create_test_dataset_with_billing_features(
|
dataset, documents = self._create_test_dataset_with_billing_features(
|
||||||
db_session_with_containers, mock_external_service_dependencies, billing_enabled=True
|
db_session_with_containers,
|
||||||
|
mock_external_service_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure sandbox plan with batch limit
|
# Configure sandbox plan with batch limit
|
||||||
@ -580,7 +580,8 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
"""
|
"""
|
||||||
# Arrange: Create test data with billing enabled
|
# Arrange: Create test data with billing enabled
|
||||||
dataset, documents = self._create_test_dataset_with_billing_features(
|
dataset, documents = self._create_test_dataset_with_billing_features(
|
||||||
db_session_with_containers, mock_external_service_dependencies, billing_enabled=True
|
db_session_with_containers,
|
||||||
|
mock_external_service_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure TEAM plan with vector space limit exceeded
|
# Configure TEAM plan with vector space limit exceeded
|
||||||
@ -818,7 +819,8 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
db_session_with_containers, mock_external_service_dependencies
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_duplicate_document_indexing_with_billing_enabled_sandbox_plan(
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
def test_duplicate_document_indexing_with_cloud_sandbox_plan(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
"""Test duplicate document indexing with billing enabled and sandbox plan."""
|
"""Test duplicate document indexing with billing enabled and sandbox plan."""
|
||||||
@ -826,6 +828,7 @@ class TestDuplicateDocumentIndexingTasks:
|
|||||||
db_session_with_containers, mock_external_service_dependencies
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_duplicate_document_indexing_with_billing_limit_exceeded(
|
def test_duplicate_document_indexing_with_billing_limit_exceeded(
|
||||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||||
):
|
):
|
||||||
|
|||||||
@ -15,10 +15,12 @@ from werkzeug.datastructures import FileStorage
|
|||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console.wraps import annotation_import_concurrency_limit, annotation_import_rate_limit
|
from controllers.console.wraps import annotation_import_concurrency_limit, annotation_import_rate_limit
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.model import App, AppMode, IconType
|
from models.model import App, AppMode, IconType
|
||||||
from services.annotation_service import AppAnnotationService
|
from services.annotation_service import AppAnnotationService
|
||||||
from tasks.annotation.batch_import_annotations_task import batch_import_annotations_task
|
from tasks.annotation.batch_import_annotations_task import batch_import_annotations_task
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
def _account() -> Account:
|
def _account() -> Account:
|
||||||
@ -212,6 +214,7 @@ class TestAnnotationImportFileValidation:
|
|||||||
class TestAnnotationImportServiceValidation:
|
class TestAnnotationImportServiceValidation:
|
||||||
"""Test service layer validation for annotation import."""
|
"""Test service layer validation for annotation import."""
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_max_records_limit_enforced(self, sqlite_session: Session):
|
def test_max_records_limit_enforced(self, sqlite_session: Session):
|
||||||
"""Test that files with too many records are rejected."""
|
"""Test that files with too many records are rejected."""
|
||||||
|
|
||||||
@ -228,8 +231,6 @@ class TestAnnotationImportServiceValidation:
|
|||||||
mock_auth.return_value = (_account(), "tenant_id")
|
mock_auth.return_value = (_account(), "tenant_id")
|
||||||
|
|
||||||
with patch("services.annotation_service.FeatureService") as mock_features:
|
with patch("services.annotation_service.FeatureService") as mock_features:
|
||||||
mock_features.get_features.return_value.billing.enabled = False
|
|
||||||
|
|
||||||
result = AppAnnotationService.batch_import_app_annotations("app_id", file, sqlite_session)
|
result = AppAnnotationService.batch_import_app_annotations("app_id", file, sqlite_session)
|
||||||
|
|
||||||
# Should return error about too many records
|
# Should return error about too many records
|
||||||
@ -273,6 +274,7 @@ class TestAnnotationImportServiceValidation:
|
|||||||
assert "error_msg" in result
|
assert "error_msg" in result
|
||||||
assert "malformed" in result["error_msg"].lower()
|
assert "malformed" in result["error_msg"].lower()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_valid_import_succeeds(self, sqlite_session: Session):
|
def test_valid_import_succeeds(self, sqlite_session: Session):
|
||||||
"""Test that valid import request succeeds."""
|
"""Test that valid import request succeeds."""
|
||||||
|
|
||||||
@ -286,8 +288,6 @@ class TestAnnotationImportServiceValidation:
|
|||||||
mock_auth.return_value = (_account(), "tenant_id")
|
mock_auth.return_value = (_account(), "tenant_id")
|
||||||
|
|
||||||
with patch("services.annotation_service.FeatureService") as mock_features:
|
with patch("services.annotation_service.FeatureService") as mock_features:
|
||||||
mock_features.get_features.return_value.billing.enabled = False
|
|
||||||
|
|
||||||
with patch("services.annotation_service.batch_import_annotations_task") as mock_task:
|
with patch("services.annotation_service.batch_import_annotations_task") as mock_task:
|
||||||
with patch("services.annotation_service.redis_client"):
|
with patch("services.annotation_service.redis_client"):
|
||||||
result = AppAnnotationService.batch_import_app_annotations("app_id", file, sqlite_session)
|
result = AppAnnotationService.batch_import_app_annotations("app_id", file, sqlite_session)
|
||||||
|
|||||||
@ -128,7 +128,6 @@ def test_workflow_run_archive_endpoint_allows_admitted_role_when_rbac_is_enabled
|
|||||||
assert tenant_id == "tenant-1"
|
assert tenant_id == "tenant-1"
|
||||||
assert exclude_vector_space
|
assert exclude_vector_space
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM},
|
"subscription": {"plan": CloudPlan.TEAM},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,6 @@ def _build_feature_flags():
|
|||||||
placeholder_quota = SimpleNamespace(limit=0, size=0)
|
placeholder_quota = SimpleNamespace(limit=0, size=0)
|
||||||
workspace_members = SimpleNamespace(enabled=False, is_available=lambda count: True)
|
workspace_members = SimpleNamespace(enabled=False, is_available=lambda count: True)
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
billing=SimpleNamespace(enabled=False),
|
|
||||||
workspace_members=workspace_members,
|
workspace_members=workspace_members,
|
||||||
members=placeholder_quota,
|
members=placeholder_quota,
|
||||||
apps=placeholder_quota,
|
apps=placeholder_quota,
|
||||||
|
|||||||
@ -784,7 +784,7 @@ class TestBillingPaidPlanRequired:
|
|||||||
def paid_view():
|
def paid_view():
|
||||||
return "paid_success"
|
return "paid_success"
|
||||||
|
|
||||||
billing_info = {"enabled": True, "subscription": {"plan": plan}}
|
billing_info = {"subscription": {"plan": plan}}
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"controllers.console.wraps.current_account_with_tenant",
|
"controllers.console.wraps.current_account_with_tenant",
|
||||||
@ -797,18 +797,15 @@ class TestBillingPaidPlanRequired:
|
|||||||
assert result == "paid_success"
|
assert result == "paid_success"
|
||||||
get_info.assert_called_once_with("tenant123", exclude_vector_space=True)
|
get_info.assert_called_once_with("tenant123", exclude_vector_space=True)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("plan", ["sandbox", "unknown"])
|
||||||
("enabled", "plan"),
|
def test_should_reject_non_paid_plan(self, plan: str):
|
||||||
[(False, "professional"), (True, "sandbox"), (True, "unknown")],
|
|
||||||
)
|
|
||||||
def test_should_reject_non_paid_plan(self, enabled: bool, plan: str):
|
|
||||||
app = create_app_with_login()
|
app = create_app_with_login()
|
||||||
|
|
||||||
@cloud_edition_billing_paid_plan_required
|
@cloud_edition_billing_paid_plan_required
|
||||||
def paid_view():
|
def paid_view():
|
||||||
return "paid_success"
|
return "paid_success"
|
||||||
|
|
||||||
billing_info = {"enabled": enabled, "subscription": {"plan": plan}}
|
billing_info = {"subscription": {"plan": plan}}
|
||||||
with app.test_request_context():
|
with app.test_request_context():
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
@ -827,11 +824,11 @@ class TestBillingPaidPlanRequired:
|
|||||||
class TestBillingResourceLimits:
|
class TestBillingResourceLimits:
|
||||||
"""Test billing resource limit decorators"""
|
"""Test billing resource limit decorators"""
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_should_allow_when_under_resource_limit(self):
|
def test_should_allow_when_under_resource_limit(self):
|
||||||
"""Test that requests are allowed when under resource limits"""
|
"""Test that requests are allowed when under resource limits"""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.members.limit = 10
|
mock_features.members.limit = 10
|
||||||
mock_features.members.size = 5
|
mock_features.members.size = 5
|
||||||
|
|
||||||
@ -881,12 +878,12 @@ class TestBillingResourceLimits:
|
|||||||
get_vector_space.assert_called_once_with("tenant123")
|
get_vector_space.assert_called_once_with("tenant123")
|
||||||
get_features.assert_not_called()
|
get_features.assert_not_called()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_should_reject_when_over_resource_limit(self):
|
def test_should_reject_when_over_resource_limit(self):
|
||||||
"""Test that requests are rejected when over resource limits"""
|
"""Test that requests are rejected when over resource limits"""
|
||||||
# Arrange
|
# Arrange
|
||||||
app = create_app_with_login()
|
app = create_app_with_login()
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.members.limit = 10
|
mock_features.members.limit = 10
|
||||||
mock_features.members.size = 10
|
mock_features.members.size = 10
|
||||||
|
|
||||||
@ -906,12 +903,12 @@ class TestBillingResourceLimits:
|
|||||||
assert exc_info.value.code == 403
|
assert exc_info.value.code == 403
|
||||||
assert "members has reached the limit" in str(exc_info.value.description)
|
assert "members has reached the limit" in str(exc_info.value.description)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_should_check_source_for_documents_limit(self):
|
def test_should_check_source_for_documents_limit(self):
|
||||||
"""Test document limit checks request source"""
|
"""Test document limit checks request source"""
|
||||||
# Arrange
|
# Arrange
|
||||||
app = create_app_with_login()
|
app = create_app_with_login()
|
||||||
mock_features = MagicMock()
|
mock_features = MagicMock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.documents_upload_quota.limit = 100
|
mock_features.documents_upload_quota.limit = 100
|
||||||
mock_features.documents_upload_quota.size = 100
|
mock_features.documents_upload_quota.size = 100
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ from controllers.openapi.workspaces import (
|
|||||||
WorkspaceMembersApi,
|
WorkspaceMembersApi,
|
||||||
WorkspaceSwitchApi,
|
WorkspaceSwitchApi,
|
||||||
)
|
)
|
||||||
|
from enums import DeploymentEdition
|
||||||
from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, reset_auth_ctx, set_auth_ctx
|
from libs.oauth_bearer import AuthContext, Scope, SubjectType, TokenType, reset_auth_ctx, set_auth_ctx
|
||||||
from models import Account, Tenant, TenantAccountJoin
|
from models import Account, Tenant, TenantAccountJoin
|
||||||
from models.account import AccountStatus, TenantAccountRole, TenantStatus
|
from models.account import AccountStatus, TenantAccountRole, TenantStatus
|
||||||
@ -55,6 +56,7 @@ from services.errors.account import (
|
|||||||
NoPermissionError,
|
NoPermissionError,
|
||||||
RoleAlreadyAssignedError,
|
RoleAlreadyAssignedError,
|
||||||
)
|
)
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
if not hasattr(builtins, "MethodView"):
|
if not hasattr(builtins, "MethodView"):
|
||||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||||
@ -444,7 +446,6 @@ def test_invite_happy_path_returns_invite_url_and_member_id(
|
|||||||
|
|
||||||
def _features(
|
def _features(
|
||||||
*,
|
*,
|
||||||
billing_enabled: bool = False,
|
|
||||||
members_size: int = 0,
|
members_size: int = 0,
|
||||||
members_limit: int = 0,
|
members_limit: int = 0,
|
||||||
workspace_members_enabled: bool = False,
|
workspace_members_enabled: bool = False,
|
||||||
@ -452,17 +453,16 @@ def _features(
|
|||||||
workspace_members_limit: int = 0,
|
workspace_members_limit: int = 0,
|
||||||
) -> SimpleNamespace:
|
) -> SimpleNamespace:
|
||||||
"""Build a feature object matching the surface `_check_member_invite_quota`
|
"""Build a feature object matching the surface `_check_member_invite_quota`
|
||||||
reads: `.billing.enabled`, `.members.{size,limit}`,
|
reads: `.members.{size,limit}`,
|
||||||
`.workspace_members.{enabled, is_available(N)}`.
|
`.workspace_members.{enabled, is_available(N)}`.
|
||||||
|
|
||||||
Defaults model CE (both flags off, both caps inert).
|
Defaults leave both quotas unrestricted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _is_available(n: int) -> bool:
|
def _is_available(n: int) -> bool:
|
||||||
return workspace_members_size + n <= workspace_members_limit
|
return workspace_members_size + n <= workspace_members_limit
|
||||||
|
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
billing=SimpleNamespace(enabled=billing_enabled),
|
|
||||||
members=SimpleNamespace(size=members_size, limit=members_limit),
|
members=SimpleNamespace(size=members_size, limit=members_limit),
|
||||||
workspace_members=SimpleNamespace(
|
workspace_members=SimpleNamespace(
|
||||||
enabled=workspace_members_enabled,
|
enabled=workspace_members_enabled,
|
||||||
@ -482,6 +482,7 @@ def _invite_request(app, ws_id: str, acct_id: uuid.UUID):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_invite_blocked_by_saas_members_cap(
|
def test_invite_blocked_by_saas_members_cap(
|
||||||
app: Flask, bypass_pipeline, monkeypatch: pytest.MonkeyPatch, database_session: Session
|
app: Flask, bypass_pipeline, monkeypatch: pytest.MonkeyPatch, database_session: Session
|
||||||
):
|
):
|
||||||
@ -507,7 +508,7 @@ def test_invite_blocked_by_saas_members_cap(
|
|||||||
"FeatureService",
|
"FeatureService",
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
get_features=Mock(
|
get_features=Mock(
|
||||||
return_value=_features(billing_enabled=True, members_size=10, members_limit=10),
|
return_value=_features(members_size=10, members_limit=10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -520,13 +521,13 @@ def test_invite_blocked_by_saas_members_cap(
|
|||||||
invite_mock.assert_not_called()
|
invite_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
|
||||||
def test_invite_blocked_by_ee_workspace_members_license(
|
def test_invite_blocked_by_ee_workspace_members_license(
|
||||||
app: Flask, bypass_pipeline, monkeypatch: pytest.MonkeyPatch, database_session: Session
|
app: Flask, bypass_pipeline, monkeypatch: pytest.MonkeyPatch, database_session: Session
|
||||||
):
|
):
|
||||||
"""EE License workspace_members cap → MemberLicenseExceeded (403).
|
"""EE License workspace_members cap → MemberLicenseExceeded (403).
|
||||||
|
|
||||||
Note: billing.enabled is False (EE without SaaS billing); only the
|
Enterprise member limits come from the license.
|
||||||
license cap fires.
|
|
||||||
"""
|
"""
|
||||||
ws_id = str(uuid.uuid4())
|
ws_id = str(uuid.uuid4())
|
||||||
acct_id = uuid.uuid4()
|
acct_id = uuid.uuid4()
|
||||||
|
|||||||
@ -559,7 +559,7 @@ class TestChatApiController:
|
|||||||
completion_module = sys.modules["controllers.service_api.app.completion"]
|
completion_module = sys.modules["controllers.service_api.app.completion"]
|
||||||
apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
|
||||||
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
|
billing_get_info = Mock(return_value={"subscription": {"plan": CloudPlan.SANDBOX}})
|
||||||
generate = Mock()
|
generate = Mock()
|
||||||
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
||||||
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
||||||
@ -583,13 +583,12 @@ class TestChatApiController:
|
|||||||
assert exc_info.value.error_code == "workflow_version_execution_not_allowed"
|
assert exc_info.value.error_code == "workflow_version_execution_not_allowed"
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("deployment_edition", "billing_enabled", "plan", "workflow_id"),
|
("deployment_edition", "plan", "workflow_id"),
|
||||||
[
|
[
|
||||||
(DeploymentEdition.COMMUNITY, True, CloudPlan.SANDBOX, str(uuid.uuid4())),
|
(DeploymentEdition.COMMUNITY, CloudPlan.SANDBOX, str(uuid.uuid4())),
|
||||||
(DeploymentEdition.ENTERPRISE, True, CloudPlan.SANDBOX, str(uuid.uuid4())),
|
(DeploymentEdition.ENTERPRISE, CloudPlan.SANDBOX, str(uuid.uuid4())),
|
||||||
(DeploymentEdition.CLOUD, False, CloudPlan.SANDBOX, str(uuid.uuid4())),
|
(DeploymentEdition.CLOUD, CloudPlan.PROFESSIONAL, str(uuid.uuid4())),
|
||||||
(DeploymentEdition.CLOUD, True, CloudPlan.PROFESSIONAL, str(uuid.uuid4())),
|
(DeploymentEdition.CLOUD, CloudPlan.SANDBOX, None),
|
||||||
(DeploymentEdition.CLOUD, True, CloudPlan.SANDBOX, None),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_allows_default_or_entitled_workflow_version_execution(
|
def test_allows_default_or_entitled_workflow_version_execution(
|
||||||
@ -598,14 +597,13 @@ class TestChatApiController:
|
|||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
orm_session: Session,
|
orm_session: Session,
|
||||||
deployment_edition: DeploymentEdition,
|
deployment_edition: DeploymentEdition,
|
||||||
billing_enabled: bool,
|
|
||||||
plan: CloudPlan,
|
plan: CloudPlan,
|
||||||
workflow_id: str | None,
|
workflow_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
completion_module = sys.modules["controllers.service_api.app.completion"]
|
completion_module = sys.modules["controllers.service_api.app.completion"]
|
||||||
apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=deployment_edition)
|
apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=deployment_edition)
|
||||||
|
|
||||||
billing_get_info = Mock(return_value={"enabled": billing_enabled, "subscription": {"plan": plan}})
|
billing_get_info = Mock(return_value={"subscription": {"plan": plan}})
|
||||||
generate = Mock(return_value={"result": "ok"})
|
generate = Mock(return_value={"result": "ok"})
|
||||||
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
||||||
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
||||||
|
|||||||
@ -608,7 +608,7 @@ class TestWorkflowRunApi:
|
|||||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
workflow_module = sys.modules["controllers.service_api.app.workflow"]
|
workflow_module = sys.modules["controllers.service_api.app.workflow"]
|
||||||
|
|
||||||
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
|
billing_get_info = Mock(return_value={"subscription": {"plan": CloudPlan.SANDBOX}})
|
||||||
generate = Mock(return_value={"result": "ok"})
|
generate = Mock(return_value={"result": "ok"})
|
||||||
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
||||||
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
||||||
@ -667,7 +667,7 @@ class TestWorkflowRunByIdApi:
|
|||||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
workflow_module = sys.modules["controllers.service_api.app.workflow"]
|
workflow_module = sys.modules["controllers.service_api.app.workflow"]
|
||||||
|
|
||||||
billing_get_info = Mock(return_value={"enabled": True, "subscription": {"plan": CloudPlan.SANDBOX}})
|
billing_get_info = Mock(return_value={"subscription": {"plan": CloudPlan.SANDBOX}})
|
||||||
generate = Mock()
|
generate = Mock()
|
||||||
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
||||||
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
||||||
@ -700,12 +700,11 @@ class TestWorkflowRunByIdApi:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("deployment_edition", "billing_enabled", "plan"),
|
("deployment_edition", "plan"),
|
||||||
[
|
[
|
||||||
(DeploymentEdition.COMMUNITY, True, CloudPlan.SANDBOX),
|
(DeploymentEdition.COMMUNITY, CloudPlan.SANDBOX),
|
||||||
(DeploymentEdition.ENTERPRISE, True, CloudPlan.SANDBOX),
|
(DeploymentEdition.ENTERPRISE, CloudPlan.SANDBOX),
|
||||||
(DeploymentEdition.CLOUD, False, CloudPlan.SANDBOX),
|
(DeploymentEdition.CLOUD, CloudPlan.PROFESSIONAL),
|
||||||
(DeploymentEdition.CLOUD, True, CloudPlan.PROFESSIONAL),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_allows_execution_outside_enabled_sandbox_plan(
|
def test_allows_execution_outside_enabled_sandbox_plan(
|
||||||
@ -713,14 +712,13 @@ class TestWorkflowRunByIdApi:
|
|||||||
app: Flask,
|
app: Flask,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
deployment_edition: DeploymentEdition,
|
deployment_edition: DeploymentEdition,
|
||||||
billing_enabled: bool,
|
|
||||||
plan: CloudPlan,
|
plan: CloudPlan,
|
||||||
sqlite_session: Session,
|
sqlite_session: Session,
|
||||||
config_overrides: Callable[..., None],
|
config_overrides: Callable[..., None],
|
||||||
) -> None:
|
) -> None:
|
||||||
config_overrides(DEPLOYMENT_EDITION=deployment_edition)
|
config_overrides(DEPLOYMENT_EDITION=deployment_edition)
|
||||||
|
|
||||||
billing_get_info = Mock(return_value={"enabled": billing_enabled, "subscription": {"plan": plan}})
|
billing_get_info = Mock(return_value={"subscription": {"plan": plan}})
|
||||||
generate = Mock(return_value={"result": "ok"})
|
generate = Mock(return_value={"result": "ok"})
|
||||||
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
monkeypatch.setattr(BillingService, "get_info", billing_get_info)
|
||||||
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
monkeypatch.setattr(AppGenerateService, "generate", generate)
|
||||||
|
|||||||
@ -1212,7 +1212,6 @@ class TestSegmentApiPost(SQLiteEndpointTest):
|
|||||||
mock_validate_token.return_value = _api_token(tenant_id)
|
mock_validate_token.return_value = _api_token(tenant_id)
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
|
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
@ -1555,7 +1554,6 @@ class TestDatasetSegmentApiUpdate(SQLiteEndpointTest):
|
|||||||
"""Configure mocks to neutralise billing/auth decorators."""
|
"""Configure mocks to neutralise billing/auth decorators."""
|
||||||
mock_validate_token.return_value = _api_token(tenant_id)
|
mock_validate_token.return_value = _api_token(tenant_id)
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
mock_vector_space.limit = 10
|
mock_vector_space.limit = 10
|
||||||
@ -2054,7 +2052,6 @@ class TestChildChunkApiPost(SQLiteEndpointTest):
|
|||||||
def _setup_billing_mocks(mock_validate_token, mock_feature_svc, tenant_id: str):
|
def _setup_billing_mocks(mock_validate_token, mock_feature_svc, tenant_id: str):
|
||||||
mock_validate_token.return_value = _api_token(tenant_id)
|
mock_validate_token.return_value = _api_token(tenant_id)
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
mock_vector_space.limit = 10
|
mock_vector_space.limit = 10
|
||||||
@ -2368,7 +2365,6 @@ class TestModelValidateDecorator(SQLiteEndpointTest):
|
|||||||
mock_validate_token.return_value = _api_token(tenant_id)
|
mock_validate_token.return_value = _api_token(tenant_id)
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
|
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
|
|||||||
@ -48,6 +48,7 @@ from controllers.service_api.dataset.document import (
|
|||||||
)
|
)
|
||||||
from controllers.service_api.dataset.error import ArchivedDocumentImmutableError
|
from controllers.service_api.dataset.error import ArchivedDocumentImmutableError
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType
|
from core.rag.index_processor.constant.index_type import IndexStructureType
|
||||||
|
from enums import DeploymentEdition
|
||||||
from extensions.storage.storage_type import StorageType
|
from extensions.storage.storage_type import StorageType
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.dataset import Dataset, Document, DocumentSegment
|
from models.dataset import Dataset, Document, DocumentSegment
|
||||||
@ -65,6 +66,7 @@ from services.dataset_ref_service import DatasetRef
|
|||||||
from services.dataset_service import DocumentService
|
from services.dataset_service import DocumentService
|
||||||
from services.entities.knowledge_entities.knowledge_entities import ProcessRule, RetrievalModel
|
from services.entities.knowledge_entities.knowledge_entities import ProcessRule, RetrievalModel
|
||||||
from services.errors.file import FileTooLargeError as FileTooLargeServiceError
|
from services.errors.file import FileTooLargeError as FileTooLargeServiceError
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
def _document_data_source_info() -> dict[str, str]:
|
def _document_data_source_info() -> dict[str, str]:
|
||||||
@ -658,6 +660,7 @@ class TestDocumentServiceFileOperations:
|
|||||||
class TestDocumentServiceSaveValidation:
|
class TestDocumentServiceSaveValidation:
|
||||||
"""Test validations during document saving."""
|
"""Test validations during document saving."""
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
@patch("services.dataset_service.DatasetService.check_doc_form")
|
@patch("services.dataset_service.DatasetService.check_doc_form")
|
||||||
@patch("services.dataset_service.FeatureService.get_features")
|
@patch("services.dataset_service.FeatureService.get_features")
|
||||||
def test_save_document_validates_doc_form(self, mock_features, mock_check_form, sqlite_session: Session):
|
def test_save_document_validates_doc_form(self, mock_features, mock_check_form, sqlite_session: Session):
|
||||||
@ -665,7 +668,6 @@ class TestDocumentServiceSaveValidation:
|
|||||||
dataset = make_dataset(tenant_id="tenant_id")
|
dataset = make_dataset(tenant_id="tenant_id")
|
||||||
config = Mock()
|
config = Mock()
|
||||||
features = Mock()
|
features = Mock()
|
||||||
features.billing.enabled = False
|
|
||||||
mock_features.return_value = features
|
mock_features.return_value = features
|
||||||
|
|
||||||
class TestStopError(Exception):
|
class TestStopError(Exception):
|
||||||
@ -1416,7 +1418,6 @@ class TestDocumentAddByTextApi(SQLiteControllerTest):
|
|||||||
mock_validate_token.return_value = api_token
|
mock_validate_token.return_value = api_token
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
|
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
@ -1591,7 +1592,6 @@ def _setup_billing_mocks(mock_validate_token, mock_feature_svc, tenant_id: str):
|
|||||||
api_token = ApiToken(tenant_id=tenant_id, type=ApiTokenType.DATASET, token="dataset-token")
|
api_token = ApiToken(tenant_id=tenant_id, type=ApiTokenType.DATASET, token="dataset-token")
|
||||||
mock_validate_token.return_value = api_token
|
mock_validate_token.return_value = api_token
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_feature_svc.get_features.return_value = mock_features
|
mock_feature_svc.get_features.return_value = mock_features
|
||||||
mock_vector_space = Mock()
|
mock_vector_space = Mock()
|
||||||
mock_vector_space.limit = 10
|
mock_vector_space.limit = 10
|
||||||
|
|||||||
@ -314,6 +314,7 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
app.config["TESTING"] = True
|
app.config["TESTING"] = True
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_allows_when_under_limit(self, mock_get_features, mock_validate_token, app: Flask):
|
def test_allows_when_under_limit(self, mock_get_features, mock_validate_token, app: Flask):
|
||||||
@ -322,7 +323,6 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.members.limit = 10
|
mock_features.members.limit = 10
|
||||||
mock_features.members.size = 5
|
mock_features.members.size = 5
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
@ -381,7 +381,6 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
mock_get_vector_space.return_value = Mock(size=0, limit=50, usage_unknown=True)
|
mock_get_vector_space.return_value = Mock(size=0, limit=50, usage_unknown=True)
|
||||||
mock_get_features.return_value = SimpleNamespace(
|
mock_get_features.return_value = SimpleNamespace(
|
||||||
billing=SimpleNamespace(
|
billing=SimpleNamespace(
|
||||||
enabled=True,
|
|
||||||
subscription=SimpleNamespace(plan=CloudPlan.SANDBOX),
|
subscription=SimpleNamespace(plan=CloudPlan.SANDBOX),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -411,7 +410,6 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
mock_get_vector_space.return_value = Mock(size=0, limit=50, usage_unknown=True)
|
mock_get_vector_space.return_value = Mock(size=0, limit=50, usage_unknown=True)
|
||||||
mock_get_features.return_value = SimpleNamespace(
|
mock_get_features.return_value = SimpleNamespace(
|
||||||
billing=SimpleNamespace(
|
billing=SimpleNamespace(
|
||||||
enabled=True,
|
|
||||||
subscription=SimpleNamespace(plan=plan),
|
subscription=SimpleNamespace(plan=plan),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -429,6 +427,7 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
assert result == "document_uploaded"
|
assert result == "document_uploaded"
|
||||||
mock_get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
|
mock_get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_loads_features_when_checking_non_vector_space_limit(
|
def test_loads_features_when_checking_non_vector_space_limit(
|
||||||
@ -439,7 +438,6 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.documents_upload_quota.limit = 10
|
mock_features.documents_upload_quota.limit = 10
|
||||||
mock_features.documents_upload_quota.size = 5
|
mock_features.documents_upload_quota.size = 5
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
@ -456,6 +454,7 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
assert result == "document_uploaded"
|
assert result == "document_uploaded"
|
||||||
mock_get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
|
mock_get_features.assert_called_once_with("tenant123", exclude_vector_space=True)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_rejects_when_at_limit(self, mock_get_features, mock_validate_token, app: Flask):
|
def test_rejects_when_at_limit(self, mock_get_features, mock_validate_token, app: Flask):
|
||||||
@ -464,7 +463,6 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.members.limit = 10
|
mock_features.members.limit = 10
|
||||||
mock_features.members.size = 10
|
mock_features.members.size = 10
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
@ -479,15 +477,15 @@ class TestCloudEditionBillingResourceCheck:
|
|||||||
add_member()
|
add_member()
|
||||||
assert "members has reached the limit" in str(exc_info.value)
|
assert "members has reached the limit" in str(exc_info.value)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_allows_when_billing_disabled(self, mock_get_features, mock_validate_token, app: Flask):
|
def test_allows_when_billing_disabled(self, mock_get_features, mock_validate_token, app: Flask):
|
||||||
"""Test that request is allowed when billing is disabled."""
|
"""Test that request is allowed outside Cloud."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = False
|
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
|
|
||||||
@cloud_edition_billing_resource_check("members", "app")
|
@cloud_edition_billing_resource_check("members", "app")
|
||||||
@ -512,6 +510,7 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
|
|||||||
app.config["TESTING"] = True
|
app.config["TESTING"] = True
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_rejects_add_segment_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
|
def test_rejects_add_segment_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
|
||||||
@ -520,7 +519,6 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
|
|||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.billing.subscription.plan = CloudPlan.SANDBOX
|
mock_features.billing.subscription.plan = CloudPlan.SANDBOX
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
|
|
||||||
@ -534,6 +532,7 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
|
|||||||
add_segment()
|
add_segment()
|
||||||
assert "upgrade to a paid plan" in str(exc_info.value)
|
assert "upgrade to a paid plan" in str(exc_info.value)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||||
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
@patch("controllers.service_api.wraps.FeatureService.get_features")
|
||||||
def test_allows_other_operations_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
|
def test_allows_other_operations_in_sandbox(self, mock_get_features, mock_validate_token, app: Flask):
|
||||||
@ -542,7 +541,6 @@ class TestCloudEditionBillingKnowledgeLimitCheck:
|
|||||||
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
mock_validate_token.return_value = Mock(tenant_id="tenant123")
|
||||||
|
|
||||||
mock_features = Mock()
|
mock_features = Mock()
|
||||||
mock_features.billing.enabled = True
|
|
||||||
mock_features.billing.subscription.plan = CloudPlan.SANDBOX
|
mock_features.billing.subscription.plan = CloudPlan.SANDBOX
|
||||||
mock_get_features.return_value = mock_features
|
mock_get_features.return_value = mock_features
|
||||||
|
|
||||||
|
|||||||
@ -273,10 +273,9 @@ def _make_lock_context() -> MagicMock:
|
|||||||
return context_manager
|
return context_manager
|
||||||
|
|
||||||
|
|
||||||
def _make_features(*, enabled: bool, plan: str = CloudPlan.PROFESSIONAL) -> SimpleNamespace:
|
def _make_features(*, plan: str = CloudPlan.PROFESSIONAL) -> SimpleNamespace:
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
billing=SimpleNamespace(
|
billing=SimpleNamespace(
|
||||||
enabled=enabled,
|
|
||||||
subscription=SimpleNamespace(plan=plan),
|
subscription=SimpleNamespace(plan=plan),
|
||||||
),
|
),
|
||||||
documents_upload_quota=SimpleNamespace(limit=1000, size=0),
|
documents_upload_quota=SimpleNamespace(limit=1000, size=0),
|
||||||
|
|||||||
@ -5,11 +5,12 @@ from unittest.mock import Mock
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from extensions.storage.storage_type import StorageType
|
from extensions.storage.storage_type import StorageType
|
||||||
from models.enums import CreatorUserRole
|
from models.enums import CreatorUserRole
|
||||||
from models.model import UploadFile
|
from models.model import UploadFile
|
||||||
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
|
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -53,13 +54,12 @@ def test_delay_with_entities_calls_dispatch(mocker: MockerFixture, proxy) -> Non
|
|||||||
# --- _dispatch ---
|
# --- _dispatch ---
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_dispatch_billing_sandbox_uses_default_tenant_queue(mocker: MockerFixture, proxy) -> None:
|
def test_dispatch_billing_sandbox_uses_default_tenant_queue(mocker: MockerFixture, proxy) -> None:
|
||||||
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
||||||
send_mock = mocker.patch.object(proxy, "_send_to_default_tenant_queue")
|
send_mock = mocker.patch.object(proxy, "_send_to_default_tenant_queue")
|
||||||
|
|
||||||
features = SimpleNamespace(
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan=CloudPlan.SANDBOX)))
|
||||||
billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.SANDBOX))
|
|
||||||
)
|
|
||||||
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
||||||
|
|
||||||
proxy._dispatch()
|
proxy._dispatch()
|
||||||
@ -68,13 +68,12 @@ def test_dispatch_billing_sandbox_uses_default_tenant_queue(mocker: MockerFixtur
|
|||||||
send_mock.assert_called_once_with("file-1")
|
send_mock.assert_called_once_with("file-1")
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_dispatch_billing_non_sandbox_uses_priority_tenant_queue(mocker: MockerFixture, proxy) -> None:
|
def test_dispatch_billing_non_sandbox_uses_priority_tenant_queue(mocker: MockerFixture, proxy) -> None:
|
||||||
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
||||||
send_mock = mocker.patch.object(proxy, "_send_to_priority_tenant_queue")
|
send_mock = mocker.patch.object(proxy, "_send_to_priority_tenant_queue")
|
||||||
|
|
||||||
features = SimpleNamespace(
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan=CloudPlan.PROFESSIONAL)))
|
||||||
billing=SimpleNamespace(enabled=True, subscription=SimpleNamespace(plan=CloudPlan.PROFESSIONAL))
|
|
||||||
)
|
|
||||||
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
||||||
|
|
||||||
proxy._dispatch()
|
proxy._dispatch()
|
||||||
@ -83,11 +82,12 @@ def test_dispatch_billing_non_sandbox_uses_priority_tenant_queue(mocker: MockerF
|
|||||||
send_mock.assert_called_once_with("file-1")
|
send_mock.assert_called_once_with("file-1")
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_dispatch_no_billing_uses_priority_direct_queue(mocker: MockerFixture, proxy) -> None:
|
def test_dispatch_no_billing_uses_priority_direct_queue(mocker: MockerFixture, proxy) -> None:
|
||||||
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
upload_mock = mocker.patch.object(proxy, "_upload_invoke_entities", return_value="file-1")
|
||||||
send_mock = mocker.patch.object(proxy, "_send_to_priority_direct_queue")
|
send_mock = mocker.patch.object(proxy, "_send_to_priority_direct_queue")
|
||||||
|
|
||||||
features = SimpleNamespace(billing=SimpleNamespace(enabled=False, subscription=SimpleNamespace(plan="free")))
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan="free")))
|
||||||
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
||||||
|
|
||||||
proxy._dispatch()
|
proxy._dispatch()
|
||||||
@ -96,10 +96,11 @@ def test_dispatch_no_billing_uses_priority_direct_queue(mocker: MockerFixture, p
|
|||||||
send_mock.assert_called_once_with("file-1")
|
send_mock.assert_called_once_with("file-1")
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_dispatch_raises_on_empty_upload_file_id(mocker: MockerFixture, proxy) -> None:
|
def test_dispatch_raises_on_empty_upload_file_id(mocker: MockerFixture, proxy) -> None:
|
||||||
mocker.patch.object(proxy, "_upload_invoke_entities", return_value="")
|
mocker.patch.object(proxy, "_upload_invoke_entities", return_value="")
|
||||||
|
|
||||||
features = SimpleNamespace(billing=SimpleNamespace(enabled=False, subscription=SimpleNamespace(plan="free")))
|
features = SimpleNamespace(billing=SimpleNamespace(subscription=SimpleNamespace(plan="free")))
|
||||||
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
mocker.patch.object(type(proxy), "features", new_callable=lambda: property(lambda self: features))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="upload_file_id is empty"):
|
with pytest.raises(ValueError, match="upload_file_id is empty"):
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from werkzeug.datastructures import FileStorage
|
|||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
import services.annotation_service as annotation_service_module
|
import services.annotation_service as annotation_service_module
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.dataset import DatasetCollectionBinding
|
from models.dataset import DatasetCollectionBinding
|
||||||
from models.enums import CollectionBindingType
|
from models.enums import CollectionBindingType
|
||||||
@ -37,6 +38,7 @@ from models.model import (
|
|||||||
)
|
)
|
||||||
from services.annotation_service import AppAnnotationService
|
from services.annotation_service import AppAnnotationService
|
||||||
from services.app_ref_service import AnnotationRef, AppRef
|
from services.app_ref_service import AnnotationRef, AppRef
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
TENANT_ID = "tenant-1"
|
TENANT_ID = "tenant-1"
|
||||||
OTHER_TENANT_ID = "tenant-2"
|
OTHER_TENANT_ID = "tenant-2"
|
||||||
@ -593,16 +595,13 @@ class TestAppAnnotationServiceBatchImport:
|
|||||||
features: Any | None = None,
|
features: Any | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if features is None:
|
if features is None:
|
||||||
features = SimpleNamespace(billing=SimpleNamespace(enabled=False), annotation_quota_limit=None)
|
features = SimpleNamespace(annotation_quota_limit=None)
|
||||||
with (
|
with (
|
||||||
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
||||||
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
||||||
patch(
|
config_overrides_context(
|
||||||
"configs.dify_config",
|
ANNOTATION_IMPORT_MAX_RECORDS=maximum,
|
||||||
new=SimpleNamespace(
|
ANNOTATION_IMPORT_MIN_RECORDS=minimum,
|
||||||
ANNOTATION_IMPORT_MAX_RECORDS=maximum,
|
|
||||||
ANNOTATION_IMPORT_MIN_RECORDS=minimum,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
return AppAnnotationService.batch_import_app_annotations(app.id, _file(content), sqlite_session)
|
return AppAnnotationService.batch_import_app_annotations(app.id, _file(content), sqlite_session)
|
||||||
@ -678,10 +677,10 @@ class TestAppAnnotationServiceBatchImport:
|
|||||||
|
|
||||||
assert "at least" in cast(str, result["error_msg"])
|
assert "at least" in cast(str, result["error_msg"])
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_rejects_subscription_quota_overflow(self, sqlite_session: Session, current_user: Account) -> None:
|
def test_rejects_subscription_quota_overflow(self, sqlite_session: Session, current_user: Account) -> None:
|
||||||
app = _persist_app(sqlite_session)
|
app = _persist_app(sqlite_session)
|
||||||
features = SimpleNamespace(
|
features = SimpleNamespace(
|
||||||
billing=SimpleNamespace(enabled=True),
|
|
||||||
annotation_quota_limit=SimpleNamespace(limit=1, size=1),
|
annotation_quota_limit=SimpleNamespace(limit=1, size=1),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -694,10 +693,11 @@ class TestAppAnnotationServiceBatchImport:
|
|||||||
|
|
||||||
assert "exceeds the limit" in cast(str, result["error_msg"])
|
assert "exceeds the limit" in cast(str, result["error_msg"])
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_valid_import_enqueues_job(self, sqlite_session: Session, current_user: Account) -> None:
|
def test_valid_import_enqueues_job(self, sqlite_session: Session, current_user: Account) -> None:
|
||||||
app = _persist_app(sqlite_session)
|
app = _persist_app(sqlite_session)
|
||||||
dataframe = pd.DataFrame({"q": ["q1"], "a": ["a1"]})
|
dataframe = pd.DataFrame({"q": ["q1"], "a": ["a1"]})
|
||||||
features = SimpleNamespace(billing=SimpleNamespace(enabled=False), annotation_quota_limit=None)
|
features = SimpleNamespace(annotation_quota_limit=None)
|
||||||
with (
|
with (
|
||||||
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
||||||
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
||||||
@ -705,10 +705,7 @@ class TestAppAnnotationServiceBatchImport:
|
|||||||
patch.object(annotation_service_module, "redis_client") as redis,
|
patch.object(annotation_service_module, "redis_client") as redis,
|
||||||
patch.object(annotation_service_module.uuid, "uuid4", return_value="uuid-3"),
|
patch.object(annotation_service_module.uuid, "uuid4", return_value="uuid-3"),
|
||||||
patch.object(annotation_service_module, "naive_utc_now", return_value=datetime.fromtimestamp(1)),
|
patch.object(annotation_service_module, "naive_utc_now", return_value=datetime.fromtimestamp(1)),
|
||||||
patch(
|
config_overrides_context(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1),
|
||||||
"configs.dify_config",
|
|
||||||
new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1),
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
result = AppAnnotationService.batch_import_app_annotations(
|
result = AppAnnotationService.batch_import_app_annotations(
|
||||||
app.id, _file(b"question,answer\nq,a\n"), sqlite_session
|
app.id, _file(b"question,answer\nq,a\n"), sqlite_session
|
||||||
@ -722,22 +719,20 @@ class TestAppAnnotationServiceBatchImport:
|
|||||||
"uuid-3", [{"question": "q1", "answer": "a1"}], app.id, TENANT_ID, current_user.id
|
"uuid-3", [{"question": "q1", "answer": "a1"}], app.id, TENANT_ID, current_user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_unexpected_error_cleans_active_job(
|
def test_unexpected_error_cleans_active_job(
|
||||||
self, sqlite_session: Session, current_user: Account, caplog: pytest.LogCaptureFixture
|
self, sqlite_session: Session, current_user: Account, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
app = _persist_app(sqlite_session)
|
app = _persist_app(sqlite_session)
|
||||||
dataframe = pd.DataFrame({"q": ["q1"], "a": ["a1"]})
|
dataframe = pd.DataFrame({"q": ["q1"], "a": ["a1"]})
|
||||||
features = SimpleNamespace(billing=SimpleNamespace(enabled=False), annotation_quota_limit=None)
|
features = SimpleNamespace(annotation_quota_limit=None)
|
||||||
with (
|
with (
|
||||||
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
patch.object(annotation_service_module.pd, "read_csv", return_value=dataframe),
|
||||||
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
patch.object(annotation_service_module.FeatureService, "get_features", return_value=features),
|
||||||
patch.object(annotation_service_module, "redis_client") as redis,
|
patch.object(annotation_service_module, "redis_client") as redis,
|
||||||
patch.object(annotation_service_module.uuid, "uuid4", return_value="uuid-4"),
|
patch.object(annotation_service_module.uuid, "uuid4", return_value="uuid-4"),
|
||||||
patch.object(annotation_service_module, "naive_utc_now", return_value=datetime.fromtimestamp(1)),
|
patch.object(annotation_service_module, "naive_utc_now", return_value=datetime.fromtimestamp(1)),
|
||||||
patch(
|
config_overrides_context(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1),
|
||||||
"configs.dify_config",
|
|
||||||
new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1),
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
redis.zadd.side_effect = RuntimeError("boom")
|
redis.zadd.side_effect = RuntimeError("boom")
|
||||||
redis.zrem.side_effect = RuntimeError("cleanup-failed")
|
redis.zrem.side_effect = RuntimeError("cleanup-failed")
|
||||||
|
|||||||
@ -5,8 +5,9 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.entities.document_task import DocumentTask
|
from core.entities.document_task import DocumentTask
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from services.document_indexing_proxy.batch_indexing_base import BatchDocumentIndexingProxy
|
from services.document_indexing_proxy.batch_indexing_base import BatchDocumentIndexingProxy
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Concrete subclass for testing (the base class is abstract)
|
# Concrete subclass for testing (the base class is abstract)
|
||||||
@ -275,13 +276,13 @@ class TestSendToTenantQueue:
|
|||||||
class TestDispatchRouting:
|
class TestDispatchRouting:
|
||||||
"""Tests for the _dispatch / delay routing logic inherited from the base class."""
|
"""Tests for the _dispatch / delay routing logic inherited from the base class."""
|
||||||
|
|
||||||
def _mock_features(self, enabled: bool, plan: CloudPlan) -> MagicMock:
|
def _mock_features(self, plan: CloudPlan) -> MagicMock:
|
||||||
features = MagicMock()
|
features = MagicMock()
|
||||||
features.billing.enabled = enabled
|
|
||||||
features.billing.subscription.plan = plan
|
features.billing.subscription.plan = plan
|
||||||
return features
|
return features
|
||||||
|
|
||||||
def test_should_send_to_normal_tenant_queue_when_billing_enabled_and_sandbox_plan(self) -> None:
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
def test_should_send_to_normal_tenant_queue_in_cloud_with_sandbox_plan(self) -> None:
|
||||||
"""Sandbox plan routes to normal priority queue with tenant isolation."""
|
"""Sandbox plan routes to normal priority queue with tenant isolation."""
|
||||||
# Arrange
|
# Arrange
|
||||||
proxy = make_proxy()
|
proxy = make_proxy()
|
||||||
@ -289,7 +290,7 @@ class TestDispatchRouting:
|
|||||||
proxy._tenant_isolated_task_queue.get_task_key.return_value = None
|
proxy._tenant_isolated_task_queue.get_task_key.return_value = None
|
||||||
|
|
||||||
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
||||||
mock_features.return_value = self._mock_features(enabled=True, plan=CloudPlan.SANDBOX)
|
mock_features.return_value = self._mock_features(plan=CloudPlan.SANDBOX)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with patch.object(proxy, "_send_to_default_tenant_queue") as mock_method:
|
with patch.object(proxy, "_send_to_default_tenant_queue") as mock_method:
|
||||||
@ -298,13 +299,14 @@ class TestDispatchRouting:
|
|||||||
# Assert
|
# Assert
|
||||||
mock_method.assert_called_once()
|
mock_method.assert_called_once()
|
||||||
|
|
||||||
def test_should_send_to_priority_tenant_queue_when_billing_enabled_and_paid_plan(self) -> None:
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
def test_should_send_to_priority_tenant_queue_in_cloud_with_paid_plan(self) -> None:
|
||||||
"""Non-sandbox paid plan routes to priority queue with tenant isolation."""
|
"""Non-sandbox paid plan routes to priority queue with tenant isolation."""
|
||||||
# Arrange
|
# Arrange
|
||||||
proxy = make_proxy()
|
proxy = make_proxy()
|
||||||
|
|
||||||
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
||||||
mock_features.return_value = self._mock_features(enabled=True, plan=CloudPlan.PROFESSIONAL)
|
mock_features.return_value = self._mock_features(plan=CloudPlan.PROFESSIONAL)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with patch.object(proxy, "_send_to_priority_tenant_queue") as mock_method:
|
with patch.object(proxy, "_send_to_priority_tenant_queue") as mock_method:
|
||||||
@ -313,13 +315,14 @@ class TestDispatchRouting:
|
|||||||
# Assert
|
# Assert
|
||||||
mock_method.assert_called_once()
|
mock_method.assert_called_once()
|
||||||
|
|
||||||
def test_should_send_to_priority_direct_queue_when_billing_not_enabled(self) -> None:
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
|
def test_should_send_to_priority_direct_queue_outside_cloud(self) -> None:
|
||||||
"""Self-hosted / no billing → priority direct queue (no tenant isolation)."""
|
"""Self-hosted / no billing → priority direct queue (no tenant isolation)."""
|
||||||
# Arrange
|
# Arrange
|
||||||
proxy = make_proxy()
|
proxy = make_proxy()
|
||||||
|
|
||||||
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
||||||
mock_features.return_value = self._mock_features(enabled=False, plan=CloudPlan.SANDBOX)
|
mock_features.return_value = self._mock_features(plan=CloudPlan.SANDBOX)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with patch.object(proxy, "_send_to_priority_direct_queue") as mock_method:
|
with patch.object(proxy, "_send_to_priority_direct_queue") as mock_method:
|
||||||
@ -340,19 +343,20 @@ class TestDispatchRouting:
|
|||||||
# Assert
|
# Assert
|
||||||
mock_dispatch.assert_called_once()
|
mock_dispatch.assert_called_once()
|
||||||
|
|
||||||
def test_should_use_feature_service_for_billing_info(self) -> None:
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
"""Verify that FeatureService.get_features is consulted during dispatch."""
|
def test_should_skip_feature_service_outside_cloud(self) -> None:
|
||||||
|
"""Self-hosted dispatch does not load Cloud plan data."""
|
||||||
# Arrange
|
# Arrange
|
||||||
proxy = make_proxy()
|
proxy = make_proxy()
|
||||||
|
|
||||||
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
with patch("services.document_indexing_proxy.base.FeatureService.get_features") as mock_features:
|
||||||
mock_features.return_value = self._mock_features(enabled=False, plan=CloudPlan.SANDBOX)
|
mock_features.return_value = self._mock_features(plan=CloudPlan.SANDBOX)
|
||||||
with patch.object(proxy, "_send_to_priority_direct_queue"):
|
with patch.object(proxy, "_send_to_priority_direct_queue"):
|
||||||
# Act
|
# Act
|
||||||
proxy._dispatch()
|
proxy._dispatch()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
mock_features.assert_called_once_with(TENANT_ID, exclude_vector_space=True)
|
mock_features.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TestBaseRouterHelpers:
|
class TestBaseRouterHelpers:
|
||||||
|
|||||||
@ -430,7 +430,6 @@ class TestBillingServiceSubscriptionInfo:
|
|||||||
# Arrange
|
# Arrange
|
||||||
tenant_id = "tenant-123"
|
tenant_id = "tenant-123"
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
||||||
"members": {"size": 1, "limit": 50},
|
"members": {"size": 1, "limit": 50},
|
||||||
"apps": {"size": 1, "limit": 200},
|
"apps": {"size": 1, "limit": 200},
|
||||||
@ -458,7 +457,6 @@ class TestBillingServiceSubscriptionInfo:
|
|||||||
# Arrange
|
# Arrange
|
||||||
tenant_id = "tenant-123"
|
tenant_id = "tenant-123"
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
||||||
"members": {"size": 1, "limit": 50},
|
"members": {"size": 1, "limit": 50},
|
||||||
"apps": {"size": 1, "limit": 200},
|
"apps": {"size": 1, "limit": 200},
|
||||||
@ -488,7 +486,6 @@ class TestBillingServiceSubscriptionInfo:
|
|||||||
# Arrange
|
# Arrange
|
||||||
tenant_id = "tenant-123"
|
tenant_id = "tenant-123"
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
||||||
"members": {"size": 1, "limit": 50},
|
"members": {"size": 1, "limit": 50},
|
||||||
"apps": {"size": 1, "limit": 200},
|
"apps": {"size": 1, "limit": 200},
|
||||||
@ -544,7 +541,6 @@ class TestBillingServiceSubscriptionInfo:
|
|||||||
def test_get_info_preserves_unknown_vector_space_usage(self, mock_send_request):
|
def test_get_info_preserves_unknown_vector_space_usage(self, mock_send_request):
|
||||||
tenant_id = "tenant-123"
|
tenant_id = "tenant-123"
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": "sandbox", "interval": "", "education": False},
|
"subscription": {"plan": "sandbox", "interval": "", "education": False},
|
||||||
"members": {"size": 1, "limit": 1},
|
"members": {"size": 1, "limit": 1},
|
||||||
"apps": {"size": 1, "limit": 10},
|
"apps": {"size": 1, "limit": 10},
|
||||||
@ -1750,7 +1746,6 @@ class TestBillingServiceIntegrationScenarios:
|
|||||||
|
|
||||||
# Step 1: Get current billing info
|
# Step 1: Get current billing info
|
||||||
mock_send_request.return_value = {
|
mock_send_request.return_value = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": "sandbox", "interval": "", "education": False},
|
"subscription": {"plan": "sandbox", "interval": "", "education": False},
|
||||||
"members": {"size": 0, "limit": 1},
|
"members": {"size": 0, "limit": 1},
|
||||||
"apps": {"size": 0, "limit": 5},
|
"apps": {"size": 0, "limit": 5},
|
||||||
@ -1822,7 +1817,6 @@ class TestBillingServiceSubscriptionInfoDataType:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def normal_billing_response(self) -> dict:
|
def normal_billing_response(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"plan": "team",
|
"plan": "team",
|
||||||
"interval": "year",
|
"interval": "year",
|
||||||
@ -1844,7 +1838,6 @@ class TestBillingServiceSubscriptionInfoDataType:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def string_billing_response(self) -> dict:
|
def string_billing_response(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"plan": "team",
|
"plan": "team",
|
||||||
"interval": "year",
|
"interval": "year",
|
||||||
@ -1865,7 +1858,6 @@ class TestBillingServiceSubscriptionInfoDataType:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_billing_info_types(result: dict):
|
def _assert_billing_info_types(result: dict):
|
||||||
assert isinstance(result["enabled"], bool)
|
|
||||||
assert isinstance(result["subscription"]["plan"], str)
|
assert isinstance(result["subscription"]["plan"], str)
|
||||||
assert isinstance(result["subscription"]["interval"], str)
|
assert isinstance(result["subscription"]["interval"], str)
|
||||||
assert isinstance(result["subscription"]["education"], bool)
|
assert isinstance(result["subscription"]["education"], bool)
|
||||||
|
|||||||
@ -6,12 +6,14 @@ from datetime import datetime
|
|||||||
from sqlalchemy import event, select
|
from sqlalchemy import event, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models.account import Tenant
|
from models.account import Tenant
|
||||||
from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment
|
from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||||
from models.model import UploadFile
|
from models.model import UploadFile
|
||||||
from models.source import DataSourceOauthBinding
|
from models.source import DataSourceOauthBinding
|
||||||
from services.dataset_ref_service import DatasetRefService
|
from services.dataset_ref_service import DatasetRefService
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
from .dataset_service_test_helpers import (
|
from .dataset_service_test_helpers import (
|
||||||
Account,
|
Account,
|
||||||
@ -555,6 +557,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
with patch("services.dataset_service.current_user", account):
|
with patch("services.dataset_service.current_user", account):
|
||||||
yield account
|
yield account
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_without_dataset_id_creates_high_quality_dataset_with_default_retrieval_model(
|
def test_save_document_without_dataset_id_creates_high_quality_dataset_with_default_retrieval_model(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -581,7 +584,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
first_document = _document_row(name="VeryLongDocumentNameForDataset.txt")
|
first_document = _document_row(name="VeryLongDocumentNameForDataset.txt")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch(
|
patch(
|
||||||
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
||||||
return_value=binding,
|
return_value=binding,
|
||||||
@ -617,6 +620,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
session=sqlite_session,
|
session=sqlite_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_without_dataset_id_uses_provided_retrieval_model(
|
def test_save_document_without_dataset_id_uses_provided_retrieval_model(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -644,7 +648,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
first_document = _document_row(name="Doc")
|
first_document = _document_row(name="Doc")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch.object(
|
patch.object(
|
||||||
DocumentService,
|
DocumentService,
|
||||||
"save_document_with_dataset_id",
|
"save_document_with_dataset_id",
|
||||||
@ -662,6 +666,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
assert dataset.collection_binding_id is None
|
assert dataset.collection_binding_id is None
|
||||||
assert sqlite_session.get(Dataset, dataset.id) is dataset
|
assert sqlite_session.get(Dataset, dataset.id) is dataset
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_without_dataset_id_rejects_sandbox_batch_upload(
|
def test_save_document_without_dataset_id_rejects_sandbox_batch_upload(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
@ -678,7 +683,7 @@ class TestDocumentServiceSaveDocumentWithoutDatasetId:
|
|||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"services.dataset_service.FeatureService.get_features",
|
"services.dataset_service.FeatureService.get_features",
|
||||||
return_value=_make_features(enabled=True, plan=CloudPlan.SANDBOX),
|
return_value=_make_features(plan=CloudPlan.SANDBOX),
|
||||||
),
|
),
|
||||||
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
||||||
):
|
):
|
||||||
@ -1080,13 +1085,14 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
):
|
):
|
||||||
yield account
|
yield account
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_with_dataset_id_requires_file_info_for_upload_source(
|
def test_save_document_with_dataset_id_requires_file_info_for_upload_source(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
dataset = _dataset_row()
|
dataset = _dataset_row()
|
||||||
knowledge_config = _make_upload_knowledge_config(file_ids=None)
|
knowledge_config = _make_upload_knowledge_config(file_ids=None)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=True)):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()):
|
||||||
with pytest.raises(ValueError, match="File source info is required"):
|
with pytest.raises(ValueError, match="File source info is required"):
|
||||||
DocumentService.save_document_with_dataset_id(
|
DocumentService.save_document_with_dataset_id(
|
||||||
dataset,
|
dataset,
|
||||||
@ -1095,6 +1101,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
session=unbound_session,
|
session=unbound_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_with_dataset_id_blocks_batch_upload_for_sandbox_plan(
|
def test_save_document_with_dataset_id_blocks_batch_upload_for_sandbox_plan(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
@ -1104,7 +1111,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"services.dataset_service.FeatureService.get_features",
|
"services.dataset_service.FeatureService.get_features",
|
||||||
return_value=_make_features(enabled=True, plan=CloudPlan.SANDBOX),
|
return_value=_make_features(plan=CloudPlan.SANDBOX),
|
||||||
),
|
),
|
||||||
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
||||||
):
|
):
|
||||||
@ -1118,6 +1125,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
|
|
||||||
check_quota.assert_not_called()
|
check_quota.assert_not_called()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_with_dataset_id_enforces_batch_upload_limit(
|
def test_save_document_with_dataset_id_enforces_batch_upload_limit(
|
||||||
self,
|
self,
|
||||||
account_context,
|
account_context,
|
||||||
@ -1129,7 +1137,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
knowledge_config = _make_upload_knowledge_config(file_ids=["file-1", "file-2"])
|
knowledge_config = _make_upload_knowledge_config(file_ids=["file-1", "file-2"])
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=True)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
||||||
):
|
):
|
||||||
with pytest.raises(ValueError, match="batch upload limit of 1"):
|
with pytest.raises(ValueError, match="batch upload limit of 1"):
|
||||||
@ -1142,6 +1150,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
|
|
||||||
check_quota.assert_not_called()
|
check_quota.assert_not_called()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_updates_existing_document_and_data_source_type(
|
def test_save_document_with_dataset_id_updates_existing_document_and_data_source_type(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1151,7 +1160,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
updated_document.batch = "batch-existing"
|
updated_document.batch = "batch-existing"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch.object(
|
patch.object(
|
||||||
DocumentService, "update_document_with_dataset_id", return_value=updated_document
|
DocumentService, "update_document_with_dataset_id", return_value=updated_document
|
||||||
) as update_document,
|
) as update_document,
|
||||||
@ -1168,13 +1177,14 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
assert batch == "batch-existing"
|
assert batch == "batch-existing"
|
||||||
update_document.assert_called_once_with(dataset, knowledge_config, account_context, session=sqlite_session)
|
update_document.assert_called_once_with(dataset, knowledge_config, account_context, session=sqlite_session)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_requires_data_source_for_new_documents(
|
def test_save_document_with_dataset_id_requires_data_source_for_new_documents(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
dataset = _dataset_row()
|
dataset = _dataset_row()
|
||||||
knowledge_config = _make_upload_knowledge_config(data_source=None)
|
knowledge_config = _make_upload_knowledge_config(data_source=None)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()):
|
||||||
with pytest.raises(ValueError, match="Data source is required when creating new documents"):
|
with pytest.raises(ValueError, match="Data source is required when creating new documents"):
|
||||||
DocumentService.save_document_with_dataset_id(
|
DocumentService.save_document_with_dataset_id(
|
||||||
dataset,
|
dataset,
|
||||||
@ -1183,6 +1193,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
session=unbound_session,
|
session=unbound_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_requires_existing_process_rule_for_custom_mode(
|
def test_save_document_with_dataset_id_requires_existing_process_rule_for_custom_mode(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1194,7 +1205,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
process_rule=ProcessRule(mode="custom"),
|
process_rule=ProcessRule(mode="custom"),
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()):
|
||||||
with pytest.raises(ValueError, match="No process rule found"):
|
with pytest.raises(ValueError, match="No process rule found"):
|
||||||
DocumentService.save_document_with_dataset_id(
|
DocumentService.save_document_with_dataset_id(
|
||||||
dataset,
|
dataset,
|
||||||
@ -1203,6 +1214,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
session=sqlite_session,
|
session=sqlite_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_rejects_invalid_indexing_technique(
|
def test_save_document_with_dataset_id_rejects_invalid_indexing_technique(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
@ -1214,7 +1226,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
indexing_technique="broken-technique",
|
indexing_technique="broken-technique",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()):
|
||||||
with pytest.raises(ValueError, match="Indexing technique is invalid"):
|
with pytest.raises(ValueError, match="Indexing technique is invalid"):
|
||||||
DocumentService.save_document_with_dataset_id(
|
DocumentService.save_document_with_dataset_id(
|
||||||
dataset,
|
dataset,
|
||||||
@ -1223,6 +1235,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
session=unbound_session,
|
session=unbound_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_returns_empty_for_invalid_process_rule_mode(
|
def test_save_document_with_dataset_id_returns_empty_for_invalid_process_rule_mode(
|
||||||
self, account_context, unbound_session: Session
|
self, account_context, unbound_session: Session
|
||||||
):
|
):
|
||||||
@ -1230,7 +1243,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
knowledge_config = _make_upload_knowledge_config(file_ids=["file-1"])
|
knowledge_config = _make_upload_knowledge_config(file_ids=["file-1"])
|
||||||
knowledge_config.process_rule = SimpleNamespace(mode="unsupported-mode", rules=None)
|
knowledge_config.process_rule = SimpleNamespace(mode="unsupported-mode", rules=None)
|
||||||
|
|
||||||
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)):
|
with patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()):
|
||||||
documents, batch = DocumentService.save_document_with_dataset_id(
|
documents, batch = DocumentService.save_document_with_dataset_id(
|
||||||
dataset,
|
dataset,
|
||||||
knowledge_config,
|
knowledge_config,
|
||||||
@ -1241,6 +1254,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
assert documents == []
|
assert documents == []
|
||||||
assert batch == ""
|
assert batch == ""
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_upload_file_creates_and_reindexes_documents(
|
def test_save_document_with_dataset_id_upload_file_creates_and_reindexes_documents(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1254,7 +1268,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
||||||
patch("services.dataset_service.DuplicateDocumentIndexingTaskProxy") as duplicate_proxy_cls,
|
patch("services.dataset_service.DuplicateDocumentIndexingTaskProxy") as duplicate_proxy_cls,
|
||||||
@ -1285,6 +1299,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
duplicate_proxy_cls.assert_called_once_with(dataset.tenant_id, dataset.id, ["doc-duplicate"])
|
duplicate_proxy_cls.assert_called_once_with(dataset.tenant_id, dataset.id, ["doc-duplicate"])
|
||||||
duplicate_proxy_cls.return_value.delay.assert_called_once()
|
duplicate_proxy_cls.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_notion_import_truncates_names_and_cleans_removed_pages(
|
def test_save_document_with_dataset_id_notion_import_truncates_names_and_cleans_removed_pages(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1330,7 +1345,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.clean_notion_document_task") as clean_task,
|
patch("services.dataset_service.clean_notion_document_task") as clean_task,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
||||||
@ -1353,6 +1368,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
document_proxy_cls.assert_called_once_with(dataset.tenant_id, dataset.id, ["doc-new"])
|
document_proxy_cls.assert_called_once_with(dataset.tenant_id, dataset.id, ["doc-new"])
|
||||||
document_proxy_cls.return_value.delay.assert_called_once()
|
document_proxy_cls.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_website_crawl_truncates_long_urls(
|
def test_save_document_with_dataset_id_website_crawl_truncates_long_urls(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1379,7 +1395,7 @@ class TestDocumentServiceSaveDocumentWithDatasetId:
|
|||||||
doc_language="English",
|
doc_language="English",
|
||||||
)
|
)
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
||||||
):
|
):
|
||||||
@ -1711,6 +1727,7 @@ class TestDocumentServiceSaveWithoutDatasetBilling:
|
|||||||
with patch("services.dataset_service.current_user", account):
|
with patch("services.dataset_service.current_user", account):
|
||||||
yield account
|
yield account
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_without_dataset_id_counts_notion_pages_for_quota(
|
def test_save_document_without_dataset_id_counts_notion_pages_for_quota(
|
||||||
self,
|
self,
|
||||||
account_context,
|
account_context,
|
||||||
@ -1741,7 +1758,7 @@ class TestDocumentServiceSaveWithoutDatasetBilling:
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
features = _make_features(enabled=True)
|
features = _make_features()
|
||||||
document = _document_row(name="Doc")
|
document = _document_row(name="Doc")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@ -1763,6 +1780,7 @@ class TestDocumentServiceSaveWithoutDatasetBilling:
|
|||||||
check_quota.assert_called_once_with(3, features)
|
check_quota.assert_called_once_with(3, features)
|
||||||
assert sqlite_session.get(Dataset, dataset.id) is dataset
|
assert sqlite_session.get(Dataset, dataset.id) is dataset
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_save_document_without_dataset_id_enforces_batch_limit_for_website_urls(
|
def test_save_document_without_dataset_id_enforces_batch_limit_for_website_urls(
|
||||||
self,
|
self,
|
||||||
account_context,
|
account_context,
|
||||||
@ -1786,7 +1804,7 @@ class TestDocumentServiceSaveWithoutDatasetBilling:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=True)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
patch.object(DocumentService, "check_documents_upload_quota") as check_quota,
|
||||||
):
|
):
|
||||||
with pytest.raises(ValueError, match="batch upload limit of 1"):
|
with pytest.raises(ValueError, match="batch upload limit of 1"):
|
||||||
@ -1936,6 +1954,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
):
|
):
|
||||||
yield account
|
yield account
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_initializes_high_quality_dataset_from_default_embedding_model(
|
def test_save_document_with_dataset_id_initializes_high_quality_dataset_from_default_embedding_model(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -1955,7 +1974,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
binding.id = "binding-1"
|
binding.id = "binding-1"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.ModelManager") as model_manager_cls,
|
patch("services.dataset_service.ModelManager") as model_manager_cls,
|
||||||
patch(
|
patch(
|
||||||
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
||||||
@ -1991,6 +2010,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
}
|
}
|
||||||
get_binding.assert_called_once_with("default-provider", "default-embedding", sqlite_session)
|
get_binding.assert_called_once_with("default-provider", "default-embedding", sqlite_session)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_uses_explicit_embedding_and_retrieval_model(
|
def test_save_document_with_dataset_id_uses_explicit_embedding_and_retrieval_model(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2020,7 +2040,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
updated_document = _document_row(document_id="doc-1")
|
updated_document = _document_row(document_id="doc-1")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.ModelManager") as model_manager_cls,
|
patch("services.dataset_service.ModelManager") as model_manager_cls,
|
||||||
patch(
|
patch(
|
||||||
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
"services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding",
|
||||||
@ -2038,6 +2058,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
assert dataset.embedding_model_provider == "explicit-provider"
|
assert dataset.embedding_model_provider == "explicit-provider"
|
||||||
assert dataset.retrieval_model == knowledge_config.retrieval_model.model_dump()
|
assert dataset.retrieval_model == knowledge_config.retrieval_model.model_dump()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_creates_custom_process_rule_for_new_upload_document(
|
def test_save_document_with_dataset_id_creates_custom_process_rule_for_new_upload_document(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2057,7 +2078,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
patch("services.dataset_service.DocumentIndexingTaskProxy") as document_proxy_cls,
|
||||||
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
||||||
@ -2082,6 +2103,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
document_proxy_cls.assert_called_once_with("tenant-1", "dataset-1", [created_document.id])
|
document_proxy_cls.assert_called_once_with("tenant-1", "dataset-1", [created_document.id])
|
||||||
document_proxy_cls.return_value.delay.assert_called_once()
|
document_proxy_cls.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_creates_automatic_process_rule_for_new_upload_document(
|
def test_save_document_with_dataset_id_creates_automatic_process_rule_for_new_upload_document(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2094,7 +2116,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy"),
|
patch("services.dataset_service.DocumentIndexingTaskProxy"),
|
||||||
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
||||||
@ -2114,6 +2136,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
assert created_rule.rules == json.dumps(DatasetProcessRule.AUTOMATIC_RULES)
|
assert created_rule.rules == json.dumps(DatasetProcessRule.AUTOMATIC_RULES)
|
||||||
assert sqlite_session.get(Document, documents[0].id) is documents[0]
|
assert sqlite_session.get(Document, documents[0].id) is documents[0]
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_creates_fallback_automatic_process_rule_when_latest_is_missing(
|
def test_save_document_with_dataset_id_creates_fallback_automatic_process_rule_when_latest_is_missing(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2123,7 +2146,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.DocumentIndexingTaskProxy"),
|
patch("services.dataset_service.DocumentIndexingTaskProxy"),
|
||||||
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
||||||
@ -2142,6 +2165,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
assert created_rule.mode == "automatic"
|
assert created_rule.mode == "automatic"
|
||||||
assert created_rule.rules == json.dumps(DatasetProcessRule.AUTOMATIC_RULES)
|
assert created_rule.rules == json.dumps(DatasetProcessRule.AUTOMATIC_RULES)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_raises_when_upload_file_lookup_is_incomplete(
|
def test_save_document_with_dataset_id_raises_when_upload_file_lookup_is_incomplete(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2151,7 +2175,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
patch("services.dataset_service.time.strftime", return_value="20260101010101"),
|
||||||
patch("services.dataset_service.secrets.randbelow", return_value=23),
|
patch("services.dataset_service.secrets.randbelow", return_value=23),
|
||||||
@ -2165,6 +2189,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
session=sqlite_session,
|
session=sqlite_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_requires_notion_info_list_for_notion_import(
|
def test_save_document_with_dataset_id_requires_notion_info_list_for_notion_import(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2180,7 +2205,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
):
|
):
|
||||||
mock_redis.lock.return_value = _make_lock_context()
|
mock_redis.lock.return_value = _make_lock_context()
|
||||||
@ -2193,6 +2218,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
session=sqlite_session,
|
session=sqlite_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_save_document_with_dataset_id_requires_website_info_list_for_website_crawl(
|
def test_save_document_with_dataset_id_requires_website_info_list_for_website_crawl(
|
||||||
self, account_context, sqlite_session: Session
|
self, account_context, sqlite_session: Session
|
||||||
):
|
):
|
||||||
@ -2208,7 +2234,7 @@ class TestDocumentServiceSaveDocumentAdditionalBranches:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features(enabled=False)),
|
patch("services.dataset_service.FeatureService.get_features", return_value=_make_features()),
|
||||||
patch("services.dataset_service.redis_client") as mock_redis,
|
patch("services.dataset_service.redis_client") as mock_redis,
|
||||||
):
|
):
|
||||||
mock_redis.lock.return_value = _make_lock_context()
|
mock_redis.lock.return_value = _make_lock_context()
|
||||||
|
|||||||
@ -42,9 +42,8 @@ def fake_current_user(monkeypatch: pytest.MonkeyPatch):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_features(monkeypatch: pytest.MonkeyPatch):
|
def fake_features(monkeypatch: pytest.MonkeyPatch):
|
||||||
"""Features.billing.enabled == False to skip quota logic."""
|
|
||||||
features = types.SimpleNamespace(
|
features = types.SimpleNamespace(
|
||||||
billing=types.SimpleNamespace(enabled=False, subscription=types.SimpleNamespace(plan="ENTERPRISE")),
|
billing=types.SimpleNamespace(subscription=types.SimpleNamespace(plan="ENTERPRISE")),
|
||||||
documents_upload_quota=types.SimpleNamespace(limit=10_000, size=0),
|
documents_upload_quota=types.SimpleNamespace(limit=10_000, size=0),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
@ -2,19 +2,19 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
from core.entities.document_task import DocumentTask
|
from core.entities.document_task import DocumentTask
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from services.document_indexing_proxy.document_indexing_task_proxy import DocumentIndexingTaskProxy
|
from services.document_indexing_proxy.document_indexing_task_proxy import DocumentIndexingTaskProxy
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class DocumentIndexingTaskProxyTestDataFactory:
|
class DocumentIndexingTaskProxyTestDataFactory:
|
||||||
"""Factory class for creating test data and mock objects for DocumentIndexingTaskProxy tests."""
|
"""Factory class for creating test data and mock objects for DocumentIndexingTaskProxy tests."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_mock_features(billing_enabled: bool = False, plan: CloudPlan | str | None = CloudPlan.SANDBOX) -> Mock:
|
def create_mock_features(plan: CloudPlan | str | None = CloudPlan.SANDBOX) -> Mock:
|
||||||
"""Create mock features with billing configuration."""
|
"""Create mock features with billing configuration."""
|
||||||
features = Mock()
|
features = Mock()
|
||||||
features.billing = Mock()
|
features.billing = Mock()
|
||||||
features.billing.enabled = billing_enabled
|
|
||||||
features.billing.subscription = Mock()
|
features.billing.subscription = Mock()
|
||||||
features.billing.subscription.plan = plan
|
features.billing.subscription.plan = plan
|
||||||
return features
|
return features
|
||||||
@ -171,13 +171,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_direct_queue.assert_called_once_with(proxy.PRIORITY_TASK_FUNC)
|
proxy._send_to_direct_queue.assert_called_once_with(proxy.PRIORITY_TASK_FUNC)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_enabled_sandbox_plan(self, mock_feature_service):
|
def test_dispatch_with_cloud_sandbox_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is enabled with sandbox plan."""
|
"""Test _dispatch method in Cloud with Sandbox plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_default_tenant_queue = Mock()
|
proxy._send_to_default_tenant_queue = Mock()
|
||||||
@ -188,13 +187,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_default_tenant_queue.assert_called_once()
|
proxy._send_to_default_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_enabled_non_sandbox_plan(self, mock_feature_service):
|
def test_dispatch_with_cloud_paid_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is enabled with non-sandbox plan."""
|
"""Test _dispatch method in Cloud with a paid plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.TEAM)
|
||||||
billing_enabled=True, plan=CloudPlan.TEAM
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -205,11 +203,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
# If billing enabled with non sandbox plan, should send to priority tenant queue
|
# If billing enabled with non sandbox plan, should send to priority tenant queue
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once()
|
proxy._send_to_priority_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_disabled(self, mock_feature_service):
|
def test_dispatch_outside_cloud(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is disabled."""
|
"""Test _dispatch method outside Cloud."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(billing_enabled=False)
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features()
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_priority_direct_queue = Mock()
|
proxy._send_to_priority_direct_queue = Mock()
|
||||||
@ -220,13 +219,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
# If billing disabled, for example: self-hosted or enterprise, should send to priority direct queue
|
# If billing disabled, for example: self-hosted or enterprise, should send to priority direct queue
|
||||||
proxy._send_to_priority_direct_queue.assert_called_once()
|
proxy._send_to_priority_direct_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_delay_method(self, mock_feature_service):
|
def test_delay_method(self, mock_feature_service):
|
||||||
"""Test delay method integration."""
|
"""Test delay method integration."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_default_tenant_queue = Mock()
|
proxy._send_to_default_tenant_queue = Mock()
|
||||||
@ -253,11 +251,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
assert task.dataset_id == dataset_id
|
assert task.dataset_id == dataset_id
|
||||||
assert task.document_ids == document_ids
|
assert task.document_ids == document_ids
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_edge_case_empty_plan(self, mock_feature_service):
|
def test_dispatch_edge_case_empty_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method with empty plan string."""
|
"""Test _dispatch method with empty plan string."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(billing_enabled=True, plan="")
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan="")
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -268,11 +267,12 @@ class TestDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once()
|
proxy._send_to_priority_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_edge_case_none_plan(self, mock_feature_service):
|
def test_dispatch_edge_case_none_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method with None plan."""
|
"""Test _dispatch method with None plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(billing_enabled=True, plan=None)
|
mock_features = DocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=None)
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
proxy = DocumentIndexingTaskProxyTestDataFactory.create_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
|
|||||||
@ -2,21 +2,21 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
from core.entities.document_task import DocumentTask
|
from core.entities.document_task import DocumentTask
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from services.document_indexing_proxy.duplicate_document_indexing_task_proxy import (
|
from services.document_indexing_proxy.duplicate_document_indexing_task_proxy import (
|
||||||
DuplicateDocumentIndexingTaskProxy,
|
DuplicateDocumentIndexingTaskProxy,
|
||||||
)
|
)
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class DuplicateDocumentIndexingTaskProxyTestDataFactory:
|
class DuplicateDocumentIndexingTaskProxyTestDataFactory:
|
||||||
"""Factory class for creating test data and mock objects for DuplicateDocumentIndexingTaskProxy tests."""
|
"""Factory class for creating test data and mock objects for DuplicateDocumentIndexingTaskProxy tests."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_mock_features(billing_enabled: bool = False, plan: CloudPlan | str | None = CloudPlan.SANDBOX) -> Mock:
|
def create_mock_features(plan: CloudPlan | str | None = CloudPlan.SANDBOX) -> Mock:
|
||||||
"""Create mock features with billing configuration."""
|
"""Create mock features with billing configuration."""
|
||||||
features = Mock()
|
features = Mock()
|
||||||
features.billing = Mock()
|
features.billing = Mock()
|
||||||
features.billing.enabled = billing_enabled
|
|
||||||
features.billing.subscription = Mock()
|
features.billing.subscription = Mock()
|
||||||
features.billing.subscription.plan = plan
|
features.billing.subscription.plan = plan
|
||||||
return features
|
return features
|
||||||
@ -196,13 +196,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_direct_queue.assert_called_once_with(proxy.PRIORITY_TASK_FUNC)
|
proxy._send_to_direct_queue.assert_called_once_with(proxy.PRIORITY_TASK_FUNC)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_enabled_sandbox_plan(self, mock_feature_service):
|
def test_dispatch_with_cloud_sandbox_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is enabled with sandbox plan."""
|
"""Test _dispatch method in Cloud with Sandbox plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_default_tenant_queue = Mock()
|
proxy._send_to_default_tenant_queue = Mock()
|
||||||
@ -213,13 +212,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_default_tenant_queue.assert_called_once()
|
proxy._send_to_default_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_enabled_non_sandbox_plan(self, mock_feature_service):
|
def test_dispatch_with_cloud_paid_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is enabled with non-sandbox plan."""
|
"""Test _dispatch method in Cloud with a paid plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.TEAM)
|
||||||
billing_enabled=True, plan=CloudPlan.TEAM
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -231,11 +229,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# If billing enabled with non sandbox plan, should send to priority tenant queue
|
# If billing enabled with non sandbox plan, should send to priority tenant queue
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once()
|
proxy._send_to_priority_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_billing_disabled(self, mock_feature_service):
|
def test_dispatch_outside_cloud(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is disabled."""
|
"""Test _dispatch method outside Cloud."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(billing_enabled=False)
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features()
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_priority_direct_queue = Mock()
|
proxy._send_to_priority_direct_queue = Mock()
|
||||||
@ -247,13 +246,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# If billing disabled, for example: self-hosted or enterprise, should send to priority direct queue
|
# If billing disabled, for example: self-hosted or enterprise, should send to priority direct queue
|
||||||
proxy._send_to_priority_direct_queue.assert_called_once()
|
proxy._send_to_priority_direct_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_delay_method(self, mock_feature_service):
|
def test_delay_method(self, mock_feature_service):
|
||||||
"""Test delay method integration."""
|
"""Test delay method integration."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_default_tenant_queue = Mock()
|
proxy._send_to_default_tenant_queue = Mock()
|
||||||
@ -265,13 +263,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# If billing enabled with sandbox plan, should send to default tenant queue
|
# If billing enabled with sandbox plan, should send to default tenant queue
|
||||||
proxy._send_to_default_tenant_queue.assert_called_once()
|
proxy._send_to_default_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_edge_case_empty_plan(self, mock_feature_service):
|
def test_dispatch_edge_case_empty_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method with empty plan string."""
|
"""Test _dispatch method with empty plan string."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan="")
|
||||||
billing_enabled=True, plan=""
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -282,13 +279,12 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once()
|
proxy._send_to_priority_tenant_queue.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_edge_case_none_plan(self, mock_feature_service):
|
def test_dispatch_edge_case_none_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method with None plan."""
|
"""Test _dispatch method with None plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(plan=None)
|
||||||
billing_enabled=True, plan=None
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -345,12 +341,13 @@ class TestDuplicateDocumentIndexingTaskProxy:
|
|||||||
assert proxy._document_ids == document_ids
|
assert proxy._document_ids == document_ids
|
||||||
assert len(proxy._document_ids) == 100
|
assert len(proxy._document_ids) == 100
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.document_indexing_proxy.base.FeatureService")
|
@patch("services.document_indexing_proxy.base.FeatureService")
|
||||||
def test_dispatch_with_professional_plan(self, mock_feature_service):
|
def test_dispatch_with_professional_plan(self, mock_feature_service):
|
||||||
"""Test _dispatch method when billing is enabled with professional plan."""
|
"""Test _dispatch method when billing is enabled with professional plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
mock_features = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_mock_features(
|
||||||
billing_enabled=True, plan=CloudPlan.PROFESSIONAL
|
plan=CloudPlan.PROFESSIONAL
|
||||||
)
|
)
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
proxy = DuplicateDocumentIndexingTaskProxyTestDataFactory.create_duplicate_document_task_proxy()
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import pytest
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan
|
||||||
from services.entities.feature_entities import LicenseLimitationModel, SubscriptionModel
|
from services.entities.feature_entities import FeatureModel, LicenseLimitationModel, SubscriptionModel
|
||||||
|
|
||||||
|
|
||||||
def test_subscription_model_uses_the_cloud_plan_value_set() -> None:
|
def test_subscription_model_uses_the_cloud_plan_value_set() -> None:
|
||||||
@ -40,3 +40,11 @@ def test_license_limitation_availability(
|
|||||||
limitation = LicenseLimitationModel(enabled=enabled, size=size, limit=limit)
|
limitation = LicenseLimitationModel(enabled=enabled, size=size, limit=limit)
|
||||||
|
|
||||||
assert limitation.is_available(required) is expected
|
assert limitation.is_available(required) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_feature_response_exposes_subscription_without_legacy_billing_switch() -> None:
|
||||||
|
features = FeatureModel(billing={"subscription": {"plan": "professional", "interval": "month"}})
|
||||||
|
|
||||||
|
assert features.model_dump(mode="json")["billing"] == {
|
||||||
|
"subscription": {"plan": "professional", "interval": "month"},
|
||||||
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@ class HumanInputEmailDeliveryCase:
|
|||||||
name: str
|
name: str
|
||||||
deployment_edition: DeploymentEdition
|
deployment_edition: DeploymentEdition
|
||||||
tenant_id: str | None
|
tenant_id: str | None
|
||||||
billing_feature_enabled: bool
|
|
||||||
plan: str
|
plan: str
|
||||||
expected: bool
|
expected: bool
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ CASES = [
|
|||||||
name="enterprise_edition",
|
name="enterprise_edition",
|
||||||
deployment_edition=DeploymentEdition.ENTERPRISE,
|
deployment_edition=DeploymentEdition.ENTERPRISE,
|
||||||
tenant_id=None,
|
tenant_id=None,
|
||||||
billing_feature_enabled=False,
|
|
||||||
plan=CloudPlan.SANDBOX,
|
plan=CloudPlan.SANDBOX,
|
||||||
expected=True,
|
expected=True,
|
||||||
),
|
),
|
||||||
@ -32,7 +30,6 @@ CASES = [
|
|||||||
name="community_edition",
|
name="community_edition",
|
||||||
deployment_edition=DeploymentEdition.COMMUNITY,
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||||
tenant_id=None,
|
tenant_id=None,
|
||||||
billing_feature_enabled=False,
|
|
||||||
plan=CloudPlan.SANDBOX,
|
plan=CloudPlan.SANDBOX,
|
||||||
expected=True,
|
expected=True,
|
||||||
),
|
),
|
||||||
@ -40,15 +37,6 @@ CASES = [
|
|||||||
name="cloud_edition_requires_tenant",
|
name="cloud_edition_requires_tenant",
|
||||||
deployment_edition=DeploymentEdition.CLOUD,
|
deployment_edition=DeploymentEdition.CLOUD,
|
||||||
tenant_id=None,
|
tenant_id=None,
|
||||||
billing_feature_enabled=True,
|
|
||||||
plan=CloudPlan.PROFESSIONAL,
|
|
||||||
expected=False,
|
|
||||||
),
|
|
||||||
HumanInputEmailDeliveryCase(
|
|
||||||
name="billing_feature_off",
|
|
||||||
deployment_edition=DeploymentEdition.CLOUD,
|
|
||||||
tenant_id="tenant-1",
|
|
||||||
billing_feature_enabled=False,
|
|
||||||
plan=CloudPlan.PROFESSIONAL,
|
plan=CloudPlan.PROFESSIONAL,
|
||||||
expected=False,
|
expected=False,
|
||||||
),
|
),
|
||||||
@ -56,7 +44,6 @@ CASES = [
|
|||||||
name="professional_plan",
|
name="professional_plan",
|
||||||
deployment_edition=DeploymentEdition.CLOUD,
|
deployment_edition=DeploymentEdition.CLOUD,
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
billing_feature_enabled=True,
|
|
||||||
plan=CloudPlan.PROFESSIONAL,
|
plan=CloudPlan.PROFESSIONAL,
|
||||||
expected=True,
|
expected=True,
|
||||||
),
|
),
|
||||||
@ -64,7 +51,6 @@ CASES = [
|
|||||||
name="team_plan",
|
name="team_plan",
|
||||||
deployment_edition=DeploymentEdition.CLOUD,
|
deployment_edition=DeploymentEdition.CLOUD,
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
billing_feature_enabled=True,
|
|
||||||
plan=CloudPlan.TEAM,
|
plan=CloudPlan.TEAM,
|
||||||
expected=True,
|
expected=True,
|
||||||
),
|
),
|
||||||
@ -72,7 +58,6 @@ CASES = [
|
|||||||
name="sandbox_plan",
|
name="sandbox_plan",
|
||||||
deployment_edition=DeploymentEdition.CLOUD,
|
deployment_edition=DeploymentEdition.CLOUD,
|
||||||
tenant_id="tenant-1",
|
tenant_id="tenant-1",
|
||||||
billing_feature_enabled=True,
|
|
||||||
plan=CloudPlan.SANDBOX,
|
plan=CloudPlan.SANDBOX,
|
||||||
expected=False,
|
expected=False,
|
||||||
),
|
),
|
||||||
@ -86,7 +71,6 @@ def test_resolve_human_input_email_delivery_enabled_matrix(
|
|||||||
):
|
):
|
||||||
config_overrides(DEPLOYMENT_EDITION=case.deployment_edition)
|
config_overrides(DEPLOYMENT_EDITION=case.deployment_edition)
|
||||||
features = FeatureModel()
|
features = FeatureModel()
|
||||||
features.billing.enabled = case.billing_feature_enabled
|
|
||||||
features.billing.subscription.plan = case.plan
|
features.billing.subscription.plan = case.plan
|
||||||
|
|
||||||
result = FeatureService._resolve_human_input_email_delivery_enabled(
|
result = FeatureService._resolve_human_input_email_delivery_enabled(
|
||||||
|
|||||||
@ -9,15 +9,14 @@ from services.feature_service import FeatureService
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("deployment_edition", "tenant_id", "billing_feature_enabled", "plan", "expected"),
|
("deployment_edition", "tenant_id", "plan", "expected"),
|
||||||
[
|
[
|
||||||
(DeploymentEdition.COMMUNITY, "tenant-1", True, CloudPlan.PROFESSIONAL, 15),
|
(DeploymentEdition.COMMUNITY, "tenant-1", CloudPlan.PROFESSIONAL, 15),
|
||||||
(DeploymentEdition.ENTERPRISE, "tenant-1", True, CloudPlan.PROFESSIONAL, 15),
|
(DeploymentEdition.ENTERPRISE, "tenant-1", CloudPlan.PROFESSIONAL, 15),
|
||||||
(DeploymentEdition.CLOUD, None, True, CloudPlan.PROFESSIONAL, 15),
|
(DeploymentEdition.CLOUD, None, CloudPlan.PROFESSIONAL, 15),
|
||||||
(DeploymentEdition.CLOUD, "tenant-1", False, CloudPlan.PROFESSIONAL, 15),
|
(DeploymentEdition.CLOUD, "tenant-1", CloudPlan.SANDBOX, 15),
|
||||||
(DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.SANDBOX, 15),
|
(DeploymentEdition.CLOUD, "tenant-1", CloudPlan.PROFESSIONAL, 50),
|
||||||
(DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.PROFESSIONAL, 50),
|
(DeploymentEdition.CLOUD, "tenant-1", CloudPlan.TEAM, 50),
|
||||||
(DeploymentEdition.CLOUD, "tenant-1", True, CloudPlan.TEAM, 50),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_knowledge_file_size_limit(
|
def test_get_knowledge_file_size_limit(
|
||||||
@ -25,7 +24,6 @@ def test_get_knowledge_file_size_limit(
|
|||||||
config_overrides: Callable[..., None],
|
config_overrides: Callable[..., None],
|
||||||
deployment_edition: DeploymentEdition,
|
deployment_edition: DeploymentEdition,
|
||||||
tenant_id: str | None,
|
tenant_id: str | None,
|
||||||
billing_feature_enabled: bool,
|
|
||||||
plan: CloudPlan,
|
plan: CloudPlan,
|
||||||
expected: int,
|
expected: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -36,7 +34,6 @@ def test_get_knowledge_file_size_limit(
|
|||||||
)
|
)
|
||||||
get_info = Mock(
|
get_info = Mock(
|
||||||
return_value={
|
return_value={
|
||||||
"enabled": billing_feature_enabled,
|
|
||||||
"subscription": {"plan": plan},
|
"subscription": {"plan": plan},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -62,7 +59,6 @@ def test_paid_knowledge_file_size_limit_never_reduces_default(
|
|||||||
feature_service_module.BillingService,
|
feature_service_module.BillingService,
|
||||||
"get_info",
|
"get_info",
|
||||||
lambda *_args, **_kwargs: {
|
lambda *_args, **_kwargs: {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from services.feature_service import FeatureService
|
|||||||
def test_get_features_exclude_vector_space_sets_vector_space_to_none(config_overrides):
|
def test_get_features_exclude_vector_space_sets_vector_space_to_none(config_overrides):
|
||||||
tenant_id = "tenant-id"
|
tenant_id = "tenant-id"
|
||||||
billing_info = {
|
billing_info = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": False},
|
"subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": False},
|
||||||
"members": {"size": 1, "limit": 10},
|
"members": {"size": 1, "limit": 10},
|
||||||
"apps": {"size": 2, "limit": 20},
|
"apps": {"size": 2, "limit": 20},
|
||||||
|
|||||||
@ -7,22 +7,22 @@ import pytest
|
|||||||
|
|
||||||
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
|
from core.app.entities.rag_pipeline_invoke_entities import RagPipelineInvokeEntity
|
||||||
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from extensions.storage.storage_type import StorageType
|
from extensions.storage.storage_type import StorageType
|
||||||
from models.enums import CreatorUserRole
|
from models.enums import CreatorUserRole
|
||||||
from models.model import UploadFile
|
from models.model import UploadFile
|
||||||
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
|
from services.rag_pipeline.rag_pipeline_task_proxy import RagPipelineTaskProxy
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
class RagPipelineTaskProxyTestDataFactory:
|
class RagPipelineTaskProxyTestDataFactory:
|
||||||
"""Factory class for creating test data and mock objects for RagPipelineTaskProxy tests."""
|
"""Factory class for creating test data and mock objects for RagPipelineTaskProxy tests."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_mock_features(billing_enabled: bool = False, plan: CloudPlan = CloudPlan.SANDBOX) -> Mock:
|
def create_mock_features(plan: CloudPlan = CloudPlan.SANDBOX) -> Mock:
|
||||||
"""Create mock features with billing configuration."""
|
"""Create mock features with billing configuration."""
|
||||||
features = Mock()
|
features = Mock()
|
||||||
features.billing = Mock()
|
features.billing = Mock()
|
||||||
features.billing.enabled = billing_enabled
|
|
||||||
features.billing.subscription = Mock()
|
features.billing.subscription = Mock()
|
||||||
features.billing.subscription.plan = plan
|
features.billing.subscription.plan = plan
|
||||||
return features
|
return features
|
||||||
@ -330,17 +330,16 @@ class TestRagPipelineTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_direct_queue.assert_called_once_with(upload_file_id, mock_task)
|
proxy._send_to_direct_queue.assert_called_once_with(upload_file_id, mock_task)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
def test_dispatch_with_billing_enabled_sandbox_plan(
|
def test_dispatch_with_cloud_sandbox_plan(
|
||||||
self, mock_db: MagicMock, mock_file_service_class: MagicMock, mock_feature_service: MagicMock
|
self, mock_db: MagicMock, mock_file_service_class: MagicMock, mock_feature_service: MagicMock
|
||||||
):
|
):
|
||||||
"""Test _dispatch method when billing is enabled with sandbox plan."""
|
"""Test _dispatch method in Cloud with Sandbox plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._send_to_default_tenant_queue = Mock()
|
proxy._send_to_default_tenant_queue = Mock()
|
||||||
@ -356,17 +355,14 @@ class TestRagPipelineTaskProxy:
|
|||||||
# If billing is enabled with sandbox plan, should send to default tenant queue
|
# If billing is enabled with sandbox plan, should send to default tenant queue
|
||||||
proxy._send_to_default_tenant_queue.assert_called_once_with("file-123")
|
proxy._send_to_default_tenant_queue.assert_called_once_with("file-123")
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
def test_dispatch_with_billing_enabled_non_sandbox_plan(
|
def test_dispatch_with_cloud_paid_plan(self, mock_db, mock_file_service_class, mock_feature_service):
|
||||||
self, mock_db, mock_file_service_class, mock_feature_service
|
"""Test _dispatch method in Cloud with a paid plan."""
|
||||||
):
|
|
||||||
"""Test _dispatch method when billing is enabled with non-sandbox plan."""
|
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.TEAM)
|
||||||
billing_enabled=True, plan=CloudPlan.TEAM
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -382,15 +378,16 @@ class TestRagPipelineTaskProxy:
|
|||||||
# If billing is enabled with non-sandbox plan, should send to priority tenant queue
|
# If billing is enabled with non-sandbox plan, should send to priority tenant queue
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
def test_dispatch_with_billing_disabled(
|
def test_dispatch_outside_cloud(
|
||||||
self, mock_db: MagicMock, mock_file_service_class: MagicMock, mock_feature_service: MagicMock
|
self, mock_db: MagicMock, mock_file_service_class: MagicMock, mock_feature_service: MagicMock
|
||||||
):
|
):
|
||||||
"""Test _dispatch method when billing is disabled."""
|
"""Test _dispatch method outside Cloud."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(billing_enabled=False)
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features()
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._send_to_priority_direct_queue = Mock()
|
proxy._send_to_priority_direct_queue = Mock()
|
||||||
@ -422,6 +419,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
with pytest.raises(ValueError, match="upload_file_id is empty"):
|
with pytest.raises(ValueError, match="upload_file_id is empty"):
|
||||||
proxy._dispatch()
|
proxy._dispatch()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
@ -430,7 +428,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
):
|
):
|
||||||
"""Test _dispatch method with empty plan string."""
|
"""Test _dispatch method with empty plan string."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(billing_enabled=True, plan="")
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(plan="")
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -446,6 +444,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
@ -454,7 +453,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
):
|
):
|
||||||
"""Test _dispatch method with None plan."""
|
"""Test _dispatch method with None plan."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(billing_enabled=True, plan=None)
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(plan=None)
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._send_to_priority_tenant_queue = Mock()
|
proxy._send_to_priority_tenant_queue = Mock()
|
||||||
@ -470,6 +469,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
# Assert
|
# Assert
|
||||||
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
proxy._send_to_priority_tenant_queue.assert_called_once_with("file-123")
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FeatureService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.FileService")
|
||||||
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
@patch("services.rag_pipeline.rag_pipeline_task_proxy.db")
|
||||||
@ -478,9 +478,7 @@ class TestRagPipelineTaskProxy:
|
|||||||
):
|
):
|
||||||
"""Test delay method integration."""
|
"""Test delay method integration."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(
|
mock_features = RagPipelineTaskProxyTestDataFactory.create_mock_features(plan=CloudPlan.SANDBOX)
|
||||||
billing_enabled=True, plan=CloudPlan.SANDBOX
|
|
||||||
)
|
|
||||||
mock_feature_service.get_features.return_value = mock_features
|
mock_feature_service.get_features.return_value = mock_features
|
||||||
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
proxy = RagPipelineTaskProxyTestDataFactory.create_rag_pipeline_task_proxy()
|
||||||
proxy._dispatch = Mock()
|
proxy._dispatch = Mock()
|
||||||
|
|||||||
@ -549,9 +549,22 @@ def test_billing_plan_lookup_excludes_vector_space_and_is_cached() -> None:
|
|||||||
service = VectorSpaceAdmissionService()
|
service = VectorSpaceAdmissionService()
|
||||||
with patch(
|
with patch(
|
||||||
"services.vector_space_admission_service.BillingService.get_info",
|
"services.vector_space_admission_service.BillingService.get_info",
|
||||||
return_value={"enabled": True, "subscription": {"plan": "professional"}},
|
return_value={"subscription": {"plan": "professional"}},
|
||||||
) as get_info:
|
) as get_info:
|
||||||
assert service._get_plan("tenant-1") == CloudPlan.PROFESSIONAL
|
assert service._get_plan("tenant-1") == CloudPlan.PROFESSIONAL
|
||||||
assert service._get_plan("tenant-1") == CloudPlan.PROFESSIONAL
|
assert service._get_plan("tenant-1") == CloudPlan.PROFESSIONAL
|
||||||
|
|
||||||
get_info.assert_called_once_with("tenant-1", exclude_vector_space=True)
|
get_info.assert_called_once_with("tenant-1", exclude_vector_space=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_billing_plan_is_not_logged(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
|
service = VectorSpaceAdmissionService()
|
||||||
|
upstream_plan = "unexpected-private-plan"
|
||||||
|
with patch(
|
||||||
|
"services.vector_space_admission_service.BillingService.get_info",
|
||||||
|
return_value={"subscription": {"plan": upstream_plan}},
|
||||||
|
):
|
||||||
|
assert service._get_plan("tenant-1") is None
|
||||||
|
|
||||||
|
assert "unknown plan tenant_id=tenant-1" in caplog.text
|
||||||
|
assert upstream_plan not in caplog.text
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from unittest.mock import MagicMock, create_autospec
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from enums import DeploymentEdition
|
||||||
from services.app_definition_query_service import AppSiteConfiguration
|
from services.app_definition_query_service import AppSiteConfiguration
|
||||||
from services.entities.feature_entities import FeatureModel
|
from services.entities.feature_entities import FeatureModel
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
@ -62,6 +63,7 @@ def _runtime_record(
|
|||||||
def _service(
|
def _service(
|
||||||
runtime: MagicMock,
|
runtime: MagicMock,
|
||||||
*,
|
*,
|
||||||
|
deployment_edition: DeploymentEdition = DeploymentEdition.COMMUNITY,
|
||||||
file_service: MagicMock | None = None,
|
file_service: MagicMock | None = None,
|
||||||
workspace_features: MagicMock | None = None,
|
workspace_features: MagicMock | None = None,
|
||||||
) -> WebAppRuntimeQueryService:
|
) -> WebAppRuntimeQueryService:
|
||||||
@ -75,6 +77,7 @@ def _service(
|
|||||||
file_service=file_service,
|
file_service=file_service,
|
||||||
workspace_features=workspace_features,
|
workspace_features=workspace_features,
|
||||||
files_url=_FILES_URL,
|
files_url=_FILES_URL,
|
||||||
|
deployment_edition=deployment_edition,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -87,13 +90,25 @@ def test_get_bootstrap_rejects_unavailable_runtime(record: WebAppRuntimeRecord |
|
|||||||
_service(runtime).get_bootstrap("app-1")
|
_service(runtime).get_bootstrap("app-1")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("deployment_edition", "copyright_enabled", "expected_copyright", "expected_placeholder"),
|
||||||
|
[
|
||||||
|
(DeploymentEdition.CLOUD, False, None, None),
|
||||||
|
(DeploymentEdition.CLOUD, True, "Copyright", "Ask anything"),
|
||||||
|
(DeploymentEdition.COMMUNITY, False, "Copyright", "Ask anything"),
|
||||||
|
(DeploymentEdition.ENTERPRISE, False, "Copyright", "Ask anything"),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_get_bootstrap_applies_feature_and_branding_policy_after_record_load(
|
def test_get_bootstrap_applies_feature_and_branding_policy_after_record_load(
|
||||||
workspace_features: MagicMock,
|
workspace_features: MagicMock,
|
||||||
|
deployment_edition: DeploymentEdition,
|
||||||
|
copyright_enabled: bool,
|
||||||
|
expected_copyright: str | None,
|
||||||
|
expected_placeholder: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
runtime: MagicMock = create_autospec(WebAppRuntimeQuery, instance=True, spec_set=True)
|
runtime: MagicMock = create_autospec(WebAppRuntimeQuery, instance=True, spec_set=True)
|
||||||
record = _runtime_record()
|
record = _runtime_record()
|
||||||
features = FeatureModel(can_replace_logo=True, webapp_copyright_enabled=False)
|
features = FeatureModel(can_replace_logo=True, webapp_copyright_enabled=copyright_enabled)
|
||||||
features.billing.enabled = True
|
|
||||||
events: list[str] = []
|
events: list[str] = []
|
||||||
runtime.get_runtime_record.side_effect = lambda _app_id: events.append("record") or record
|
runtime.get_runtime_record.side_effect = lambda _app_id: events.append("record") or record
|
||||||
workspace_features.side_effect = lambda _tenant_id, **_kwargs: events.append("features") or features
|
workspace_features.side_effect = lambda _tenant_id, **_kwargs: events.append("features") or features
|
||||||
@ -104,6 +119,7 @@ def test_get_bootstrap_applies_feature_and_branding_policy_after_record_load(
|
|||||||
runtime,
|
runtime,
|
||||||
file_service=file_service,
|
file_service=file_service,
|
||||||
workspace_features=workspace_features,
|
workspace_features=workspace_features,
|
||||||
|
deployment_edition=deployment_edition,
|
||||||
).get_bootstrap("app-1")
|
).get_bootstrap("app-1")
|
||||||
|
|
||||||
assert result == WebAppBootstrap(
|
assert result == WebAppBootstrap(
|
||||||
@ -112,8 +128,8 @@ def test_get_bootstrap_applies_feature_and_branding_policy_after_record_load(
|
|||||||
enable_site=True,
|
enable_site=True,
|
||||||
site={
|
site={
|
||||||
**record.site._asdict(),
|
**record.site._asdict(),
|
||||||
"copyright": None,
|
"copyright": expected_copyright,
|
||||||
"input_placeholder": None,
|
"input_placeholder": expected_placeholder,
|
||||||
"icon_url": "https://icon",
|
"icon_url": "https://icon",
|
||||||
},
|
},
|
||||||
plan="pro",
|
plan="pro",
|
||||||
|
|||||||
@ -27,7 +27,6 @@ def test_get_effective_credit_pool_prefers_available_paid_pool(
|
|||||||
quota_used=quota_used,
|
quota_used=quota_used,
|
||||||
)
|
)
|
||||||
billing_info = {
|
billing_info = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM},
|
"subscription": {"plan": CloudPlan.TEAM},
|
||||||
"next_credit_reset_date": 1775001600,
|
"next_credit_reset_date": 1775001600,
|
||||||
}
|
}
|
||||||
@ -59,7 +58,6 @@ def test_get_effective_credit_pool_exposes_exhausted_trial_pool(unbound_session:
|
|||||||
exhausted_at=1772323200,
|
exhausted_at=1772323200,
|
||||||
)
|
)
|
||||||
billing_info = {
|
billing_info = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.SANDBOX},
|
"subscription": {"plan": CloudPlan.SANDBOX},
|
||||||
}
|
}
|
||||||
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ def test_get_current_workspace_summary_sandbox_uses_trial_only() -> None:
|
|||||||
quota_used=20,
|
quota_used=20,
|
||||||
)
|
)
|
||||||
billing_info = {
|
billing_info = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.SANDBOX},
|
"subscription": {"plan": CloudPlan.SANDBOX},
|
||||||
}
|
}
|
||||||
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@ -61,7 +60,6 @@ def test_get_current_workspace_summary_falls_back_from_exhausted_paid_pool() ->
|
|||||||
quota_used=40,
|
quota_used=40,
|
||||||
)
|
)
|
||||||
billing_info = {
|
billing_info = {
|
||||||
"enabled": True,
|
|
||||||
"subscription": {"plan": CloudPlan.TEAM},
|
"subscription": {"plan": CloudPlan.TEAM},
|
||||||
}
|
}
|
||||||
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
config = SimpleNamespace(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from core.indexing_runner import DocumentIsPausedError
|
from core.indexing_runner import DocumentIsPausedError
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||||
from enums import CloudPlan
|
from enums import CloudPlan, DeploymentEdition
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from models.dataset import Dataset, Document
|
from models.dataset import Dataset, Document
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||||
@ -27,7 +27,7 @@ from tasks.document_indexing_task import (
|
|||||||
normal_document_indexing_task,
|
normal_document_indexing_task,
|
||||||
priority_document_indexing_task,
|
priority_document_indexing_task,
|
||||||
)
|
)
|
||||||
from tests.unit_tests.config_override import apply_config_overrides
|
from tests.unit_tests.config_override import apply_config_overrides, config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -68,13 +68,12 @@ def indexing_runner(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
|
|||||||
|
|
||||||
def _features(
|
def _features(
|
||||||
*,
|
*,
|
||||||
billing_enabled: bool = False,
|
|
||||||
plan: CloudPlan = CloudPlan.PROFESSIONAL,
|
plan: CloudPlan = CloudPlan.PROFESSIONAL,
|
||||||
vector_limit: int = 1000,
|
vector_limit: int = 1000,
|
||||||
vector_size: int = 0,
|
vector_size: int = 0,
|
||||||
) -> SimpleNamespace:
|
) -> SimpleNamespace:
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
billing=SimpleNamespace(enabled=billing_enabled, subscription=SimpleNamespace(plan=plan)),
|
billing=SimpleNamespace(subscription=SimpleNamespace(plan=plan)),
|
||||||
vector_space=SimpleNamespace(limit=vector_limit, size=vector_size),
|
vector_space=SimpleNamespace(limit=vector_limit, size=vector_size),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -137,6 +136,7 @@ def _persisted_documents(session: Session, document_ids: list[str]) -> list[Docu
|
|||||||
|
|
||||||
|
|
||||||
class TestTaskEnqueuing:
|
class TestTaskEnqueuing:
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_self_hosted_dispatches_directly_to_priority_task(
|
def test_self_hosted_dispatches_directly_to_priority_task(
|
||||||
self, tenant_id: str, dataset_id: str, document_ids: list[str], mock_redis: MagicMock
|
self, tenant_id: str, dataset_id: str, document_ids: list[str], mock_redis: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -144,7 +144,6 @@ class TestTaskEnqueuing:
|
|||||||
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
||||||
patch.object(DocumentIndexingTaskProxy, "PRIORITY_TASK_FUNC", Mock()) as task,
|
patch.object(DocumentIndexingTaskProxy, "PRIORITY_TASK_FUNC", Mock()) as task,
|
||||||
):
|
):
|
||||||
features.billing.enabled = False
|
|
||||||
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
||||||
|
|
||||||
task.delay.assert_called_once_with(
|
task.delay.assert_called_once_with(
|
||||||
@ -153,6 +152,7 @@ class TestTaskEnqueuing:
|
|||||||
document_ids=document_ids,
|
document_ids=document_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("plan", "task_attribute"),
|
("plan", "task_attribute"),
|
||||||
[
|
[
|
||||||
@ -173,13 +173,13 @@ class TestTaskEnqueuing:
|
|||||||
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
||||||
patch.object(DocumentIndexingTaskProxy, task_attribute, Mock()) as task,
|
patch.object(DocumentIndexingTaskProxy, task_attribute, Mock()) as task,
|
||||||
):
|
):
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = plan
|
features.billing.subscription.plan = plan
|
||||||
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
||||||
|
|
||||||
mock_redis.setex.assert_called()
|
mock_redis.setex.assert_called()
|
||||||
task.delay.assert_called_once()
|
task.delay.assert_called_once()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
def test_running_tenant_task_queues_followup_work(
|
def test_running_tenant_task_queues_followup_work(
|
||||||
self, tenant_id: str, dataset_id: str, document_ids: list[str], mock_redis: MagicMock
|
self, tenant_id: str, dataset_id: str, document_ids: list[str], mock_redis: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -188,7 +188,6 @@ class TestTaskEnqueuing:
|
|||||||
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
patch.object(DocumentIndexingTaskProxy, "features") as features,
|
||||||
patch.object(DocumentIndexingTaskProxy, "PRIORITY_TASK_FUNC", Mock()) as task,
|
patch.object(DocumentIndexingTaskProxy, "PRIORITY_TASK_FUNC", Mock()) as task,
|
||||||
):
|
):
|
||||||
features.billing.enabled = True
|
|
||||||
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
features.billing.subscription.plan = CloudPlan.PROFESSIONAL
|
||||||
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
DocumentIndexingTaskProxy(tenant_id, dataset_id, document_ids).delay()
|
||||||
|
|
||||||
@ -285,12 +284,13 @@ class TestDocumentIndexing:
|
|||||||
get_features.assert_not_called()
|
get_features.assert_not_called()
|
||||||
runner_class.assert_not_called()
|
runner_class.assert_not_called()
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("features", "batch_limit", "message"),
|
("features", "batch_limit", "message"),
|
||||||
[
|
[
|
||||||
(_features(billing_enabled=True), 1, "batch upload limit"),
|
(_features(), 1, "batch upload limit"),
|
||||||
(_features(billing_enabled=True, plan=CloudPlan.SANDBOX), 100, "does not support batch upload"),
|
(_features(plan=CloudPlan.SANDBOX), 100, "does not support batch upload"),
|
||||||
(_features(billing_enabled=True, vector_limit=100, vector_size=100), 100, "over the limit"),
|
(_features(vector_limit=100, vector_size=100), 100, "over the limit"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_validation_failure_marks_every_scoped_document_error(
|
def test_validation_failure_marks_every_scoped_document_error(
|
||||||
|
|||||||
@ -4,13 +4,16 @@ from uuid import uuid4
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models import Account, Tenant, TenantAccountJoin
|
from models import Account, Tenant, TenantAccountJoin
|
||||||
from models.account import TenantAccountRole
|
from models.account import TenantAccountRole
|
||||||
from models.dataset import Dataset, Document
|
from models.dataset import Dataset, Document
|
||||||
from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus
|
from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||||
from tasks.retry_document_indexing_task import retry_document_indexing_task
|
from tasks.retry_document_indexing_task import retry_document_indexing_task
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_retry_enforces_vector_space_admission(sqlite_session: Session) -> None:
|
def test_retry_enforces_vector_space_admission(sqlite_session: Session) -> None:
|
||||||
tenant = Tenant(name="Retry tenant")
|
tenant = Tenant(name="Retry tenant")
|
||||||
user = Account(name="Retry user", email=f"retry-{uuid4()}@example.com")
|
user = Account(name="Retry user", email=f"retry-{uuid4()}@example.com")
|
||||||
@ -49,7 +52,6 @@ def test_retry_enforces_vector_space_admission(sqlite_session: Session) -> None:
|
|||||||
sqlite_session.add_all([tenant, user, membership, dataset, document])
|
sqlite_session.add_all([tenant, user, membership, dataset, document])
|
||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
features = MagicMock()
|
features = MagicMock()
|
||||||
features.billing.enabled = False
|
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("tasks.retry_document_indexing_task.FeatureService.get_features", return_value=features),
|
patch("tasks.retry_document_indexing_task.FeatureService.get_features", return_value=features),
|
||||||
|
|||||||
@ -5,9 +5,11 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.rag.index_processor.constant.index_type import IndexStructureType
|
from core.rag.index_processor.constant.index_type import IndexStructureType
|
||||||
|
from enums import DeploymentEdition
|
||||||
from models.dataset import Dataset, Document, DocumentSegment
|
from models.dataset import Dataset, Document, DocumentSegment
|
||||||
from models.enums import DataSourceType, DocumentCreatedFrom
|
from models.enums import DataSourceType, DocumentCreatedFrom
|
||||||
from tasks.sync_website_document_indexing_task import sync_website_document_indexing_task
|
from tasks.sync_website_document_indexing_task import sync_website_document_indexing_task
|
||||||
|
from tests.unit_tests.config_override import config_overrides_context
|
||||||
|
|
||||||
|
|
||||||
def _dataset(tenant_id: str) -> Dataset:
|
def _dataset(tenant_id: str) -> Dataset:
|
||||||
@ -66,6 +68,7 @@ def test_rejects_document_outside_dataset_before_side_effects(sqlite_session: Se
|
|||||||
processor_factory.assert_not_called()
|
processor_factory.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@config_overrides_context(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
|
||||||
def test_cleanup_is_owner_scoped_and_skips_empty_vector_ids(sqlite_session: Session) -> None:
|
def test_cleanup_is_owner_scoped_and_skips_empty_vector_ids(sqlite_session: Session) -> None:
|
||||||
tenant_id = str(uuid.uuid4())
|
tenant_id = str(uuid.uuid4())
|
||||||
dataset = _dataset(tenant_id)
|
dataset = _dataset(tenant_id)
|
||||||
@ -83,7 +86,6 @@ def test_cleanup_is_owner_scoped_and_skips_empty_vector_ids(sqlite_session: Sess
|
|||||||
sqlite_session.commit()
|
sqlite_session.commit()
|
||||||
|
|
||||||
features = MagicMock()
|
features = MagicMock()
|
||||||
features.billing.enabled = False
|
|
||||||
with (
|
with (
|
||||||
patch("tasks.sync_website_document_indexing_task.FeatureService.get_features", return_value=features),
|
patch("tasks.sync_website_document_indexing_task.FeatureService.get_features", return_value=features),
|
||||||
patch("tasks.sync_website_document_indexing_task.IndexProcessorFactory") as processor_factory,
|
patch("tasks.sync_website_document_indexing_task.IndexProcessorFactory") as processor_factory,
|
||||||
|
|||||||
@ -46,7 +46,6 @@ export type Quota = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type BillingModel = {
|
export type BillingModel = {
|
||||||
enabled: boolean
|
|
||||||
subscription: SubscriptionModel
|
subscription: SubscriptionModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,6 @@ export const zSubscriptionModel = z.object({
|
|||||||
* BillingModel
|
* BillingModel
|
||||||
*/
|
*/
|
||||||
export const zBillingModel = z.object({
|
export const zBillingModel = z.object({
|
||||||
enabled: z.boolean().default(false),
|
|
||||||
subscription: zSubscriptionModel.default({ interval: '', plan: 'sandbox' }),
|
subscription: zSubscriptionModel.default({ interval: '', plan: 'sandbox' }),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -94,10 +93,7 @@ export const zFeatureModel = z.object({
|
|||||||
usage: 0,
|
usage: 0,
|
||||||
}),
|
}),
|
||||||
apps: zLimitationModel.default({ limit: 10, size: 0 }),
|
apps: zLimitationModel.default({ limit: 10, size: 0 }),
|
||||||
billing: zBillingModel.default({
|
billing: zBillingModel.default({ subscription: { interval: '', plan: 'sandbox' } }),
|
||||||
enabled: false,
|
|
||||||
subscription: { interval: '', plan: 'sandbox' },
|
|
||||||
}),
|
|
||||||
can_replace_logo: z.boolean().default(false),
|
can_replace_logo: z.boolean().default(false),
|
||||||
dataset_operator_enabled: z.boolean().default(false),
|
dataset_operator_enabled: z.boolean().default(false),
|
||||||
docs_processing: z.string().default('standard'),
|
docs_processing: z.string().default('standard'),
|
||||||
|
|||||||
@ -32,7 +32,6 @@ const render = (ui: React.ReactElement) => {
|
|||||||
})
|
})
|
||||||
seedFeatures(queryClient, {
|
seedFeatures(queryClient, {
|
||||||
billing: {
|
billing: {
|
||||||
enabled: true,
|
|
||||||
subscription: { interval: 'month', plan: mockCurrentPlan },
|
subscription: { interval: 'month', plan: mockCurrentPlan },
|
||||||
},
|
},
|
||||||
education: { enabled: mockEducationEnabled },
|
education: { enabled: mockEducationEnabled },
|
||||||
|
|||||||
@ -61,7 +61,6 @@ describe('billing utils', () => {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
},
|
},
|
||||||
billing: {
|
billing: {
|
||||||
enabled: true,
|
|
||||||
subscription: {
|
subscription: {
|
||||||
interval: '',
|
interval: '',
|
||||||
plan: 'sandbox',
|
plan: 'sandbox',
|
||||||
@ -144,7 +143,6 @@ describe('billing utils', () => {
|
|||||||
it('should derive vector space total from plan config', () => {
|
it('should derive vector space total from plan config', () => {
|
||||||
const data = createMockPlanData({
|
const data = createMockPlanData({
|
||||||
billing: {
|
billing: {
|
||||||
enabled: true,
|
|
||||||
subscription: {
|
subscription: {
|
||||||
interval: '',
|
interval: '',
|
||||||
plan: 'professional',
|
plan: 'professional',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user