@@ -7,13 +7,17 @@ import { requestUtilsMockFns, resetEnvMock, setEnv } from '@sim/testing'
77import { NextRequest } from 'next/server'
88import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
99
10- const { mockEnqueueTikTokWebhookIngress, mockRelease } = vi . hoisted ( ( ) => ( {
11- mockEnqueueTikTokWebhookIngress : vi . fn ( ) ,
12- mockRelease : vi . fn ( ) ,
13- } ) )
10+ const { mockDispatchResolvedWebhookTarget, mockFindWebhooksByRoutingKey, mockRelease } = vi . hoisted (
11+ ( ) => ( {
12+ mockDispatchResolvedWebhookTarget : vi . fn ( ) ,
13+ mockFindWebhooksByRoutingKey : vi . fn ( ) ,
14+ mockRelease : vi . fn ( ) ,
15+ } )
16+ )
1417
15- vi . mock ( '@/background/tiktok-webhook-ingress' , ( ) => ( {
16- enqueueTikTokWebhookIngress : mockEnqueueTikTokWebhookIngress ,
18+ vi . mock ( '@/lib/webhooks/processor' , ( ) => ( {
19+ dispatchResolvedWebhookTarget : mockDispatchResolvedWebhookTarget ,
20+ findWebhooksByRoutingKey : mockFindWebhooksByRoutingKey ,
1721} ) )
1822
1923vi . mock ( '@/lib/core/admission/gate' , ( ) => ( {
@@ -29,12 +33,17 @@ vi.mock('@/lib/core/utils/with-route-handler', () => ({
2933
3034import { POST } from '@/app/api/webhooks/tiktok/route'
3135
32- function signedRequest ( overrides ?: { clientKey ?: string } ) : NextRequest {
36+ const target = ( id : string ) => ( {
37+ webhook : { id, path : null , provider : 'tiktok' } ,
38+ workflow : { id : `workflow-${ id } ` } ,
39+ } )
40+
41+ function signedRequest ( overrides ?: { clientKey ?: string ; userOpenId ?: string } ) : NextRequest {
3342 const body = JSON . stringify ( {
3443 client_key : overrides ?. clientKey ?? 'client-key' ,
3544 event : 'post.publish.complete' ,
3645 create_time : 1_725_000_000 ,
37- user_openid : 'act.user' ,
46+ user_openid : overrides ?. userOpenId ?? 'act.user' ,
3847 content : '{"publish_id":"publish-1"}' ,
3948 } )
4049 const timestamp = String ( Math . floor ( Date . now ( ) / 1000 ) )
@@ -53,38 +62,78 @@ function signedRequest(overrides?: { clientKey?: string }): NextRequest {
5362 } )
5463}
5564
56- describe ( 'TikTok webhook ingress route' , ( ) => {
65+ describe ( 'TikTok app webhook route' , ( ) => {
5766 beforeEach ( ( ) => {
5867 vi . clearAllMocks ( )
5968 setEnv ( { TIKTOK_CLIENT_ID : 'client-key' , TIKTOK_CLIENT_SECRET : 'client-secret' } )
6069 requestUtilsMockFns . mockGenerateRequestId . mockReturnValue ( 'request-1' )
61- mockEnqueueTikTokWebhookIngress . mockResolvedValue ( 'ingress-job-1' )
70+ mockFindWebhooksByRoutingKey . mockResolvedValue ( [ ] )
71+ mockDispatchResolvedWebhookTarget . mockResolvedValue ( { outcome : 'queued' , reason : 'queued' } )
6272 } )
6373
6474 afterAll ( ( ) => {
6575 resetEnvMock ( )
6676 requestUtilsMockFns . mockGenerateRequestId . mockReset ( )
6777 } )
6878
69- it ( 'returns 200 only after the verified delivery is accepted by the job queue' , async ( ) => {
70- const response = await POST ( signedRequest ( ) )
79+ it ( 'routes a verified delivery by user_openid on the TikTok provider' , async ( ) => {
80+ mockFindWebhooksByRoutingKey . mockResolvedValue ( [ target ( 'webhook-1' ) ] )
81+
82+ const response = await POST ( signedRequest ( { userOpenId : 'user-open-id' } ) )
7183
7284 expect ( response . status ) . toBe ( 200 )
7385 await expect ( response . json ( ) ) . resolves . toEqual ( { ok : true } )
74- expect ( mockEnqueueTikTokWebhookIngress ) . toHaveBeenCalledWith (
86+ expect ( mockFindWebhooksByRoutingKey ) . toHaveBeenCalledWith ( 'user-open-id' , 'request-1' , 'tiktok' )
87+ expect ( mockDispatchResolvedWebhookTarget ) . toHaveBeenCalledWith (
88+ expect . objectContaining ( { id : 'webhook-1' } ) ,
89+ expect . objectContaining ( { id : 'workflow-webhook-1' } ) ,
90+ expect . objectContaining ( { user_openid : 'user-open-id' } ) ,
91+ expect . any ( NextRequest ) ,
7592 expect . objectContaining ( {
76- envelope : expect . objectContaining ( {
77- client_key : 'client-key' ,
78- user_openid : 'act.user' ,
79- } ) ,
8093 requestId : 'request-1' ,
94+ triggerTimestampMs : 1_725_000_000_000 ,
8195 } )
8296 )
8397 expect ( mockRelease ) . toHaveBeenCalledOnce ( )
8498 } )
8599
86- it ( 'returns 503 when durable acceptance fails so TikTok retries' , async ( ) => {
87- mockEnqueueTikTokWebhookIngress . mockRejectedValue ( new Error ( 'queue unavailable' ) )
100+ it ( 'acknowledges a verified delivery when no workflow targets match' , async ( ) => {
101+ const response = await POST ( signedRequest ( ) )
102+
103+ expect ( response . status ) . toBe ( 200 )
104+ expect ( mockDispatchResolvedWebhookTarget ) . not . toHaveBeenCalled ( )
105+ } )
106+
107+ it ( 'dispatches matching workflows sequentially' , async ( ) => {
108+ mockFindWebhooksByRoutingKey . mockResolvedValue ( [ target ( 'webhook-1' ) , target ( 'webhook-2' ) ] )
109+ const order : string [ ] = [ ]
110+ mockDispatchResolvedWebhookTarget . mockImplementation ( async ( webhook : { id : string } ) => {
111+ order . push ( `start:${ webhook . id } ` )
112+ await Promise . resolve ( )
113+ order . push ( `end:${ webhook . id } ` )
114+ return { outcome : 'queued' , reason : 'queued' }
115+ } )
116+
117+ const response = await POST ( signedRequest ( ) )
118+
119+ expect ( response . status ) . toBe ( 200 )
120+ expect ( order ) . toEqual ( [ 'start:webhook-1' , 'end:webhook-1' , 'start:webhook-2' , 'end:webhook-2' ] )
121+ } )
122+
123+ it ( 'acknowledges provider-local dispatch failures like the Slack app route' , async ( ) => {
124+ mockFindWebhooksByRoutingKey . mockResolvedValue ( [ target ( 'webhook-1' ) ] )
125+ mockDispatchResolvedWebhookTarget . mockResolvedValue ( {
126+ outcome : 'failed' ,
127+ reason : 'queue-failed' ,
128+ } )
129+
130+ const response = await POST ( signedRequest ( ) )
131+
132+ expect ( response . status ) . toBe ( 200 )
133+ } )
134+
135+ it ( 'returns 503 when target lookup fails' , async ( ) => {
136+ mockFindWebhooksByRoutingKey . mockRejectedValue ( new Error ( 'database unavailable' ) )
88137
89138 const response = await POST ( signedRequest ( ) )
90139
@@ -96,6 +145,6 @@ describe('TikTok webhook ingress route', () => {
96145 const response = await POST ( signedRequest ( { clientKey : 'other-client-key' } ) )
97146
98147 expect ( response . status ) . toBe ( 401 )
99- expect ( mockEnqueueTikTokWebhookIngress ) . not . toHaveBeenCalled ( )
148+ expect ( mockFindWebhooksByRoutingKey ) . not . toHaveBeenCalled ( )
100149 } )
101150} )
0 commit comments