diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 8b329e13536..2bc5b454696 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -54,6 +54,10 @@ class TriggerSubscriptionBuilderCreatePayload(BaseModel): class TriggerSubscriptionBuilderVerifyPayload(BaseModel): + credentials: dict[str, Any] | None = None + + +class TriggerSubscriptionVerifyPayload(BaseModel): credentials: dict[str, Any] @@ -120,6 +124,7 @@ register_schema_models( TriggerSubscriptionBuilderCreatePayload, TriggerSubscriptionBuilderVerifyPayload, TriggerSubscriptionBuilderUpdatePayload, + TriggerSubscriptionVerifyPayload, TriggerOAuthClientPayload, ) register_response_schema_models( @@ -812,7 +817,7 @@ class TriggerOAuthClientManageApi(Resource): "/workspaces/current/trigger-provider//subscriptions/verify/", ) class TriggerSubscriptionVerifyApi(Resource): - @console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__]) + @console_ns.expect(console_ns.models[TriggerSubscriptionVerifyPayload.__name__]) @console_ns.response( 200, "Trigger subscription verified successfully", @@ -825,10 +830,10 @@ class TriggerSubscriptionVerifyApi(Resource): @account_initialization_required @with_current_user @with_current_tenant_id - @model_validate(TriggerSubscriptionBuilderVerifyPayload) + @model_validate(TriggerSubscriptionVerifyPayload) def post( self, - req_data: TriggerSubscriptionBuilderVerifyPayload, + req_data: TriggerSubscriptionVerifyPayload, tenant_id: str, user: Account, provider: str, diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ee2aace9b0a..4f092258d06 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -12868,7 +12868,7 @@ Import a Skill zip package from multipart form field `file`. | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [TriggerSubscriptionBuilderVerifyPayload](#triggersubscriptionbuilderverifypayload)
| +| Yes | **application/json**: [TriggerSubscriptionVerifyPayload](#triggersubscriptionverifypayload)
| #### Responses @@ -23453,6 +23453,12 @@ The identity of the trigger provider #### TriggerSubscriptionBuilderVerifyPayload +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| credentials | object | | No | + +#### TriggerSubscriptionVerifyPayload + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | credentials | object | | Yes | diff --git a/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py index 6464abcfa60..34de5c05f93 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py @@ -7,10 +7,15 @@ from unittest.mock import patch import pytest from flask import Flask +from pydantic import ValidationError from sqlalchemy.engine import Engine from werkzeug.exceptions import BadRequest -from controllers.console.workspace.trigger_providers import TriggerSubscriptionDeleteApi +from controllers.console.workspace.trigger_providers import ( + TriggerSubscriptionBuilderVerifyPayload, + TriggerSubscriptionDeleteApi, + TriggerSubscriptionVerifyPayload, +) from models.engine import db trigger_provider_module = import_module("controllers.console.workspace.trigger_providers") @@ -59,3 +64,14 @@ def test_delete_subscription_translates_value_error(flask_sqlite_engine: Engine) pytest.raises(BadRequest, match="bad"), ): method(api, "t1", "sub1") + + +def test_builder_verify_payload_allows_credentials_to_be_omitted() -> None: + payload = TriggerSubscriptionBuilderVerifyPayload.model_validate({}) + + assert payload.credentials is None + + +def test_subscription_verify_payload_requires_credentials() -> None: + with pytest.raises(ValidationError): + TriggerSubscriptionVerifyPayload.model_validate({}) diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 7fb1fba5d27..689966e444b 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -1191,9 +1191,9 @@ export type SubscriptionBuilderApiEntity = { } export type TriggerSubscriptionBuilderVerifyPayload = { - credentials: { + credentials?: { [key: string]: unknown - } + } | null } export type TriggerVerificationResponse = { @@ -1212,6 +1212,12 @@ export type TriggerOAuthAuthorizeResponse = { subscription_builder_id: string } +export type TriggerSubscriptionVerifyPayload = { + credentials: { + [key: string]: unknown + } +} + export type TriggerProviderListResponse = Array export type WorkspaceCustomConfigResponse = { @@ -6314,7 +6320,7 @@ export type GetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAutho export type PostWorkspacesCurrentTriggerProviderByProviderSubscriptionsVerifyBySubscriptionIdData = { - body: TriggerSubscriptionBuilderVerifyPayload + body: TriggerSubscriptionVerifyPayload path: { provider: string subscription_id: string diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index c6e1571a423..148b04d9c82 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -727,7 +727,7 @@ export const zTriggerSubscriptionBuilderCreatePayload = z.object({ * TriggerSubscriptionBuilderVerifyPayload */ export const zTriggerSubscriptionBuilderVerifyPayload = z.object({ - credentials: z.record(z.string(), z.unknown()), + credentials: z.record(z.string(), z.unknown()).nullish(), }) /** @@ -744,6 +744,13 @@ export const zTriggerProviderErrorResponse = z.object({ error: z.string(), }) +/** + * TriggerSubscriptionVerifyPayload + */ +export const zTriggerSubscriptionVerifyPayload = z.object({ + credentials: z.record(z.string(), z.unknown()), +}) + /** * WorkspaceCustomConfigResponse */ @@ -6031,7 +6038,7 @@ export const zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAut zTriggerOAuthAuthorizeResponse export const zPostWorkspacesCurrentTriggerProviderByProviderSubscriptionsVerifyBySubscriptionIdBody = - zTriggerSubscriptionBuilderVerifyPayload + zTriggerSubscriptionVerifyPayload export const zPostWorkspacesCurrentTriggerProviderByProviderSubscriptionsVerifyBySubscriptionIdPath = z.object({ diff --git a/web/service/__tests__/use-triggers.spec.ts b/web/service/__tests__/use-triggers.spec.ts index f007a5952e5..c43bd091667 100644 --- a/web/service/__tests__/use-triggers.spec.ts +++ b/web/service/__tests__/use-triggers.spec.ts @@ -1,6 +1,73 @@ import type { TriggerProviderApiEntity as GeneratedTriggerProvider } from '@dify/contracts/api/console/workspaces/types.gen' +import type { ReactNode } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { act, renderHook } from '@testing-library/react' +import { createElement } from 'react' import { describe, expect, it } from 'vite-plus/test' -import { convertToTriggerWithProvider, normalizeTriggerProvider } from '../use-triggers' +import { + convertToTriggerWithProvider, + normalizeTriggerProvider, + useVerifyAndUpdateTriggerSubscriptionBuilder, +} from '../use-triggers' + +const mocks = vi.hoisted(() => ({ + mutationKey: vi.fn(() => ['trigger-builder-verify']), + verifyAndUpdate: vi.fn().mockResolvedValue({ verified: false }), +})) + +vi.mock('@/service/client', () => ({ + consoleClient: { + workspaces: { + current: { + triggerProvider: { + byProvider: { + subscriptions: { + builder: { + verifyAndUpdate: { + bySubscriptionBuilderId: { + post: mocks.verifyAndUpdate, + }, + }, + }, + }, + }, + }, + }, + }, + }, + consoleQuery: { + workspaces: { + current: { + triggerProvider: { + byProvider: { + subscriptions: { + builder: { + verifyAndUpdate: { + bySubscriptionBuilderId: { + post: { + mutationKey: mocks.mutationKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +})) + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + }, + }) + + return ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children) +} const createGeneratedTriggerProvider = (): GeneratedTriggerProvider => ({ author: 'Dify', @@ -42,3 +109,31 @@ describe('trigger provider normalization', () => { expect(triggerWithProvider.events[0]?.parameters[0]?.default).toBe(0) }) }) + +describe('trigger subscription builder verification', () => { + it('should omit credentials while polling OAuth authorization', async () => { + const { result } = renderHook(() => useVerifyAndUpdateTriggerSubscriptionBuilder(), { + wrapper: createWrapper(), + }) + + await act(async () => { + await result.current.mutateAsync({ + provider: 'langgenius/gmail_trigger/gmail_trigger', + subscriptionBuilderId: 'builder-1', + }) + }) + + expect(mocks.verifyAndUpdate).toHaveBeenCalledWith( + { + params: { + provider: 'langgenius/gmail_trigger/gmail_trigger', + subscription_builder_id: 'builder-1', + }, + body: {}, + }, + { + context: { silent: true }, + }, + ) + }) +}) diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index 27d654377f2..c8bd44a4d81 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -657,7 +657,7 @@ export const useVerifyAndUpdateTriggerSubscriptionBuilder = () => { return consoleClient.workspaces.current.triggerProvider.byProvider.subscriptions.builder.verifyAndUpdate.bySubscriptionBuilderId.post( { params: { provider, subscription_builder_id: subscriptionBuilderId }, - body: { credentials: credentials ?? {} }, + body: credentials === undefined ? {} : { credentials }, }, { context: { silent: true },