diff --git a/bin/configure-fastly.js b/bin/configure-fastly.js index d1a4d89dc24..5dcfbf8398d 100644 --- a/bin/configure-fastly.js +++ b/bin/configure-fastly.js @@ -5,11 +5,17 @@ const languages = require('scratch-l10n').default; const routeJson = require('../src/routes.json'); +/** + * @import {FastlyVclResponseObject} from './lib/fastly-extended'; + */ + +const FASTLY_API_KEY = process.env.FASTLY_API_KEY || ''; const FASTLY_SERVICE_ID = process.env.FASTLY_SERVICE_ID || ''; const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || ''; const RADISH_URL = process.env.RADISH_URL || ''; -const fastly = require('./lib/fastly-extended')(process.env.FASTLY_API_KEY, FASTLY_SERVICE_ID); +const FastlyExtended = require('./lib/fastly-extended'); +const fastly = new FastlyExtended(FASTLY_API_KEY, FASTLY_SERVICE_ID); const extraAppRoutes = [ // Homepage with querystring. @@ -22,9 +28,11 @@ const extraAppRoutes = [ const routeJsonPreProcessed = routeJson.map( route => { if (route.redirect) { - process.stdout.write(`Updating: ${route.redirect} to `); - route.redirect = route.redirect.replace('RADISH_URL', RADISH_URL); - process.stdout.write(`${route.redirect}\n`); + const newRedirect = route.redirect.replace('RADISH_URL', RADISH_URL); + if (newRedirect !== route.redirect) { + console.log(`Updating: ${route.redirect} to ${newRedirect}`); + route.redirect = newRedirect; + } } return route; } @@ -33,6 +41,23 @@ const routes = routeJsonPreProcessed.map( route => defaults({}, {pattern: fastlyConfig.expressPatternToRegex(route.pattern)}, route) ); +/** + * Partitions an array into two arrays based on a predicate. + * Items that pass the predicate are placed in the first array, and those that fail are placed in the second. + * @template T + * @param {T[]} array - The array to partition + * @param {function(T): boolean} predicate - The predicate function to test each item + * @returns {[T[], T[]]} - The partitioned arrays that [pass, fail] the predicate + */ +const partition = (array, predicate) => { + const pass = []; + const fail = []; + array.forEach(item => { + (predicate(item) ? pass : fail).push(item); + }); + return [pass, fail]; +}; + async.auto({ version: function (cb) { fastly.getLatestActiveVersion((err, response) => { @@ -40,7 +65,7 @@ async.auto({ // Validate latest version before continuing if (response.active || response.locked) { fastly.cloneVersion(response.number, (e, resp) => { - if (e) return cb(`Failed to clone latest version: ${e}`); + if (e) return cb(new Error('Failed to clone latest version', {cause: e})); cb(null, resp.number); }); } else { @@ -48,6 +73,87 @@ async.auto({ } }); }, + responseObjects: ['version', function (results, cb) { + fastly.getResponseObjects(results.version, cb); + }], + clean: ['responseObjects', function (results, cb) { + const redirectRoutes = routes.filter(route => route.redirect); + console.log(`Found ${redirectRoutes.length} redirect routes in routes.json`); + + const allResponseObjects = /** @type {FastlyVclResponseObject[]} */ (results.responseObjects); + console.log(`Fastly reports ${allResponseObjects.length} response objects`); + + const keepResponses = redirectRoutes.map( + redirectRoute => fastlyConfig.getResponseNameForRoute(redirectRoute) + ); + const keepConditions = redirectRoutes.map( + redirectRoute => fastlyConfig.getConditionNameForRoute(redirectRoute, 'request') + ); + + // These two don't come from the `routes` file. + // They're hard-coded as later steps in this configuration script. + keepResponses.push('redirects/?tip_bar='); + keepResponses.push('redirects/projects/embed'); + + // Keep some statistics + const keepReasons = {}; + const incrementKeepReason = key => { + keepReasons[key] = (keepReasons[key] || 0) + 1; + }; + + /** + * @param {FastlyVclResponseObject} responseObject - The response object to check + * @returns {boolean} - Whether the response object should be removed + */ + const shouldRemove = responseObject => { + // Fastly provides strings but some of our code uses integers, so allow for both + if (responseObject.status.toString() !== '301') { + // we only want to remove 301 redirects + incrementKeepReason('statusCode'); + return false; + } + // generated redirects have names like "redirects/^/asdf/?$" + if (!responseObject.name.startsWith('redirects/')) { + // name doesn't look like one of our generated redirects + incrementKeepReason('nameShape'); + return false; + } + // generated redirects have conditions like "routes/^/asdf/?$ (request)" + if (!( + responseObject.request_condition.startsWith('routes/') && + responseObject.request_condition.endsWith(' (request)') + )) { + // condition doesn't look like one of our generated redirects + incrementKeepReason('conditionShape'); + return false; + } + if (keepResponses.indexOf(responseObject.name) !== -1) { + // matches a route we'll update later + incrementKeepReason('nameStillRelevant'); + return false; + } + if (keepConditions.indexOf(responseObject.request_condition) !== -1) { + // this should probably never happen...? + incrementKeepReason('conditionStillRelevant'); + return false; + } + + // I guess we don't need to keep this one + return true; + }; + // The keep array isn't really necessary, but it can be nice for debugging and reporting stats. + // If you don't care about that, you could just use `filter()`. + const [remove, keep] = partition(allResponseObjects, shouldRemove); + console.log(`Found ${remove.length} response objects to remove and ${keep.length} to keep`); + console.log('Reasons for keeping response objects:', keepReasons); + async.each(remove, (responseObject, cb2) => { + console.log(`Removing response object with name "${responseObject.name}"`); + fastly.deleteResponseObject(results.version, responseObject.name, cb2); + }, err => { + if (err) return cb(err); + cb(); // success + }); + }], recvCustomVCL: ['version', function (results, cb) { // For all the routes in routes.json, construct a varnish-style regex that matches // on any of those route conditions. @@ -101,7 +207,7 @@ async.auto({ const ttlCondition = fastlyConfig.setResponseTTL(passStatement); fastly.setCustomVCL(results.version, 'fetch-condition', ttlCondition, cb); }], - appRouteRequestConditions: ['version', function (results, cb) { + appRouteRequestConditions: ['version', 'clean', function (results, cb) { const conditions = {}; async.forEachOf(routes, (route, id, cb2) => { const condition = { @@ -183,7 +289,7 @@ async.auto({ cb(null, headers); }); }], - tipbarRedirectHeaders: ['version', function (results, cb) { + tipbarRedirectHeaders: ['version', 'clean', function (results, cb) { async.auto({ requestCondition: function (cb2) { const condition = { @@ -229,7 +335,7 @@ async.auto({ cb(null, redirectResults); }); }], - embedRedirectHeaders: ['version', function (results, cb) { + embedRedirectHeaders: ['version', 'clean', function (results, cb) { async.auto({ requestCondition: function (cb2) { const condition = { @@ -276,14 +382,14 @@ async.auto({ }); }] }, (err, results) => { - if (err) throw new Error(err); + if (err) throw err; if (process.env.FASTLY_ACTIVATE_CHANGES) { fastly.activateVersion(results.version, (e, resp) => { - if (e) throw new Error(e); + if (e) throw e; process.stdout.write(`Successfully configured and activated version ${resp.number}\n`); // purge static-assets using surrogate key fastly.purgeKey(FASTLY_SERVICE_ID, 'static-assets', error => { - if (error) throw new Error(error); + if (error) throw error; process.stdout.write('Purged static assets.\n'); }); }); diff --git a/bin/lib/fastly-extended.js b/bin/lib/fastly-extended.js index 0ff1aacb282..bf429923862 100644 --- a/bin/lib/fastly-extended.js +++ b/bin/lib/fastly-extended.js @@ -1,210 +1,351 @@ const Fastly = require('fastly'); +/** + * The custom `Error` object reported by the Fastly library's implementation of `request` + * includes an HTTP status code when an HTTP failure occurs. + * @typedef {Error & { statusCode?: number }} FastlyError + */ + +/** + * Node-style `(err, res)` callback. Success is reported by `err` being null or undefined. + * @template Terr - The type of the error object. Usually `Error` or a subclass. + * @template Tres - The type of the result object. Use `unknown` (not `any`) to "black box" the results. + * @callback NodeStyleCallback + * @param {Terr?} err - The error object. Set this if an error occurred. + * @param {Tres} [result] - The result of the operation. Might be missing if `err` is set. + * @returns {void} + */ + +/** + * Callback used by FastlyExtended and the Fastly library's implementation of `request` + * @template T - The type of the result object. Use `unknown` (not `any`) to "black box" the results. + * @typedef {NodeStyleCallback} FastlyCallback + */ + +/* ******** Fastly data model ******** */ +// Many of these objects have more fields than shown here. +// See here for details: https://www.fastly.com/documentation/reference/api/ + +/** + * @typedef {Object} FastlyServiceVersion + * @property {number} number Version number + * @property {boolean} active Whether the version is active + * @property {boolean} locked Whether the version is locked + */ + +/** + * @typedef {Object} FastlyVclCondition + * @property {string} name Condition name + * @property {string} statement Condition VCL statement + */ + +/** + * @typedef {Object} FastlyVclHeader + * @property {string} name Header name + * @property {string} action Header action + * @property {string} ignore_if_set If this is '1', the header will be ignored if it is already set + * @property {string} type Header type + * @property {string} dst Header to set + * @property {string} src Variable to be used as a source for header content + * @property {string} response_condition Response condition name + */ + +/** + * @typedef {Object} FastlyVclLintReport + * @property {Array} errors List of linting errors found + * @property {Array} warnings List of linting warnings found + */ + +/** + * @typedef {Object} FastlyVclResponseObject + * @property {string} name Response object name + * @property {string} request_condition Request condition name + * @property {number} status HTTP status code + * @property {string} response Response body + */ + /* * Fastly library extended to allow configuration for a particular service * and some helper methods. - * - * @param {string} API key - * @param {string} Service id */ -module.exports = function (apiKey, serviceId) { - const fastly = Fastly(apiKey); - fastly.serviceId = serviceId; +class FastlyExtended { + /** + * @param {string} apiKey - Fastly API key + * @param {string} serviceId - Fastly service ID + */ + constructor (apiKey, serviceId) { + this.fastly = Fastly(apiKey); + this.serviceId = serviceId; - /* + /** + * @template T + * @type {{ + * ( + * httpMethod: string, url: string, callback: FastlyCallback + * ): void; + * ( + * httpMethod: string, url: string, formData: Record., callback: FastlyCallback + * ): void; + * }} + */ + this.request = this.fastly.request.bind(this.fastly); + + /** + * @param {string} service - The Fastly service ID + * @param {string} key - The cache key to purge + * @param {FastlyCallback} cb - Callback for when the purge request is complete + * @returns {void} + */ + this.purgeKey = this.fastly.purgeKey.bind(this.fastly); + } + + /** * Helper method for constructing Fastly API urls - * - * @param {string} Service id - * @param {number} Version - * - * @return {string} + * @param {string} servId - Fastly service ID + * @param {number} version - Fastly service version + * @return {string} Fastly API url prefix */ - fastly.getFastlyAPIPrefix = function (servId, version) { + getFastlyAPIPrefix (servId, version) { return `/service/${encodeURIComponent(servId)}/version/${version}`; - }; + } - /* + /** * getLatestActiveVersion: Get the most recent version for the configured service - * - * @param {callback} Callback with signature *err, latestVersion) + * @param {FastlyCallback} cb - Callback for the latest active version + * @returns {void} */ - fastly.getLatestActiveVersion = function (cb) { + getLatestActiveVersion (cb) { if (!this.serviceId) { - return cb('Failed to get latest version. No serviceId configured'); + return cb(new Error('Failed to get latest version. No serviceId configured')); } const url = `/service/${encodeURIComponent(this.serviceId)}/version`; - this.request('GET', url, (err, versions) => { - if (err) { - return cb(`Failed to fetch versions: ${err}`); + this.request('GET', url, + (/** @type {FastlyError?} */ err, /** @type {FastlyServiceVersion[]} */ versions) => { + if (err) { + return cb(new Error('Failed to fetch versions', {cause: err})); + } + const latestVersion = versions.reduce((latestActiveSoFar, cur) => { + // if one of [latestActiveSoFar, cur] is active and the other isn't, + // return whichever is active. If both are not active, return + // latestActiveSoFar. + if (!cur || !cur.active) return latestActiveSoFar; + if (!latestActiveSoFar || !latestActiveSoFar.active) return cur; + // when both are active, prefer whichever has a higher version number. + return (cur.number > latestActiveSoFar.number) ? cur : latestActiveSoFar; + }, /** @type {FastlyServiceVersion?} */ (null)); + return cb(null, latestVersion); } - const latestVersion = versions.reduce((latestActiveSoFar, cur) => { - // if one of [latestActiveSoFar, cur] is active and the other isn't, - // return whichever is active. If both are not active, return - // latestActiveSoFar. - if (!cur || !cur.active) return latestActiveSoFar; - if (!latestActiveSoFar || !latestActiveSoFar.active) return cur; - // when both are active, prefer whichever has a higher version number. - return (cur.number > latestActiveSoFar.number) ? cur : latestActiveSoFar; - }, null); - return cb(null, latestVersion); - }); - }; - - /* + ); + } + + /** * setCondition: Upsert a Fastly condition entry * Attempts to PUT and POSTs if the PUT request is a 404 * - * @param {number} Version number - * @param {object} Condition object sent to the API - * @param {callback} Callback for fastly.request + * @param {number} version - Fastly service's version number + * @param {FastlyVclCondition} condition - Condition object sent to the API + * @param {FastlyCallback} cb - Callback returning the created or updated object on success + * @returns {void} */ - fastly.setCondition = function (version, condition, cb) { + setCondition (version, condition, cb) { if (!this.serviceId) { - return cb('Failed to set condition. No serviceId configured'); + return cb(new Error('Failed to set condition. No serviceId configured')); } const name = condition.name; const putUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/condition/${encodeURIComponent(name)}`; const postUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/condition`; - return this.request('PUT', putUrl, condition, (err, response) => { - if (err && err.statusCode === 404) { - this.request('POST', postUrl, condition, (e, resp) => { - if (e) { - return cb(`Failed while inserting condition "${condition.statement}": ${e}`); - } - return cb(null, resp); - }); - return; - } - if (err) { - return cb(`Failed to update condition "${condition.statement}": ${err}`); + return this.request('PUT', putUrl, condition, + (/** @type {FastlyError?} */ err, /** @type {FastlyVclCondition} */ response) => { + if (err && err.statusCode === 404) { + this.request('POST', postUrl, condition, + (/** @type {FastlyError?} */ e, /** @type {FastlyVclCondition} */ resp) => { + if (e) { + return cb( + new Error(`Failed while inserting condition "${condition.statement}"`, {cause: e}) + ); + } + return cb(null, resp); + } + ); + return; + } + if (err) { + return cb(new Error(`Failed to update condition "${condition.statement}"`, {cause: err})); + } + return cb(null, response); } - return cb(null, response); - }); - }; + ); + } - /* + /** * setFastlyHeader: Upsert a Fastly header entry * Attempts to PUT and POSTs if the PUT request is a 404 * - * @param {number} Version number - * @param {object} Header object sent to the API - * @param {callback} Callback for fastly.request + * @param {number} version - Fastly service's version number + * @param {FastlyVclHeader} header - Header object sent to the API + * @param {FastlyCallback} cb - Callback returning the created or updated object on success + * @returns {void} */ - fastly.setFastlyHeader = function (version, header, cb) { + setFastlyHeader (version, header, cb) { if (!this.serviceId) { - cb('Failed to set header. No serviceId configured'); + cb(new Error('Failed to set header. No serviceId configured')); } const name = header.name; const putUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/header/${encodeURIComponent(name)}`; const postUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/header`; - return this.request('PUT', putUrl, header, (err, response) => { - if (err && err.statusCode === 404) { - this.request('POST', postUrl, header, (e, resp) => { - if (e) { - return cb(`Failed to insert header: ${e}`); - } - return cb(null, resp); - }); - return; + return this.request('PUT', putUrl, header, + (/** @type {FastlyError?} */ err, /** @type {FastlyVclHeader} */ response) => { + if (err && err.statusCode === 404) { + this.request('POST', postUrl, header, + (/** @type {FastlyError?} */ e, /** @type {FastlyVclHeader} */ resp) => { + if (e) { + return cb(new Error('Failed to insert header', {cause: e})); + } + return cb(null, resp); + } + ); + return; + } + if (err) { + return cb(new Error('Failed to update header', {cause: err})); + } + return cb(null, response); } - if (err) { - return cb(`Failed to update header: ${err}`); - } - return cb(null, response); - }); - }; + ); + } + + /** + * Get response objects for a specific version + * @param {number} version - The version number of the Fastly service + * @param {FastlyCallback} cb - Callback listing response objects on success + * @returns {void} + */ + getResponseObjects (version, cb) { + if (!this.serviceId) { + return cb(new Error('Failed to get response objects. No serviceId configured.')); + } + const url = `${this.getFastlyAPIPrefix(this.serviceId, version)}/response_object`; + this.request('GET', url, cb); + } - /* + /** * setResponseObject: Upsert a Fastly response object * Attempts to PUT and POSTs if the PUT request is a 404 * - * @param {number} Version number - * @param {object} Response object sent to the API - * @param {callback} Callback for fastly.request + * @param {number} version - Fastly service's version number + * @param {object} responseObj - Response object sent to the API + * @param {FastlyCallback} cb - Callback returning the created/updated object on success + * @returns {void} */ - fastly.setResponseObject = function (version, responseObj, cb) { + setResponseObject (version, responseObj, cb) { if (!this.serviceId) { - cb('Failed to set response object. No serviceId configured'); + cb(new Error('Failed to set response object. No serviceId configured')); } const name = responseObj.name; const putUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/response_object/${encodeURIComponent(name)}`; const postUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/response_object`; - return this.request('PUT', putUrl, responseObj, (err, response) => { - if (err && err.statusCode === 404) { - this.request('POST', postUrl, responseObj, (e, resp) => { - if (e) { - return cb(`Failed to insert response object: ${e}`); - } - return cb(null, resp); - }); - return; + return this.request('PUT', putUrl, responseObj, + (/** @type {FastlyError?} */ err, /** @type {FastlyVclResponseObject} */ response) => { + if (err && err.statusCode === 404) { + this.request('POST', postUrl, responseObj, + (/** @type {FastlyError?} */ e, /** @type {FastlyVclResponseObject} */ resp) => { + if (e) { + return cb(new Error('Failed to insert response object', {cause: e})); + } + return cb(null, resp); + } + ); + return; + } + if (err) { + return cb(new Error('Failed to update response object', {cause: err})); + } + return cb(null, response); } - if (err) { - return cb(`Failed to update response object: ${err}`); - } - return cb(null, response); - }); - }; + ); + } - /* + /** + * Delete a Fastly response object + * @param {number} version - The version number of the Fastly service + * @param {string} name - The name of the response object to delete + * @param {FastlyCallback} cb - Completion callback + * @returns {void} + */ + deleteResponseObject (version, name, cb) { + if (!this.serviceId) { + return cb(new Error('Failed to delete response object. No serviceId configured.')); + } + const url = `${this.getFastlyAPIPrefix(this.serviceId, version)}/response_object/${encodeURIComponent(name)}`; + this.request('DELETE', url, cb); + } + + /** * cloneVersion: Clone a version to create a new version * - * @param {number} Version to clone - * @param {callback} Callback for fastly.request + * @param {number} version - Version to clone + * @param {FastlyCallback} cb - Callback returning the cloned version on success + * @returns {void} */ - fastly.cloneVersion = function (version, cb) { - if (!this.serviceId) return cb('Failed to clone version. No serviceId configured.'); + cloneVersion (version, cb) { + if (!this.serviceId) return cb(new Error('Failed to clone version. No serviceId configured.')); const url = `${this.getFastlyAPIPrefix(this.serviceId, version)}/clone`; this.request('PUT', url, cb); - }; + } - /* + /** * activateVersion: Activate a version * - * @param {number} Version number - * @param {callback} Callback for fastly.request + * @param {number} version - Version to activate + * @param {FastlyCallback} cb - Callback returning the activated version on success + * @returns {void} */ - fastly.activateVersion = function (version, cb) { - if (!this.serviceId) return cb('Failed to activate version. No serviceId configured.'); + activateVersion (version, cb) { + if (!this.serviceId) return cb(new Error('Failed to activate version. No serviceId configured.')); const url = `${this.getFastlyAPIPrefix(this.serviceId, version)}/activate`; this.request('PUT', url, cb); - }; + } - /* + /** * Upsert a custom vcl file. Attempts a PUT, and falls back * to POST if not there already. * - * @param {number} version current version number for fastly service - * @param {string} name name of the custom vcl file to be upserted - * @param {string} vcl stringified custom vcl to be uploaded - * @param {Function} cb function that takes in two args: err, response + * @param {number} version - Target version number for Fastly service + * @param {string} name - Name of the custom vcl file to be upserted + * @param {string} vcl - Stringified custom vcl to be uploaded + * @param {FastlyCallback} cb - Callback returning the lint report on success + * @returns {void} */ - fastly.setCustomVCL = function (version, name, vcl, cb) { + setCustomVCL (version, name, vcl, cb) { if (!this.serviceId) { - return cb('Failed to set response object. No serviceId configured'); + return cb(new Error('Failed to set response object. No serviceId configured.')); } const url = `${this.getFastlyAPIPrefix(this.serviceId, version)}/vcl/${name}`; const postUrl = `${this.getFastlyAPIPrefix(this.serviceId, version)}/vcl`; const content = {content: vcl}; - return this.request('PUT', url, content, (err, response) => { - if (err && err.statusCode === 404) { - content.name = name; - this.request('POST', postUrl, content, (e, resp) => { - if (e) { - return cb(`Failed while adding custom vcl "${name}": ${e}`); - } - return cb(null, resp); - }); - return; - } - if (err) { - return cb(`Failed to update custom vcl "${name}": ${err}`); + return this.request('PUT', url, content, + (/** @type {FastlyError?} */ err, /** @type {FastlyVclLintReport} */ response) => { + if (err && err.statusCode === 404) { + content.name = name; + this.request('POST', postUrl, content, + (/** @type {FastlyError?} */ e, /** @type {FastlyVclLintReport} */ resp) => { + if (e) { + return cb(new Error(`Failed while adding custom vcl "${name}"`, {cause: e})); + } + return cb(null, resp); + } + ); + return; + } + if (err) { + return cb(new Error(`Failed to update custom vcl "${name}"`, {cause: err})); + } + return cb(null, response); } - return cb(null, response); - }); - }; + ); + } +} - return fastly; -}; +module.exports = FastlyExtended; diff --git a/test/unit/lib/fastly-extended.test.js b/test/unit/lib/fastly-extended.test.js index 451902c16d3..faf01752624 100644 --- a/test/unit/lib/fastly-extended.test.js +++ b/test/unit/lib/fastly-extended.test.js @@ -2,9 +2,10 @@ describe('fastly library', () => { let mockedFastlyRequest = {}; jest.mock('fastly', () => (() => ({ - request: mockedFastlyRequest + request: mockedFastlyRequest, + purgeKey: jest.fn() }))); - const fastlyExtended = require('../../../bin/lib/fastly-extended'); // eslint-disable-line global-require + const FastlyExtended = require('../../../bin/lib/fastly-extended'); // eslint-disable-line global-require test('getLatestActiveVersion returns largest active VCL number, ' + 'when called with VCLs in sequential order', done => { @@ -28,7 +29,7 @@ describe('fastly library', () => { } ]); }); - const fastlyInstance = fastlyExtended('api_key', 'service_id'); + const fastlyInstance = new FastlyExtended('api_key', 'service_id'); fastlyInstance.getLatestActiveVersion((err, response) => { expect(err).toBe(null); @@ -64,7 +65,7 @@ describe('fastly library', () => { } ]); }); - const fastlyInstance = fastlyExtended('api_key', 'service_id'); + const fastlyInstance = new FastlyExtended('api_key', 'service_id'); fastlyInstance.getLatestActiveVersion((err, response) => { expect(err).toBe(null); @@ -100,7 +101,7 @@ describe('fastly library', () => { } ]); }); - const fastlyInstance = fastlyExtended('api_key', 'service_id'); + const fastlyInstance = new FastlyExtended('api_key', 'service_id'); fastlyInstance.getLatestActiveVersion((err, response) => { expect(err).toBe(null); @@ -122,7 +123,7 @@ describe('fastly library', () => { } ]); }); - const fastlyInstance = fastlyExtended('api_key', 'service_id'); + const fastlyInstance = new FastlyExtended('api_key', 'service_id'); fastlyInstance.getLatestActiveVersion((err, response) => { expect(err).toBe(null); @@ -146,7 +147,7 @@ describe('fastly library', () => { } ]); }); - const fastlyInstance = fastlyExtended('api_key', 'service_id'); + const fastlyInstance = new FastlyExtended('api_key', 'service_id'); fastlyInstance.getLatestActiveVersion((err, response) => { expect(err).toBe(null);