Skip to content

Commit ebc893c

Browse files
authored
feat: acid test Asset Manager (#121) (#157)
* test #121: enhance asset manager coverage with edge cases and boundary tests Add 19 new tests covering: - Token expiry validation (30-min TTL across all 10 prepare methods) - Optional fields pass-through to preview - Edit snippet partial updates (name-only, language-only, content-only) - Clone/archive non-cloneable type completeness (file, snippet) - Clone without newName - Boundary tests through prepare methods (200/201 chars, 100k content) - Constructor URL encoding for special characters Total: 83 → 102 tests * feat #121: improve asset manager CLI help text and file list output - Enhanced descriptions for parent command and all 6 sub-type groups with Bloomreach-specific context (what each asset type is used for) - Added fileSize display to files list human-readable output
1 parent ffa9fb7 commit ebc893c

File tree

2 files changed

+287
-7
lines changed

2 files changed

+287
-7
lines changed

packages/cli/src/bin/bloomreach.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,9 +3256,9 @@ vouchers
32563256

32573257
const assets = program
32583258
.command('assets')
3259-
.description('Manage Asset Manager templates, snippets, and files');
3259+
.description('Manage Bloomreach Asset Manager — email/weblayer templates, blocks, custom rows, snippets, and files used in campaigns');
32603260

3261-
const assetEmailTemplates = assets.command('email-templates').description('Manage email templates');
3261+
const assetEmailTemplates = assets.command('email-templates').description('Manage email templates — reusable HTML layouts for marketing and transactional emails');
32623262

32633263
assetEmailTemplates
32643264
.command('list')
@@ -3340,7 +3340,7 @@ assetEmailTemplates
33403340

33413341
const assetWeblayerTemplates = assets
33423342
.command('weblayer-templates')
3343-
.description('Manage weblayer templates');
3343+
.description('Manage weblayer templates — pop-ups, banners, and in-page content personalization layers');
33443344

33453345
assetWeblayerTemplates
33463346
.command('list')
@@ -3416,7 +3416,7 @@ assetWeblayerTemplates
34163416
},
34173417
);
34183418

3419-
const assetBlocks = assets.command('blocks').description('Manage content blocks');
3419+
const assetBlocks = assets.command('blocks').description('Manage content blocks — reusable drag-and-drop sections for the visual email editor');
34203420

34213421
assetBlocks
34223422
.command('list')
@@ -3492,7 +3492,7 @@ assetBlocks
34923492
},
34933493
);
34943494

3495-
const assetCustomRows = assets.command('custom-rows').description('Manage custom rows');
3495+
const assetCustomRows = assets.command('custom-rows').description('Manage custom rows — custom HTML rows that extend the visual editor row library');
34963496

34973497
assetCustomRows
34983498
.command('list')
@@ -3568,7 +3568,7 @@ assetCustomRows
35683568
},
35693569
);
35703570

3571-
const assetSnippets = assets.command('snippets').description('Manage snippets');
3571+
const assetSnippets = assets.command('snippets').description('Manage snippets — reusable Jinja or HTML fragments embedded in templates');
35723572

35733573
assetSnippets
35743574
.command('list')
@@ -3696,7 +3696,7 @@ assetSnippets
36963696
},
36973697
);
36983698

3699-
const assetFiles = assets.command('files').description('Manage files');
3699+
const assetFiles = assets.command('files').description('Manage files — images, documents, and fonts uploaded for use in campaigns');
37003700

37013701
assetFiles
37023702
.command('list')
@@ -3724,6 +3724,7 @@ assetFiles
37243724
console.log(` ${file.name}`);
37253725
console.log(` Category: ${file.category}`);
37263726
console.log(` MIME: ${file.mimeType}`);
3727+
console.log(` Size: ${file.fileSize !== undefined ? `${file.fileSize} bytes` : 'n/a'}`);
37273728
console.log(` ID: ${file.id}`);
37283729
console.log(` URL: ${file.url}`);
37293730
}

packages/core/src/__tests__/bloomreachAssetManager.test.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/^pa_/);
723+
expect(result.confirmToken).toMatch(/^ct_stub_/);
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(/^pa_/);
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(/^pa_/);
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

Comments
 (0)