Files
dify/api/tests/unit_tests/services/test_trigger_provider_service.py

1250 lines
44 KiB
Python

from __future__ import annotations
import contextlib
import json
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from pytest_mock import MockerFixture
from constants import HIDDEN_VALUE
from core.plugin.entities.plugin_daemon import CredentialType
from models.provider_ids import TriggerProviderID
from services.trigger.trigger_provider_service import TriggerProviderService
def _patch_redis_lock(mocker: MockerFixture) -> None:
mock_redis = mocker.patch("services.trigger.trigger_provider_service.redis_client")
mock_redis.lock.return_value = contextlib.nullcontext()
def _mock_get_trigger_provider(mocker: MockerFixture, provider: object | None) -> None:
mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.get_trigger_provider",
return_value=provider,
)
def _encrypter_mock(
*,
decrypted: dict | None = None,
encrypted: dict | None = None,
masked: dict | None = None,
) -> MagicMock:
enc = MagicMock()
enc.decrypt.return_value = decrypted or {}
enc.encrypt.return_value = encrypted or {}
enc.mask_credentials.return_value = masked or {}
enc.mask_plugin_credentials.return_value = masked or {}
return enc
@pytest.fixture
def provider_id() -> TriggerProviderID:
# Arrange
return TriggerProviderID("langgenius/github/github")
@pytest.fixture(autouse=True)
def mock_db_engine(mocker: MockerFixture) -> SimpleNamespace:
# Arrange
mocked_db = SimpleNamespace(engine=object())
mocker.patch("services.trigger.trigger_provider_service.db", mocked_db)
return mocked_db
@pytest.fixture
def mock_session(mocker: MockerFixture) -> MagicMock:
"""Mocks the database session context manager used by TriggerProviderService."""
# Arrange
mock_session_instance = MagicMock()
mock_session_cm = MagicMock()
mock_session_cm.__enter__.return_value = mock_session_instance
mock_session_cm.__exit__.return_value = False
mocker.patch("services.trigger.trigger_provider_service.Session", return_value=mock_session_cm)
return mock_session_instance
@pytest.fixture
def provider_controller() -> MagicMock:
# Arrange
controller = MagicMock()
controller.get_credential_schema_config.return_value = []
controller.get_properties_schema.return_value = []
controller.get_oauth_client_schema.return_value = []
controller.plugin_unique_identifier = "langgenius/github:0.0.1"
return controller
def test_get_trigger_provider_should_return_api_entity_from_manager(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
provider = MagicMock()
provider.to_api_entity.return_value = {"provider": "ok"}
_mock_get_trigger_provider(mocker, provider)
# Act
result = TriggerProviderService.get_trigger_provider("tenant-1", provider_id)
# Assert
assert result == {"provider": "ok"}
def test_list_trigger_providers_should_return_api_entities_from_manager(mocker: MockerFixture) -> None:
# Arrange
provider_a = MagicMock()
provider_b = MagicMock()
provider_a.to_api_entity.return_value = {"id": "a"}
provider_b.to_api_entity.return_value = {"id": "b"}
mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.list_all_trigger_providers",
return_value=[provider_a, provider_b],
)
# Act
result = TriggerProviderService.list_trigger_providers("tenant-1")
# Assert
assert result == [{"id": "a"}, {"id": "b"}]
def test_list_trigger_provider_subscriptions_should_return_empty_list_when_no_subscriptions(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
query = MagicMock()
query.filter_by.return_value.order_by.return_value.all.return_value = []
mock_session.query.return_value = query
# Act
result = TriggerProviderService.list_trigger_provider_subscriptions("tenant-1", provider_id)
# Assert
assert result == []
def test_list_trigger_provider_subscriptions_should_mask_fields_and_attach_workflow_counts(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
api_sub = SimpleNamespace(
id="sub-1",
credentials={"token": "enc"},
properties={"hook": "enc"},
parameters={"event": "push"},
workflows_in_use=0,
)
db_sub = SimpleNamespace(to_api_entity=lambda: api_sub)
usage_row = SimpleNamespace(subscription_id="sub-1", app_count=2)
query_subs = MagicMock()
query_subs.filter_by.return_value.order_by.return_value.all.return_value = [db_sub]
query_usage = MagicMock()
query_usage.filter.return_value.group_by.return_value.all.return_value = [usage_row]
mock_session.query.side_effect = [query_subs, query_usage]
_mock_get_trigger_provider(mocker, provider_controller)
cred_enc = _encrypter_mock(decrypted={"token": "plain"}, masked={"token": "****"})
prop_enc = _encrypter_mock(decrypted={"hook": "plain"}, masked={"hook": "****"})
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(cred_enc, MagicMock()),
)
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_properties",
return_value=(prop_enc, MagicMock()),
)
# Act
result = TriggerProviderService.list_trigger_provider_subscriptions("tenant-1", provider_id)
# Assert
assert len(result) == 1
assert result[0].credentials == {"token": "****"}
assert result[0].properties == {"hook": "****"}
assert result[0].workflows_in_use == 2
def test_add_trigger_subscription_should_create_subscription_successfully_for_api_key(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
query_count = MagicMock()
query_count.filter_by.return_value.count.return_value = 0
query_existing = MagicMock()
query_existing.filter_by.return_value.first.return_value = None
mock_session.query.side_effect = [query_count, query_existing]
_mock_get_trigger_provider(mocker, provider_controller)
cred_enc = _encrypter_mock(encrypted={"api_key": "enc"})
prop_enc = _encrypter_mock(encrypted={"project": "enc"})
mocker.patch(
"services.trigger.trigger_provider_service.create_provider_encrypter",
side_effect=[(cred_enc, MagicMock()), (prop_enc, MagicMock())],
)
# Act
result = TriggerProviderService.add_trigger_subscription(
tenant_id="tenant-1",
user_id="user-1",
name="main",
provider_id=provider_id,
endpoint_id="endpoint-1",
credential_type=CredentialType.API_KEY,
parameters={"event": "push"},
properties={"project": "demo"},
credentials={"api_key": "plain"},
)
# Assert
assert result["result"] == "success"
mock_session.add.assert_called_once()
mock_session.commit.assert_called_once()
def test_add_trigger_subscription_should_store_empty_credentials_for_unauthorized_type(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
query_count = MagicMock()
query_count.filter_by.return_value.count.return_value = 0
query_existing = MagicMock()
query_existing.filter_by.return_value.first.return_value = None
mock_session.query.side_effect = [query_count, query_existing]
_mock_get_trigger_provider(mocker, provider_controller)
prop_enc = _encrypter_mock(encrypted={"p": "enc"})
mocker.patch(
"services.trigger.trigger_provider_service.create_provider_encrypter",
return_value=(prop_enc, MagicMock()),
)
# Act
result = TriggerProviderService.add_trigger_subscription(
tenant_id="tenant-1",
user_id="user-1",
name="main",
provider_id=provider_id,
endpoint_id="endpoint-1",
credential_type=CredentialType.UNAUTHORIZED,
parameters={},
properties={"p": "v"},
credentials={},
subscription_id="sub-fixed",
)
# Assert
assert result == {"result": "success", "id": "sub-fixed"}
def test_add_trigger_subscription_should_raise_error_when_provider_limit_reached(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
query_count = MagicMock()
query_count.filter_by.return_value.count.return_value = TriggerProviderService.__MAX_TRIGGER_PROVIDER_COUNT__
mock_session.query.return_value = query_count
_mock_get_trigger_provider(mocker, provider_controller)
mock_logger = mocker.patch("services.trigger.trigger_provider_service.logger")
# Act + Assert
with pytest.raises(ValueError, match="Maximum number of providers"):
TriggerProviderService.add_trigger_subscription(
tenant_id="tenant-1",
user_id="user-1",
name="main",
provider_id=provider_id,
endpoint_id="endpoint-1",
credential_type=CredentialType.API_KEY,
parameters={},
properties={},
credentials={},
)
mock_logger.exception.assert_called_once()
def test_add_trigger_subscription_should_raise_error_when_name_exists(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
query_count = MagicMock()
query_count.filter_by.return_value.count.return_value = 0
query_existing = MagicMock()
query_existing.filter_by.return_value.first.return_value = object()
mock_session.query.side_effect = [query_count, query_existing]
_mock_get_trigger_provider(mocker, provider_controller)
# Act + Assert
with pytest.raises(ValueError, match="Credential name 'main' already exists"):
TriggerProviderService.add_trigger_subscription(
tenant_id="tenant-1",
user_id="user-1",
name="main",
provider_id=provider_id,
endpoint_id="endpoint-1",
credential_type=CredentialType.API_KEY,
parameters={},
properties={},
credentials={},
)
def test_update_trigger_subscription_should_raise_error_when_subscription_not_found(
mocker: MockerFixture,
mock_session: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
query_sub = MagicMock()
query_sub.filter_by.return_value.first.return_value = None
mock_session.query.return_value = query_sub
# Act + Assert
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.update_trigger_subscription("tenant-1", "sub-1")
def test_update_trigger_subscription_should_raise_error_when_name_conflicts(
mocker: MockerFixture,
mock_session: MagicMock,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
subscription = SimpleNamespace(
id="sub-1",
name="old",
provider_id="langgenius/github/github",
credential_type=CredentialType.API_KEY.value,
)
query_sub = MagicMock()
query_sub.filter_by.return_value.first.return_value = subscription
query_existing = MagicMock()
query_existing.filter_by.return_value.first.return_value = object()
mock_session.query.side_effect = [query_sub, query_existing]
_mock_get_trigger_provider(mocker, provider_controller)
# Act + Assert
with pytest.raises(ValueError, match="already exists"):
TriggerProviderService.update_trigger_subscription("tenant-1", "sub-1", name="new-name")
def test_update_trigger_subscription_should_update_fields_and_clear_cache(
mocker: MockerFixture,
mock_session: MagicMock,
provider_controller: MagicMock,
) -> None:
# Arrange
_patch_redis_lock(mocker)
subscription = SimpleNamespace(
id="sub-1",
name="old",
tenant_id="tenant-1",
provider_id="langgenius/github/github",
properties={"project": "enc-old"},
parameters={"event": "old"},
credentials={"api_key": "enc-old"},
credential_type=CredentialType.API_KEY.value,
credential_expires_at=0,
expires_at=0,
)
query_sub = MagicMock()
query_sub.filter_by.return_value.first.return_value = subscription
query_existing = MagicMock()
query_existing.filter_by.return_value.first.return_value = None
mock_session.query.side_effect = [query_sub, query_existing]
_mock_get_trigger_provider(mocker, provider_controller)
prop_enc = _encrypter_mock(decrypted={"project": "old-value"}, encrypted={"project": "new-value"})
cred_enc = _encrypter_mock(encrypted={"api_key": "new-key"})
mocker.patch(
"services.trigger.trigger_provider_service.create_provider_encrypter",
side_effect=[(prop_enc, MagicMock()), (cred_enc, MagicMock())],
)
mock_delete_cache = mocker.patch("services.trigger.trigger_provider_service.delete_cache_for_subscription")
# Act
TriggerProviderService.update_trigger_subscription(
tenant_id="tenant-1",
subscription_id="sub-1",
name="new",
properties={"project": HIDDEN_VALUE, "region": "us"},
parameters={"event": "new"},
credentials={"api_key": "plain-key"},
credential_expires_at=100,
expires_at=200,
)
# Assert
assert subscription.name == "new"
assert subscription.parameters == {"event": "new"}
assert subscription.credentials == {"api_key": "new-key"}
assert subscription.credential_expires_at == 100
assert subscription.expires_at == 200
mock_session.commit.assert_called_once()
mock_delete_cache.assert_called_once()
def test_get_subscription_by_id_should_return_none_when_missing(mocker: MockerFixture, mock_session: MagicMock) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act
result = TriggerProviderService.get_subscription_by_id("tenant-1", "sub-1")
# Assert
assert result is None
def test_get_subscription_by_id_should_decrypt_credentials_and_properties(
mocker: MockerFixture,
mock_session: MagicMock,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-1",
tenant_id="tenant-1",
provider_id="langgenius/github/github",
credentials={"token": "enc"},
properties={"project": "enc"},
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
cred_enc = _encrypter_mock(decrypted={"token": "plain"})
prop_enc = _encrypter_mock(decrypted={"project": "plain"})
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(cred_enc, MagicMock()),
)
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_properties",
return_value=(prop_enc, MagicMock()),
)
# Act
result = TriggerProviderService.get_subscription_by_id("tenant-1", "sub-1")
# Assert
assert result is subscription
assert subscription.credentials == {"token": "plain"}
assert subscription.properties == {"project": "plain"}
def test_delete_trigger_provider_should_raise_error_when_subscription_missing(
mocker: MockerFixture,
mock_session: MagicMock,
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act + Assert
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.delete_trigger_provider(mock_session, "tenant-1", "sub-1")
def test_delete_trigger_provider_should_delete_and_clear_cache_even_if_unsubscribe_fails(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-1",
user_id="user-1",
provider_id=str(provider_id),
credential_type=CredentialType.OAUTH2.value,
credentials={"token": "enc"},
to_entity=lambda: SimpleNamespace(id="sub-1"),
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
cred_enc = _encrypter_mock(decrypted={"token": "plain"})
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(cred_enc, MagicMock()),
)
mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.unsubscribe_trigger",
side_effect=RuntimeError("remote fail"),
)
mock_delete_cache = mocker.patch("services.trigger.trigger_provider_service.delete_cache_for_subscription")
# Act
TriggerProviderService.delete_trigger_provider(mock_session, "tenant-1", "sub-1")
# Assert
mock_session.delete.assert_called_once_with(subscription)
mock_delete_cache.assert_called_once()
def test_delete_trigger_provider_should_skip_unsubscribe_for_unauthorized(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-2",
user_id="user-1",
provider_id=str(provider_id),
credential_type=CredentialType.UNAUTHORIZED.value,
credentials={},
to_entity=lambda: SimpleNamespace(id="sub-2"),
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
mock_unsubscribe = mocker.patch("services.trigger.trigger_provider_service.TriggerManager.unsubscribe_trigger")
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(_encrypter_mock(decrypted={}), MagicMock()),
)
# Act
TriggerProviderService.delete_trigger_provider(mock_session, "tenant-1", "sub-2")
# Assert
mock_unsubscribe.assert_not_called()
mock_session.delete.assert_called_once_with(subscription)
def test_refresh_oauth_token_should_raise_error_when_subscription_missing(
mocker: MockerFixture, mock_session: MagicMock
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act + Assert
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.refresh_oauth_token("tenant-1", "sub-1")
def test_refresh_oauth_token_should_raise_error_for_non_oauth_credentials(
mocker: MockerFixture, mock_session: MagicMock
) -> None:
# Arrange
subscription = SimpleNamespace(credential_type=CredentialType.API_KEY.value)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
# Act + Assert
with pytest.raises(ValueError, match="Only OAuth credentials can be refreshed"):
TriggerProviderService.refresh_oauth_token("tenant-1", "sub-1")
def test_refresh_oauth_token_should_refresh_and_persist_new_credentials(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
provider_id=str(provider_id),
user_id="user-1",
credential_type=CredentialType.OAUTH2.value,
credentials={"access_token": "enc"},
credential_expires_at=0,
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
cache = MagicMock()
cred_enc = _encrypter_mock(decrypted={"access_token": "old"}, encrypted={"access_token": "new"})
mocker.patch(
"services.trigger.trigger_provider_service.create_provider_encrypter",
return_value=(cred_enc, cache),
)
mocker.patch.object(TriggerProviderService, "get_oauth_client", return_value={"client_id": "id"})
refreshed = SimpleNamespace(credentials={"access_token": "new"}, expires_at=12345)
oauth_handler = MagicMock()
oauth_handler.refresh_credentials.return_value = refreshed
mocker.patch("services.trigger.trigger_provider_service.OAuthHandler", return_value=oauth_handler)
# Act
result = TriggerProviderService.refresh_oauth_token("tenant-1", "sub-1")
# Assert
assert result == {"result": "success", "expires_at": 12345}
assert subscription.credentials == {"access_token": "new"}
assert subscription.credential_expires_at == 12345
mock_session.commit.assert_called_once()
cache.delete.assert_called_once()
def test_refresh_subscription_should_raise_error_when_subscription_missing(
mocker: MockerFixture, mock_session: MagicMock
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act + Assert
with pytest.raises(ValueError, match="not found"):
TriggerProviderService.refresh_subscription("tenant-1", "sub-1", now=100)
def test_refresh_subscription_should_skip_when_not_due(mocker: MockerFixture, mock_session: MagicMock) -> None:
# Arrange
subscription = SimpleNamespace(expires_at=200)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
# Act
result = TriggerProviderService.refresh_subscription("tenant-1", "sub-1", now=100)
# Assert
assert result == {"result": "skipped", "expires_at": 200}
def test_refresh_subscription_should_refresh_and_persist_properties(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-1",
tenant_id="tenant-1",
endpoint_id="endpoint-1",
expires_at=50,
provider_id=str(provider_id),
parameters={"event": "push"},
properties={"p": "enc"},
credentials={"c": "enc"},
credential_type=CredentialType.API_KEY.value,
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
cred_enc = _encrypter_mock(decrypted={"c": "plain"})
prop_cache = MagicMock()
prop_enc = _encrypter_mock(decrypted={"p": "plain"}, encrypted={"p": "new-enc"})
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(cred_enc, MagicMock()),
)
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_properties",
return_value=(prop_enc, prop_cache),
)
mocker.patch(
"services.trigger.trigger_provider_service.generate_plugin_trigger_endpoint_url",
return_value="https://endpoint",
)
provider_controller.refresh_trigger.return_value = SimpleNamespace(properties={"p": "new"}, expires_at=999)
# Act
result = TriggerProviderService.refresh_subscription("tenant-1", "sub-1", now=100)
# Assert
assert result == {"result": "success", "expires_at": 999}
assert subscription.properties == {"p": "new-enc"}
assert subscription.expires_at == 999
mock_session.commit.assert_called_once()
prop_cache.delete.assert_called_once()
def test_get_oauth_client_should_return_tenant_client_when_available(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
tenant_client = SimpleNamespace(oauth_params={"client_id": "enc"})
system_client = None
query_tenant = MagicMock()
query_tenant.filter_by.return_value.first.return_value = tenant_client
mock_session.query.return_value = query_tenant
_mock_get_trigger_provider(mocker, provider_controller)
enc = _encrypter_mock(decrypted={"client_id": "plain"})
mocker.patch("services.trigger.trigger_provider_service.create_provider_encrypter", return_value=(enc, MagicMock()))
# Act
result = TriggerProviderService.get_oauth_client("tenant-1", provider_id)
# Assert
assert result == {"client_id": "plain"}
def test_get_oauth_client_should_return_none_when_plugin_not_verified(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
query_tenant = MagicMock()
query_tenant.filter_by.return_value.first.return_value = None
query_system = MagicMock()
query_system.filter_by.return_value.first.return_value = None
mock_session.query.side_effect = [query_tenant, query_system]
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch("services.trigger.trigger_provider_service.PluginService.is_plugin_verified", return_value=False)
# Act
result = TriggerProviderService.get_oauth_client("tenant-1", provider_id)
# Assert
assert result is None
def test_get_oauth_client_should_return_decrypted_system_client_when_verified(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
query_tenant = MagicMock()
query_tenant.filter_by.return_value.first.return_value = None
query_system = MagicMock()
query_system.filter_by.return_value.first.return_value = SimpleNamespace(encrypted_oauth_params="enc")
mock_session.query.side_effect = [query_tenant, query_system]
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch("services.trigger.trigger_provider_service.PluginService.is_plugin_verified", return_value=True)
mocker.patch(
"services.trigger.trigger_provider_service.decrypt_system_oauth_params",
return_value={"client_id": "system"},
)
# Act
result = TriggerProviderService.get_oauth_client("tenant-1", provider_id)
# Assert
assert result == {"client_id": "system"}
def test_get_oauth_client_should_raise_error_when_system_decryption_fails(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
query_tenant = MagicMock()
query_tenant.filter_by.return_value.first.return_value = None
query_system = MagicMock()
query_system.filter_by.return_value.first.return_value = SimpleNamespace(encrypted_oauth_params="enc")
mock_session.query.side_effect = [query_tenant, query_system]
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch("services.trigger.trigger_provider_service.PluginService.is_plugin_verified", return_value=True)
mocker.patch(
"services.trigger.trigger_provider_service.decrypt_system_oauth_params",
side_effect=RuntimeError("bad data"),
)
# Act + Assert
with pytest.raises(ValueError, match="Error decrypting system oauth params"):
TriggerProviderService.get_oauth_client("tenant-1", provider_id)
def test_is_oauth_system_client_exists_should_return_false_when_unverified(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch("services.trigger.trigger_provider_service.PluginService.is_plugin_verified", return_value=False)
# Act
result = TriggerProviderService.is_oauth_system_client_exists("tenant-1", provider_id)
# Assert
assert result is False
@pytest.mark.parametrize("has_client", [True, False])
def test_is_oauth_system_client_exists_should_reflect_database_record(
has_client: bool,
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = object() if has_client else None
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch("services.trigger.trigger_provider_service.PluginService.is_plugin_verified", return_value=True)
# Act
result = TriggerProviderService.is_oauth_system_client_exists("tenant-1", provider_id)
# Assert
assert result is has_client
def test_save_custom_oauth_client_params_should_return_success_when_nothing_to_update(
provider_id: TriggerProviderID,
) -> None:
# Arrange
# Act
result = TriggerProviderService.save_custom_oauth_client_params("tenant-1", provider_id, None, None)
# Assert
assert result == {"result": "success"}
def test_save_custom_oauth_client_params_should_create_record_and_clear_params_when_client_params_none(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
query = MagicMock()
query.filter_by.return_value.first.return_value = None
mock_session.query.return_value = query
_mock_get_trigger_provider(mocker, provider_controller)
fake_model = SimpleNamespace(encrypted_oauth_params="", enabled=False, oauth_params={})
mocker.patch("services.trigger.trigger_provider_service.TriggerOAuthTenantClient", return_value=fake_model)
# Act
result = TriggerProviderService.save_custom_oauth_client_params(
tenant_id="tenant-1",
provider_id=provider_id,
client_params=None,
enabled=True,
)
# Assert
assert result == {"result": "success"}
assert fake_model.encrypted_oauth_params == "{}"
assert fake_model.enabled is True
mock_session.add.assert_called_once_with(fake_model)
mock_session.commit.assert_called_once()
def test_save_custom_oauth_client_params_should_merge_hidden_values_and_delete_cache(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
custom_client = SimpleNamespace(oauth_params={"client_id": "enc-old"}, enabled=False)
mock_session.query.return_value.filter_by.return_value.first.return_value = custom_client
_mock_get_trigger_provider(mocker, provider_controller)
cache = MagicMock()
enc = _encrypter_mock(decrypted={"client_id": "old-id"}, encrypted={"client_id": "new-id"})
mocker.patch(
"services.trigger.trigger_provider_service.create_provider_encrypter",
return_value=(enc, cache),
)
# Act
result = TriggerProviderService.save_custom_oauth_client_params(
tenant_id="tenant-1",
provider_id=provider_id,
client_params={"client_id": HIDDEN_VALUE, "client_secret": "new"},
enabled=None,
)
# Assert
assert result == {"result": "success"}
assert json.loads(custom_client.encrypted_oauth_params) == {"client_id": "new-id"}
cache.delete.assert_called_once()
mock_session.commit.assert_called_once()
def test_get_custom_oauth_client_params_should_return_empty_when_record_missing(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act
result = TriggerProviderService.get_custom_oauth_client_params("tenant-1", provider_id)
# Assert
assert result == {}
def test_get_custom_oauth_client_params_should_return_masked_decrypted_values(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
custom_client = SimpleNamespace(oauth_params={"client_id": "enc"})
mock_session.query.return_value.filter_by.return_value.first.return_value = custom_client
_mock_get_trigger_provider(mocker, provider_controller)
enc = _encrypter_mock(decrypted={"client_id": "plain"}, masked={"client_id": "pl***id"})
mocker.patch("services.trigger.trigger_provider_service.create_provider_encrypter", return_value=(enc, MagicMock()))
# Act
result = TriggerProviderService.get_custom_oauth_client_params("tenant-1", provider_id)
# Assert
assert result == {"client_id": "pl***id"}
def test_delete_custom_oauth_client_params_should_delete_record_and_commit(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.delete.return_value = 1
# Act
result = TriggerProviderService.delete_custom_oauth_client_params("tenant-1", provider_id)
# Assert
assert result == {"result": "success"}
mock_session.commit.assert_called_once()
@pytest.mark.parametrize("exists", [True, False])
def test_is_oauth_custom_client_enabled_should_return_expected_boolean(
exists: bool,
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = object() if exists else None
# Act
result = TriggerProviderService.is_oauth_custom_client_enabled("tenant-1", provider_id)
# Assert
assert result is exists
def test_get_subscription_by_endpoint_should_return_none_when_not_found(
mocker: MockerFixture, mock_session: MagicMock
) -> None:
# Arrange
mock_session.query.return_value.filter_by.return_value.first.return_value = None
# Act
result = TriggerProviderService.get_subscription_by_endpoint("endpoint-1")
# Assert
assert result is None
def test_get_subscription_by_endpoint_should_decrypt_credentials_and_properties(
mocker: MockerFixture,
mock_session: MagicMock,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
tenant_id="tenant-1",
provider_id="langgenius/github/github",
credentials={"token": "enc"},
properties={"hook": "enc"},
)
mock_session.query.return_value.filter_by.return_value.first.return_value = subscription
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_subscription",
return_value=(_encrypter_mock(decrypted={"token": "plain"}), MagicMock()),
)
mocker.patch(
"services.trigger.trigger_provider_service.create_trigger_provider_encrypter_for_properties",
return_value=(_encrypter_mock(decrypted={"hook": "plain"}), MagicMock()),
)
# Act
result = TriggerProviderService.get_subscription_by_endpoint("endpoint-1")
# Assert
assert result is subscription
assert subscription.credentials == {"token": "plain"}
assert subscription.properties == {"hook": "plain"}
def test_verify_subscription_credentials_should_raise_when_provider_not_found(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
_mock_get_trigger_provider(mocker, None)
# Act + Assert
with pytest.raises(ValueError, match="Provider .* not found"):
TriggerProviderService.verify_subscription_credentials(
tenant_id="tenant-1",
user_id="user-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
)
def test_verify_subscription_credentials_should_raise_when_subscription_not_found(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=None)
# Act + Assert
with pytest.raises(ValueError, match="Subscription sub-1 not found"):
TriggerProviderService.verify_subscription_credentials(
tenant_id="tenant-1",
user_id="user-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
)
def test_verify_subscription_credentials_should_raise_when_api_key_validation_fails(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(credential_type=CredentialType.API_KEY.value, credentials={"api_key": "old"})
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
provider_controller.validate_credentials.side_effect = RuntimeError("bad credentials")
# Act + Assert
with pytest.raises(ValueError, match="Invalid credentials: bad credentials"):
TriggerProviderService.verify_subscription_credentials(
tenant_id="tenant-1",
user_id="user-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={"api_key": HIDDEN_VALUE},
)
def test_verify_subscription_credentials_should_return_verified_when_api_key_validation_succeeds(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(credential_type=CredentialType.API_KEY.value, credentials={"api_key": "old"})
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
# Act
result = TriggerProviderService.verify_subscription_credentials(
tenant_id="tenant-1",
user_id="user-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={"api_key": HIDDEN_VALUE},
)
# Assert
assert result == {"verified": True}
def test_verify_subscription_credentials_should_return_verified_for_non_api_key_credentials(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(credential_type=CredentialType.OAUTH2.value, credentials={})
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
# Act
result = TriggerProviderService.verify_subscription_credentials(
tenant_id="tenant-1",
user_id="user-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
)
# Assert
assert result == {"verified": True}
def test_rebuild_trigger_subscription_should_raise_when_provider_not_found(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
) -> None:
# Arrange
_mock_get_trigger_provider(mocker, None)
# Act + Assert
with pytest.raises(ValueError, match="Provider .* not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id="tenant-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_should_raise_when_subscription_not_found(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=None)
# Act + Assert
with pytest.raises(ValueError, match="Subscription sub-1 not found"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id="tenant-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_should_raise_for_unsupported_credential_type(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(credential_type=CredentialType.UNAUTHORIZED.value)
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
# Act + Assert
with pytest.raises(ValueError, match="not supported for auto creation"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id="tenant-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_should_raise_when_unsubscribe_fails(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-1",
user_id="user-1",
endpoint_id="endpoint-1",
credential_type=CredentialType.API_KEY.value,
credentials={"api_key": "old"},
to_entity=lambda: SimpleNamespace(id="sub-1"),
)
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.unsubscribe_trigger",
return_value=SimpleNamespace(success=False, message="remote error"),
)
# Act + Assert
with pytest.raises(ValueError, match="Failed to delete previous subscription"):
TriggerProviderService.rebuild_trigger_subscription(
tenant_id="tenant-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={},
parameters={},
)
def test_rebuild_trigger_subscription_should_resubscribe_and_update_existing_subscription(
mocker: MockerFixture,
mock_session: MagicMock,
provider_id: TriggerProviderID,
provider_controller: MagicMock,
) -> None:
# Arrange
subscription = SimpleNamespace(
id="sub-1",
user_id="user-1",
endpoint_id="endpoint-1",
credential_type=CredentialType.API_KEY.value,
credentials={"api_key": "old-key"},
to_entity=lambda: SimpleNamespace(id="sub-1"),
)
new_subscription = SimpleNamespace(properties={"project": "new"}, expires_at=888)
_mock_get_trigger_provider(mocker, provider_controller)
mocker.patch.object(TriggerProviderService, "get_subscription_by_id", return_value=subscription)
mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.unsubscribe_trigger",
return_value=SimpleNamespace(success=True, message="ok"),
)
mock_subscribe = mocker.patch(
"services.trigger.trigger_provider_service.TriggerManager.subscribe_trigger",
return_value=new_subscription,
)
mocker.patch(
"services.trigger.trigger_provider_service.generate_plugin_trigger_endpoint_url",
return_value="https://endpoint",
)
mock_update = mocker.patch.object(TriggerProviderService, "update_trigger_subscription")
# Act
TriggerProviderService.rebuild_trigger_subscription(
tenant_id="tenant-1",
provider_id=provider_id,
subscription_id="sub-1",
credentials={"api_key": HIDDEN_VALUE, "region": "us"},
parameters={"event": "push"},
name="updated",
)
# Assert
call_kwargs = mock_subscribe.call_args.kwargs
assert call_kwargs["credentials"]["api_key"] == "old-key"
assert call_kwargs["credentials"]["region"] == "us"
mock_update.assert_called_once_with(
tenant_id="tenant-1",
subscription_id="sub-1",
name="updated",
parameters={"event": "push"},
credentials={"api_key": "old-key", "region": "us"},
properties={"project": "new"},
expires_at=888,
)