@bernierllc/content-type-audio 1.0.1

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 (38) hide show
  1. package/.eslintrc.js +42 -0
  2. package/README.md +500 -0
  3. package/__tests__/AudioContentTypeManager.test.ts +592 -0
  4. package/__tests__/integration.test.ts +279 -0
  5. package/__tests__/utils.test.ts +142 -0
  6. package/coverage/AudioContentTypeManager.ts.html +799 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +161 -0
  11. package/coverage/lcov-report/AudioContentTypeManager.ts.html +799 -0
  12. package/coverage/lcov-report/base.css +224 -0
  13. package/coverage/lcov-report/block-navigation.js +87 -0
  14. package/coverage/lcov-report/favicon.png +0 -0
  15. package/coverage/lcov-report/index.html +161 -0
  16. package/coverage/lcov-report/prettify.css +1 -0
  17. package/coverage/lcov-report/prettify.js +2 -0
  18. package/coverage/lcov-report/schemas.ts.html +211 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov-report/types.ts.html +331 -0
  22. package/coverage/lcov-report/utils.ts.html +325 -0
  23. package/coverage/lcov.info +200 -0
  24. package/coverage/prettify.css +1 -0
  25. package/coverage/prettify.js +2 -0
  26. package/coverage/schemas.ts.html +211 -0
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -0
  29. package/coverage/types.ts.html +331 -0
  30. package/coverage/utils.ts.html +325 -0
  31. package/jest.config.cjs +32 -0
  32. package/package.json +50 -0
  33. package/src/AudioContentTypeManager.ts +238 -0
  34. package/src/index.ts +31 -0
  35. package/src/schemas.ts +42 -0
  36. package/src/types.ts +82 -0
  37. package/src/utils.ts +80 -0
  38. package/tsconfig.json +18 -0
@@ -0,0 +1,592 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { AudioContentTypeManager, validateAudio } from '../src/AudioContentTypeManager';
10
+ import { AudioFormat, AudioContentType, AudioMetadata } from '../src/types';
11
+ import { ContentTypeRegistry } from '@bernierllc/content-type-registry';
12
+
13
+ describe('AudioContentTypeManager', () => {
14
+ let manager: AudioContentTypeManager;
15
+
16
+ beforeEach(() => {
17
+ manager = new AudioContentTypeManager({
18
+ maxFileSize: 50 * 1024 * 1024, // 50MB
19
+ allowedFormats: [AudioFormat.MP3, AudioFormat.WAV],
20
+ requireMetadata: true
21
+ });
22
+ });
23
+
24
+ describe('constructor', () => {
25
+ it('should create manager with default config', () => {
26
+ const defaultManager = new AudioContentTypeManager();
27
+ expect(defaultManager).toBeDefined();
28
+ });
29
+
30
+ it('should create manager with custom config', () => {
31
+ const customManager = new AudioContentTypeManager({
32
+ maxFileSize: 10 * 1024 * 1024,
33
+ allowedFormats: [AudioFormat.MP3],
34
+ requireMetadata: false
35
+ });
36
+ expect(customManager).toBeDefined();
37
+ });
38
+
39
+ it('should merge custom config with defaults', () => {
40
+ const partialManager = new AudioContentTypeManager({
41
+ maxFileSize: 20 * 1024 * 1024
42
+ });
43
+ expect(partialManager).toBeDefined();
44
+ });
45
+ });
46
+
47
+ describe('register', () => {
48
+ it('should register audio content type with registry', () => {
49
+ const registry = new ContentTypeRegistry();
50
+ manager.register(registry);
51
+
52
+ const result = registry.get('audio');
53
+ expect(result.success).toBe(true);
54
+ expect(result.data).toBeDefined();
55
+ expect(result.data?.name).toBe('Audio');
56
+ expect(result.data?.baseType).toBe('audio');
57
+ });
58
+
59
+ it('should include editor configuration', () => {
60
+ const registry = new ContentTypeRegistry();
61
+ manager.register(registry);
62
+
63
+ const result = registry.get('audio');
64
+ expect(result.success).toBe(true);
65
+ expect(result.data?.editor).toBeDefined();
66
+ expect(result.data?.editor.component).toBe('file-upload-with-player');
67
+ });
68
+
69
+ it('should include storage and publishing configuration', () => {
70
+ const registry = new ContentTypeRegistry();
71
+ manager.register(registry);
72
+
73
+ const result = registry.get('audio');
74
+ expect(result.success).toBe(true);
75
+ expect(result.data?.storage).toBeDefined();
76
+ expect(result.data?.publishing).toBeDefined();
77
+ });
78
+ });
79
+
80
+ describe('getSchema', () => {
81
+ it('should return Zod schema', () => {
82
+ const schema = manager.getSchema();
83
+ expect(schema).toBeDefined();
84
+ });
85
+ });
86
+
87
+ describe('validate', () => {
88
+ it('should validate correct audio content', async () => {
89
+ const content: AudioContentType = {
90
+ id: 'audio_123',
91
+ type: 'audio',
92
+ metadata: {
93
+ filename: 'song.mp3',
94
+ mimeType: AudioFormat.MP3,
95
+ size: 5 * 1024 * 1024, // 5MB
96
+ duration: 180, // 3 minutes
97
+ bitrate: 192,
98
+ title: 'Test Song'
99
+ },
100
+ createdAt: new Date(),
101
+ updatedAt: new Date()
102
+ };
103
+
104
+ const result = manager.validate(content);
105
+
106
+ expect(result.valid).toBe(true);
107
+ expect(result.errors).toBeUndefined();
108
+ expect(result.metadata?.title).toBe('Test Song');
109
+ });
110
+
111
+ it('should reject audio exceeding max file size', async () => {
112
+ const content: AudioContentType = {
113
+ id: 'audio_456',
114
+ type: 'audio',
115
+ metadata: {
116
+ filename: 'large.mp3',
117
+ mimeType: AudioFormat.MP3,
118
+ size: 100 * 1024 * 1024, // 100MB (exceeds 50MB limit)
119
+ duration: 3600,
120
+ bitrate: 192,
121
+ title: 'Large File'
122
+ },
123
+ createdAt: new Date(),
124
+ updatedAt: new Date()
125
+ };
126
+
127
+ const result = manager.validate(content);
128
+
129
+ expect(result.valid).toBe(false);
130
+ expect(result.errors).toBeDefined();
131
+ expect(result.errors?.[0]).toContain('exceeds maximum');
132
+ });
133
+
134
+ it('should reject disallowed audio formats', async () => {
135
+ const content: AudioContentType = {
136
+ id: 'audio_789',
137
+ type: 'audio',
138
+ metadata: {
139
+ filename: 'song.flac',
140
+ mimeType: AudioFormat.FLAC, // Not in allowedFormats
141
+ size: 1024 * 1024,
142
+ duration: 60,
143
+ bitrate: 128,
144
+ title: 'Test Song'
145
+ },
146
+ createdAt: new Date(),
147
+ updatedAt: new Date()
148
+ };
149
+
150
+ const result = manager.validate(content);
151
+
152
+ expect(result.valid).toBe(false);
153
+ expect(result.errors).toBeDefined();
154
+ expect(result.errors?.[0]).toContain('not allowed');
155
+ });
156
+
157
+ it('should require title when requireMetadata is true', async () => {
158
+ const content: AudioContentType = {
159
+ id: 'audio_101',
160
+ type: 'audio',
161
+ metadata: {
162
+ filename: 'song.mp3',
163
+ mimeType: AudioFormat.MP3,
164
+ size: 1024 * 1024,
165
+ duration: 60,
166
+ bitrate: 128
167
+ // No title
168
+ },
169
+ createdAt: new Date(),
170
+ updatedAt: new Date()
171
+ };
172
+
173
+ const result = manager.validate(content);
174
+
175
+ expect(result.valid).toBe(false);
176
+ expect(result.errors).toBeDefined();
177
+ expect(result.errors?.[0]).toContain('Title is required');
178
+ });
179
+
180
+ it('should handle invalid content structure', async () => {
181
+ const invalidContent = {
182
+ id: 'audio_999',
183
+ type: 'video', // Wrong type
184
+ metadata: {}
185
+ };
186
+
187
+ const result = manager.validate(invalidContent);
188
+
189
+ expect(result.valid).toBe(false);
190
+ expect(result.errors).toBeDefined();
191
+ });
192
+
193
+ it('should handle missing required fields', async () => {
194
+ const incompleteContent = {
195
+ id: 'audio_888',
196
+ type: 'audio'
197
+ // Missing metadata
198
+ };
199
+
200
+ const result = manager.validate(incompleteContent);
201
+
202
+ expect(result.valid).toBe(false);
203
+ expect(result.errors).toBeDefined();
204
+ });
205
+
206
+ it('should validate optional fields', async () => {
207
+ const content: AudioContentType = {
208
+ id: 'audio_777',
209
+ type: 'audio',
210
+ metadata: {
211
+ filename: 'song.mp3',
212
+ mimeType: AudioFormat.MP3,
213
+ size: 1024 * 1024,
214
+ duration: 60,
215
+ bitrate: 128,
216
+ title: 'Test Song',
217
+ artist: 'Test Artist',
218
+ album: 'Test Album',
219
+ year: 2024,
220
+ sampleRate: 44100,
221
+ channels: 2
222
+ },
223
+ filePath: '/path/to/song.mp3',
224
+ createdAt: new Date(),
225
+ updatedAt: new Date()
226
+ };
227
+
228
+ const result = manager.validate(content);
229
+
230
+ expect(result.valid).toBe(true);
231
+ expect(result.metadata?.artist).toBe('Test Artist');
232
+ expect(result.metadata?.album).toBe('Test Album');
233
+ expect(result.metadata?.year).toBe(2024);
234
+ });
235
+
236
+ it('should reject negative duration', async () => {
237
+ const content: AudioContentType = {
238
+ id: 'audio_666',
239
+ type: 'audio',
240
+ metadata: {
241
+ filename: 'song.mp3',
242
+ mimeType: AudioFormat.MP3,
243
+ size: 1024 * 1024,
244
+ duration: -10, // Invalid
245
+ bitrate: 128,
246
+ title: 'Test Song'
247
+ },
248
+ createdAt: new Date(),
249
+ updatedAt: new Date()
250
+ };
251
+
252
+ const result = manager.validate(content);
253
+
254
+ expect(result.valid).toBe(false);
255
+ expect(result.errors).toBeDefined();
256
+ });
257
+
258
+ it('should reject negative bitrate', async () => {
259
+ const content: AudioContentType = {
260
+ id: 'audio_555',
261
+ type: 'audio',
262
+ metadata: {
263
+ filename: 'song.mp3',
264
+ mimeType: AudioFormat.MP3,
265
+ size: 1024 * 1024,
266
+ duration: 60,
267
+ bitrate: -128, // Invalid
268
+ title: 'Test Song'
269
+ },
270
+ createdAt: new Date(),
271
+ updatedAt: new Date()
272
+ };
273
+
274
+ const result = manager.validate(content);
275
+
276
+ expect(result.valid).toBe(false);
277
+ expect(result.errors).toBeDefined();
278
+ });
279
+
280
+ it('should reject invalid year range', () => {
281
+ const content: AudioContentType = {
282
+ id: 'audio_444',
283
+ type: 'audio',
284
+ metadata: {
285
+ filename: 'song.mp3',
286
+ mimeType: AudioFormat.MP3,
287
+ size: 1024 * 1024,
288
+ duration: 60,
289
+ bitrate: 128,
290
+ title: 'Test Song',
291
+ year: 3000 // Too far in future
292
+ },
293
+ createdAt: new Date(),
294
+ updatedAt: new Date()
295
+ };
296
+
297
+ const result = manager.validate(content);
298
+
299
+ expect(result.valid).toBe(false);
300
+ expect(result.errors).toBeDefined();
301
+ });
302
+
303
+ it('should handle validation with allowed formats config', () => {
304
+ const manager = new AudioContentTypeManager({
305
+ allowedFormats: [AudioFormat.MP3, AudioFormat.WAV]
306
+ });
307
+
308
+ const content: AudioContentType = {
309
+ id: 'audio_333',
310
+ type: 'audio',
311
+ metadata: {
312
+ filename: 'song.mp3',
313
+ mimeType: AudioFormat.MP3,
314
+ size: 1024 * 1024,
315
+ duration: 60,
316
+ bitrate: 128
317
+ },
318
+ createdAt: new Date(),
319
+ updatedAt: new Date()
320
+ };
321
+
322
+ const result = manager.validate(content);
323
+ expect(result.valid).toBe(true);
324
+ });
325
+
326
+ it('should handle validation without maxFileSize config', () => {
327
+ const manager = new AudioContentTypeManager({
328
+ maxFileSize: undefined
329
+ });
330
+
331
+ const content: AudioContentType = {
332
+ id: 'audio_222',
333
+ type: 'audio',
334
+ metadata: {
335
+ filename: 'huge.mp3',
336
+ mimeType: AudioFormat.MP3,
337
+ size: 500 * 1024 * 1024, // 500MB
338
+ duration: 3600,
339
+ bitrate: 320
340
+ },
341
+ createdAt: new Date(),
342
+ updatedAt: new Date()
343
+ };
344
+
345
+ const result = manager.validate(content);
346
+ expect(result.valid).toBe(true);
347
+ });
348
+
349
+ it('should handle validation without allowedFormats config', () => {
350
+ const manager = new AudioContentTypeManager({
351
+ allowedFormats: undefined
352
+ });
353
+
354
+ const content: AudioContentType = {
355
+ id: 'audio_111',
356
+ type: 'audio',
357
+ metadata: {
358
+ filename: 'song.flac',
359
+ mimeType: AudioFormat.FLAC,
360
+ size: 1024 * 1024,
361
+ duration: 60,
362
+ bitrate: 1411
363
+ },
364
+ createdAt: new Date(),
365
+ updatedAt: new Date()
366
+ };
367
+
368
+ const result = manager.validate(content);
369
+ expect(result.valid).toBe(true);
370
+ });
371
+
372
+ it('should handle validation catching non-Zod errors', () => {
373
+ const manager = new AudioContentTypeManager();
374
+
375
+ // Trigger an error by passing something that causes a non-ZodError
376
+ const result = manager.validate(undefined);
377
+
378
+ expect(result.valid).toBe(false);
379
+ expect(result.errors).toBeDefined();
380
+ });
381
+ });
382
+
383
+ describe('create', () => {
384
+ it('should create audio content with metadata', () => {
385
+ const metadata: AudioMetadata = {
386
+ filename: 'test.mp3',
387
+ mimeType: AudioFormat.MP3,
388
+ size: 1024 * 1024,
389
+ duration: 60,
390
+ bitrate: 128,
391
+ title: 'Test Audio'
392
+ };
393
+
394
+ const content = manager.create(metadata, {
395
+ filePath: '/path/to/test.mp3',
396
+ streamingUrl: 'https://cdn.example.com/test.mp3'
397
+ });
398
+
399
+ expect(content.type).toBe('audio');
400
+ expect(content.metadata.title).toBe('Test Audio');
401
+ expect(content.filePath).toBe('/path/to/test.mp3');
402
+ expect(content.streamingUrl).toBe('https://cdn.example.com/test.mp3');
403
+ expect(content.id).toMatch(/^audio_/);
404
+ expect(content.createdAt).toBeInstanceOf(Date);
405
+ expect(content.updatedAt).toBeInstanceOf(Date);
406
+ });
407
+
408
+ it('should create audio content without optional fields', () => {
409
+ const metadata: AudioMetadata = {
410
+ filename: 'test.mp3',
411
+ mimeType: AudioFormat.MP3,
412
+ size: 1024 * 1024,
413
+ duration: 60,
414
+ bitrate: 128
415
+ };
416
+
417
+ const content = manager.create(metadata);
418
+
419
+ expect(content.type).toBe('audio');
420
+ expect(content.filePath).toBeUndefined();
421
+ expect(content.fileUrl).toBeUndefined();
422
+ expect(content.streamingUrl).toBeUndefined();
423
+ });
424
+
425
+ it('should generate unique IDs', () => {
426
+ const metadata: AudioMetadata = {
427
+ filename: 'test.mp3',
428
+ mimeType: AudioFormat.MP3,
429
+ size: 1024 * 1024,
430
+ duration: 60,
431
+ bitrate: 128
432
+ };
433
+
434
+ const content1 = manager.create(metadata);
435
+ const content2 = manager.create(metadata);
436
+
437
+ expect(content1.id).not.toBe(content2.id);
438
+ });
439
+ });
440
+
441
+ describe('utility methods', () => {
442
+ it('should parse duration', () => {
443
+ expect(manager.parseDuration('1:23:45')).toBe(5025);
444
+ expect(manager.parseDuration('23:45')).toBe(1425);
445
+ expect(manager.parseDuration('123')).toBe(123);
446
+ expect(manager.parseDuration(180)).toBe(180);
447
+ });
448
+
449
+ it('should format duration', () => {
450
+ expect(manager.formatDuration(5025)).toBe('1:23:45');
451
+ expect(manager.formatDuration(1425)).toBe('23:45');
452
+ expect(manager.formatDuration(45)).toBe('0:45');
453
+ });
454
+
455
+ it('should calculate bitrate', () => {
456
+ const fileSize = 5 * 1024 * 1024;
457
+ const duration = 180;
458
+ const bitrate = manager.calculateBitrate(fileSize, duration);
459
+
460
+ expect(bitrate).toBeGreaterThan(0);
461
+ expect(bitrate).toBeCloseTo(233, 0);
462
+ });
463
+ });
464
+
465
+ describe('validateAudio wrapper', () => {
466
+ it('should return success result for valid content', () => {
467
+ const content: AudioContentType = {
468
+ id: 'audio_123',
469
+ type: 'audio',
470
+ metadata: {
471
+ filename: 'song.mp3',
472
+ mimeType: AudioFormat.MP3,
473
+ size: 5 * 1024 * 1024,
474
+ duration: 180,
475
+ bitrate: 192,
476
+ title: 'Test Song'
477
+ },
478
+ createdAt: new Date(),
479
+ updatedAt: new Date()
480
+ };
481
+
482
+ const result = validateAudio(manager, content);
483
+
484
+ expect(result.success).toBe(true);
485
+ expect(result.data).toBeDefined();
486
+ expect(result.data?.valid).toBe(true);
487
+ expect(result.error).toBeUndefined();
488
+ });
489
+
490
+ it('should return error result for invalid content with error message', () => {
491
+ const invalidContent = {
492
+ id: 'audio_999',
493
+ type: 'invalid'
494
+ };
495
+
496
+ const result = validateAudio(manager, invalidContent);
497
+
498
+ expect(result.success).toBe(false);
499
+ expect(result.data).toBeDefined();
500
+ expect(result.data?.valid).toBe(false);
501
+ expect(result.error).toBeDefined();
502
+ expect(result.error).toContain('type');
503
+ });
504
+
505
+ it('should return error result for content with validation errors but no error array', () => {
506
+ const strictManager = new AudioContentTypeManager({
507
+ maxFileSize: 1024,
508
+ requireMetadata: true
509
+ });
510
+
511
+ const content = strictManager.create({
512
+ filename: 'test.mp3',
513
+ mimeType: AudioFormat.MP3,
514
+ size: 2048, // Exceeds limit
515
+ duration: 60,
516
+ bitrate: 128
517
+ // Missing title
518
+ });
519
+
520
+ const result = validateAudio(strictManager, content);
521
+
522
+ expect(result.success).toBe(false);
523
+ expect(result.data).toBeDefined();
524
+ expect(result.data?.valid).toBe(false);
525
+ });
526
+
527
+ it('should handle exceptions gracefully', () => {
528
+ const result = validateAudio(manager, null);
529
+
530
+ expect(result.success).toBe(false);
531
+ expect(result.error).toBeDefined();
532
+ });
533
+ });
534
+
535
+ describe('edge cases', () => {
536
+ it('should handle empty strings in parseDuration', () => {
537
+ expect(manager.parseDuration('')).toBe(0);
538
+ });
539
+
540
+ it('should handle zero bitrate calculation', () => {
541
+ expect(manager.calculateBitrate(1024, 0)).toBe(0);
542
+ });
543
+
544
+ it('should format zero duration', () => {
545
+ expect(manager.formatDuration(0)).toBe('0:00');
546
+ });
547
+
548
+ it('should validate content with no optional URLs', () => {
549
+ const manager = new AudioContentTypeManager();
550
+ const content = manager.create({
551
+ filename: 'test.mp3',
552
+ mimeType: AudioFormat.MP3,
553
+ size: 1024 * 1024,
554
+ duration: 60,
555
+ bitrate: 128
556
+ });
557
+
558
+ expect(content.filePath).toBeUndefined();
559
+ expect(content.fileUrl).toBeUndefined();
560
+ expect(content.streamingUrl).toBeUndefined();
561
+ });
562
+
563
+ it('should validate content with all optional fields', () => {
564
+ const manager = new AudioContentTypeManager();
565
+ const content = manager.create(
566
+ {
567
+ filename: 'test.mp3',
568
+ mimeType: AudioFormat.MP3,
569
+ size: 1024 * 1024,
570
+ duration: 60,
571
+ bitrate: 128,
572
+ sampleRate: 44100,
573
+ channels: 2,
574
+ title: 'Test',
575
+ artist: 'Artist',
576
+ album: 'Album',
577
+ year: 2024,
578
+ coverArt: 'https://example.com/cover.jpg'
579
+ },
580
+ {
581
+ filePath: '/path/to/file',
582
+ fileUrl: 'https://example.com/file',
583
+ streamingUrl: 'https://stream.example.com/file'
584
+ }
585
+ );
586
+
587
+ expect(content.filePath).toBe('/path/to/file');
588
+ expect(content.fileUrl).toBe('https://example.com/file');
589
+ expect(content.streamingUrl).toBe('https://stream.example.com/file');
590
+ });
591
+ });
592
+ });