From f64b2967499b016564d654efb4fb495df3079d1a Mon Sep 17 00:00:00 2001 From: Erik Straub Date: Wed, 30 Nov 2016 14:33:08 -0500 Subject: [PATCH] Adds schedule checking --- cli.js | 6 +- index.js | 5 + lib/client.js | 200 ++++++++++++------ lib/fetch-brightcove-playlist.js | 51 ++--- lib/fetch-brightcove-video.js | 12 +- package.json | 1 + test/create-playlist-handler-test.js | 7 +- test/create-video-handler-test.js | 121 ++++++++++- .../get-video-scheduled-response.json | 70 ++++++ 9 files changed, 373 insertions(+), 100 deletions(-) create mode 100644 test/fixtures/get-video-scheduled-response.json diff --git a/cli.js b/cli.js index 2ebd03f..12afee5 100644 --- a/cli.js +++ b/cli.js @@ -10,11 +10,11 @@ REQUEST_METHODS.getAccessToken = '{}'; REQUEST_METHODS.getPlaylistCount = '{"query": "OBJECT"}'; REQUEST_METHODS.getPlaylists = '{"query": "OBJECT"}'; REQUEST_METHODS.getPlaylist = '{"playlistId": "STRING"}'; -REQUEST_METHODS.getVideosByPlaylist = '{"playlistId": "STRING"}'; +REQUEST_METHODS.getVideosByPlaylist = '{"playlistId": "STRING", "skipScheduleCheck": "BOOLEAN"}'; REQUEST_METHODS.getVideoCountByPlaylist = '{"playlistId": "STRING"}'; REQUEST_METHODS.getVideoCount = '{"query": "OBJECT"}'; -REQUEST_METHODS.getVideos = '{"query": "OBJECT"}'; -REQUEST_METHODS.getVideo = '{"videoId": "STRING"}'; +REQUEST_METHODS.getVideos = '{"query": "OBJECT", "skipScheduleCheck": "BOOLEAN"}'; +REQUEST_METHODS.getVideo = '{"videoId": "STRING", "skipScheduleCheck": "BOOLEAN"}'; REQUEST_METHODS.getVideoSources = '{"videoId": "STRING"}'; const listCommand = () => { diff --git a/index.js b/index.js index 4999cbd..a2bb693 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ 'use strict'; const Promise = require('bluebird'); +const debug = require('debug')('oddworks:provider:brightcove'); const Client = require('./lib/client'); const defaultVideoTransform = require('./lib/default-video-transform'); const defaultCollectionTransform = require('./lib/default-collection-transform'); @@ -20,6 +21,7 @@ const DEFAULTS = { // options.collectionTransform // options.videoTransform exports.initialize = options => { + debug('initialize'); options = Object.assign({}, DEFAULTS, options || {}); const bus = options.bus; @@ -57,6 +59,7 @@ exports.initialize = options => { }; exports.createPlaylistHandler = (bus, getChannel, client, transform) => { + debug('createPlaylistHandler'); const getCollection = fetchBrightcovePlaylist(bus, client, transform); // Called from Oddworks core via bus.query @@ -83,6 +86,7 @@ exports.createPlaylistHandler = (bus, getChannel, client, transform) => { }; exports.createVideoHandler = (bus, getChannel, client, transform) => { + debug('createVideoHandler'); const getVideo = fetchBrightcoveVideo(bus, client, transform); // Called from Oddworks core via bus.query @@ -111,6 +115,7 @@ exports.createVideoHandler = (bus, getChannel, client, transform) => { // options.accountId *required // options.bus *optional exports.createClient = options => { + debug('createClient'); options = Object.assign({}, DEFAULTS, options || {}); const bus = options.bus; diff --git a/lib/client.js b/lib/client.js index 9b2a35d..7bd235c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,6 +1,7 @@ 'use strict'; const Promise = require('bluebird'); +const _ = require('lodash'); const Boom = require('boom'); const request = require('request'); const taskQueue = require('promise-task-queue'); @@ -47,15 +48,17 @@ class Client { // args.clientSecret *required // args.accountId *required // args.concurrentRequestLimit *optional + // args.skipScheduleCheck *optional constructor(args) { this.bus = args.bus || null; this.clientId = args.clientId; this.clientSecret = args.clientSecret; this.accountId = args.accountId; + this.skipScheduleCheck = _.get(args, 'skipScheduleCheck', false); this.concurrentRequestLimit = parseInt(args.concurrentRequestLimit, 10) || CONCURRENT_REQUEST_LIMIT; - if (args.concurrentRequestLimit === 'NaN') { + if (!_.isNumber(this.concurrentRequestLimit)) { throw new Error('Client requires concurrentRequestLimit to be a Number'); } @@ -100,21 +103,21 @@ class Client { args = args || {}; const accessToken = args.accessToken; - if (accessToken && typeof accessToken === 'string') { + if (_.isString(accessToken)) { return Promise.resolve({ access_token: accessToken // eslint-disable-line camelcase }); } - const clientId = args.clientId || this.clientId; - const clientSecret = args.clientSecret || this.clientSecret; + const clientId = _.get(args, 'clientId', this.clientId); + const clientSecret = _.get(args, 'clientSecret', this.clientSecret); - if (!clientId || typeof clientId !== 'string') { - throw new Error('A clientId is required to getAccessToken()'); + if (!_.isString(clientId)) { + throw new Error('A clientId string is required for getAccessToken()'); } - if (!clientSecret || typeof clientSecret !== 'string') { - throw new Error('A clientSecret is required to getAccessToken()'); + if (!_.isString(clientSecret)) { + throw new Error('A clientSecret string is required for getAccessToken()'); } const params = { @@ -137,8 +140,8 @@ class Client { args = args || {}; const accountId = args.accountId || this.accountId; - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getPlaylistCount()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getPlaylistCount()'); } return this.getAccessToken(args).then(res => { @@ -159,10 +162,10 @@ class Client { // args.query *optional - See: https://docs.brightcove.com/en/video-cloud/cms-api/references/cms-api/versions/v1/index.html#api-playlistGroup-Get_Playlists getPlaylists(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getPlaylists()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getPlaylists()'); } return this.getAccessToken(args).then(auth => { @@ -184,15 +187,15 @@ class Client { // playlist IDs separated by commas getPlaylist(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); const playlistId = args.playlistId; - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getPlaylist()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getPlaylist()'); } - if (!playlistId || typeof playlistId !== 'string') { - throw new Error('An playlistId is required to getPlaylist()'); + if (!_.isString(playlistId)) { + throw new Error('A playlistId string is required for getPlaylist()'); } return this.getAccessToken(args).then(auth => { @@ -211,17 +214,19 @@ class Client { // args.accountId *required // args.playlistId *required + // args.skipScheduleCheck *optional getVideosByPlaylist(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); const playlistId = args.playlistId; + const skipScheduleCheck = _.get(args, 'skipScheduleCheck', this.skipScheduleCheck); - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideosByPlaylist()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideosByPlaylist()'); } - if (!playlistId || typeof playlistId !== 'string') { - throw new Error('An playlistId is required to getVideosByPlaylist()'); + if (!_.isString(playlistId)) { + throw new Error('A playlistId string is required for getVideosByPlaylist()'); } return this.getAccessToken(args).then(auth => { @@ -231,10 +236,24 @@ class Client { path: `/accounts/${accountId}/playlists/${playlistId}/videos`, contentType: Client.DEFAULT_CONTENT_TYPE, authorization: this.getBearerAuthorization(auth.access_token), - query: {} + query: Object.assign({}, args.query) }); - return this.makeRequest(args); + return this + .makeRequest(args) + .then(videos => { + if (!_.isEmpty(videos) && !skipScheduleCheck) { + // using the Client.resolveIfScheduled, resolve with only published videos + return Promise.reduce(videos.map(Client.resolveIfScheduled), (published, video) => { + if (video) { + published.push(video); + } + return Promise.resolve(published); + }, []); + } + debug(`not checking schedule for playlist "${playlistId}"`); + return Promise.resolve(videos); + }); }); } @@ -242,15 +261,15 @@ class Client { // args.playlistId *required getVideoCountByPlaylist(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); const playlistId = args.playlistId; - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideoCountByPlaylist()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideoCountByPlaylist()'); } - if (!playlistId || typeof playlistId !== 'string') { - throw new Error('An playlistId is required to getVideoCountByPlaylist()'); + if (!_.isString(playlistId)) { + throw new Error('A playlistId string is required for getVideoCountByPlaylist()'); } return this.getAccessToken(args).then(auth => { @@ -271,10 +290,10 @@ class Client { // args.query *optional - See: https://docs.brightcove.com/en/video-cloud/cms-api/references/cms-api/versions/v1/index.html#api-videoGroup-Get_Video_Count getVideoCount(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideoCount()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideoCount()'); } return this.getAccessToken(args).then(auth => { @@ -284,7 +303,7 @@ class Client { path: `/accounts/${accountId}/counts/videos`, contentType: Client.DEFAULT_CONTENT_TYPE, authorization: this.getBearerAuthorization(auth.access_token), - query: {} + query: Object.assign({}, args.query) }); return this.makeRequest(args); @@ -293,12 +312,14 @@ class Client { // args.accountId *required // args.query *optional - See: https://docs.brightcove.com/en/video-cloud/cms-api/references/cms-api/versions/v1/index.html#api-videoGroup-Get_Videos + // args.skipScheduleCheck *optional getVideos(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); + const skipScheduleCheck = _.get(args, 'skipScheduleCheck', this.skipScheduleCheck); - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideos()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideos()'); } return this.getAccessToken(args).then(auth => { @@ -311,7 +332,22 @@ class Client { query: Object.assign({}, args.query) }); - return this.makeRequest(args); + return this + .makeRequest(args) + .then(videos => { + if (!_.isEmpty(videos) && !skipScheduleCheck) { + // using the Client.resolveIfScheduled, resolve with only published videos + return Promise.reduce(videos.map(Client.resolveIfScheduled), (published, video) => { + if (video) { + published.push(video); + } + return Promise.resolve(published); + }, []); + } + + debug('not checking schedule for videos'); + return Promise.resolve(videos); + }); }); } @@ -319,17 +355,18 @@ class Client { // args.videoId *required - Can be a Video Cloud video ID, multiple IDs // separated by commas, or a single reference // ID (ref:reference_id). See: https://docs.brightcove.com/en/video-cloud/cms-api/references/cms-api/versions/v1/index.html#api-videoGroup-Get_Video_by_ID_or_Reference_ID + // args.skipScheduleCheck *optional getVideo(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); const videoId = args.videoId; - - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideo()'); + const skipScheduleCheck = _.get(args, 'skipScheduleCheck', this.skipScheduleCheck); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideo()'); } - if (!videoId || typeof videoId !== 'string') { - throw new Error('A videoId is required to getVideo()'); + if (!_.isString(videoId)) { + throw new Error('A videoId string is required for getVideo()'); } return this.getAccessToken(args).then(auth => { @@ -342,7 +379,17 @@ class Client { query: {} }); - return this.makeRequest(args); + return this + .makeRequest(args) + .then(video => { + debug(`video "${videoId}" exists: ${Boolean(video)} skipScheduleCheck: ${skipScheduleCheck}`); + if (video && !skipScheduleCheck) { + // using the Client.resolveIfScheduled, resolve with only published videos + return Client.resolveIfScheduled(video); + } + + return Promise.resolve(video); + }); }); } @@ -351,15 +398,15 @@ class Client { // ID (ref:reference_id). See: https://docs.brightcove.com/en/video-cloud/cms-api/references/cms-api/versions/v1/index.html#api-videoGroup-Get_Video_Sources getVideoSources(args) { args = args || {}; - const accountId = args.accountId || this.accountId; + const accountId = _.get(args, 'accountId', this.accountId); const videoId = args.videoId; - if (!accountId || typeof accountId !== 'string') { - throw new Error('An accountId is required to getVideo()'); + if (!_.isString(accountId)) { + throw new Error('An accountId string is required for getVideo()'); } - if (!videoId || typeof videoId !== 'string') { - throw new Error('A videoId is required to getVideo()'); + if (!_.isString(videoId)) { + throw new Error('A videoId string is required for getVideo()'); } return this.getAccessToken(args).then(auth => { @@ -378,21 +425,21 @@ class Client { makeRequest(args) { args = args || {}; - const method = args.method || 'GET'; + const method = _.get(args, 'method', 'GET'); const path = args.path; - const baseUrl = args.baseUrl || Client.CMS_API_BASE_URL; + const baseUrl = _.get(args, 'baseUrl', Client.CMS_API_BASE_URL); - const contentType = args.contentType || Client.DEFAULT_CONTENT_TYPE; + const contentType = _.get(args, 'contentType', Client.DEFAULT_CONTENT_TYPE); const authorization = args.authorization; let body = ''; - if (method === 'POST' && args.body) { + if (method === 'POST' && _.has(args, 'body')) { body = Object.assign({}, args.body); body = JSON.stringify(body); } - if (!authorization || typeof authorization !== 'string') { - throw new Error('An authorization header is required to makeRequest()'); + if (!_.isString(authorization)) { + throw new Error('An authorization string is required for makeRequest()'); } const headers = { @@ -402,8 +449,6 @@ class Client { const qs = Object.assign({}, args.query); const url = `${baseUrl}${path}`; - debug(`makeRequest method: ${method} url: ${url} qs: ${JSON.stringify(qs)}`); - return this._queue.push('request', {method, url, qs, headers, body}); } @@ -427,48 +472,73 @@ class Client { return 'application/json'; } + static resolveIfScheduled(video, date) { + if (_.has(video, 'schedule') && + !_.isNull(video.schedule) && + !_.isUndefined(video.schedule)) { + const now = date || _.now(); + const startsAt = Date.parse(_.get(video, 'schedule.starts_at')); + const endsAt = Date.parse(_.get(video, 'schedule.ends_at')); + + if ((_.isNumber(endsAt) && _.isNumber(startsAt)) && _.inRange(now, startsAt, endsAt + 1)) { + // video is not scheduled yet + debug(`video "${video.id} is not scheduled: ${JSON.stringify(video.schedule)}`); + return Promise.resolve(null); + } else if (_.isNumber(startsAt) && now <= startsAt) { + // video is not scheduled yet + debug(`video "${video.id} is not scheduled: ${JSON.stringify(video.schedule)}`); + return Promise.resolve(null); + } + + return Promise.resolve(video); + } + + return Promise.resolve(video); + } + static request(params) { return new Promise((resolve, reject) => { request(params, (err, res, body) => { if (err) { - debug(`Client.request error: ${err}`); + debug(`ERROR ${params.method} ${params.url} qs:${JSON.stringify(params.qs)} error: ${err}`); return reject(err); } if (res.statusCode === 404) { - debug(`Client.request status: 404`); + debug(`404 ${params.method} ${params.url} qs:${JSON.stringify(params.qs)}`); return resolve(null); } if (!Client.STATUS_CODE_20X_MATCHER.test(res.statusCode)) { - debug(`Client.request status: ${res.statusCode} body: ${body}`); + debug(`${res.statusCode} ${params.method} ${params.url} qs:${JSON.stringify(params.qs)} body: ${body}`); return reject(Boom.create(res.statusCode, res.statusMessage, body)); } else if (res.statusCode === 204) { return resolve({}); } const isJson = Client.CONTENT_TYPE_MATCHER.test(res.headers['content-type']); - if (isJson && typeof body === 'string') { + if (isJson && _.isString(body)) { try { body = JSON.parse(body); } catch (err) { - debug(`Client.request error: JSON parsing error message: ${err.message}`); + debug(`${res.statusCode} ${params.method} ${params.url} qs:${JSON.stringify(params.qs)} error: JSON parsing error message: ${err.message}`); return reject(new Error( `brightcove client JSON parsing error ${err.message}` )); } } else if (isJson) { - debug(`Client.request error: received empty JSON body`); + debug(`${res.statusCode} ${params.method} ${params.url} qs:${JSON.stringify(params.qs)} error: received empty JSON body`); return reject(new Error( `brightcove client received an empty JSON body` )); } else { - debug(`Client.request error: expects content-type to be application/json`); + debug(`${res.statusCode} ${params.method} ${params.url} qs:${JSON.stringify(params.qs)} error: expects content-type to be application/json`); return reject(new Error( `brightcove client expects content-type to be application/json` )); } + debug(`${res.statusCode} ${params.method} ${params.url} qs:${JSON.stringify(params.qs)}`); return resolve(body); }); }); diff --git a/lib/fetch-brightcove-playlist.js b/lib/fetch-brightcove-playlist.js index 6e761da..0307a8b 100644 --- a/lib/fetch-brightcove-playlist.js +++ b/lib/fetch-brightcove-playlist.js @@ -1,6 +1,7 @@ 'use strict'; const Promise = require('bluebird'); +const _ = require('lodash'); const debug = require('debug')('oddworks:provider:brightcove:fetch-brightcove-playlist'); module.exports = (bus, client, transform) => { @@ -10,18 +11,19 @@ module.exports = (bus, client, transform) => { const spec = args.spec; let collection = args.collection; const playlistId = args.playlistId; + const skipScheduleCheck = Boolean(_.get(args, 'skipScheduleCheck')); // allow override of existing provider creds when secrets change const creds = Object.create(null); - if (secrets.brightcove && secrets.brightcove.clientId) { + if (_.has(secrets, 'brightcove.clientId')) { creds.clientId = secrets.brightcove.clientId; } - if (secrets.brightcove && secrets.brightcove.clientSecret) { + if (_.has(secrets, 'brightcove.clientSecret')) { creds.clientSecret = secrets.brightcove.clientSecret; } - if (secrets.brightcove && secrets.brightcove.accountId) { + if (_.has(secrets, 'brightcove.accountId')) { creds.accountId = secrets.brightcove.accountId; } @@ -33,7 +35,7 @@ module.exports = (bus, client, transform) => { if (playlist) { collection = Object.assign({}, collection, transform(spec, playlist)); - return client.getVideosByPlaylist(Object.assign({playlistId}, creds)); + return client.getVideosByPlaylist(Object.assign({playlistId, skipScheduleCheck}, creds)); // NOTE We may need to filter out unfinished videos using `video.state === 'ACTIVE' && video.complete === true` } @@ -52,27 +54,28 @@ module.exports = (bus, client, transform) => { return Promise.reject(error); }) .then(videos => { - if (videos && videos.length) { + if (!_.isEmpty(videos)) { return Promise.all( - videos.sort((a, b) => { - // sort newest to oldest - const aDate = new Date(a.published_at); - const bDate = new Date(b.published_at); - return bDate - aDate; - }).map(video => { - const spec = { - channel: channel.id, - type: 'videoSpec', - source: 'brightcove-video', - video - }; - - if (video.id) { - spec.id = `spec-brightcove-video-${video.id}`; - } - - return bus.sendCommand({role: 'catalog', cmd: 'setItemSpec'}, spec); - }) + videos + .sort((a, b) => { + // sort newest to oldest + const aDate = new Date(a.published_at); + const bDate = new Date(b.published_at); + return bDate - aDate; + }).map(video => { + const spec = { + channel: channel.id, + type: 'videoSpec', + source: 'brightcove-video', + video + }; + + if (video.id) { + spec.id = `spec-brightcove-video-${video.id}`; + } + + return bus.sendCommand({role: 'catalog', cmd: 'setItemSpec'}, spec); + }) ); } diff --git a/lib/fetch-brightcove-video.js b/lib/fetch-brightcove-video.js index 7ef4f18..b2817cf 100644 --- a/lib/fetch-brightcove-video.js +++ b/lib/fetch-brightcove-video.js @@ -1,29 +1,33 @@ 'use strict'; const Promise = require('bluebird'); +const _ = require('lodash'); +const debug = require('debug')('oddworks:provider:brightcove:fetch-brightcove-video'); module.exports = (bus, client, transform) => { return args => { + debug('fetchBrightcoveVideo'); const channel = args.channel; const secrets = channel.secrets || {}; const spec = args.spec; const videoId = args.videoId; + const skipScheduleCheck = Boolean(_.get(spec, 'skipScheduleCheck')); // allow override of existing provider creds when secrets change const creds = Object.create(null); - if (secrets.brightcove && secrets.brightcove.clientId) { + if (_.has(secrets, 'brightcove.clientId')) { creds.clientId = secrets.brightcove.clientId; } - if (secrets.brightcove && secrets.brightcove.clientSecret) { + if (_.has(secrets, 'brightcove.clientSecret')) { creds.clientSecret = secrets.brightcove.clientSecret; } - if (secrets.brightcove && secrets.brightcove.accountId) { + if (_.has(secrets, 'brightcove.accountId')) { creds.accountId = secrets.brightcove.accountId; } - const params = Object.assign({videoId}, creds); + const params = Object.assign({videoId, skipScheduleCheck}, creds); return client.getVideo(params) .then(video => { if (video) { diff --git a/package.json b/package.json index c6e2103..d3e7f30 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "bluebird": "^3.4.6", "boom": "^4.0.0", "debug": "^2.2.0", + "lodash": "^4.17.2", "promise-task-queue": "^1.2.0", "request": "^2.75.0", "yargs": "^5.0.0" diff --git a/test/create-playlist-handler-test.js b/test/create-playlist-handler-test.js index 68cbc60..3162be8 100644 --- a/test/create-playlist-handler-test.js +++ b/test/create-playlist-handler-test.js @@ -147,8 +147,9 @@ test('when Brightcove playlist found', t => { // order by published_at desc (newest to oldest) t.is(res.relationships.entities.data[0].id, 'res-brightcove-video-V222222222222'); - t.is(res.relationships.entities.data[1].id, 'res-brightcove-video-V444444444444'); - t.is(res.relationships.entities.data[2].id, 'res-brightcove-video-V111111111111'); - t.is(res.relationships.entities.data[3].id, 'res-brightcove-video-V333333333333'); + // res-brightcove-video-V444444444444 is not scheduled + // t.is(res.relationships.entities.data[1].id, 'res-brightcove-video-V444444444444'); + t.is(res.relationships.entities.data[1].id, 'res-brightcove-video-V111111111111'); + t.is(res.relationships.entities.data[2].id, 'res-brightcove-video-V333333333333'); }); }); diff --git a/test/create-video-handler-test.js b/test/create-video-handler-test.js index c5e42da..557d342 100644 --- a/test/create-video-handler-test.js +++ b/test/create-video-handler-test.js @@ -3,10 +3,12 @@ const test = require('ava'); const nock = require('nock'); const Promise = require('bluebird'); +// const debug = require('debug')('oddworks:provider:brightcove:create-video-handler-test'); const provider = require('../'); const videoTransform = require('../lib/default-video-transform'); const videoResponse = require('./fixtures/get-video-response'); +const videoScheduledResponse = require('./fixtures/get-video-scheduled-response'); const videoSourcesResponse = require('./fixtures/get-video-sources-response'); const accessTokenResponse = require('./fixtures/get-access-token-response'); const helpers = require('./helpers'); @@ -48,9 +50,20 @@ test.before(() => { } }) .post('/access_token?grant_type=client_credentials') - .times(3) // this gets called before most client.get* functions + .times(6) // this gets called before most client.get* functions .reply(200, accessTokenResponse); + nock( + 'https://cms.api.brightcove.com/v1', + { + reqheaders: { + authorization: cmsAuthHeader + } + }) + .get(`/accounts/${accountId}/videos/${videoScheduledResponse.id}`) + .times(2) + .reply(200, videoScheduledResponse); + nock( 'https://cms.api.brightcove.com/v1', { @@ -71,6 +84,16 @@ test.before(() => { .get(`/accounts/${accountId}/videos/${videoResponse.id}/sources`) .reply(200, videoSourcesResponse); + nock( + 'https://cms.api.brightcove.com/v1', + { + reqheaders: { + authorization: cmsAuthHeader + } + }) + .get(`/accounts/${accountId}/videos/${videoScheduledResponse.id}/sources`) + .reply(200, videoSourcesResponse); + nock( 'https://cms.api.brightcove.com/v1', { @@ -123,6 +146,102 @@ test('when Brightcove video not found', t => { }); }); +test('when Brightcove video not scheduled', t => { + const spec = { + channel, + type, + id: `spec-brightcove-video-${videoScheduledResponse.id}`, + video: {id: videoScheduledResponse.id} + }; + + const obs = new Promise(resolve => { + bus.observe({level: 'error'}, payload => { + resolve(payload); + }); + }); + + return videoHandler({spec}).catch(err => { + return obs.then(event => { + // test bus event + t.deepEqual(event.error, {code: 'VIDEO_NOT_FOUND'}); + t.is(event.code, 'VIDEO_NOT_FOUND'); + t.deepEqual(event.spec, spec); + t.is(event.message, 'video not found'); + + // test video handler rejection + t.is(err.message, `Video not found for id "${spec.video.id}"`); + }); + }); +}); + +test('when Brihgtcove video not scheduled, but skipScheduleCheck === true', t => { + const spec = { + channel, + type, + id: `spec-brightcove-video-${videoScheduledResponse.id}`, + video: {id: videoScheduledResponse.id}, + skipScheduleCheck: true + }; + + return videoHandler({spec}) + .then(res => { + const source1 = res.sources[0]; + const source4 = res.sources[3]; + + t.deepEqual(Object.keys(res), [ + 'id', + 'title', + 'description', + 'images', + 'sources', + 'duration', + 'releaseDate' + ]); + + t.is(res.id, `res-brightcove-video-${videoScheduledResponse.id}`); + t.is(res.title, videoScheduledResponse.name); + t.is(res.description, videoScheduledResponse.long_description); + + const poster = videoScheduledResponse.images.poster.sources[1]; + const thumbnail = videoScheduledResponse.images.thumbnail.sources[1]; + t.is(res.images.length, 2); + t.is(res.images[0].url, poster.src); + t.is(res.images[1].url, thumbnail.src); + t.is(res.images[1].height, thumbnail.height); + t.is(res.images[1].width, thumbnail.width); + t.is(res.images[1].label, `thumbnail-${thumbnail.width}x${thumbnail.height}`); + + t.is(res.sources.length, 4); + + // sources (first MP4 with https) + const responseSourceMP4 = videoSourcesResponse.filter(source => { + return typeof source.src !== 'undefined' && source.src.match(/^https/); + }).shift(); + const responseSourceHLS = videoSourcesResponse.filter(source => { + return (typeof source.src !== 'undefined' && typeof source.type !== 'undefined') && source.src.match(/^https/) && source.type.match(/^application\/x-mpegURL/); + }).pop(); + + t.is(source1.url, responseSourceMP4.src); + t.is(source1.label, `mp4-${responseSourceMP4.width}x${responseSourceMP4.height}`); + t.is(source1.mimeType, 'video/mp4'); + t.is(source1.width, responseSourceMP4.width); + t.is(source1.height, responseSourceMP4.height); + t.is(source1.container, responseSourceMP4.container); + t.is(source1.maxBitrate, responseSourceMP4.encoding_rate); + // sources (HLS with https) + t.is(source4.url, responseSourceHLS.src); + t.is(source4.label, 'hls'); + t.is(source4.mimeType, responseSourceHLS.type); + t.is(source4.width, 0); + t.is(source4.height, 0); + t.is(source4.container, responseSourceHLS.container); + t.is(source4.maxBitrate, 0); + + t.is(res.duration, videoScheduledResponse.duration); + t.is(res.releaseDate, videoScheduledResponse.published_at); + }); +}); + test('when Brightcove video found', t => { const spec = { channel, diff --git a/test/fixtures/get-video-scheduled-response.json b/test/fixtures/get-video-scheduled-response.json new file mode 100644 index 0000000..c14deb4 --- /dev/null +++ b/test/fixtures/get-video-scheduled-response.json @@ -0,0 +1,70 @@ +{ + "id": "S11111111111", + "account_id": "A111111111111", + "ad_keys": null, + "complete": true, + "created_at": "2016-07-15T13:55:51.101Z", + "cue_points": [], + "custom_fields": {}, + "delivery_type": "static_origin", + "description": null, + "digital_master_id": "5037062135001", + "duration": 31593, + "economics": "AD_SUPPORTED", + "folder_id": null, + "geo": null, + "has_digital_master": true, + "images": { + "thumbnail": { + "asset_id": "AST1111111111", + "remote": false, + "src": "http://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST1111111111_S11111111111-th.jpg?pubId=A111111111111&videoId=S11111111111", + "sources": [ + { + "src": "http://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST1111111111_S11111111111-th.jpg?pubId=A111111111111&videoId=S11111111111", + "height": 90, + "width": 160 + }, + { + "src": "https://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST1111111111_S11111111111-th.jpg?pubId=A111111111111&videoId=S11111111111", + "height": 90, + "width": 160 + } + ] + }, + "poster": { + "asset_id": "AST2222222222", + "remote": false, + "src": "http://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST2222222222_S11111111111-vs.jpg?pubId=A111111111111&videoId=S11111111111", + "sources": [ + { + "src": "http://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST2222222222_S11111111111-vs.jpg?pubId=A111111111111&videoId=S11111111111", + "height": 360, + "width": 640 + }, + { + "src": "https://placehold.it/720x480?text=/e1/pd/A111111111111/A111111111111_AST2222222222_S11111111111-vs.jpg?pubId=A111111111111&videoId=S11111111111", + "height": 360, + "width": 640 + } + ] + } + }, + "link": null, + "long_description": "Video with only long description", + "name": "Video Name", + "original_filename": "orig-video-name.mov", + "published_at": "2016-07-15T13:55:51.101Z", + "reference_id": null, + "schedule": { + "ends_at": null, + "starts_at": "2116-11-30T17:29:22.000Z" + }, + "sharing": null, + "state": "ACTIVE", + "tags": [ + "sample" + ], + "text_tracks": [], + "updated_at": "2016-09-12T11:29:24.706Z" +}