1 const assert
= require('assert');
2 const { validate
, convert
, lookup
} = require('../lib/custom-media');
3 const http
= require('http');
5 describe('custom-media', () => {
12 thumbnail
: 'https://example.com/thumb.jpg',
15 url
: 'https://example.com/video.mp4',
16 contentType
: 'video/mp4',
23 url
: 'https://example.com/subtitles.vtt',
24 contentType
: 'text/vtt',
25 name
: 'English Subtitles'
31 describe('#validate', () => {
32 it('accepts valid metadata', () => {
36 it('accepts valid metadata with no optional params', () => {
38 delete valid
.thumbnail
;
39 delete valid
.textTracks
;
40 delete valid
.sources
[0].bitrate
;
45 it('rejects missing title', () => {
48 assert
.throws(() => validate(invalid
), /title must be a string/);
51 it('rejects blank title', () => {
54 assert
.throws(() => validate(invalid
), /title must not be blank/);
57 it('rejects non-numeric duration', () => {
58 invalid
.duration
= 'twenty four seconds';
60 assert
.throws(() => validate(invalid
), /duration must be a number/);
63 it('rejects non-finite duration', () => {
64 invalid
.duration
= NaN
;
66 assert
.throws(() => validate(invalid
), /duration must be a non-negative finite number/);
69 it('rejects negative duration', () => {
70 invalid
.duration
= -1;
72 assert
.throws(() => validate(invalid
), /duration must be a non-negative finite number/);
75 it('rejects non-boolean live', () => {
76 invalid
.live
= 'false';
78 assert
.throws(() => validate(invalid
), /live must be a boolean/);
81 it('rejects non-string thumbnail', () => {
82 invalid
.thumbnail
= 1234;
84 assert
.throws(() => validate(invalid
), /thumbnail must be a string/);
87 it('rejects invalid thumbnail URL', () => {
88 invalid
.thumbnail
= 'http://example.com/thumb.jpg';
90 assert
.throws(() => validate(invalid
), /URL protocol must be HTTPS/);
93 it('rejects non-live DASH', () => {
95 invalid
.sources
[0].contentType
= 'application/dash+xml';
98 () => validate(invalid
),
99 /contentType "application\/dash\+xml" requires live: true/
104 describe('#validateSources', () => {
105 it('rejects non-array sources', () => {
106 invalid
.sources
= { a
: 'b' };
108 assert
.throws(() => validate(invalid
), /sources must be a list/);
111 it('rejects empty source list', () => {
112 invalid
.sources
= [];
114 assert
.throws(() => validate(invalid
), /source list must be nonempty/);
117 it('rejects non-string source url', () => {
118 invalid
.sources
[0].url
= 1234;
120 assert
.throws(() => validate(invalid
), /source URL must be a string/);
123 it('rejects invalid source URL', () => {
124 invalid
.sources
[0].url
= 'http://example.com/thumb.jpg';
126 assert
.throws(() => validate(invalid
), /URL protocol must be HTTPS/);
129 it('rejects unacceptable source contentType', () => {
130 invalid
.sources
[0].contentType
= 'rtmp/flv';
132 assert
.throws(() => validate(invalid
), /unacceptable source contentType/);
135 it('rejects unacceptable source quality', () => {
136 invalid
.sources
[0].quality
= 144;
138 assert
.throws(() => validate(invalid
), /unacceptable source quality/);
141 it('rejects non-numeric source bitrate', () => {
142 invalid
.sources
[0].bitrate
= '1000kbps'
144 assert
.throws(() => validate(invalid
), /source bitrate must be a number/);
147 it('rejects non-finite source bitrate', () => {
148 invalid
.sources
[0].bitrate
= Infinity
;
150 assert
.throws(() => validate(invalid
), /source bitrate must be a non-negative finite number/);
153 it('rejects negative source bitrate', () => {
154 invalid
.sources
[0].bitrate
= -1000;
156 assert
.throws(() => validate(invalid
), /source bitrate must be a non-negative finite number/);
160 describe('#validateTextTracks', () => {
161 it('rejects non-array text track list', () => {
162 invalid
.textTracks
= { a
: 'b' };
164 assert
.throws(() => validate(invalid
), /textTracks must be a list/);
167 it('rejects non-string track url', () => {
168 invalid
.textTracks
[0].url
= 1234;
170 assert
.throws(() => validate(invalid
), /text track URL must be a string/);
173 it('rejects invalid track URL', () => {
174 invalid
.textTracks
[0].url
= 'http://example.com/thumb.jpg';
176 assert
.throws(() => validate(invalid
), /URL protocol must be HTTPS/);
179 it('rejects unacceptable track contentType', () => {
180 invalid
.textTracks
[0].contentType
= 'text/plain';
182 assert
.throws(() => validate(invalid
), /unacceptable text track contentType/);
185 it('rejects non-string track name', () => {
186 invalid
.textTracks
[0].name
= 1234;
188 assert
.throws(() => validate(invalid
), /text track name must be a string/);
191 it('rejects blank track name', () => {
192 invalid
.textTracks
[0].name
= '';
194 assert
.throws(() => validate(invalid
), /text track name must be nonempty/);
198 describe('#validateURL', () => {
199 it('rejects non-URLs', () => {
200 invalid
.sources
[0].url
= 'not a url';
202 assert
.throws(() => validate(invalid
), /invalid URL/);
205 it('rejects non-https', () => {
206 invalid
.sources
[0].url
= 'http://example.com/thumb.jpg';
208 assert
.throws(() => validate(invalid
), /URL protocol must be HTTPS/);
211 it('rejects IP addresses', () => {
212 invalid
.sources
[0].url
= 'https://0.0.0.0/thumb.jpg';
214 assert
.throws(() => validate(invalid
), /URL hostname must be a domain name/);
218 describe('#convert', () => {
233 link
: 'https://example.com/video.mp4',
234 contentType
: 'video/mp4',
241 url
: 'https://example.com/subtitles.vtt',
242 contentType
: 'text/vtt',
243 name
: 'English Subtitles'
250 function cleanForComparison(actual
) {
251 actual
= actual
.pack();
253 // Strip out extraneous undefineds
254 for (let key
in actual
.meta
) {
255 if (actual
.meta
[key
] === undefined) delete actual
.meta
[key
];
261 it('converts custom metadata to a CyTube Media object', () => {
262 const media
= convert(id
, valid
);
263 const actual
= cleanForComparison(media
);
265 assert
.deepStrictEqual(actual
, expected
);
268 it('sets duration to 0 if live = true', () => {
270 expected
.duration
= '00:00';
271 expected
.seconds
= 0;
273 const media
= convert(id
, valid
);
274 const actual
= cleanForComparison(media
);
276 assert
.deepStrictEqual(actual
, expected
);
280 describe('#lookup', () => {
285 serveFunc = function (req
, res
) {
286 res
.writeHead(200, { 'Content-Type': 'application/json' });
287 res
.write(JSON
.stringify(valid
, null, 2));
291 server
= http
.createServer((req
, res
) => serveFunc(req
, res
));
292 server
.listen(10111);
296 server
.close(() => done());
299 it('retrieves metadata', () => {
300 function cleanForComparison(actual
) {
301 actual
= actual
.pack();
304 // Strip out extraneous undefineds
305 for (let key
in actual
.meta
) {
306 if (actual
.meta
[key
] === undefined) delete actual
.meta
[key
];
321 link
: 'https://example.com/video.mp4',
322 contentType
: 'video/mp4',
329 url
: 'https://example.com/subtitles.vtt',
330 contentType
: 'text/vtt',
331 name
: 'English Subtitles'
337 return lookup('http://127.0.0.1:10111/').then(result
=> {
338 assert
.deepStrictEqual(cleanForComparison(result
), expected
);
342 it('rejects the wrong content-type', () => {
343 serveFunc
= (req
, res
) => {
344 res
.writeHead(200, { 'Content-Type': 'text/plain' });
345 res
.write(JSON
.stringify(valid
, null, 2));
349 return lookup('http://127.0.0.1:10111/').then(() => {
350 throw new Error('Expected failure due to wrong content-type');
354 'Expected content-type application/json, not text/plain'
359 it('rejects non-200 status codes', () => {
360 serveFunc
= (req
, res
) => {
361 res
.writeHead(404, { 'Content-Type': 'application/json' });
362 res
.write(JSON
.stringify(valid
, null, 2));
366 return lookup('http://127.0.0.1:10111/').then(() => {
367 throw new Error('Expected failure due to 404');
371 'Expected HTTP 200 OK, not 404 Not Found'
376 it('rejects responses >100KB', () => {
377 serveFunc
= (req
, res
) => {
378 res
.writeHead(200, { 'Content-Type': 'application/json' });
379 res
.write(Buffer
.alloc(200 * 1024));
383 return lookup('http://127.0.0.1:10111/').then(() => {
384 throw new Error('Expected failure due to response size');
388 'Response size exceeds 100KB'
393 it('times out', () => {
394 serveFunc
= (req
, res
) => {
395 res
.writeHead(200, { 'Content-Type': 'application/json' });
396 res
.write(JSON
.stringify(valid
, null, 2));
398 setTimeout(() => res
.end(), 100);
401 return lookup('http://127.0.0.1:10111/', { timeout
: 1 }).then(() => {
402 throw new Error('Expected failure due to request timeout');
408 assert
.strictEqual(error
.code
, 'ETIMEDOUT');
412 it('rejects URLs with non-http(s) protocols', () => {
413 return lookup('ftp://127.0.0.1:10111/').then(() => {
414 throw new Error('Expected failure due to unacceptable URL protocol');
418 'Unacceptable protocol "ftp:". Custom metadata must be retrieved'
419 + ' by HTTP or HTTPS'
424 it('rejects invalid URLs', () => {
425 return lookup('not valid').then(() => {
426 throw new Error('Expected failure due to invalid URL');
430 'Invalid URL "not valid"'