22import { beforeEach , describe , expect , test , vi } from "vitest"
33
44const resumableUploadImage = vi . fn ( async ( ) => "new-handle" )
5+ const createPageMessageTemplate = vi . fn ( )
6+ const syncTemplates = vi . fn ( )
7+ const findByIdForWorkspace = vi . fn ( )
8+ const listCloneTargetsForUser = vi . fn ( )
9+ const findByIdForIntegration = vi . fn ( )
10+
11+ // Captures the handler the safe-action chain wraps so the action's
12+ // authorization can be exercised directly.
13+ let cloneHandler : ( ( props : unknown ) => Promise < unknown > ) | null = null
514
615vi . mock ( "@chatbotx.io/integration-messenger/apis/upload" , ( ) => ( {
716 resumableUploadImage,
817} ) )
918
10- vi . mock ( "@chatbotx.io/database/client" , ( ) => ( {
11- db : { } ,
12- inArray : vi . fn ( ) ,
13- } ) )
14-
15- vi . mock ( "@chatbotx.io/database/schema" , ( ) => ( {
16- integrationMessengerModel : { } ,
17- messengerMessageTemplateModel : { } ,
18- } ) )
19-
2019vi . mock ( "@chatbotx.io/business" , ( ) => ( {
2120 messengerIntegrationService : {
22- findByIdForWorkspace : vi . fn ( ) ,
23- findByIds : vi . fn ( ) ,
21+ findByIdForWorkspace : ( ...args : unknown [ ] ) => findByIdForWorkspace ( ...args ) ,
22+ listCloneTargetsForUser : ( ...args : unknown [ ] ) =>
23+ listCloneTargetsForUser ( ...args ) ,
2424 } ,
2525 messengerMessageTemplateService : {
26- findByIdForIntegration : vi . fn ( ) ,
26+ findByIdForIntegration : ( ...args : unknown [ ] ) =>
27+ findByIdForIntegration ( ...args ) ,
2728 } ,
2829} ) )
2930
3031vi . mock ( "@chatbotx.io/integration-messenger/apis/message-templates" , ( ) => ( {
31- createPageMessageTemplate : vi . fn ( ) ,
32+ createPageMessageTemplate : ( ...args : unknown [ ] ) =>
33+ createPageMessageTemplate ( ...args ) ,
3234} ) )
3335
3436vi . mock ( "@chatbotx.io/redis" , ( ) => ( {
@@ -38,21 +40,21 @@ vi.mock("@chatbotx.io/redis", () => ({
3840vi . mock (
3941 "@/features/integration-messenger/message-templates/actions/sync-message-templates" ,
4042 ( ) => ( {
41- syncMessengerMessageTemplatesForIntegration : vi . fn ( ) ,
43+ syncMessengerMessageTemplatesForIntegration : ( ...args : unknown [ ] ) =>
44+ syncTemplates ( ...args ) ,
4245 } ) ,
4346)
4447
45- vi . mock ( "@/features/workspace-members/queries" , ( ) => ( {
46- getAllWorkspaceMembers : vi . fn ( ) ,
47- } ) )
48-
4948vi . mock ( "@/lib/safe-action" , ( ) => ( {
5049 workspaceActionClient : {
51- bindArgsSchemas : vi . fn ( ( ) => ( {
52- schema : vi . fn ( ( ) => ( {
53- action : vi . fn ( ) ,
54- } ) ) ,
55- } ) ) ,
50+ bindArgsSchemas : ( ) => ( {
51+ schema : ( ) => ( {
52+ action : ( handler : ( props : unknown ) => Promise < unknown > ) => {
53+ cloneHandler = handler
54+ return handler
55+ } ,
56+ } ) ,
57+ } ) ,
5658 } ,
5759} ) )
5860
@@ -148,3 +150,105 @@ describe("prepareComponentsForClone", () => {
148150 } )
149151 } )
150152} )
153+
154+ type CloneResult = {
155+ succeeded : { channel : string } [ ]
156+ failed : { channel : string ; error : string } [ ]
157+ }
158+
159+ const sourceTemplate = {
160+ id : "tpl-src" ,
161+ integrationMessengerId : "im-source" ,
162+ name : "promo" ,
163+ language : "vi" ,
164+ category : "MARKETING" ,
165+ parameterFormat : "POSITIONAL" ,
166+ components : [ { type : "BODY" , text : "Hi" } ] ,
167+ }
168+ const target = ( id : string , workspaceId = "ws-other" ) => ( {
169+ id,
170+ name : `Page ${ id } ` ,
171+ workspaceId,
172+ pageId : `page-${ id } ` ,
173+ auth : { accessToken : "token" } ,
174+ } )
175+
176+ const run = ( targetIntegrationMessengerIds : string [ ] ) => {
177+ if ( ! cloneHandler ) {
178+ throw new Error ( "clone action handler was not captured" )
179+ }
180+ return cloneHandler ( {
181+ bindArgsParsedInputs : [ "ws-source" , "im-source" , "tpl-src" ] ,
182+ parsedInput : { targetIntegrationMessengerIds } ,
183+ ctx : { user : { id : "user-1" } } ,
184+ } ) as Promise < CloneResult >
185+ }
186+
187+ describe ( "cloneMessengerMessageTemplateAction authorization" , ( ) => {
188+ beforeEach ( ( ) => {
189+ findByIdForIntegration . mockReset ( ) . mockResolvedValue ( sourceTemplate )
190+ findByIdForWorkspace
191+ . mockReset ( )
192+ . mockResolvedValue ( { id : "im-source" , pageId : "page-source" } )
193+ listCloneTargetsForUser . mockReset ( )
194+ createPageMessageTemplate . mockReset ( ) . mockResolvedValue ( {
195+ id : "meta-new" ,
196+ status : "APPROVED" ,
197+ } )
198+ syncTemplates . mockReset ( ) . mockResolvedValue ( undefined )
199+ resumableUploadImage . mockClear ( )
200+ } )
201+
202+ test ( "clones onto the requested pages the user administers, read uncached" , async ( ) => {
203+ listCloneTargetsForUser . mockResolvedValue ( [
204+ target ( "im-admin" ) ,
205+ target ( "im-owner" , "ws-owner" ) ,
206+ ] )
207+
208+ const result = await run ( [ "im-admin" , "im-owner" ] )
209+
210+ expect ( listCloneTargetsForUser ) . toHaveBeenCalledWith ( {
211+ userId : "user-1" ,
212+ excludePageId : "page-source" ,
213+ authoritative : true ,
214+ } )
215+ expect ( createPageMessageTemplate ) . toHaveBeenCalledTimes ( 2 )
216+ expect ( result . succeeded ) . toEqual ( [
217+ { channel : "Page im-admin" } ,
218+ { channel : "Page im-owner" } ,
219+ ] )
220+ expect ( result . failed ) . toEqual ( [ ] )
221+ } )
222+
223+ test ( "drops requested pages outside the user's admin workspaces" , async ( ) => {
224+ listCloneTargetsForUser . mockResolvedValue ( [ target ( "im-admin" ) ] )
225+
226+ const result = await run ( [ "im-admin" , "im-foreign" ] )
227+
228+ expect ( createPageMessageTemplate ) . toHaveBeenCalledTimes ( 1 )
229+ expect ( result . succeeded ) . toEqual ( [ { channel : "Page im-admin" } ] )
230+ } )
231+
232+ test ( "fails when none of the requested pages is authorized" , async ( ) => {
233+ listCloneTargetsForUser . mockResolvedValue ( [ target ( "im-admin" ) ] )
234+
235+ await expect ( run ( [ "im-foreign" ] ) ) . rejects . toThrow (
236+ "No authorized target channels found" ,
237+ )
238+ expect ( createPageMessageTemplate ) . not . toHaveBeenCalled ( )
239+ } )
240+
241+ test ( "reports a page whose Meta create fails without blocking the others" , async ( ) => {
242+ listCloneTargetsForUser . mockResolvedValue ( [ target ( "im-a" ) , target ( "im-b" ) ] )
243+ createPageMessageTemplate
244+ . mockRejectedValueOnce ( new Error ( "rate limited" ) )
245+ . mockResolvedValueOnce ( { id : "meta-b" , status : "APPROVED" } )
246+
247+ const result = await run ( [ "im-a" , "im-b" ] )
248+
249+ expect ( result . failed ) . toEqual ( [
250+ { channel : "Page im-a" , error : "rate limited" } ,
251+ ] )
252+ expect ( result . succeeded ) . toEqual ( [ { channel : "Page im-b" } ] )
253+ } )
254+ } )
0 commit comments