@@ -551,6 +551,160 @@ describe('#actions handler', () => {
551551 await stageFn . apply ( handler , [ { actions : [ action ] } ] ) ;
552552 } ) ;
553553
554+ it ( 'should fetch modules from API even when actionModules are provided in assets' , async ( ) => {
555+ const actionId = 'action-with-modules-id' ;
556+ const moduleId = 'module-id-from-api' ;
557+ const moduleVersionId = 'version-uuid-from-api' ;
558+
559+ const action = {
560+ name : 'action-with-modules' ,
561+ supported_triggers : [ { id : 'post-login' , version : 'v1' } ] ,
562+ modules : [ { module_name : 'test-module' , module_version_number : 1 } ] ,
563+ } ;
564+
565+ // Local config module — has no `id` field (desired state only)
566+ const localConfigModule = { name : 'test-module' , code : 'module.exports = {};' } ;
567+
568+ let modulesListCalled = false ;
569+ let createCalledWith = null ;
570+
571+ const auth0 = {
572+ actions : {
573+ get : ( ) => Promise . resolve ( { data : { ...action , id : actionId } } ) ,
574+ create : ( data ) => {
575+ createCalledWith = data ;
576+ return Promise . resolve ( { data : { ...data , id : actionId } } ) ;
577+ } ,
578+ update : ( ) => Promise . resolve ( { data : [ ] } ) ,
579+ delete : ( ) => Promise . resolve ( { data : [ ] } ) ,
580+ list : ( ) => {
581+ if ( ! auth0 . listCalled ) {
582+ auth0 . listCalled = true ;
583+ return mockPagedData ( { include_totals : true } , 'actions' , [ ] ) ;
584+ }
585+ return mockPagedData ( { include_totals : true } , 'actions' , [
586+ { name : action . name , supported_triggers : action . supported_triggers , id : actionId } ,
587+ ] ) ;
588+ } ,
589+ createVersion : ( ) =>
590+ Promise . resolve ( {
591+ data : {
592+ code : 'action-code' ,
593+ dependencies : [ ] ,
594+ id : 'version-id' ,
595+ runtime : 'node12' ,
596+ secrets : [ ] ,
597+ } ,
598+ } ) ,
599+ modules : {
600+ list : ( ) => {
601+ modulesListCalled = true ;
602+ return mockPagedData ( { paginate : true } , 'modules' , [
603+ { id : moduleId , name : 'test-module' , code : 'module.exports = {};' } ,
604+ ] ) ;
605+ } ,
606+ versions : {
607+ list : ( ) =>
608+ Promise . resolve (
609+ mockPagedData ( { paginate : true } , 'versions' , [
610+ { id : moduleVersionId , version_number : 1 } ,
611+ ] )
612+ ) ,
613+ } ,
614+ } ,
615+ } ,
616+ pool : {
617+ addEachTask : ( data ) => {
618+ const results = data . data . map ( data . generator ) ;
619+ return { promise : ( ) => Promise . all ( results ) } ;
620+ } ,
621+ } ,
622+ listCalled : false ,
623+ } ;
624+
625+ const handler = new actions . default ( { client : pageClient ( auth0 ) , config } ) ;
626+ const stageFn = Object . getPrototypeOf ( handler ) . processChanges ;
627+
628+ // Pass both actions and actionModules in assets (the bug scenario)
629+ await stageFn . apply ( handler , [ { actions : [ action ] , actionModules : [ localConfigModule ] } ] ) ;
630+
631+ // API must have been called to resolve module IDs — not the local config shortcut
632+ expect ( modulesListCalled ) . to . equal ( true ) ;
633+
634+ // The created action must have a valid module_version_id, not undefined or ''
635+ expect ( createCalledWith . modules [ 0 ] . module_version_id ) . to . equal ( moduleVersionId ) ;
636+ expect ( createCalledWith . modules [ 0 ] . module_id ) . to . equal ( moduleId ) ;
637+ } ) ;
638+
639+ it ( 'should throw an error when the requested module version number is not found in API' , async ( ) => {
640+ const actionId = 'action-missing-version-id' ;
641+ const moduleId = 'module-id-from-api' ;
642+
643+ const action = {
644+ name : 'action-missing-version' ,
645+ supported_triggers : [ { id : 'post-login' , version : 'v1' } ] ,
646+ // references version 99 which does not exist in the API
647+ modules : [ { module_name : 'test-module' , module_version_number : 99 } ] ,
648+ } ;
649+
650+ const auth0 = {
651+ actions : {
652+ get : ( ) => Promise . resolve ( { data : { ...action , id : actionId } } ) ,
653+ create : ( data ) => Promise . resolve ( { data : { ...data , id : actionId } } ) ,
654+ update : ( ) => Promise . resolve ( { data : [ ] } ) ,
655+ delete : ( ) => Promise . resolve ( { data : [ ] } ) ,
656+ list : ( ) => {
657+ if ( ! auth0 . listCalled ) {
658+ auth0 . listCalled = true ;
659+ return mockPagedData ( { include_totals : true } , 'actions' , [ ] ) ;
660+ }
661+ return mockPagedData ( { include_totals : true } , 'actions' , [
662+ { name : action . name , supported_triggers : action . supported_triggers , id : actionId } ,
663+ ] ) ;
664+ } ,
665+ createVersion : ( ) =>
666+ Promise . resolve ( {
667+ data : {
668+ code : 'action-code' ,
669+ dependencies : [ ] ,
670+ id : 'version-id' ,
671+ runtime : 'node12' ,
672+ secrets : [ ] ,
673+ } ,
674+ } ) ,
675+ modules : {
676+ list : ( ) =>
677+ mockPagedData ( { paginate : true } , 'modules' , [
678+ { id : moduleId , name : 'test-module' , code : 'module.exports = {};' } ,
679+ ] ) ,
680+ versions : {
681+ list : ( ) =>
682+ Promise . resolve (
683+ // Only version 1 exists — version 99 is absent
684+ mockPagedData ( { paginate : true } , 'versions' , [
685+ { id : 'v1-uuid' , version_number : 1 } ,
686+ ] )
687+ ) ,
688+ } ,
689+ } ,
690+ } ,
691+ pool : {
692+ addEachTask : ( data ) => {
693+ const results = data . data . map ( data . generator ) ;
694+ return { promise : ( ) => Promise . all ( results ) } ;
695+ } ,
696+ } ,
697+ listCalled : false ,
698+ } ;
699+
700+ const handler = new actions . default ( { client : pageClient ( auth0 ) , config } ) ;
701+ const stageFn = Object . getPrototypeOf ( handler ) . processChanges ;
702+
703+ await expect ( stageFn . apply ( handler , [ { actions : [ action ] } ] ) ) . to . be . rejectedWith (
704+ / C o u l d n o t f i n d a c t i o n m o d u l e v e r s i o n i d f o r m o d u l e ' t e s t - m o d u l e ' v e r s i o n ' 9 9 ' /
705+ ) ;
706+ } ) ;
707+
554708 it ( 'should handle actions without modules' , async ( ) => {
555709 const actionId = 'action-no-modules-id' ;
556710 const action = {
0 commit comments