@movable/rollup-plugin-manifest-validator 3.2.0-app-validation

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.mocharc.yml +3 -0
  2. package/babel.config.json +11 -0
  3. package/dist/apps/index.js +15 -0
  4. package/dist/apps/plugins/app-manifest-validator.js +44 -0
  5. package/dist/apps/utils/app-manifest-validator.js +44 -0
  6. package/dist/apps/validators/ajvCompiler.js +43 -0
  7. package/dist/apps/validators/erroringValidator.js +16 -0
  8. package/dist/apps/validators/index.js +23 -0
  9. package/dist/apps/validators/warningValidator.js +16 -0
  10. package/dist/index.js +21 -0
  11. package/dist/packages/index.js +15 -0
  12. package/dist/packages/plugins/package-manifest-validator.js +44 -0
  13. package/dist/packages/utils/package-manifest-validator.js +44 -0
  14. package/dist/packages/validators/ajvCompiler.js +43 -0
  15. package/dist/packages/validators/erroringValidator.js +16 -0
  16. package/dist/packages/validators/index.js +23 -0
  17. package/dist/packages/validators/warningValidator.js +16 -0
  18. package/dist/schemas/enums.js +14 -0
  19. package/dist/schemas/erroringSchemaApps.js +809 -0
  20. package/dist/schemas/erroringSchemaPackages.js +17 -0
  21. package/dist/schemas/index.js +39 -0
  22. package/dist/schemas/warningSchemaApps.js +8 -0
  23. package/dist/schemas/warningSchemaPackages.js +504 -0
  24. package/package.json +42 -0
  25. package/src/apps/index.js +3 -0
  26. package/src/apps/plugins/app-manifest-validator.js +22 -0
  27. package/src/apps/utils/app-manifest-validator.js +31 -0
  28. package/src/apps/validators/ajvCompiler.js +28 -0
  29. package/src/apps/validators/erroringValidator.js +4 -0
  30. package/src/apps/validators/index.js +4 -0
  31. package/src/apps/validators/warningValidator.js +4 -0
  32. package/src/index.js +4 -0
  33. package/src/packages/index.js +3 -0
  34. package/src/packages/plugins/package-manifest-validator.js +19 -0
  35. package/src/packages/utils/package-manifest-validator.js +31 -0
  36. package/src/packages/validators/ajvCompiler.js +28 -0
  37. package/src/packages/validators/erroringValidator.js +4 -0
  38. package/src/packages/validators/index.js +4 -0
  39. package/src/packages/validators/warningValidator.js +4 -0
  40. package/src/schemas/enums.js +130 -0
  41. package/src/schemas/erroringSchemaApps.js +468 -0
  42. package/src/schemas/erroringSchemaPackages.js +8 -0
  43. package/src/schemas/index.js +6 -0
  44. package/src/schemas/warningSchemaApps.js +1 -0
  45. package/src/schemas/warningSchemaPackages.js +304 -0
  46. package/test/fixtures/blankFile.js +0 -0
  47. package/test/fixtures/invalidField.yml +3 -0
  48. package/test/fixtures/master-app-manifest.yml +72 -0
  49. package/test/fixtures/master-package-manifest.yml +100 -0
  50. package/test/fixtures/noName.yml +1 -0
  51. package/test/helpers/lib.js +10 -0
  52. package/test/plugins/app-manifest-validation.js +34 -0
  53. package/test/plugins/package-manifest-validation.js +62 -0
  54. package/test/utils/app-manifest-validation.js +785 -0
  55. package/test/utils/package-manifest-validation.js +1515 -0
@@ -0,0 +1,1515 @@
1
+ import YAML from 'yamljs';
2
+ import { describe, it } from 'mocha';
3
+ import { expect } from 'chai';
4
+ import { readFileSync } from 'fs';
5
+ import manifestValidator from '../../src/packages/utils/package-manifest-validator';
6
+ import { fieldTypes } from '../../src/schemas/enums';
7
+ import {
8
+ generateShouldHavePropertyMessage,
9
+ MANIFEST_PATH,
10
+ ALLOWED_VALUES_MESSAGE,
11
+ TOP_LEVEL_PROHIBITED_PROPERTY_MESSAGE
12
+ } from '../helpers/lib';
13
+
14
+ const parseYAML = (filePath) => YAML.parse(readFileSync(filePath).toString('utf-8'));
15
+
16
+ describe('pacakgeManifestValidator utility', async () => {
17
+ it('can validate the master manifest', async () => {
18
+ const masterManifest = parseYAML(MANIFEST_PATH);
19
+
20
+ const { warnings, errors } = manifestValidator(masterManifest);
21
+
22
+ expect(warnings.length).to.eq(0, 'a valid manifest has no warnings');
23
+ expect(errors.length).to.eq(0, 'a valid manifest thows no errors');
24
+ });
25
+
26
+ describe('warnings', async () => {
27
+ describe('manifest', async () => {
28
+ it('cannot have properties that are not allowed and must have required properties', async () => {
29
+ const manifest = { cat: 'dog' };
30
+ const { warnings } = manifestValidator(manifest);
31
+
32
+ expect(warnings).to.deep.eq([
33
+ {
34
+ message: 'should NOT have additional properties',
35
+ dataPath: 'package-manifest.yml',
36
+ params: { additionalProperty: 'cat' }
37
+ },
38
+ {
39
+ message: "should have required property 'name'",
40
+ dataPath: 'package-manifest.yml',
41
+ params: { missingProperty: 'name' }
42
+ }
43
+ ]);
44
+ });
45
+
46
+ it('types must be correct', async () => {
47
+ const manifest = {
48
+ name: 1,
49
+ label: 1,
50
+ query_params: [1],
51
+ cold_states: [1],
52
+ integration_ids: [1],
53
+ fields: [1],
54
+ studio_options: [1]
55
+ };
56
+ const { warnings } = manifestValidator(manifest);
57
+
58
+ const typeLookup = warnings.reduce((acc, w) => {
59
+ const nameFromPath = w.dataPath.match(/([a-zA-Z]|_)+/)[0];
60
+ acc[nameFromPath] = w.params.type;
61
+ return acc;
62
+ }, {});
63
+
64
+ expect(typeLookup).to.deep.eq(
65
+ {
66
+ name: 'string',
67
+ label: 'string',
68
+ query_params: 'object',
69
+ cold_states: 'object',
70
+ integration_ids: 'string',
71
+ fields: 'object',
72
+ studio_options: 'object'
73
+ },
74
+ 'types must be correct'
75
+ );
76
+ });
77
+
78
+ it('integration_id must be valid', async () => {
79
+ const manifest = {
80
+ name: 'test',
81
+ integration_ids: ['tooooooshort', 'tooooooolooooooo', 'badCharacters!!']
82
+ };
83
+ const { warnings } = manifestValidator(manifest);
84
+
85
+ const messages = warnings.map((w) => w.message);
86
+
87
+ expect(messages).to.have.members(
88
+ [
89
+ 'should NOT be longer than 15 characters',
90
+ 'should NOT be shorter than 15 characters',
91
+ 'should match pattern "^[a-zA-Z0-9]+$"'
92
+ ],
93
+ 'correctly evaluates invalid integration_id'
94
+ );
95
+ });
96
+ });
97
+
98
+ describe('field', async () => {
99
+ it('cannot have properties that are not allowed and must have required properties', async () => {
100
+ const manifest = {
101
+ name: 'test',
102
+ fields: [
103
+ {
104
+ type: 'text', // type tested separately
105
+ cat: 'dog'
106
+ }
107
+ ]
108
+ };
109
+ const { warnings } = manifestValidator(manifest);
110
+
111
+ const params = { additional: [], missing: [] };
112
+ warnings.forEach((w) => {
113
+ const missing = w.params.missingProperty;
114
+ if (missing) return params.missing.push(missing);
115
+ const additional = w.params.additionalProperty;
116
+ if (additional) params.additional.push(additional);
117
+ });
118
+
119
+ expect(params.additional).to.have.members(
120
+ ['cat'],
121
+ 'cannot have properties that are not allowed'
122
+ );
123
+
124
+ expect(params.missing).to.have.members(['label', 'name'], 'must have required properties');
125
+ });
126
+
127
+ it('types must be correct', async () => {
128
+ const manifest = {
129
+ name: 'test',
130
+ fields: [
131
+ {
132
+ type: 1,
133
+ allows_dynamic: 1,
134
+ default: [],
135
+ description: 1,
136
+ label: 1,
137
+ max: 'one',
138
+ name: 1,
139
+ options: [1],
140
+ placeholder: [],
141
+ preview_text: 1,
142
+ provider: 1,
143
+ template: 1,
144
+ value: [],
145
+ heading: 1
146
+ }
147
+ ]
148
+ };
149
+ const { warnings } = manifestValidator(manifest);
150
+
151
+ const typeLookup = warnings.reduce((acc, w) => {
152
+ const { type } = w.params;
153
+ if (type) {
154
+ const nameFromPath = w.dataPath.split('.').pop();
155
+ acc[nameFromPath] = type;
156
+ }
157
+ return acc;
158
+ }, {});
159
+
160
+ expect(typeLookup).to.deep.eq(
161
+ {
162
+ type: 'string',
163
+ allows_dynamic: 'boolean',
164
+ default: 'string,number,boolean',
165
+ description: 'string',
166
+ label: 'string',
167
+ max: 'number',
168
+ name: 'string',
169
+ 'options[0]': 'object',
170
+ placeholder: 'string,number',
171
+ preview_text: 'array',
172
+ provider: 'string',
173
+ template: 'string',
174
+ value: 'string,number,boolean',
175
+ heading: 'string'
176
+ },
177
+ 'types must be correct'
178
+ );
179
+ });
180
+
181
+ it('only allows valid field types', async () => {
182
+ const manifest = {
183
+ name: 'test',
184
+ fields: [
185
+ {
186
+ name: 'test',
187
+ label: 'test',
188
+ type: 'dog'
189
+ }
190
+ ]
191
+ };
192
+ const {
193
+ warnings: [warning]
194
+ } = manifestValidator(manifest);
195
+
196
+ expect(warning.message).to.eq(ALLOWED_VALUES_MESSAGE, 'fields may only have valid types');
197
+ expect(warning.params.allowedValues).to.have.members(
198
+ fieldTypes,
199
+ 'valid field types are correct'
200
+ );
201
+ });
202
+
203
+ it('only oAuthAccount fields may and must have a provider property', async () => {
204
+ const missingProviderManifest = {
205
+ name: 'test',
206
+ fields: [
207
+ {
208
+ name: 'test',
209
+ label: 'test',
210
+ type: 'oAuthAccount'
211
+ }
212
+ ]
213
+ };
214
+ const { warnings: missingProviderWarnings } = manifestValidator(missingProviderManifest);
215
+
216
+ const extraProviderManifest = {
217
+ name: 'test',
218
+ fields: [
219
+ {
220
+ name: 'test',
221
+ label: 'test',
222
+ type: 'text',
223
+ provider: 'provider'
224
+ }
225
+ ]
226
+ };
227
+ const { warnings: extraProviderWarnings } = manifestValidator(extraProviderManifest);
228
+
229
+ const validManifest = {
230
+ name: 'test',
231
+ fields: [
232
+ {
233
+ name: 'test',
234
+ label: 'test',
235
+ type: 'oAuthAccount',
236
+ provider: 'provider'
237
+ }
238
+ ]
239
+ };
240
+ const { warnings: validManifestWarnings } = manifestValidator(validManifest);
241
+
242
+ expect(validManifestWarnings.length).to.eq(
243
+ 0,
244
+ 'oAuthAccount type field allows the provider property'
245
+ );
246
+ expect(missingProviderWarnings.map((w) => w.message)).to.include(
247
+ generateShouldHavePropertyMessage('provider'),
248
+ 'oAuthAccount type fields require a provider property'
249
+ );
250
+ expect(extraProviderWarnings.length).to.be.greaterThan(
251
+ 0,
252
+ 'only oAuthAccount type fields allow a provider property'
253
+ );
254
+ });
255
+
256
+ it('only token or number fields may have a max property', async () => {
257
+ const extraMaxManifest = {
258
+ name: 'test',
259
+ fields: [
260
+ {
261
+ name: 'test',
262
+ label: 'test',
263
+ type: 'text',
264
+ max: 5
265
+ }
266
+ ]
267
+ };
268
+ const { warnings: extraMaxWarnings } = manifestValidator(extraMaxManifest);
269
+
270
+ const validNumberFieldManifest = {
271
+ name: 'test',
272
+ fields: [
273
+ {
274
+ name: 'test',
275
+ label: 'test',
276
+ type: 'token',
277
+ max: 555
278
+ }
279
+ ]
280
+ };
281
+ const { warnings: validNumberFieldManifestWarnings } =
282
+ manifestValidator(validNumberFieldManifest);
283
+
284
+ const validNumberTokenManifest = {
285
+ name: 'test',
286
+ fields: [
287
+ {
288
+ name: 'test',
289
+ label: 'test',
290
+ type: 'token',
291
+ max: 555
292
+ }
293
+ ]
294
+ };
295
+ const { warnings: validTokenFieldManifestWarnings } =
296
+ manifestValidator(validNumberTokenManifest);
297
+
298
+ expect(validTokenFieldManifestWarnings.length).to.eq(
299
+ 0,
300
+ 'token type field allows the max property'
301
+ );
302
+ expect(validNumberFieldManifestWarnings.length).to.eq(
303
+ 0,
304
+ 'number type field allows the max property'
305
+ );
306
+ expect(extraMaxWarnings.length).to.be.greaterThan(
307
+ 0,
308
+ 'only number and token type fields allow a max property'
309
+ );
310
+ });
311
+
312
+ it('only data-source fields may have a template property', async () => {
313
+ const extraTemplateManifest = {
314
+ name: 'test',
315
+ fields: [
316
+ {
317
+ name: 'test',
318
+ label: 'test',
319
+ type: 'text',
320
+ template: 'template'
321
+ }
322
+ ]
323
+ };
324
+ const { warnings: extraTemplateWarnings } = manifestValidator(extraTemplateManifest);
325
+
326
+ const validManifest = {
327
+ name: 'test',
328
+ fields: [
329
+ {
330
+ name: 'test',
331
+ label: 'test',
332
+ type: 'data-source',
333
+ template: 'template'
334
+ }
335
+ ]
336
+ };
337
+ const { warnings: validManifestWarnings } = manifestValidator(validManifest);
338
+
339
+ expect(validManifestWarnings.length).to.eq(
340
+ 0,
341
+ 'data-source type field allows the template property'
342
+ );
343
+ expect(extraTemplateWarnings.length).to.be.greaterThan(
344
+ 0,
345
+ 'only data-source type fields allow a template property'
346
+ );
347
+ });
348
+
349
+ it('nonoption fields may not have an options property', async () => {
350
+ ['select', 'radio', 'checkbox'].forEach((type) => {
351
+ const manifest = {
352
+ name: 'test',
353
+ fields: [
354
+ {
355
+ name: 'test',
356
+ label: 'test',
357
+ type: type
358
+ }
359
+ ]
360
+ };
361
+ const { warnings } = manifestValidator(manifest);
362
+
363
+ expect(warnings.length).to.eq(0, `${type} type fields may have the options`);
364
+ });
365
+
366
+ const manifest = {
367
+ name: 'test',
368
+ fields: [
369
+ {
370
+ name: 'test',
371
+ label: 'test',
372
+ type: 'text',
373
+ options: [{ value: 2, label: 'two' }]
374
+ }
375
+ ]
376
+ };
377
+ const { warnings } = manifestValidator(manifest);
378
+
379
+ expect(warnings.length).to.be.greaterThan(0, `text fields may not have the options`);
380
+ });
381
+ });
382
+
383
+ describe('studio_options', async () => {
384
+ it('cannot have properties that are not allowed', async () => {
385
+ const manifest = {
386
+ name: 'test',
387
+ studio_options: {
388
+ cat: 'dog'
389
+ }
390
+ };
391
+ const { warnings } = manifestValidator(manifest);
392
+
393
+ expect(warnings[0]).to.deep.eq(
394
+ {
395
+ message: 'should NOT have additional properties',
396
+ dataPath: '.studio_options',
397
+ params: { additionalProperty: 'cat' }
398
+ },
399
+ 'extra properties are not allowed'
400
+ );
401
+ });
402
+
403
+ it('types must be correct', async () => {
404
+ const manifest = {
405
+ name: 1,
406
+ studio_options: {
407
+ prohibit_custom_params: 1,
408
+ framework_version: 1,
409
+ tools: [1],
410
+ preview_fields: [1],
411
+ property_groups: [1],
412
+ element_modifiers: [1],
413
+ property_modifiers: [1]
414
+ }
415
+ };
416
+ const { warnings } = manifestValidator(manifest);
417
+
418
+ const typeLookup = warnings.reduce((acc, w) => {
419
+ const nameFromPath = w.dataPath.split('.').pop();
420
+ acc[nameFromPath] = w.params.type;
421
+ return acc;
422
+ }, {});
423
+
424
+ expect(typeLookup).to.deep.eq(
425
+ {
426
+ name: 'string',
427
+ prohibit_custom_params: 'boolean',
428
+ framework_version: 'string',
429
+ 'tools[0]': 'object',
430
+ 'preview_fields[0]': 'string',
431
+ 'property_groups[0]': 'object',
432
+ 'element_modifiers[0]': 'object',
433
+ 'property_modifiers[0]': 'object'
434
+ },
435
+ 'types must be correct'
436
+ );
437
+ });
438
+
439
+ it('has a single property group', async () => {
440
+ const noPropertyGroupsManifest = {
441
+ name: 'test',
442
+ studio_options: {
443
+ property_groups: []
444
+ }
445
+ };
446
+ const { warnings: noPropertyGroupsWarnings } = manifestValidator(noPropertyGroupsManifest);
447
+
448
+ const tooManyMPropertyGroupsManifest = {
449
+ name: 'test',
450
+ studio_options: {
451
+ property_groups: [
452
+ {
453
+ name: 'test',
454
+ label: 'test',
455
+ properties: [
456
+ {
457
+ name: 'dog',
458
+ label: 'dog'
459
+ }
460
+ ]
461
+ },
462
+ {
463
+ name: 'test',
464
+ label: 'test',
465
+ properties: [
466
+ {
467
+ name: 'cat',
468
+ label: 'cat'
469
+ }
470
+ ]
471
+ }
472
+ ]
473
+ }
474
+ };
475
+ const { warnings: tooManyMPropertyGroupsWarnings } = manifestValidator(
476
+ tooManyMPropertyGroupsManifest
477
+ );
478
+
479
+ expect(noPropertyGroupsWarnings[0].message).to.eq(
480
+ 'should NOT have fewer than 1 items',
481
+ 'must have exactly one property group'
482
+ );
483
+ expect(tooManyMPropertyGroupsWarnings[0].message).to.eq(
484
+ 'should NOT have more than 1 items',
485
+ 'cannot have more than one property group'
486
+ );
487
+ });
488
+ });
489
+
490
+ describe('property_group', async () => {
491
+ it('cannot have properties that are not allowed and must have required properties', async () => {
492
+ const manifest = {
493
+ name: 'test',
494
+ studio_options: {
495
+ property_groups: [
496
+ {
497
+ cat: 'dog'
498
+ }
499
+ ]
500
+ }
501
+ };
502
+ const { warnings } = manifestValidator(manifest);
503
+
504
+ const params = { additional: [], missing: [] };
505
+ warnings.forEach((w) => {
506
+ const missing = w.params.missingProperty;
507
+ if (missing) return params.missing.push(missing);
508
+ params.additional.push(w.params.additionalProperty);
509
+ });
510
+
511
+ expect(params.additional).to.have.members(
512
+ ['cat'],
513
+ 'cannot have properties that are not allowed'
514
+ );
515
+
516
+ expect(params.missing).to.have.members(
517
+ ['name', 'label', 'properties'],
518
+ 'must have required properties'
519
+ );
520
+ });
521
+
522
+ it('types must be correct', async () => {
523
+ const manifest = {
524
+ name: 'test',
525
+ studio_options: {
526
+ property_groups: [
527
+ {
528
+ name: 'test', // name tested separately
529
+ label: 1,
530
+ description: 1,
531
+ properties: [1]
532
+ }
533
+ ]
534
+ }
535
+ };
536
+ const { warnings } = manifestValidator(manifest);
537
+
538
+ const typeLookup = warnings.reduce((acc, w) => {
539
+ const nameFromPath = w.dataPath.split('.').pop();
540
+ acc[nameFromPath] = w.params.type;
541
+ return acc;
542
+ }, {});
543
+
544
+ expect(typeLookup).to.deep.eq(
545
+ {
546
+ label: 'string',
547
+ description: 'string',
548
+ 'properties[0]': 'object'
549
+ },
550
+ 'types must be correct'
551
+ );
552
+ });
553
+
554
+ it('name must match package name', async () => {
555
+ const manifest = {
556
+ name: 'test',
557
+ studio_options: {
558
+ property_groups: [
559
+ {
560
+ name: 'dog',
561
+ label: 'dog',
562
+ properties: [
563
+ {
564
+ name: 'dog',
565
+ label: 'dog'
566
+ }
567
+ ]
568
+ }
569
+ ]
570
+ }
571
+ };
572
+ const { warnings } = manifestValidator(manifest);
573
+
574
+ expect(warnings).to.deep.eq(
575
+ [
576
+ {
577
+ message: 'property_group name must match package name',
578
+ dataPath: '.studio_options.propertyGroups[0]',
579
+ params: { name: 'dog' }
580
+ }
581
+ ],
582
+ 'property_group name must match package name'
583
+ );
584
+ });
585
+ });
586
+
587
+ describe('property', async () => {
588
+ it('cannot have properties that are not allowed and must have required properties', async () => {
589
+ const manifest = {
590
+ name: 'test',
591
+ studio_options: {
592
+ property_groups: [
593
+ {
594
+ name: 'test',
595
+ label: 'test',
596
+ properties: [
597
+ {
598
+ cat: 'dog'
599
+ }
600
+ ]
601
+ }
602
+ ]
603
+ }
604
+ };
605
+ const { warnings } = manifestValidator(manifest);
606
+
607
+ const params = { additional: [], missing: [] };
608
+ warnings.forEach((w) => {
609
+ const missing = w.params.missingProperty;
610
+ if (missing) return params.missing.push(missing);
611
+ params.additional.push(w.params.additionalProperty);
612
+ });
613
+
614
+ expect(params.additional).to.have.members(
615
+ ['cat'],
616
+ 'cannot have properties that are not allowed'
617
+ );
618
+
619
+ expect(params.missing).to.have.members(['name', 'label'], 'must have required properties');
620
+ });
621
+
622
+ it('types must be correct', async () => {
623
+ const manifest = {
624
+ name: 'test',
625
+ studio_options: {
626
+ property_groups: [
627
+ {
628
+ name: 'test',
629
+ label: 'test',
630
+ properties: [
631
+ {
632
+ name: 1,
633
+ type: 'text', // type tested separately
634
+ label: 1,
635
+ description: 1,
636
+ preview_text: [{}],
637
+ preview_values: [{}],
638
+ context_options: [1]
639
+ }
640
+ ]
641
+ }
642
+ ]
643
+ }
644
+ };
645
+ const { warnings } = manifestValidator(manifest);
646
+
647
+ const typeLookup = warnings.reduce((acc, w) => {
648
+ const nameFromPath = w.dataPath.split('.').pop();
649
+ acc[nameFromPath] = w.params.type;
650
+ return acc;
651
+ }, {});
652
+
653
+ expect(typeLookup).to.deep.eq(
654
+ {
655
+ name: 'string',
656
+ label: 'string',
657
+ description: 'string',
658
+ 'preview_text[0]': 'string,number,boolean',
659
+ 'preview_values[0]': 'string,number,boolean',
660
+ 'context_options[0]': 'object'
661
+ },
662
+ 'types must be correct'
663
+ );
664
+ });
665
+
666
+ it('type property must be an allowed value', async () => {
667
+ const manifest = {
668
+ name: 'test',
669
+ studio_options: {
670
+ property_groups: [
671
+ {
672
+ name: 'test',
673
+ label: 'test',
674
+ properties: [
675
+ {
676
+ name: 'test',
677
+ type: 'dog',
678
+ label: 'test'
679
+ }
680
+ ]
681
+ }
682
+ ]
683
+ }
684
+ };
685
+ const { warnings } = manifestValidator(manifest);
686
+
687
+ expect(warnings[0].message).to.eq(
688
+ 'should be equal to one of the allowed values',
689
+ 'type property must match an allowed value'
690
+ );
691
+ });
692
+
693
+ describe('context_options', async () => {
694
+ it('cannot have properties that are not allowed and must have required properties', async () => {
695
+ const manifest = {
696
+ name: 'test',
697
+ studio_options: {
698
+ property_groups: [
699
+ {
700
+ name: 'test',
701
+ label: 'test',
702
+ properties: [
703
+ {
704
+ name: 'test',
705
+ type: 'text',
706
+ label: 'test',
707
+ context_options: [
708
+ {
709
+ cat: 'dog',
710
+ type: 'text' // type tested separately
711
+ }
712
+ ]
713
+ }
714
+ ]
715
+ }
716
+ ]
717
+ }
718
+ };
719
+ const { warnings } = manifestValidator(manifest);
720
+
721
+ const params = { additional: [], missing: [] };
722
+ warnings.forEach((w) => {
723
+ const missing = w.params.missingProperty;
724
+ if (missing) return params.missing.push(missing);
725
+ params.additional.push(w.params.additionalProperty);
726
+ });
727
+
728
+ expect(params.additional).to.have.members(
729
+ ['cat'],
730
+ 'cannot have properties that are not allowed'
731
+ );
732
+
733
+ expect(params.missing).to.have.members(['name'], 'must have required properties');
734
+ });
735
+
736
+ it('types must be correct', async () => {
737
+ const manifest = {
738
+ name: 'test',
739
+ studio_options: {
740
+ property_groups: [
741
+ {
742
+ name: 'test',
743
+ label: 'test',
744
+ properties: [
745
+ {
746
+ name: 'test',
747
+ label: 'test',
748
+ context_options: [
749
+ {
750
+ name: 1,
751
+ type: 'select',
752
+ description: 1,
753
+ defaultValue: 'one',
754
+ options: [
755
+ {
756
+ label: 1,
757
+ value: {}
758
+ }
759
+ ]
760
+ }
761
+ ]
762
+ }
763
+ ]
764
+ }
765
+ ]
766
+ }
767
+ };
768
+ const { warnings } = manifestValidator(manifest);
769
+
770
+ const typeLookup = warnings.reduce((acc, w) => {
771
+ const nameFromPath = w.dataPath.split('.').pop();
772
+ acc[nameFromPath] = w.params.type;
773
+ return acc;
774
+ }, {});
775
+
776
+ expect(typeLookup).to.deep.eq(
777
+ {
778
+ name: 'string',
779
+ description: 'string',
780
+ defaultValue: 'number',
781
+ label: 'string',
782
+ value: 'string,number,boolean'
783
+ },
784
+ 'types must be correct'
785
+ );
786
+ });
787
+
788
+ it('option context_options require options', async () => {
789
+ const manifest = {
790
+ name: 'test',
791
+ studio_options: {
792
+ property_groups: [
793
+ {
794
+ name: 'test',
795
+ label: 'test',
796
+ properties: [
797
+ {
798
+ name: 'test',
799
+ type: 'text',
800
+ label: 'test',
801
+ context_options: [
802
+ {
803
+ name: 'test',
804
+ type: 'select'
805
+ }
806
+ ]
807
+ }
808
+ ]
809
+ }
810
+ ]
811
+ }
812
+ };
813
+ const { warnings } = manifestValidator(manifest);
814
+
815
+ expect(warnings.some((w) => w.message === generateShouldHavePropertyMessage('options')))
816
+ .to.be.true;
817
+ });
818
+
819
+ it('options must have at least one option', async () => {
820
+ const manifest = {
821
+ name: 'test',
822
+ studio_options: {
823
+ property_groups: [
824
+ {
825
+ name: 'test',
826
+ label: 'test',
827
+ properties: [
828
+ {
829
+ name: 'test',
830
+ type: 'text',
831
+ label: 'test',
832
+ context_options: [
833
+ {
834
+ name: 'test',
835
+ type: 'select',
836
+ options: []
837
+ }
838
+ ]
839
+ }
840
+ ]
841
+ }
842
+ ]
843
+ }
844
+ };
845
+ const { warnings } = manifestValidator(manifest);
846
+
847
+ expect(warnings[0].message).to.eq(
848
+ 'should NOT have fewer than 1 items',
849
+ 'must have at least one option'
850
+ );
851
+ });
852
+
853
+ it('options cannot have have properties that are not allowed and must have required properties', async () => {
854
+ const manifest = {
855
+ name: 'test',
856
+ studio_options: {
857
+ property_groups: [
858
+ {
859
+ name: 'test',
860
+ label: 'test',
861
+ properties: [
862
+ {
863
+ name: 'test',
864
+ type: 'text',
865
+ label: 'test',
866
+ context_options: [
867
+ {
868
+ name: 'test',
869
+ type: 'select',
870
+ options: [{ cat: 'dog' }]
871
+ }
872
+ ]
873
+ }
874
+ ]
875
+ }
876
+ ]
877
+ }
878
+ };
879
+ const { warnings } = manifestValidator(manifest);
880
+
881
+ const params = { additional: [], missing: [] };
882
+ warnings.forEach((w) => {
883
+ const missing = w.params.missingProperty;
884
+ if (missing) return params.missing.push(missing);
885
+ params.additional.push(w.params.additionalProperty);
886
+ });
887
+
888
+ expect(params.additional).to.have.members(
889
+ ['cat'],
890
+ 'cannot have properties that are not allowed'
891
+ );
892
+
893
+ expect(params.missing).to.have.members(
894
+ ['label', 'value'],
895
+ 'must have required properties'
896
+ );
897
+ });
898
+ });
899
+ });
900
+
901
+ describe('tool', async () => {
902
+ it('cannot have properties that are not allowed and must have required properties', async () => {
903
+ const manifest = {
904
+ name: 'test',
905
+ studio_options: {
906
+ tools: [
907
+ {
908
+ cat: 'dog'
909
+ }
910
+ ]
911
+ }
912
+ };
913
+ const { warnings } = manifestValidator(manifest);
914
+
915
+ const params = { additional: [], missing: [] };
916
+ warnings.forEach((w) => {
917
+ const missing = w.params.missingProperty;
918
+ if (missing) return params.missing.push(missing);
919
+ params.additional.push(w.params.additionalProperty);
920
+ });
921
+
922
+ expect(params.additional).to.have.members(
923
+ ['cat'],
924
+ 'cannot have properties that are not allowed'
925
+ );
926
+
927
+ expect(params.missing).to.have.members(
928
+ ['icon', 'label', 'type', 'name'],
929
+ 'must have required properties'
930
+ );
931
+ });
932
+
933
+ it('types must be correct', async () => {
934
+ const manifest = {
935
+ name: 'test',
936
+ studio_options: {
937
+ tools: [
938
+ {
939
+ class_names: 1,
940
+ fields: 1,
941
+ icon: 'avatar', // icon tested separately
942
+ label: 1,
943
+ name: 1,
944
+ type: 1,
945
+ locked: 'one',
946
+ dynamic_fields: {},
947
+ defaults: []
948
+ }
949
+ ]
950
+ }
951
+ };
952
+ const { warnings } = manifestValidator(manifest);
953
+
954
+ const typeLookup = warnings.reduce((acc, w) => {
955
+ const nameFromPath = w.dataPath.split('.').pop();
956
+ acc[nameFromPath] = w.params.type;
957
+ return acc;
958
+ }, {});
959
+
960
+ expect(typeLookup).to.deep.eq(
961
+ {
962
+ class_names: 'string',
963
+ fields: 'array',
964
+ label: 'string',
965
+ name: 'string',
966
+ type: 'string',
967
+ locked: 'boolean',
968
+ dynamic_fields: 'array',
969
+ defaults: 'object'
970
+ },
971
+ 'types must be correct'
972
+ );
973
+ });
974
+
975
+ it('icon must be an allowed value', async () => {
976
+ const manifest = {
977
+ name: 'test',
978
+ studio_options: {
979
+ tools: [
980
+ {
981
+ name: 'test',
982
+ label: 'test',
983
+ icon: 'bananas',
984
+ type: 'current-value'
985
+ }
986
+ ]
987
+ }
988
+ };
989
+
990
+ const {
991
+ warnings: [{ dataPath, message }]
992
+ } = manifestValidator(manifest);
993
+
994
+ expect({ dataPath, message }).to.deep.equal(
995
+ {
996
+ message: ALLOWED_VALUES_MESSAGE,
997
+ dataPath: '.studio_options.tools[0].icon'
998
+ },
999
+ 'icon must be an allowed value'
1000
+ );
1001
+ });
1002
+
1003
+ describe('dynamic_fields', async () => {
1004
+ it('cannot have properties that are not allowed and must have required properties', async () => {
1005
+ const manifest = {
1006
+ name: 'test',
1007
+ studio_options: {
1008
+ tools: [
1009
+ {
1010
+ name: 'test',
1011
+ icon: 'avatar',
1012
+ label: 'test',
1013
+ type: 'test-value',
1014
+ dynamic_fields: [
1015
+ {
1016
+ type: 'text', // type tested separately
1017
+ cat: 'dog'
1018
+ }
1019
+ ]
1020
+ }
1021
+ ]
1022
+ }
1023
+ };
1024
+ const { warnings } = manifestValidator(manifest);
1025
+
1026
+ const params = { additional: [], missing: [] };
1027
+ warnings.forEach((w) => {
1028
+ const missing = w.params.missingProperty;
1029
+ if (missing) return params.missing.push(missing);
1030
+ params.additional.push(w.params.additionalProperty);
1031
+ });
1032
+
1033
+ expect(params.additional).to.have.members(
1034
+ ['cat'],
1035
+ 'cannot have properties that are not allowed'
1036
+ );
1037
+ expect(params.missing).to.have.members(
1038
+ ['label', 'name'],
1039
+ 'must have required properties'
1040
+ );
1041
+ });
1042
+
1043
+ it('types must be correct', async () => {
1044
+ const manifest = {
1045
+ name: 'test',
1046
+ studio_options: {
1047
+ tools: [
1048
+ {
1049
+ name: 'test',
1050
+ icon: 'avatar',
1051
+ label: 'test',
1052
+ type: 'test-value',
1053
+ dynamic_fields: [
1054
+ {
1055
+ name: 1,
1056
+ label: 1,
1057
+ type: 'text', // type tested separately
1058
+ allow_static_option: 'one'
1059
+ }
1060
+ ]
1061
+ }
1062
+ ]
1063
+ }
1064
+ };
1065
+ const { warnings } = manifestValidator(manifest);
1066
+
1067
+ const typeLookup = warnings.reduce((acc, w) => {
1068
+ const nameFromPath = w.dataPath.split('.').pop();
1069
+ acc[nameFromPath] = w.params.type;
1070
+ return acc;
1071
+ }, {});
1072
+
1073
+ expect(typeLookup).to.deep.eq(
1074
+ { name: 'string', label: 'string', allow_static_option: 'boolean' },
1075
+ 'types must be correct'
1076
+ );
1077
+ });
1078
+
1079
+ it('type property must be an allowed value', async () => {
1080
+ const manifest = {
1081
+ name: 'test',
1082
+ studio_options: {
1083
+ tools: [
1084
+ {
1085
+ name: 'test',
1086
+ icon: 'avatar',
1087
+ label: 'test',
1088
+ type: 'test-value',
1089
+ dynamic_fields: [
1090
+ {
1091
+ name: 'test',
1092
+ label: 'test',
1093
+ type: 'dog'
1094
+ }
1095
+ ]
1096
+ }
1097
+ ]
1098
+ }
1099
+ };
1100
+
1101
+ const {
1102
+ warnings: [{ dataPath, message }]
1103
+ } = manifestValidator(manifest);
1104
+
1105
+ expect({ dataPath, message }).to.deep.eq(
1106
+ {
1107
+ message: ALLOWED_VALUES_MESSAGE,
1108
+ dataPath: '.studio_options.tools[0].dynamic_fields[0].type'
1109
+ },
1110
+ 'icon must be an allowed value'
1111
+ );
1112
+ });
1113
+ });
1114
+
1115
+ describe('defaults', async () => {
1116
+ it('cannot have properties that are not allowed', async () => {
1117
+ const manifest = {
1118
+ name: 'test',
1119
+ studio_options: {
1120
+ tools: [
1121
+ {
1122
+ name: 'test',
1123
+ icon: 'avatar',
1124
+ label: 'test',
1125
+ type: 'test-value',
1126
+ defaults: {
1127
+ cat: 'dog'
1128
+ }
1129
+ }
1130
+ ]
1131
+ }
1132
+ };
1133
+
1134
+ const {
1135
+ warnings: [
1136
+ {
1137
+ message,
1138
+ params: { additionalProperty }
1139
+ }
1140
+ ]
1141
+ } = manifestValidator(manifest);
1142
+
1143
+ expect({ message, additionalProperty }).to.deep.eq(
1144
+ {
1145
+ message: 'should NOT have additional properties',
1146
+ additionalProperty: 'cat'
1147
+ },
1148
+ 'cannot have properties that are not allowed'
1149
+ );
1150
+ });
1151
+
1152
+ it('types must be correct', async () => {
1153
+ const manifest = {
1154
+ name: 'test',
1155
+ studio_options: {
1156
+ tools: [
1157
+ {
1158
+ name: 'test',
1159
+ icon: 'avatar',
1160
+ label: 'test',
1161
+ type: 'test-value',
1162
+ defaults: {
1163
+ width: 'one',
1164
+ height: 'one',
1165
+ dynamicProperty: 1
1166
+ }
1167
+ }
1168
+ ]
1169
+ }
1170
+ };
1171
+ const { warnings } = manifestValidator(manifest);
1172
+
1173
+ const typeLookup = warnings.reduce((acc, w) => {
1174
+ const nameFromPath = w.dataPath.split('.').pop();
1175
+ acc[nameFromPath] = w.params.type;
1176
+ return acc;
1177
+ }, {});
1178
+
1179
+ expect(typeLookup).to.deep.eq(
1180
+ { dynamicProperty: 'object', height: 'number', width: 'number' },
1181
+ 'types must be correct'
1182
+ );
1183
+ });
1184
+ });
1185
+ });
1186
+
1187
+ describe('property_modifiers', async () => {
1188
+ it('cannot have properties that are not allowed and must have required properties', async () => {
1189
+ const manifest = {
1190
+ name: 'test',
1191
+ studio_options: {
1192
+ property_modifiers: [
1193
+ {
1194
+ cat: 'dog'
1195
+ }
1196
+ ]
1197
+ }
1198
+ };
1199
+ const { warnings } = manifestValidator(manifest);
1200
+
1201
+ const params = { additional: [], missing: [] };
1202
+ warnings.forEach((w) => {
1203
+ const missing = w.params.missingProperty;
1204
+ if (missing) return params.missing.push(missing);
1205
+ params.additional.push(w.params.additionalProperty);
1206
+ });
1207
+
1208
+ expect(params.additional).to.have.members(
1209
+ ['cat'],
1210
+ 'cannot have properties that are not allowed'
1211
+ );
1212
+ expect(params.missing).to.have.members(['name', 'label'], 'must have required properties');
1213
+ });
1214
+
1215
+ it('has correct types', async () => {
1216
+ const manifest = {
1217
+ name: 'test',
1218
+ studio_options: {
1219
+ property_modifiers: [
1220
+ {
1221
+ name: 1,
1222
+ label: 1,
1223
+ type: 'text', // type tested separately
1224
+ preview_text: [{}],
1225
+ description: 1,
1226
+ propertyInputs: {}
1227
+ }
1228
+ ]
1229
+ }
1230
+ };
1231
+ const { warnings } = manifestValidator(manifest);
1232
+
1233
+ const typeLookup = warnings.reduce((acc, w) => {
1234
+ const nameFromPath = w.dataPath.split('.').pop();
1235
+ acc[nameFromPath] = w.params.type;
1236
+ return acc;
1237
+ }, {});
1238
+
1239
+ expect(typeLookup).to.deep.eq(
1240
+ {
1241
+ name: 'string',
1242
+ label: 'string',
1243
+ 'preview_text[0]': 'string,number,boolean',
1244
+ description: 'string',
1245
+ propertyInputs: 'array'
1246
+ },
1247
+ 'types must be correct'
1248
+ );
1249
+ });
1250
+
1251
+ it('type property must be an allowed value', async () => {
1252
+ const manifest = {
1253
+ name: 'test',
1254
+ studio_options: {
1255
+ property_modifiers: [
1256
+ {
1257
+ name: 'test',
1258
+ label: 'test',
1259
+ type: 'dog'
1260
+ }
1261
+ ]
1262
+ }
1263
+ };
1264
+ const {
1265
+ warnings: [{ dataPath, message }]
1266
+ } = manifestValidator(manifest);
1267
+
1268
+ expect({ dataPath, message }).to.deep.eq(
1269
+ {
1270
+ message: ALLOWED_VALUES_MESSAGE,
1271
+ dataPath: '.studio_options.property_modifiers[0].type'
1272
+ },
1273
+ 'icon must be an allowed value'
1274
+ );
1275
+ });
1276
+
1277
+ describe('propertyInputs', async () => {
1278
+ it('cannot have properties that are not allowed and must have required properties', async () => {
1279
+ const manifest = {
1280
+ name: 'test',
1281
+ studio_options: {
1282
+ property_modifiers: [
1283
+ {
1284
+ name: 'test',
1285
+ label: 'test',
1286
+ type: 'text',
1287
+ propertyInputs: [{ cat: 'dog' }]
1288
+ }
1289
+ ]
1290
+ }
1291
+ };
1292
+ const { warnings } = manifestValidator(manifest);
1293
+
1294
+ const params = { additional: [], missing: [] };
1295
+ warnings.forEach((w) => {
1296
+ const missing = w.params.missingProperty;
1297
+ if (missing) return params.missing.push(missing);
1298
+ params.additional.push(w.params.additionalProperty);
1299
+ });
1300
+
1301
+ expect(params.additional).to.have.members(
1302
+ ['cat'],
1303
+ 'cannot have properties that are not allowed'
1304
+ );
1305
+ expect(params.missing).to.have.members(
1306
+ ['name', 'label', 'variableName'],
1307
+ 'must have required properties'
1308
+ );
1309
+ });
1310
+
1311
+ it('types must be correct', async () => {
1312
+ const manifest = {
1313
+ name: 'test',
1314
+ studio_options: {
1315
+ property_modifiers: [
1316
+ {
1317
+ name: 'test',
1318
+ label: 'test',
1319
+ type: 'text',
1320
+ propertyInputs: [
1321
+ {
1322
+ name: 1,
1323
+ variableName: 1,
1324
+ label: 1,
1325
+ allow_static_option: 1,
1326
+ type: 1
1327
+ }
1328
+ ]
1329
+ }
1330
+ ]
1331
+ }
1332
+ };
1333
+ const { warnings } = manifestValidator(manifest);
1334
+
1335
+ const typeLookup = warnings.reduce((acc, w) => {
1336
+ const nameFromPath = w.dataPath.split('.').pop();
1337
+ acc[nameFromPath] = w.params.type;
1338
+ return acc;
1339
+ }, {});
1340
+
1341
+ expect(typeLookup).to.deep.eq(
1342
+ {
1343
+ name: 'string',
1344
+ variableName: 'string',
1345
+ label: 'string',
1346
+ allow_static_option: 'boolean',
1347
+ type: 'string'
1348
+ },
1349
+ 'types must be correct'
1350
+ );
1351
+ });
1352
+ });
1353
+ });
1354
+
1355
+ describe('element_modifiers', async () => {
1356
+ it('cannot have properties that are not allowed and must have required properties', async () => {
1357
+ const manifest = {
1358
+ name: 'test',
1359
+ studio_options: {
1360
+ element_modifiers: [
1361
+ {
1362
+ cat: 'dog'
1363
+ }
1364
+ ]
1365
+ }
1366
+ };
1367
+ const { warnings } = manifestValidator(manifest);
1368
+
1369
+ const params = { additional: [], missing: [] };
1370
+ warnings.forEach((w) => {
1371
+ const missing = w.params.missingProperty;
1372
+ if (missing) return params.missing.push(missing);
1373
+ params.additional.push(w.params.additionalProperty);
1374
+ });
1375
+
1376
+ expect(params.additional).to.have.members(
1377
+ ['cat'],
1378
+ 'cannot have properties that are not allowed'
1379
+ );
1380
+ expect(params.missing).to.have.members(['name', 'label'], 'must have required properties');
1381
+ });
1382
+
1383
+ it('types must be correct', async () => {
1384
+ const manifest = {
1385
+ name: 'test',
1386
+ studio_options: {
1387
+ element_modifiers: [
1388
+ {
1389
+ name: 1,
1390
+ label: 1,
1391
+ description: 1
1392
+ }
1393
+ ]
1394
+ }
1395
+ };
1396
+ const { warnings } = manifestValidator(manifest);
1397
+
1398
+ const typeLookup = warnings.reduce((acc, w) => {
1399
+ const nameFromPath = w.dataPath.split('.').pop();
1400
+ acc[nameFromPath] = w.params.type;
1401
+ return acc;
1402
+ }, {});
1403
+
1404
+ expect(typeLookup).to.deep.eq(
1405
+ { name: 'string', label: 'string', description: 'string' },
1406
+ 'types must be correct'
1407
+ );
1408
+ });
1409
+ });
1410
+
1411
+ describe('dynamicProperty', async () => {
1412
+ it('cannot have properties that are not allowed and must have required properties', async () => {
1413
+ const manifest = {
1414
+ name: 'test',
1415
+ studio_options: {
1416
+ tools: [
1417
+ {
1418
+ name: 'test',
1419
+ icon: 'avatar',
1420
+ label: 'test',
1421
+ type: 'test-value',
1422
+ defaults: {
1423
+ dynamicProperty: {
1424
+ cat: 'dog'
1425
+ }
1426
+ }
1427
+ }
1428
+ ]
1429
+ }
1430
+ };
1431
+ const { warnings } = manifestValidator(manifest);
1432
+
1433
+ const params = { additional: [], missing: [] };
1434
+ warnings.forEach((w) => {
1435
+ const missing = w.params.missingProperty;
1436
+ if (missing) return params.missing.push(missing);
1437
+ params.additional.push(w.params.additionalProperty);
1438
+ });
1439
+
1440
+ expect(params.additional).to.have.members(
1441
+ ['cat'],
1442
+ 'cannot have properties that are not allowed'
1443
+ );
1444
+ expect(params.missing).to.have.members(['propertyPath'], 'must have required properties');
1445
+ });
1446
+
1447
+ it('types must be correct', async () => {
1448
+ const manifest = {
1449
+ name: 'test',
1450
+ studio_options: {
1451
+ tools: [
1452
+ {
1453
+ name: 'test',
1454
+ icon: 'avatar',
1455
+ label: 'test',
1456
+ type: 'test-value',
1457
+ defaults: {
1458
+ dynamicProperty: {
1459
+ propertyPath: 1,
1460
+ propertyGroupKey: 1,
1461
+ propertyPreview: [],
1462
+ propertyFallback: [],
1463
+ propertyValue: [],
1464
+ context: []
1465
+ }
1466
+ }
1467
+ }
1468
+ ]
1469
+ }
1470
+ };
1471
+ const { warnings } = manifestValidator(manifest);
1472
+
1473
+ const typeLookup = warnings.reduce((acc, w) => {
1474
+ const nameFromPath = w.dataPath.split('.').pop();
1475
+ acc[nameFromPath] = w.params.type;
1476
+ return acc;
1477
+ }, {});
1478
+
1479
+ expect(typeLookup).to.deep.eq(
1480
+ {
1481
+ propertyGroupKey: 'string',
1482
+ propertyPath: 'string',
1483
+ propertyPreview: 'string,number,boolean',
1484
+ propertyFallback: 'string,number,boolean',
1485
+ propertyValue: 'string',
1486
+ context: 'object'
1487
+ },
1488
+ 'types must be correct'
1489
+ );
1490
+ });
1491
+ });
1492
+ });
1493
+
1494
+ describe('errors', async () => {
1495
+ it('must have a name', async () => {
1496
+ const manifest = { cat: 'dog' };
1497
+ const { errors } = manifestValidator(manifest);
1498
+
1499
+ expect(errors.map((w) => w.message)).to.include(
1500
+ generateShouldHavePropertyMessage('name'),
1501
+ 'name property is required'
1502
+ );
1503
+ });
1504
+
1505
+ it('must not have prohibited keys', async () => {
1506
+ const manifest = { name: 'dog', clickthrough_dynamic_property: 'www.example.com' };
1507
+ const { errors } = manifestValidator(manifest);
1508
+
1509
+ expect(errors.map((w) => w.message)).to.include(
1510
+ TOP_LEVEL_PROHIBITED_PROPERTY_MESSAGE,
1511
+ 'prohibited properties are prohibited'
1512
+ );
1513
+ });
1514
+ });
1515
+ });