@@ -382,6 +382,14 @@ describe('BloomreachAssetManagerService', () => {
382382 it ( 'throws for empty project' , ( ) => {
383383 expect ( ( ) => new BloomreachAssetManagerService ( '' ) ) . toThrow ( 'must not be empty' ) ;
384384 } ) ;
385+
386+ it ( 'encodes special characters in project name' , ( ) => {
387+ const service = new BloomreachAssetManagerService ( 'my project' ) ;
388+ expect ( service . emailTemplatesUrl ) . toContain ( 'my%20project' ) ;
389+
390+ const service2 = new BloomreachAssetManagerService ( 'org/proj' ) ;
391+ expect ( service2 . emailTemplatesUrl ) . toContain ( 'org%2Fproj' ) ;
392+ } ) ;
385393 } ) ;
386394
387395 describe ( 'list methods' , ( ) => {
@@ -691,6 +699,38 @@ describe('BloomreachAssetManagerService', () => {
691699 } ) ,
692700 ) . toThrow ( 'cloneable types' ) ;
693701 } ) ;
702+
703+ it ( 'throws for file asset type' , ( ) => {
704+ const service = new BloomreachAssetManagerService ( 'test' ) ;
705+ expect ( ( ) =>
706+ service . prepareCloneTemplate ( {
707+ project : 'test' ,
708+ templateId : 'tmpl-1' ,
709+ assetType : 'file' ,
710+ } ) ,
711+ ) . toThrow ( 'cloneable types' ) ;
712+ } ) ;
713+
714+ it ( 'succeeds without newName' , ( ) => {
715+ const service = new BloomreachAssetManagerService ( 'test' ) ;
716+ const result = service . prepareCloneTemplate ( {
717+ project : 'test' ,
718+ templateId : 'tmpl-1' ,
719+ assetType : 'weblayer_template' ,
720+ } ) ;
721+
722+ expect ( result . preparedActionId ) . toMatch ( / ^ p a _ / ) ;
723+ expect ( result . confirmToken ) . toMatch ( / ^ c t _ s t u b _ / ) ;
724+ expect ( result . preview ) . toEqual (
725+ expect . objectContaining ( {
726+ action : 'asset_manager.clone_template' ,
727+ project : 'test' ,
728+ templateId : 'tmpl-1' ,
729+ assetType : 'weblayer_template' ,
730+ } ) ,
731+ ) ;
732+ expect ( result . preview . newName ) . toBeUndefined ( ) ;
733+ } ) ;
694734 } ) ;
695735
696736 describe ( 'prepareArchiveTemplate' , ( ) => {
@@ -724,6 +764,17 @@ describe('BloomreachAssetManagerService', () => {
724764 } ) ,
725765 ) . toThrow ( 'cloneable types' ) ;
726766 } ) ;
767+
768+ it ( 'throws for snippet asset type' , ( ) => {
769+ const service = new BloomreachAssetManagerService ( 'test' ) ;
770+ expect ( ( ) =>
771+ service . prepareArchiveTemplate ( {
772+ project : 'test' ,
773+ templateId : 'tmpl-2' ,
774+ assetType : 'snippet' ,
775+ } ) ,
776+ ) . toThrow ( 'cloneable types' ) ;
777+ } ) ;
727778 } ) ;
728779
729780 describe ( 'prepare methods shared validation' , ( ) => {
@@ -806,4 +857,232 @@ describe('BloomreachAssetManagerService', () => {
806857 ) . toThrow ( 'must not be empty' ) ;
807858 } ) ;
808859 } ) ;
860+
861+ describe ( 'token expiry' , ( ) => {
862+ it ( 'sets expiresAtMs to approximately 30 minutes from now' , ( ) => {
863+ const service = new BloomreachAssetManagerService ( 'test' ) ;
864+ const before = Date . now ( ) ;
865+ const result = service . prepareCreateEmailTemplate ( {
866+ project : 'test' ,
867+ name : 'Expiry Test' ,
868+ } ) ;
869+ const after = Date . now ( ) ;
870+
871+ const expectedTtl = 30 * 60 * 1000 ;
872+ expect ( result . expiresAtMs ) . toBeGreaterThanOrEqual ( before + expectedTtl ) ;
873+ expect ( result . expiresAtMs ) . toBeLessThanOrEqual ( after + expectedTtl ) ;
874+ } ) ;
875+
876+ it ( 'all prepare methods set consistent expiry' , ( ) => {
877+ const service = new BloomreachAssetManagerService ( 'test' ) ;
878+ const tolerance = 1000 ;
879+ const expectedTtl = 30 * 60 * 1000 ;
880+
881+ const results = [
882+ service . prepareCreateEmailTemplate ( { project : 'test' , name : 'Email' } ) ,
883+ service . prepareCreateWeblayerTemplate ( { project : 'test' , name : 'Weblayer' } ) ,
884+ service . prepareCreateBlock ( { project : 'test' , name : 'Block' } ) ,
885+ service . prepareCreateCustomRow ( { project : 'test' , name : 'Row' } ) ,
886+ service . prepareCreateSnippet ( {
887+ project : 'test' ,
888+ name : 'Snippet' ,
889+ language : 'jinja' ,
890+ content : 'x' ,
891+ } ) ,
892+ service . prepareEditSnippet ( { project : 'test' , snippetId : 'snippet-1' } ) ,
893+ service . prepareUploadFile ( { project : 'test' , name : 'file.png' , mimeType : 'image/png' } ) ,
894+ service . prepareDeleteFile ( { project : 'test' , fileId : 'file-1' } ) ,
895+ service . prepareCloneTemplate ( {
896+ project : 'test' ,
897+ templateId : 'tmpl-1' ,
898+ assetType : 'email_template' ,
899+ } ) ,
900+ service . prepareArchiveTemplate ( {
901+ project : 'test' ,
902+ templateId : 'tmpl-2' ,
903+ assetType : 'weblayer_template' ,
904+ } ) ,
905+ ] ;
906+
907+ for ( const result of results ) {
908+ const now = Date . now ( ) ;
909+ expect ( result . expiresAtMs ) . toBeGreaterThanOrEqual ( now + expectedTtl - tolerance ) ;
910+ expect ( result . expiresAtMs ) . toBeLessThanOrEqual ( now + expectedTtl + tolerance ) ;
911+ }
912+ } ) ;
913+ } ) ;
914+
915+ describe ( 'optional fields in preview' , ( ) => {
916+ it ( 'prepareCreateEmailTemplate includes all optional fields when provided' , ( ) => {
917+ const service = new BloomreachAssetManagerService ( 'test' ) ;
918+ const result = service . prepareCreateEmailTemplate ( {
919+ project : 'test' ,
920+ name : 'Email With Optional Fields' ,
921+ builderType : 'html' ,
922+ htmlContent : '<html><body>Hello</body></html>' ,
923+ operatorNote : 'optional-fields-test' ,
924+ } ) ;
925+
926+ expect ( result . preview . builderType ) . toBe ( 'html' ) ;
927+ expect ( result . preview . htmlContent ) . toBe ( '<html><body>Hello</body></html>' ) ;
928+ expect ( result . preview . operatorNote ) . toBe ( 'optional-fields-test' ) ;
929+ } ) ;
930+
931+ it ( 'prepareCreateEmailTemplate excludes optional fields when omitted' , ( ) => {
932+ const service = new BloomreachAssetManagerService ( 'test' ) ;
933+ const result = service . prepareCreateEmailTemplate ( {
934+ project : 'test' ,
935+ name : 'Email Without Optional Fields' ,
936+ } ) ;
937+
938+ expect ( result . preview . builderType ) . toBeUndefined ( ) ;
939+ expect ( result . preview . htmlContent ) . toBeUndefined ( ) ;
940+ expect ( result . preview . operatorNote ) . toBeUndefined ( ) ;
941+ } ) ;
942+
943+ it ( 'prepareCreateWeblayerTemplate includes htmlContent when provided' , ( ) => {
944+ const service = new BloomreachAssetManagerService ( 'test' ) ;
945+ const result = service . prepareCreateWeblayerTemplate ( {
946+ project : 'test' ,
947+ name : 'Weblayer' ,
948+ htmlContent : '<div>banner</div>' ,
949+ } ) ;
950+
951+ expect ( result . preview . htmlContent ) . toBe ( '<div>banner</div>' ) ;
952+ } ) ;
953+
954+ it ( 'prepareUploadFile includes all optional fields when provided' , ( ) => {
955+ const service = new BloomreachAssetManagerService ( 'test' ) ;
956+ const result = service . prepareUploadFile ( {
957+ project : 'test' ,
958+ name : 'asset.pdf' ,
959+ mimeType : 'application/pdf' ,
960+ fileSize : 12345 ,
961+ category : 'document' ,
962+ operatorNote : 'upload-optional-fields-test' ,
963+ } ) ;
964+
965+ expect ( result . preview . fileSize ) . toBe ( 12345 ) ;
966+ expect ( result . preview . category ) . toBe ( 'document' ) ;
967+ expect ( result . preview . operatorNote ) . toBe ( 'upload-optional-fields-test' ) ;
968+ } ) ;
969+
970+ it ( 'prepareUploadFile omits optional fields when not provided' , ( ) => {
971+ const service = new BloomreachAssetManagerService ( 'test' ) ;
972+ const result = service . prepareUploadFile ( {
973+ project : 'test' ,
974+ name : 'logo.png' ,
975+ mimeType : 'image/png' ,
976+ } ) ;
977+
978+ expect ( result . preview . fileSize ) . toBeUndefined ( ) ;
979+ expect ( result . preview . category ) . toBeUndefined ( ) ;
980+ expect ( result . preview . operatorNote ) . toBeUndefined ( ) ;
981+ } ) ;
982+ } ) ;
983+
984+ describe ( 'edit snippet partial updates' , ( ) => {
985+ it ( 'accepts update with only name' , ( ) => {
986+ const service = new BloomreachAssetManagerService ( 'test' ) ;
987+ const result = service . prepareEditSnippet ( {
988+ project : 'test' ,
989+ snippetId : 'snippet-1' ,
990+ name : 'New Name' ,
991+ } ) ;
992+
993+ expect ( result . preview . name ) . toBe ( 'New Name' ) ;
994+ expect ( result . preview . language ) . toBeUndefined ( ) ;
995+ expect ( result . preview . content ) . toBeUndefined ( ) ;
996+ } ) ;
997+
998+ it ( 'accepts update with only language' , ( ) => {
999+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1000+ const result = service . prepareEditSnippet ( {
1001+ project : 'test' ,
1002+ snippetId : 'snippet-1' ,
1003+ language : 'html' ,
1004+ } ) ;
1005+
1006+ expect ( result . preparedActionId ) . toMatch ( / ^ p a _ / ) ;
1007+ expect ( result . preview . language ) . toBe ( 'html' ) ;
1008+ } ) ;
1009+
1010+ it ( 'accepts update with only content' , ( ) => {
1011+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1012+ const result = service . prepareEditSnippet ( {
1013+ project : 'test' ,
1014+ snippetId : 'snippet-1' ,
1015+ content : '<p>new</p>' ,
1016+ } ) ;
1017+
1018+ expect ( result . preparedActionId ) . toMatch ( / ^ p a _ / ) ;
1019+ expect ( result . preview . content ) . toBe ( '<p>new</p>' ) ;
1020+ } ) ;
1021+
1022+ it ( 'accepts update with no optional fields' , ( ) => {
1023+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1024+ const result = service . prepareEditSnippet ( {
1025+ project : 'test' ,
1026+ snippetId : 'snippet-1' ,
1027+ } ) ;
1028+
1029+ expect ( result . preview ) . toEqual (
1030+ expect . objectContaining ( {
1031+ action : 'asset_manager.edit_snippet' ,
1032+ project : 'test' ,
1033+ snippetId : 'snippet-1' ,
1034+ } ) ,
1035+ ) ;
1036+ expect ( result . preview . name ) . toBeUndefined ( ) ;
1037+ expect ( result . preview . language ) . toBeUndefined ( ) ;
1038+ expect ( result . preview . content ) . toBeUndefined ( ) ;
1039+ } ) ;
1040+ } ) ;
1041+
1042+ describe ( 'prepare method boundary validation' , ( ) => {
1043+ it ( 'accepts asset name at exactly 200 characters' , ( ) => {
1044+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1045+ const result = service . prepareCreateEmailTemplate ( {
1046+ project : 'test' ,
1047+ name : 'x' . repeat ( 200 ) ,
1048+ } ) ;
1049+
1050+ expect ( result . preview . name ) . toBe ( 'x' . repeat ( 200 ) ) ;
1051+ } ) ;
1052+
1053+ it ( 'rejects asset name at 201 characters' , ( ) => {
1054+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1055+ expect ( ( ) =>
1056+ service . prepareCreateEmailTemplate ( {
1057+ project : 'test' ,
1058+ name : 'x' . repeat ( 201 ) ,
1059+ } ) ,
1060+ ) . toThrow ( 'must not exceed 200 characters' ) ;
1061+ } ) ;
1062+
1063+ it ( 'accepts snippet content at exactly 100000 characters' , ( ) => {
1064+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1065+ const content = 'x' . repeat ( 100_000 ) ;
1066+ const result = service . prepareCreateSnippet ( {
1067+ project : 'test' ,
1068+ name : 'Boundary Snippet' ,
1069+ language : 'jinja' ,
1070+ content,
1071+ } ) ;
1072+
1073+ expect ( result . preview . content ) . toBe ( content ) ;
1074+ } ) ;
1075+
1076+ it ( 'rejects snippet content at 100001 characters' , ( ) => {
1077+ const service = new BloomreachAssetManagerService ( 'test' ) ;
1078+ expect ( ( ) =>
1079+ service . prepareCreateSnippet ( {
1080+ project : 'test' ,
1081+ name : 'Too Long Snippet' ,
1082+ language : 'jinja' ,
1083+ content : 'x' . repeat ( 100_001 ) ,
1084+ } ) ,
1085+ ) . toThrow ( 'must not exceed 100000 characters' ) ;
1086+ } ) ;
1087+ } ) ;
8091088} ) ;
0 commit comments