test: use typed config overrides in service coverage (#40848)

This commit is contained in:
Asuka Minato authored and GitHub committed 2026-08-18 11:59:58 +00:00
1 parent b064a13aa4
commit da6ca23b50
4 files changed
+208 -344

No files matched your search

+29 -84
View File
@@ -21,28 +21,23 @@ class TestQuotaType:
class TestQuotaService:
def test_reserve_outside_cloud_edition(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService"),
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
@pytest.fixture(autouse=True)
def _cloud_edition(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
def test_reserve_outside_cloud_edition(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
with patch("services.billing_service.BillingService"):
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1")
assert charge.success is True
assert charge.charge_id is None
def test_reserve_zero_amount_raises(self):
with patch("services.quota_service.dify_config") as mock_cfg:
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=0)
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=0)
def test_reserve_success(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_reserve.return_value = {"reservation_id": "rid-1", "available": 99}
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1", amount=1)
@@ -57,11 +52,7 @@ class TestQuotaService:
def test_reserve_no_reservation_id_raises(self):
from services.errors.app import QuotaExceededError
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_reserve.return_value = {}
with pytest.raises(QuotaExceededError):
@@ -70,22 +61,14 @@ class TestQuotaService:
def test_reserve_quota_exceeded_propagates(self):
from services.errors.app import QuotaExceededError
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_reserve.side_effect = QuotaExceededError(feature="trigger", tenant_id="t1", required=1)
with pytest.raises(QuotaExceededError):
QuotaService.reserve(QuotaType.TRIGGER, "t1")
def test_reserve_api_exception_returns_unlimited(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_reserve.side_effect = RuntimeError("network")
charge = QuotaService.reserve(QuotaType.TRIGGER, "t1")
@@ -93,11 +76,7 @@ class TestQuotaService:
assert charge.charge_id is None
def test_consume_calls_reserve_and_commit(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_reserve.return_value = {"reservation_id": "rid-c"}
mock_bs.quota_commit.return_value = {}
@@ -105,73 +84,43 @@ class TestQuotaService:
assert charge.success is True
mock_bs.quota_commit.assert_called_once()
def test_check_outside_cloud_edition(self):
with patch("services.quota_service.dify_config") as mock_cfg:
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
assert QuotaService.check(QuotaType.TRIGGER, "t1") is True
def test_check_outside_cloud_edition(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
assert QuotaService.check(QuotaType.TRIGGER, "t1") is True
def test_check_zero_amount_raises(self):
with patch("services.quota_service.dify_config") as mock_cfg:
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.check(QuotaType.TRIGGER, "t1", amount=0)
with pytest.raises(ValueError, match="greater than 0"):
QuotaService.check(QuotaType.TRIGGER, "t1", amount=0)
def test_check_sufficient_quota(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=100),
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch.object(QuotaService, "get_remaining", return_value=100):
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=50) is True
def test_check_insufficient_quota(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=5),
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch.object(QuotaService, "get_remaining", return_value=5):
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=10) is False
def test_check_unlimited_quota(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", return_value=-1),
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch.object(QuotaService, "get_remaining", return_value=-1):
assert QuotaService.check(QuotaType.TRIGGER, "t1", amount=999) is True
def test_check_exception_returns_true(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch.object(QuotaService, "get_remaining", side_effect=RuntimeError),
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch.object(QuotaService, "get_remaining", side_effect=RuntimeError):
assert QuotaService.check(QuotaType.TRIGGER, "t1") is True
def test_release_outside_cloud_edition(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
def test_release_outside_cloud_edition(self, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY)
with patch("services.billing_service.BillingService") as mock_bs:
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
mock_bs.quota_release.assert_not_called()
def test_release_empty_reservation(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
QuotaService.release(QuotaType.TRIGGER, "", "t1", "trigger_event")
mock_bs.quota_release.assert_not_called()
def test_release_success(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_release.return_value = {}
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
mock_bs.quota_release.assert_called_once_with(
@@ -179,11 +128,7 @@ class TestQuotaService:
)
def test_release_exception_swallowed(self):
with (
patch("services.quota_service.dify_config") as mock_cfg,
patch("services.billing_service.BillingService") as mock_bs,
):
mock_cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
with patch("services.billing_service.BillingService") as mock_bs:
mock_bs.quota_release.side_effect = RuntimeError("fail")
QuotaService.release(QuotaType.TRIGGER, "rid-1", "t1", "trigger_event")
+12 -19
View File
@@ -12,18 +12,16 @@ class TestPassportService:
"""Test PassportService JWT operations"""
@pytest.fixture
def passport_service(self):
def passport_service(self, config_overrides):
"""Create PassportService instance with test secret key"""
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = "test-secret-key-for-testing"
return PassportService()
config_overrides(SECRET_KEY="test-secret-key-for-testing")
return PassportService()
@pytest.fixture
def another_passport_service(self):
def another_passport_service(self, config_overrides):
"""Create another PassportService instance with different secret key"""
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = "another-secret-key-for-testing"
return PassportService()
config_overrides(SECRET_KEY="another-secret-key-for-testing")
return PassportService()
# Core functionality tests
def test_should_issue_and_verify_token(self, passport_service):
@@ -98,10 +96,8 @@ class TestPassportService:
payload = {"user_id": "123"}
# Create token with different algorithm
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = "test-secret-key-for-testing"
# Create token with HS512 instead of HS256
wrong_alg_token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS512")
# Create token with HS512 instead of HS256
wrong_alg_token = jwt.encode(payload, "test-secret-key-for-testing", algorithm="HS512")
# Should fail because service expects HS256
# InvalidAlgorithmError is now caught by PyJWTError handler
@@ -134,20 +130,17 @@ class TestPassportService:
past_time = datetime.now(UTC) - timedelta(hours=1)
payload = {"user_id": "123", "exp": past_time.timestamp()}
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = "test-secret-key-for-testing"
token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS256")
token = jwt.encode(payload, "test-secret-key-for-testing", algorithm="HS256")
with pytest.raises(Unauthorized) as exc_info:
passport_service.verify(token)
assert str(exc_info.value) == "401 Unauthorized: Token has expired."
# Configuration tests
def test_should_use_configured_secret_key_without_policy_validation(self):
def test_should_use_configured_secret_key_without_policy_validation(self, config_overrides):
"""Test that policy decisions are owned by config, not PassportService."""
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = "configured"
service = PassportService()
config_overrides(SECRET_KEY="configured")
service = PassportService()
assert service.sk == "configured"
@@ -26,12 +26,17 @@ def mock_repo():
return MagicMock()
@pytest.fixture(autouse=True)
def _cleanup_config(config_overrides):
config_overrides(
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=0,
DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY,
)
@pytest.fixture
def cleanup(mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
yield WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
return WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
# ---------------------------------------------------------------------------
@@ -41,91 +46,68 @@ def cleanup(mock_repo):
class TestWorkflowRunCleanupInit:
def test_only_start_from_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
def test_only_end_before_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
batch_size=10,
end_before=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
with pytest.raises(ValueError, match="both set or both omitted"):
WorkflowRunCleanup(
days=30,
batch_size=10,
end_before=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
def test_end_before_not_greater_than_start_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="end_before must be greater than start_from"):
WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 6, 1),
end_before=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
with pytest.raises(ValueError, match="end_before must be greater than start_from"):
WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 6, 1),
end_before=datetime.datetime(2024, 1, 1),
workflow_run_repo=mock_repo,
)
def test_equal_start_end_raises(self, mock_repo):
dt = datetime.datetime(2024, 1, 1)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError):
WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=dt,
end_before=dt,
workflow_run_repo=mock_repo,
)
def test_zero_batch_size_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError, match="batch_size must be greater than 0"):
WorkflowRunCleanup(days=30, batch_size=0, workflow_run_repo=mock_repo)
def test_negative_batch_size_raises(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(ValueError):
WorkflowRunCleanup(days=30, batch_size=-1, workflow_run_repo=mock_repo)
def test_valid_window_init(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 7
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
start = datetime.datetime(2024, 1, 1)
end = datetime.datetime(2024, 6, 1)
c = WorkflowRunCleanup(
with pytest.raises(ValueError):
WorkflowRunCleanup(
days=30,
batch_size=5,
start_from=start,
end_before=end,
batch_size=10,
start_from=dt,
end_before=dt,
workflow_run_repo=mock_repo,
)
assert c.window_start == start
assert c.window_end == end
def test_zero_batch_size_raises(self, mock_repo):
with pytest.raises(ValueError, match="batch_size must be greater than 0"):
WorkflowRunCleanup(days=30, batch_size=0, workflow_run_repo=mock_repo)
def test_negative_batch_size_raises(self, mock_repo):
with pytest.raises(ValueError):
WorkflowRunCleanup(days=30, batch_size=-1, workflow_run_repo=mock_repo)
def test_valid_window_init(self, mock_repo, config_overrides):
config_overrides(SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=7)
start = datetime.datetime(2024, 1, 1)
end = datetime.datetime(2024, 6, 1)
c = WorkflowRunCleanup(
days=30,
batch_size=5,
start_from=start,
end_before=end,
workflow_run_repo=mock_repo,
)
assert c.window_start == start
assert c.window_end == end
def test_default_task_label_is_custom(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
assert c._metrics._base_attributes["task_label"] == "custom"
@@ -219,21 +201,15 @@ class TestIsWithinGracePeriod:
class TestGetCleanupWhitelist:
def test_non_cloud_edition_returns_empty(self, cleanup):
cleanup._cleanup_whitelist = None
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
result = cleanup._get_cleanup_whitelist()
result = cleanup._get_cleanup_whitelist()
assert result == set()
def test_cloud_edition_fetches_whitelist(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_expired_subscription_cleanup_whitelist.return_value = ["t1", "t2"]
result = c._get_cleanup_whitelist()
def test_cloud_edition_fetches_whitelist(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_expired_subscription_cleanup_whitelist.return_value = ["t1", "t2"]
result = c._get_cleanup_whitelist()
assert result == {"t1", "t2"}
def test_cached_whitelist_returned(self, cleanup):
@@ -241,16 +217,12 @@ class TestGetCleanupWhitelist:
result = cleanup._get_cleanup_whitelist()
assert result == {"cached"}
def test_billing_service_error_returns_empty(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_expired_subscription_cleanup_whitelist.side_effect = Exception("error")
result = c._get_cleanup_whitelist()
def test_billing_service_error_returns_empty(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_expired_subscription_cleanup_whitelist.side_effect = Exception("error")
result = c._get_cleanup_whitelist()
assert result == set()
@@ -264,68 +236,51 @@ class TestFilterFreeTenants:
result = cleanup._filter_free_tenants(["t1", "t2"])
assert result == {"t1", "t2"}
def test_empty_tenants_returns_empty(self, cleanup):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
result = cleanup._filter_free_tenants([])
def test_empty_tenants_returns_empty(self, cleanup, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
result = cleanup._filter_free_tenants([])
assert result == set()
def test_whitelisted_tenant_excluded(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = {"t1"}
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_plan_bulk_with_cache.return_value = {
"t1": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
"t2": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
}
result = c._filter_free_tenants(["t1", "t2"])
def test_whitelisted_tenant_excluded(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = {"t1"}
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_plan_bulk_with_cache.return_value = {
"t1": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
"t2": {"plan": CloudPlan.SANDBOX, "expiration_date": -1},
}
result = c._filter_free_tenants(["t1", "t2"])
assert "t1" not in result
assert "t2" in result
def test_paid_tenant_excluded(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_plan_bulk_with_cache.return_value = {
"t1": {"plan": "professional", "expiration_date": -1},
}
result = c._filter_free_tenants(["t1"])
def test_paid_tenant_excluded(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_plan_bulk_with_cache.return_value = {
"t1": {"plan": "professional", "expiration_date": -1},
}
result = c._filter_free_tenants(["t1"])
assert result == set()
def test_missing_billing_info_treats_as_non_free(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_plan_bulk_with_cache.return_value = {}
result = c._filter_free_tenants(["t1"])
def test_missing_billing_info_treats_as_non_free(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_plan_bulk_with_cache.return_value = {}
result = c._filter_free_tenants(["t1"])
assert result == set()
def test_billing_bulk_error_treats_as_non_free(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch(
"services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService"
) as bs:
bs.get_plan_bulk_with_cache.side_effect = Exception("fail")
result = c._filter_free_tenants(["t1"])
def test_billing_bulk_error_treats_as_non_free(self, mock_repo, config_overrides):
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
c = WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
c._cleanup_whitelist = set()
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.BillingService") as bs:
bs.get_plan_bulk_with_cache.side_effect = Exception("fail")
result = c._filter_free_tenants(["t1"])
assert result == set()
@@ -336,17 +291,12 @@ class TestFilterFreeTenants:
class TestRunDeleteMode:
def _make_cleanup(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
return WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
return WorkflowRunCleanup(days=30, batch_size=10, workflow_run_repo=mock_repo)
def test_no_rows_stops_immediately(self, mock_repo):
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
def test_all_paid_skips_delete(self, mock_repo):
@@ -355,9 +305,7 @@ class TestRunDeleteMode:
c = self._make_cleanup(mock_repo)
# Override the non-Cloud default to exercise the no-deletion path.
c._filter_free_tenants = MagicMock(return_value=set())
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
def test_runs_deleted_successfully(self, mock_repo):
@@ -373,10 +321,8 @@ class TestRunDeleteMode:
"pause_reasons": 0,
}
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.time.sleep"):
c.run()
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.time.sleep"):
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_called_once()
def test_delete_exception_reraises(self, mock_repo):
@@ -384,24 +330,19 @@ class TestRunDeleteMode:
mock_repo.get_cleanup_refs_batch_by_time_range.side_effect = [[ref], [ref]]
mock_repo.delete_runs_with_related_by_ids.side_effect = RuntimeError("db error")
c = self._make_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
with pytest.raises(RuntimeError):
c.run()
with pytest.raises(RuntimeError):
c.run()
def test_summary_with_window_start(self, mock_repo):
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
end_before=datetime.datetime(2024, 6, 1),
workflow_run_repo=mock_repo,
)
c.run()
c = WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
end_before=datetime.datetime(2024, 6, 1),
workflow_run_repo=mock_repo,
)
c.run()
# ---------------------------------------------------------------------------
@@ -411,15 +352,12 @@ class TestRunDeleteMode:
class TestRunDryRunMode:
def _make_dry_cleanup(self, mock_repo):
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
return WorkflowRunCleanup(
days=30,
batch_size=10,
workflow_run_repo=mock_repo,
dry_run=True,
)
return WorkflowRunCleanup(
days=30,
batch_size=10,
workflow_run_repo=mock_repo,
dry_run=True,
)
def test_dry_run_no_delete_called(self, mock_repo):
ref = make_ref("t1")
@@ -434,35 +372,28 @@ class TestRunDryRunMode:
"pause_reasons": 0,
}
c = self._make_dry_cleanup(mock_repo)
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
c.run()
mock_repo.delete_runs_with_related_by_ids.assert_not_called()
mock_repo.count_runs_with_related_by_ids.assert_called_once()
def test_dry_run_summary_with_window_start(self, mock_repo):
mock_repo.get_cleanup_refs_batch_by_time_range.return_value = []
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD = 0
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c = WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
end_before=datetime.datetime(2024, 6, 1),
workflow_run_repo=mock_repo,
dry_run=True,
)
c.run()
c = WorkflowRunCleanup(
days=30,
batch_size=10,
start_from=datetime.datetime(2024, 1, 1),
end_before=datetime.datetime(2024, 6, 1),
workflow_run_repo=mock_repo,
dry_run=True,
)
c.run()
def test_dry_run_all_paid_skips_count(self, mock_repo):
ref = make_ref("t1")
mock_repo.get_cleanup_refs_batch_by_time_range.side_effect = [[ref], []]
c = self._make_dry_cleanup(mock_repo)
c._filter_free_tenants = MagicMock(return_value=set())
with patch("services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs.dify_config") as cfg:
cfg.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
c.run()
c.run()
mock_repo.count_runs_with_related_by_ids.assert_not_called()
@@ -27,13 +27,12 @@ class TestSystemEncrypter:
expected_key = hashlib.sha256(secret_key.encode()).digest()
assert encrypter.key == expected_key
def test_init_with_none_secret_key(self):
def test_init_with_none_secret_key(self, config_overrides):
"""Test initialization with None secret key falls back to config"""
with patch("core.tools.utils.system_encryption.dify_config") as mock_config:
mock_config.SECRET_KEY = "config_secret"
encrypter = SystemEncrypter(secret_key=None)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
config_overrides(SECRET_KEY="config_secret")
encrypter = SystemEncrypter(secret_key=None)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
def test_init_with_empty_secret_key(self):
"""Test initialization with empty secret key"""
@@ -41,13 +40,12 @@ class TestSystemEncrypter:
expected_key = hashlib.sha256(b"").digest()
assert encrypter.key == expected_key
def test_init_without_secret_key_uses_config(self):
def test_init_without_secret_key_uses_config(self, config_overrides):
"""Test initialization without secret key uses config"""
with patch("core.tools.utils.system_encryption.dify_config") as mock_config:
mock_config.SECRET_KEY = "default_secret"
encrypter = SystemEncrypter()
expected_key = hashlib.sha256(b"default_secret").digest()
assert encrypter.key == expected_key
config_overrides(SECRET_KEY="default_secret")
encrypter = SystemEncrypter()
expected_key = hashlib.sha256(b"default_secret").digest()
assert encrypter.key == expected_key
def test_encrypt_params_basic(self):
"""Test basic parameters encryption"""
@@ -368,25 +366,23 @@ class TestFactoryFunctions:
expected_key = hashlib.sha256(secret_key.encode()).digest()
assert encrypter.key == expected_key
def test_create_system_encrypter_without_secret(self):
def test_create_system_encrypter_without_secret(self, config_overrides):
"""Test factory function without secret key"""
with patch("core.tools.utils.system_encryption.dify_config") as mock_config:
mock_config.SECRET_KEY = "config_secret"
encrypter = create_system_encrypter()
config_overrides(SECRET_KEY="config_secret")
encrypter = create_system_encrypter()
assert isinstance(encrypter, SystemEncrypter)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
assert isinstance(encrypter, SystemEncrypter)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
def test_create_system_encrypter_with_none_secret(self):
def test_create_system_encrypter_with_none_secret(self, config_overrides):
"""Test factory function with None secret key"""
with patch("core.tools.utils.system_encryption.dify_config") as mock_config:
mock_config.SECRET_KEY = "config_secret"
encrypter = create_system_encrypter(None)
config_overrides(SECRET_KEY="config_secret")
encrypter = create_system_encrypter(None)
assert isinstance(encrypter, SystemEncrypter)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
assert isinstance(encrypter, SystemEncrypter)
expected_key = hashlib.sha256(b"config_secret").digest()
assert encrypter.key == expected_key
class TestGlobalEncrypterInstance:
@@ -405,19 +401,18 @@ class TestGlobalEncrypterInstance:
assert encrypter1 is encrypter2
assert isinstance(encrypter1, SystemEncrypter)
def test_get_system_encrypter_uses_config(self):
def test_get_system_encrypter_uses_config(self, config_overrides):
"""Test that global encrypter uses config"""
# Clear the global instance first
import core.tools.utils.system_encryption
core.tools.utils.system_encryption._encrypter = None
with patch("core.tools.utils.system_encryption.dify_config") as mock_config:
mock_config.SECRET_KEY = "global_secret"
encrypter = get_system_encrypter()
config_overrides(SECRET_KEY="global_secret")
encrypter = get_system_encrypter()
expected_key = hashlib.sha256(b"global_secret").digest()
assert encrypter.key == expected_key
expected_key = hashlib.sha256(b"global_secret").digest()
assert encrypter.key == expected_key
class TestConvenienceFunctions: