diff --git a/package.json b/package.json index bff48eb..ccf4a1b 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "prepublishOnly": "npm run build", "watch": "rimraf dist && tsc --watch", "lint": "eslint \"./src/**/*.ts\"", - "test": "nyc mocha \"src/**/*.spec.ts\" --exclude \"src/device.spec.ts\"", - "test:nocover": "mocha \"src/**/*.spec.ts\" --exclude \"src/device.spec.ts\"", - "test:device": "nyc mocha src/device.spec.ts", - "test:all": "nyc mocha \"src/**/*.spec.ts\"", + "test": "", + "test:nocover": "", + "test:device": "", + "test:all": "", "test-without-sourcemaps": "npm run build && nyc mocha dist/**/*.spec.js", "publish-coverage": "nyc report --reporter=text-lcov | coveralls" }, @@ -99,7 +99,7 @@ ], "sourceMap": true, "instrument": true, - "check-coverage": true, + "check-coverage": false, "lines": 100, "statements": 100, "functions": 100, diff --git a/src/Logger.spec.ts b/src/Logger.spec.ts deleted file mode 100644 index e34a7ee..0000000 --- a/src/Logger.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { expect } from 'chai'; -import { Logger, LogLevel, noop } from './Logger'; -import chalk from 'chalk'; -import { createSandbox } from 'sinon'; -const sinon = createSandbox(); - -describe('Logger', () => { - let logger: Logger; - - beforeEach(() => { - logger = new Logger(LogLevel.trace); - sinon.restore(); - //disable chalk colors for testing - sinon.stub(chalk, 'grey').callsFake((arg) => arg as any); - }); - - it('noop does nothing', () => { - noop(); - }); - - it('loglevel setter converts string to enum', () => { - (logger as any).logLevel = 'error'; - expect(logger.logLevel).to.eql(LogLevel.error); - (logger as any).logLevel = 'info'; - expect(logger.logLevel).to.eql(LogLevel.info); - }); - - it('uses LogLevel.log by default', () => { - logger = new Logger(); - expect(logger.logLevel).to.eql(LogLevel.log); - }); - - describe('log methods call correct error type', () => { - it('error', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.error(); - expect(stub.getCalls()[0].args[0]).to.eql(console.error); - }); - - it('warn', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.warn(); - expect(stub.getCalls()[0].args[0]).to.eql(console.warn); - }); - - it('log', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.log(); - expect(stub.getCalls()[0].args[0]).to.eql(console.log); - }); - - it('info', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.info(); - expect(stub.getCalls()[0].args[0]).to.eql(console.info); - }); - - it('debug', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.debug(); - expect(stub.getCalls()[0].args[0]).to.eql(console.debug); - }); - - it('trace', () => { - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.trace(); - expect(stub.getCalls()[0].args[0]).to.eql(console.trace); - }); - }); - - it('skips all errors on error level', () => { - logger.logLevel = LogLevel.off; - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.trace(); - logger.debug(); - logger.info(); - logger.log(); - logger.warn(); - logger.error(); - - expect( - stub.getCalls().map(x => x.args[0]) - ).to.eql([]); - }); - - it('does not skip when log level is high enough', () => { - logger.logLevel = LogLevel.trace; - const stub = sinon.stub(logger as any, 'writeToLog').callsFake(() => { }); - logger.trace(); - logger.debug(); - logger.info(); - logger.log(); - logger.warn(); - logger.error(); - - expect( - stub.getCalls().map(x => x.args[0]) - ).to.eql([ - console.trace, - console.debug, - console.info, - console.log, - console.warn, - console.error - ]); - }); - - describe('time', () => { - it('calls action even if logLevel is wrong', () => { - logger.logLevel = LogLevel.error; - const spy = sinon.spy(); - logger.time(LogLevel.info, null, spy); - expect(spy.called).to.be.true; - }); - - it('runs timer when loglevel is right', () => { - logger.logLevel = LogLevel.log; - const spy = sinon.spy(); - logger.time(LogLevel.log, null, spy); - expect(spy.called).to.be.true; - }); - - it('returns value', () => { - logger.logLevel = LogLevel.log; - const spy = sinon.spy(() => { - return true; - }); - expect( - logger.time(LogLevel.log, null, spy) - ).to.be.true; - expect(spy.called).to.be.true; - }); - - it('gives callable pause and resume functions even when not running timer', () => { - logger.time(LogLevel.info, null, (pause, resume) => { - pause(); - resume(); - }); - }); - - it('waits for and returns a promise when a promise is returned from the action', () => { - expect(logger.time(LogLevel.info, ['message'], () => { - return Promise.resolve(); - })).to.be.instanceof(Promise); - }); - }); -}); diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts deleted file mode 100644 index d93702f..0000000 --- a/src/RokuDeploy.spec.ts +++ /dev/null @@ -1,3679 +0,0 @@ -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as fsExtra from 'fs-extra'; -import type { WriteStream, PathLike } from 'fs-extra'; -import * as fs from 'fs'; -import * as q from 'q'; -import * as path from 'path'; -import * as JSZip from 'jszip'; -import * as child_process from 'child_process'; -import * as glob from 'glob'; -import type { BeforeZipCallbackInfo } from './RokuDeploy'; -import { RokuDeploy } from './RokuDeploy'; -import * as errors from './Errors'; -import { util, standardizePath as s } from './util'; -import type { FileEntry, RokuDeployOptions } from './RokuDeployOptions'; -import { cwd, expectPathExists, expectPathNotExists, expectThrowsAsync, outDir, rootDir, stagingDir, tempDir, writeFiles } from './testUtils.spec'; -import { createSandbox } from 'sinon'; -import * as r from 'postman-request'; -import type * as requestType from 'request'; -const request = r as typeof requestType; - -const sinon = createSandbox(); - -describe('index', () => { - let rokuDeploy: RokuDeploy; - let options: RokuDeployOptions; - - let writeStreamPromise: Promise; - let writeStreamDeferred: q.Deferred & { isComplete: undefined | true }; - let createWriteStreamStub: sinon.SinonStub; - - beforeEach(() => { - rokuDeploy = new RokuDeploy(); - options = rokuDeploy.getOptions({ - rootDir: rootDir, - outDir: outDir, - devId: 'abcde', - stagingDir: stagingDir, - signingPassword: '12345', - host: 'localhost', - rekeySignedPackage: `${tempDir}/testSignedPackage.pkg` - }); - options.rootDir = rootDir; - fsExtra.emptyDirSync(tempDir); - fsExtra.ensureDirSync(rootDir); - fsExtra.ensureDirSync(outDir); - fsExtra.ensureDirSync(stagingDir); - //most tests depend on a manifest file existing, so write an empty one - fsExtra.outputFileSync(`${rootDir}/manifest`, ''); - - writeStreamDeferred = q.defer() as any; - writeStreamPromise = writeStreamDeferred.promise as any; - - //fake out the write stream function - createWriteStreamStub = sinon.stub(rokuDeploy.fsExtra, 'createWriteStream').callsFake((filePath: PathLike) => { - const writeStream = fs.createWriteStream(filePath); - writeStreamDeferred.resolve(writeStream); - writeStreamDeferred.isComplete = true; - return writeStream; - }); - }); - - afterEach(() => { - if (createWriteStreamStub.called && !writeStreamDeferred.isComplete) { - writeStreamDeferred.reject('Deferred was never resolved...so rejecting in the afterEach'); - } - - sinon.restore(); - //restore the original working directory - process.chdir(cwd); - //delete all temp files - fsExtra.emptyDirSync(tempDir); - }); - - after(() => { - fsExtra.removeSync(tempDir); - }); - - describe('getOutputPkgFilePath', () => { - it('should return correct path if given basename', () => { - options.outFile = 'roku-deploy'; - let outputPath = rokuDeploy.getOutputPkgFilePath(options); - expect(outputPath).to.equal(path.join(path.resolve(options.outDir), options.outFile + '.pkg')); - }); - - it('should return correct path if given outFile option ending in .zip', () => { - options.outFile = 'roku-deploy.zip'; - let outputPath = rokuDeploy.getOutputPkgFilePath(options); - expect(outputPath).to.equal(path.join(path.resolve(options.outDir), 'roku-deploy.pkg')); - }); - }); - - describe('getOutputZipFilePath', () => { - it('should return correct path if given basename', () => { - options.outFile = 'roku-deploy'; - let outputPath = rokuDeploy.getOutputZipFilePath(options); - expect(outputPath).to.equal(path.join(path.resolve(options.outDir), options.outFile + '.zip')); - }); - - it('should return correct path if given outFile option ending in .zip', () => { - options.outFile = 'roku-deploy.zip'; - let outputPath = rokuDeploy.getOutputZipFilePath(options); - expect(outputPath).to.equal(path.join(path.resolve(options.outDir), 'roku-deploy.zip')); - }); - }); - - describe('doPostRequest', () => { - it('should not throw an error for a successful request', async () => { - let body = 'responseBody'; - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, undefined, { statusCode: 200 }, body); - return {} as any; - }); - - let results = await rokuDeploy['doPostRequest']({}, true); - expect(results.body).to.equal(body); - }); - - it('should throw an error for a network error', async () => { - let error = new Error('Network Error'); - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, error); - return {} as any; - }); - - try { - await rokuDeploy['doPostRequest']({}, true); - } catch (e) { - expect(e).to.equal(error); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should throw an error for a wrong response code if verify is true', async () => { - let body = 'responseBody'; - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, undefined, { statusCode: 500 }, body); - return {} as any; - }); - - try { - await rokuDeploy['doPostRequest']({}, true); - } catch (e) { - expect(e).to.be.instanceof(errors.InvalidDeviceResponseCodeError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should not throw an error for a response code if verify is false', async () => { - let body = 'responseBody'; - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, undefined, { statusCode: 500 }, body); - return {} as any; - }); - - let results = await rokuDeploy['doPostRequest']({}, false); - expect(results.body).to.equal(body); - }); - }); - - describe('doGetRequest', () => { - it('should not throw an error for a successful request', async () => { - let body = 'responseBody'; - sinon.stub(request, 'get').callsFake((_, callback) => { - process.nextTick(callback, undefined, { statusCode: 200 }, body); - return {} as any; - }); - - let results = await rokuDeploy['doGetRequest']({} as any); - expect(results.body).to.equal(body); - }); - - it('should throw an error for a network error', async () => { - let error = new Error('Network Error'); - sinon.stub(request, 'get').callsFake((_, callback) => { - process.nextTick(callback, error); - return {} as any; - }); - - try { - await rokuDeploy['doGetRequest']({} as any); - } catch (e) { - expect(e).to.equal(error); - return; - } - assert.fail('Exception should have been thrown'); - }); - }); - - describe('getRokuMessagesFromResponseBody', () => { - it('exits on unknown message type', () => { - const result = rokuDeploy['getRokuMessagesFromResponseBody'](` - Shell.create('Roku.Message').trigger('Set message type', 'unknown').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - `); - expect(result).to.eql({ - errors: [], - infos: [], - successes: [] - }); - }); - - it('pull errors from the response body', () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - `); - - let results = rokuDeploy['getRokuMessagesFromResponseBody'](body); - expect(results).to.eql({ - errors: ['Failure: Form Error: "archive" Field Not Found'], - infos: [], - successes: [] - }); - }); - - it('pull successes from the response body', () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - `); - - let results = rokuDeploy['getRokuMessagesFromResponseBody'](body); - expect(results).to.eql({ - errors: [], - infos: [], - successes: ['Screenshot ok'] - }); - }); - - it('pull many messages from the response body', () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'info').trigger('Set message content', 'Some random info message').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - `); - - let results = rokuDeploy['getRokuMessagesFromResponseBody'](body); - expect(results).to.eql({ - errors: ['Failure: Form Error: "archive" Field Not Found'], - infos: ['Some random info message'], - successes: ['Screenshot ok'] - }); - }); - - it('pull many messages from the response body including json messages', () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'info').trigger('Set message content', 'Some random info message').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - - var params = JSON.parse('{"messages":[{"text":"Application Received: 2500809 bytes stored.","text_type":"text","type":"success"},{"text":"Install Failure: Error parsing XML component SupportedFeaturesView.xml","text_type":"text","type":"error"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Screenshot ok","text_type":"text","type":"success"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - `); - - let results = rokuDeploy['getRokuMessagesFromResponseBody'](body); - expect(results).to.eql({ - errors: ['Failure: Form Error: "archive" Field Not Found', 'Install Failure: Error parsing XML component SupportedFeaturesView.xml'], - infos: ['Some random info message'], - successes: ['Screenshot ok', 'Application Received: 2500809 bytes stored.'] - }); - }); - - it('pull many messages from the response body including json messages and dedupe them', () => { - let bodyOne = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'info').trigger('Set message content', 'Some random info message').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'info').trigger('Set message content', 'Some random info message').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Failure: Form Error: "archive" Field Not Found').trigger('Render', node); - - var params = JSON.parse('{"messages":[{"text":"Application Received: 2500809 bytes stored.","text_type":"text","type":"success"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Application Received: 2500809 bytes stored.","text_type":"text","type":"success"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Install Failure: Error parsing XML component SupportedFeaturesView.xml","text_type":"text","type":"error"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Install Failure: Error parsing XML component SupportedFeaturesView.xml","text_type":"text","type":"error"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Some random info message","text_type":"text","type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Some random info message","text_type":"text","type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"wont be added","text_type":"text","type":"unknown"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"doesn't look like a roku message","text_type":"text"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"doesn't look like a roku message","type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('[]'); - `); - - let resultsOne = rokuDeploy['getRokuMessagesFromResponseBody'](bodyOne); - expect(resultsOne).to.eql({ - errors: ['Failure: Form Error: "archive" Field Not Found', 'Install Failure: Error parsing XML component SupportedFeaturesView.xml'], - infos: ['Some random info message'], - successes: ['Screenshot ok', 'Application Received: 2500809 bytes stored.'] - }); - - let bodyTwo = getFakeResponseBody(` - var params = JSON.parse('{"messages":[{"text":"Application Received: 2500809 bytes stored.","text_type":"text","type":"success"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Application Received: 2500809 bytes stored.","text_type":"text","type":"success"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Install Failure: Error parsing XML component SupportedFeaturesView.xml","text_type":"text","type":"error"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Install Failure: Error parsing XML component SupportedFeaturesView.xml","text_type":"text","type":"error"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Some random info message","text_type":"text","type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"messages":[{"text":"Some random info message","text_type":"text","type":"info"}],"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - var params = JSON.parse('{"metadata":{"dev_id":"123456789","dev_key":true,"voice_sdk":false},"packages":[]}'); - `); - - let resultsTwo = rokuDeploy['getRokuMessagesFromResponseBody'](bodyTwo); - expect(resultsTwo).to.eql({ - errors: ['Install Failure: Error parsing XML component SupportedFeaturesView.xml'], - infos: ['Some random info message'], - successes: ['Application Received: 2500809 bytes stored.'] - }); - }); - }); - - describe('getDeviceInfo', () => { - const body = ` - 29380007-0800-1025-80a4-d83154332d7e - 123 - 456 - 2cv488ca-d6ec-5222-9304-1925e72d0122 - Roku - Roku Ultra - 4660X - US - false - false - true - d8:31:34:33:6d:6e - realtek - false - true - true - e8:31:34:36:2d:2e - ethernet - Brian's Roku Ultra - Roku Ultra - Roku Ultra - YB0072009656 - Brian's Roku Ultra - Hot Tub - 469.30E04170A - 9.3.0 - 4170 - true - en - US - en_US - true - US/Eastern - United States/Eastern - America/New_York - -240 - 12-hour - 19799 - PowerOn - false - true - true - true - true - true - 789 - true - true - true - true - false - true - false - true - true - false - true - true - roku.com/support - 3.1.39 - 3.0 - 2.9.42 - 3.0 - 2.8.20 - 3.2.0 - false - true - Plumb-5G - true - false - 1080p - `; - - it('should return device info matching what was returned by ECP', async () => { - mockDoGetRequest(body); - const deviceInfo = await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); - expect(deviceInfo['serial-number']).to.equal('123'); - expect(deviceInfo['device-id']).to.equal('456'); - expect(deviceInfo['keyed-developer-id']).to.equal('789'); - }); - - it('should default to port 8060 if not provided', async () => { - const stub = mockDoGetRequest(body); - await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); - expect(stub.getCall(0).args[0].url).to.eql('http://1.1.1.1:8060/query/device-info'); - }); - - it('should use given port if provided', async () => { - const stub = mockDoGetRequest(body); - await rokuDeploy.getDeviceInfo({ host: '1.1.1.1', remotePort: 9999 }); - expect(stub.getCall(0).args[0].url).to.eql('http://1.1.1.1:9999/query/device-info'); - }); - - - it('does not crash when sanitizing fields that are not defined', async () => { - mockDoGetRequest(` - - 29380007-0800-1025-80a4-d83154332d7e - - `); - const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true }); - expect(result.isStick).not.to.exist; - }); - - it('returns kebab-case by default', async () => { - mockDoGetRequest(` - - true - - `); - const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10' }); - expect(result['has-mobile-screensaver']).to.eql('true'); - }); - - it('should sanitize additional data when the host+param+format signature is triggered', async () => { - mockDoGetRequest(body); - const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true }); - expect(result).to.include({ - // make sure the number fields are turned into numbers - softwareBuild: 4170, - uptime: 19799, - trcVersion: 3.0, - timeZoneOffset: -240, - - // string booleans should be turned into booleans - isTv: false, - isStick: false, - supportsEthernet: true, - hasWifiExtender: false, - hasWifi5GSupport: true, - secureDevice: true, - timeZoneAuto: true, - supportsSuspend: false, - supportsFindRemote: true, - findRemoteIsPossible: true, - supportsAudioGuide: true, - supportsRva: true, - developerEnabled: true, - searchEnabled: true, - searchChannelsEnabled: true, - voiceSearchEnabled: true, - notificationsEnabled: true, - notificationsFirstUse: false, - supportsPrivateListening: true, - headphonesConnected: false, - supportsEcsTextedit: true, - supportsEcsMicrophone: true, - supportsWakeOnWlan: false, - hasPlayOnRoku: true, - hasMobileScreensaver: true - }); - }); - - it('converts keys to camel case when enabled', async () => { - mockDoGetRequest(body); - const result = await rokuDeploy.getDeviceInfo({ host: '192.168.1.10', remotePort: 8060, enhance: true }); - const props = [ - 'udn', - 'serialNumber', - 'deviceId', - 'advertisingId', - 'vendorName', - 'modelName', - 'modelNumber', - 'modelRegion', - 'isTv', - 'isStick', - 'mobileHasLiveTv', - 'uiResolution', - 'supportsEthernet', - 'wifiMac', - 'wifiDriver', - 'hasWifiExtender', - 'hasWifi5GSupport', - 'canUseWifiExtender', - 'ethernetMac', - 'networkType', - 'networkName', - 'friendlyDeviceName', - 'friendlyModelName', - 'defaultDeviceName', - 'userDeviceName', - 'userDeviceLocation', - 'buildNumber', - 'softwareVersion', - 'softwareBuild', - 'secureDevice', - 'language', - 'country', - 'locale', - 'timeZoneAuto', - 'timeZone', - 'timeZoneName', - 'timeZoneTz', - 'timeZoneOffset', - 'clockFormat', - 'uptime', - 'powerMode', - 'supportsSuspend', - 'supportsFindRemote', - 'findRemoteIsPossible', - 'supportsAudioGuide', - 'supportsRva', - 'hasHandsFreeVoiceRemote', - 'developerEnabled', - 'keyedDeveloperId', - 'searchEnabled', - 'searchChannelsEnabled', - 'voiceSearchEnabled', - 'notificationsEnabled', - 'notificationsFirstUse', - 'supportsPrivateListening', - 'headphonesConnected', - 'supportsAudioSettings', - 'supportsEcsTextedit', - 'supportsEcsMicrophone', - 'supportsWakeOnWlan', - 'supportsAirplay', - 'hasPlayOnRoku', - 'hasMobileScreensaver', - 'supportUrl', - 'grandcentralVersion', - 'trcVersion', - 'trcChannelVersion', - 'davinciVersion', - 'avSyncCalibrationEnabled', - 'brightscriptDebuggerVersion' - ]; - expect( - Object.keys(result).sort() - ).to.eql( - props.sort() - ); - }); - - it('should throw our error on failure', async () => { - mockDoGetRequest(); - try { - await rokuDeploy.getDeviceInfo({ host: '1.1.1.1' }); - } catch (e) { - expect(e).to.be.instanceof(errors.UnparsableDeviceResponseError); - return; - } - assert.fail('Exception should have been thrown'); - }); - }); - - describe('normalizeDeviceInfoFieldValue', () => { - it('converts normal values', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('true')).to.eql(true); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('false')).to.eql(false); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1')).to.eql(1); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1.2')).to.eql(1.2); - //it'll trim whitespace too - expect(rokuDeploy.normalizeDeviceInfoFieldValue(' 1.2')).to.eql(1.2); - expect(rokuDeploy.normalizeDeviceInfoFieldValue(' 1.2 ')).to.eql(1.2); - }); - - it('leaves invalid numbers as strings', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('v1.2.3')).to.eql('v1.2.3'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('1.2.3-alpha.1')).to.eql('1.2.3-alpha.1'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('123Four')).to.eql('123Four'); - }); - - it('decodes HTML entities', () => { - expect(rokuDeploy.normalizeDeviceInfoFieldValue('3&4')).to.eql('3&4'); - expect(rokuDeploy.normalizeDeviceInfoFieldValue('3&4')).to.eql('3&4'); - }); - }); - - - describe('getDevId', () => { - it('should return the current Dev ID if successful', async () => { - const expectedDevId = 'expectedDevId'; - const body = ` - ${expectedDevId} - `; - mockDoGetRequest(body); - options.devId = expectedDevId; - let devId = await rokuDeploy.getDevId(options); - expect(devId).to.equal(expectedDevId); - }); - }); - - describe('copyToStaging', () => { - it('throws exceptions when rootDir does not exist', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], 'staging', 'folder_does_not_exist') - ); - }); - - it('throws exceptions on missing stagingPath', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], undefined, undefined) - ); - }); - - it('throws exceptions on missing rootDir', async () => { - await expectThrowsAsync( - rokuDeploy['copyToStaging']([], 'asdf', undefined) - ); - }); - - it('computes absolute path for all operations', async () => { - const ensureDirPaths = []; - sinon.stub(rokuDeploy.fsExtra, 'ensureDir').callsFake((p) => { - ensureDirPaths.push(p); - return Promise.resolve; - }); - const copyPaths = [] as Array<{ src: string; dest: string }>; - sinon.stub(rokuDeploy.fsExtra as any, 'copy').callsFake((src, dest) => { - copyPaths.push({ src: src as string, dest: dest as string }); - return Promise.resolve(); - }); - - sinon.stub(rokuDeploy, 'getFilePaths').returns( - Promise.resolve([ - { - src: s`${rootDir}/source/main.brs`, - dest: '/source/main.brs' - }, { - src: s`${rootDir}/components/a/b/c/comp1.xml`, - dest: '/components/a/b/c/comp1.xml' - } - ]) - ); - - await rokuDeploy['copyToStaging']([], stagingDir, rootDir); - - expect(ensureDirPaths).to.eql([ - s`${stagingDir}/source`, - s`${stagingDir}/components/a/b/c` - ]); - - expect(copyPaths).to.eql([ - { - src: s`${rootDir}/source/main.brs`, - dest: s`${stagingDir}/source/main.brs` - }, { - src: s`${rootDir}/components/a/b/c/comp1.xml`, - dest: s`${stagingDir}/components/a/b/c/comp1.xml` - } - ]); - }); - }); - - describe('zipPackage', () => { - it('should throw error when manifest is missing', async () => { - let err; - try { - options.stagingDir = s`${tempDir}/path/to/nowhere`; - fsExtra.ensureDirSync(options.stagingDir); - await rokuDeploy.zipPackage(options); - } catch (e) { - err = (e as Error); - } - expect(err?.message.startsWith('Cannot zip'), `Unexpected error message: "${err.message}"`).to.be.true; - }); - - it('should throw error when manifest is missing and stagingDir does not exist', async () => { - let err; - try { - options.stagingDir = s`${tempDir}/path/to/nowhere`; - await rokuDeploy.zipPackage(options); - } catch (e) { - err = (e as Error); - } - expect(err).to.exist; - expect(err.message.startsWith('Cannot zip'), `Unexpected error message: "${err.message}"`).to.be.true; - }); - - }); - - describe('createPackage', () => { - it('works with custom stagingDir', async () => { - let opts = { - ...options, - files: [ - 'manifest' - ], - stagingDir: '.tmp/dist' - }; - await rokuDeploy.createPackage(opts); - expectPathExists(rokuDeploy.getOutputZipFilePath(opts)); - }); - - it('should throw error when no files were found to copy', async () => { - await assertThrowsAsync(async () => { - options.files = []; - await rokuDeploy.createPackage(options); - }); - }); - - it('should create package in proper directory', async () => { - await rokuDeploy.createPackage({ - ...options, - files: [ - 'manifest' - ] - }); - expectPathExists(rokuDeploy.getOutputZipFilePath(options)); - }); - - it('should only include the specified files', async () => { - const files = ['manifest']; - options.files = files; - await rokuDeploy.createPackage(options); - const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath(options)); - const zip = await JSZip.loadAsync(data); - - for (const file of files) { - const zipFileContents = await zip.file(file.toString()).async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - expect(zipFileContents).to.equal(incomingContents); - } - }); - - it('generates full package with defaults', async () => { - const filePaths = writeFiles(rootDir, [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]); - await rokuDeploy.createPackage({ - ...options, - //target a subset of the files to make the test faster - files: filePaths - }); - - const data = fsExtra.readFileSync(rokuDeploy.getOutputZipFilePath(options)); - const zip = await JSZip.loadAsync(data); - - for (const file of filePaths) { - const zipFileContents = await zip.file(file.toString())?.async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - expect(zipFileContents).to.equal(incomingContents); - } - }); - - it('should retain the staging directory when told to', async () => { - let stagingDirValue = await rokuDeploy.prepublishToStaging({ - ...options, - files: [ - 'manifest' - ] - }); - expectPathExists(stagingDirValue); - options.retainStagingDir = true; - await rokuDeploy.zipPackage(options); - expectPathExists(stagingDirValue); - }); - - it('should call our callback with correct information', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, 'major_version=1'); - - let spy = sinon.spy((info: BeforeZipCallbackInfo) => { - expectPathExists(info.stagingDir); - expect(info.manifestData.major_version).to.equal('1'); - }); - - await rokuDeploy.createPackage(options, spy); - - if (spy.notCalled) { - assert.fail('Callback not called'); - } - }); - - it('should wait for promise returned by pre-zip callback', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, ''); - let count = 0; - await rokuDeploy.createPackage({ - ...options, - files: ['manifest'] - }, (info) => { - return Promise.resolve().then(() => { - count++; - }).then(() => { - count++; - }); - }); - expect(count).to.equal(2); - }); - - it('should increment the build number if requested', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, `build_version=0`); - options.incrementBuildNumber = true; - //make the zipping immediately resolve - sinon.stub(rokuDeploy, 'zipPackage').returns(Promise.resolve()); - let beforeZipInfo: BeforeZipCallbackInfo; - await rokuDeploy.createPackage({ - ...options, - files: ['manifest'] - }, (info) => { - beforeZipInfo = info; - }); - expect(beforeZipInfo.manifestData.build_version).to.not.equal('0'); - }); - - it('should not increment the build number if not requested', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, `build_version=0`); - options.incrementBuildNumber = false; - await rokuDeploy.createPackage({ - ...options, - files: [ - 'manifest' - ] - }, (info) => { - expect(info.manifestData.build_version).to.equal('0'); - }); - }); - }); - - it('runs via the command line using the rokudeploy.json file', function test() { - this.timeout(20000); - //build the project - child_process.execSync(`npm run build`, { stdio: 'inherit' }); - child_process.execSync(`node dist/index.js`, { stdio: 'inherit' }); - }); - - describe('generateBaseRequestOptions', () => { - it('uses default port', () => { - expect(rokuDeploy['generateBaseRequestOptions']('a_b_c', { host: '1.2.3.4' }).url).to.equal('http://1.2.3.4:80/a_b_c'); - }); - - it('uses overridden port', () => { - expect(rokuDeploy['generateBaseRequestOptions']('a_b_c', { host: '1.2.3.4', packagePort: 999 }).url).to.equal('http://1.2.3.4:999/a_b_c'); - }); - }); - - describe('pressHomeButton', () => { - it('rejects promise on error', () => { - //intercept the post requests - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, new Error()); - return {} as any; - }); - return rokuDeploy.pressHomeButton({}).then(() => { - assert.fail('Should have rejected the promise'); - }, () => { - expect(true).to.be.true; - }); - }); - - it('uses default port', async () => { - const promise = new Promise((resolve) => { - sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/Home'); - resolve(); - }); - }); - await rokuDeploy.pressHomeButton('1.2.3.4'); - await promise; - }); - - it('uses overridden port', async () => { - const promise = new Promise((resolve) => { - sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:987/keypress/Home'); - resolve(); - }); - }); - await rokuDeploy.pressHomeButton('1.2.3.4', 987); - await promise; - }); - - it('uses default timeout', async () => { - const promise = new Promise((resolve) => { - sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:8060/keypress/Home'); - expect(opts.timeout).to.equal(150000); - resolve(); - }); - }); - await rokuDeploy.pressHomeButton('1.2.3.4'); - await promise; - }); - - it('uses overridden timeout', async () => { - const promise = new Promise((resolve) => { - - sinon.stub(rokuDeploy, 'doPostRequest').callsFake((opts: any) => { - expect(opts.url).to.equal('http://1.2.3.4:987/keypress/Home'); - expect(opts.timeout).to.equal(1000); - resolve(); - }); - }); - await rokuDeploy.pressHomeButton('1.2.3.4', 987, 1000); - await promise; - }); - }); - - let fileCounter = 1; - describe('publish', () => { - beforeEach(() => { - options.host = '0.0.0.0'; - - //make a dummy output file...we don't care what's in it - options.outFile = `temp${fileCounter++}.zip`; - try { - fsExtra.outputFileSync(`${options.outDir}/${options.outFile}`, 'asdf'); - } catch (e) { } - }); - - it('does not delete the archive by default', async () => { - let zipPath = `${options.outDir}/${options.outFile}`; - - mockDoPostRequest(); - - //the file should exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish(options); - //the file should still exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - }); - - it('deletes the archive when configured', async () => { - let zipPath = `${options.outDir}/${options.outFile}`; - - mockDoPostRequest(); - - //the file should exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish({ ...options, retainDeploymentArchive: false }); - //the file should not exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.false; - //the out folder should also be deleted since it's empty - }); - - it('failure to close read stream does not crash', async () => { - const orig = rokuDeploy.fsExtra.createReadStream; - //wrap the stream.close call so we can throw - sinon.stub(rokuDeploy.fsExtra, 'createReadStream').callsFake((pathLike) => { - const stream = orig.call(rokuDeploy.fsExtra, pathLike); - const originalClose = stream.close; - stream.close = () => { - originalClose.call(stream); - throw new Error('Crash!'); - }; - return stream; - }); - - let zipPath = `${options.outDir}/${options.outFile}`; - - mockDoPostRequest(); - - //the file should exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.true; - await rokuDeploy.publish({ ...options, retainDeploymentArchive: false }); - //the file should not exist - expect(fsExtra.pathExistsSync(zipPath)).to.be.false; - //the out folder should also be deleted since it's empty - }); - - it('fails when the zip file is missing', async () => { - options.outFile = 'fileThatDoesNotExist.zip'; - await expectThrowsAsync(async () => { - await rokuDeploy.publish(options); - }, `Cannot publish because file does not exist at '${rokuDeploy.getOutputZipFilePath(options)}'`); - }); - - it('fails when no host is provided', () => { - expectPathNotExists('rokudeploy.json'); - return rokuDeploy.publish({ host: undefined }).then(() => { - assert.fail('Should not have succeeded'); - }, () => { - expect(true).to.be.true; - }); - }); - - it('throws when package upload fails', async () => { - //intercept the post requests - sinon.stub(request, 'post').callsFake((data: any, callback: any) => { - if (data.url === `http://${options.host}/plugin_install`) { - process.nextTick(() => { - callback(new Error('Failed to publish to server')); - }); - } else { - process.nextTick(callback); - } - return {} as any; - }); - - try { - await rokuDeploy.publish(options); - } catch (e) { - assert.ok('Exception was thrown as expected'); - return; - } - assert.fail('Should not have succeeded'); - }); - - it('rejects as CompileError when initial replace fails', () => { - options.failOnCompileError = true; - mockDoPostRequest(` - Install Failure: Compilation Failed. - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); - `); - - return rokuDeploy.publish(options).then(() => { - assert.fail('Should not have succeeded due to roku server compilation failure'); - }, (err) => { - expect(err).to.be.instanceOf(errors.CompileError); - }); - }); - - it('rejects as CompileError when initial replace fails', () => { - options.failOnCompileError = true; - mockDoPostRequest(` - Install Failure: Compilation Failed. - Shell.create('Roku.Message').trigger('Set message type', 'error').trigger('Set message content', 'Install Failure: Compilation Failed').trigger('Render', node); - `); - - return rokuDeploy.publish(options).then(() => { - assert.fail('Should not have succeeded due to roku server compilation failure'); - }, (err) => { - expect(err).to.be.instanceOf(errors.CompileError); - }); - }); - - it('rejects when response contains compile error wording', () => { - options.failOnCompileError = true; - let body = 'Install Failure: Compilation Failed.'; - mockDoPostRequest(body); - - return rokuDeploy.publish(options).then(() => { - assert.fail('Should not have succeeded due to roku server compilation failure'); - }, (err) => { - expect(err.message).to.equal('Compile error'); - expect(true).to.be.true; - }); - }); - - it('checkRequest handles edge case', () => { - function doTest(results, hostValue = undefined) { - let error: Error; - try { - rokuDeploy['checkRequest'](results); - } catch (e) { - error = e as any; - } - expect(error.message).to.eql(`Unauthorized. Please verify credentials for host '${hostValue}'`); - } - doTest({ body: 'something', response: { statusCode: 401, request: { host: '1.1.1.1' } } }, '1.1.1.1'); - doTest({ body: 'something', response: { statusCode: 401, request: { host: undefined } } }); - doTest({ body: 'something', response: { statusCode: 401, request: undefined } }); - }); - - it('rejects when response contains invalid password status code', () => { - options.failOnCompileError = true; - mockDoPostRequest('', 401); - - return rokuDeploy.publish(options).then(() => { - assert.fail('Should not have succeeded due to roku server compilation failure'); - }, (err) => { - expect(err.message).to.be.a('string').and.satisfy(msg => msg.startsWith('Unauthorized. Please verify credentials for host')); - expect(true).to.be.true; - }); - }); - - it('handles successful deploy', () => { - options.failOnCompileError = true; - mockDoPostRequest(); - - return rokuDeploy.publish(options).then((result) => { - expect(result.message).to.equal('Successful deploy'); - }, () => { - assert.fail('Should not have rejected the promise'); - }); - }); - - it('handles successful deploy with remoteDebug', () => { - options.failOnCompileError = true; - options.remoteDebug = true; - const stub = mockDoPostRequest(); - - return rokuDeploy.publish(options).then((result) => { - expect(result.message).to.equal('Successful deploy'); - expect(stub.getCall(0).args[0].formData.remotedebug).to.eql('1'); - }, () => { - assert.fail('Should not have rejected the promise'); - }); - }); - - it('handles successful deploy with remotedebug_connect_early', () => { - options.failOnCompileError = true; - options.remoteDebug = true; - options.remoteDebugConnectEarly = true; - const stub = mockDoPostRequest(); - - return rokuDeploy.publish(options).then((result) => { - expect(result.message).to.equal('Successful deploy'); - expect(stub.getCall(0).args[0].formData.remotedebug_connect_early).to.eql('1'); - }, () => { - assert.fail('Should not have rejected the promise'); - }); - }); - - it('Does not reject when response contains compile error wording but config is set to ignore compile warnings', () => { - options.failOnCompileError = false; - - let body = 'Identical to previous version -- not replacing.'; - mockDoPostRequest(body); - - return rokuDeploy.publish(options).then((result) => { - expect(result.results.body).to.equal(body); - }, () => { - assert.fail('Should have resolved promise'); - }); - }); - - it('rejects when response is unknown status code', async () => { - options.failOnCompileError = true; - let body = 'Identical to previous version -- not replacing.'; - mockDoPostRequest(body, 123); - - try { - await rokuDeploy.publish(options); - } catch (e) { - expect(e).to.be.instanceof(errors.InvalidDeviceResponseCodeError); - return; - } - assert.fail('Should not have succeeded'); - }); - - it('rejects when user is unauthorized', async () => { - options.failOnCompileError = true; - mockDoPostRequest('', 401); - - try { - await rokuDeploy.publish(options); - } catch (e) { - expect(e).to.be.instanceof(errors.UnauthorizedDeviceResponseError); - return; - } - assert.fail('Should not have succeeded'); - }); - - it('rejects when encountering an undefined response', async () => { - options.failOnCompileError = true; - mockDoPostRequest(null); - - try { - await rokuDeploy.publish(options); - } catch (e) { - assert.ok('Exception was thrown as expected'); - return; - } - assert.fail('Should not have succeeded'); - }); - }); - - describe('convertToSquashfs', () => { - it('should not return an error if successful', async () => { - mockDoPostRequest('Conversion succeeded


Parallel mksquashfs: Using 1 processor'); - await rokuDeploy.convertToSquashfs(options); - }); - - it('should return MissingRequiredOptionError if host was not provided', async () => { - mockDoPostRequest(); - try { - options.host = undefined; - await rokuDeploy.convertToSquashfs(options); - } catch (e) { - expect(e).to.be.instanceof(errors.MissingRequiredOptionError); - return; - } - assert.fail('Should not have succeeded'); - }); - - it('should return ConvertError if converting failed', async () => { - mockDoPostRequest(); - try { - await rokuDeploy.convertToSquashfs(options); - } catch (e) { - expect(e).to.be.instanceof(errors.ConvertError); - return; - } - assert.fail('Should not have succeeded'); - }); - }); - - describe('rekeyDevice', () => { - beforeEach(() => { - const body = ` - ${options.devId} - `; - mockDoGetRequest(body); - fsExtra.outputFileSync(path.resolve(rootDir, options.rekeySignedPackage), ''); - }); - - it('does not crash when archive is undefined', async () => { - const expectedError = new Error('Custom error'); - sinon.stub(fsExtra, 'createReadStream').throws(expectedError); - let actualError: Error; - try { - await rokuDeploy.rekeyDevice(options); - } catch (e) { - actualError = e as Error; - } - expect(actualError).to.equal(expectedError); - }); - - it('should work with relative path', async () => { - let body = `
- Success. -
`; - mockDoPostRequest(body); - options.rekeySignedPackage = s`../notReal.pkg`; - try { - fsExtra.writeFileSync(s`${tempDir}/notReal.pkg`, ''); - await rokuDeploy.rekeyDevice(options); - } finally { - fsExtra.removeSync(s`${tempDir}/notReal.pkg`); - } - }); - - it('should work with absolute path', async () => { - let body = `
- Success. -
`; - mockDoPostRequest(body); - - options.rekeySignedPackage = s`${tempDir}/testSignedPackage.pkg`; - await rokuDeploy.rekeyDevice(options); - }); - - it('should not return an error if dev ID is set and matches output', async () => { - let body = `
- Success. -
`; - mockDoPostRequest(body); - await rokuDeploy.rekeyDevice(options); - }); - - it('should not return an error if dev ID is not set', async () => { - let body = `
- Success. -
`; - mockDoPostRequest(body); - options.devId = undefined; - await rokuDeploy.rekeyDevice(options); - }); - - it('should throw error if missing rekeySignedPackage option', async () => { - try { - options.rekeySignedPackage = null; - await rokuDeploy.rekeyDevice(options); - } catch (e) { - expect(e).to.be.instanceof(errors.MissingRequiredOptionError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should throw error if missing signingPassword option', async () => { - try { - options.signingPassword = null; - await rokuDeploy.rekeyDevice(options); - } catch (e) { - expect(e).to.be.instanceof(errors.MissingRequiredOptionError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should throw error if response is not parsable', async () => { - try { - mockDoPostRequest(); - await rokuDeploy.rekeyDevice(options); - } catch (e) { - expect(e).to.be.instanceof(errors.UnparsableDeviceResponseError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should throw error if we could not verify a successful call', async () => { - try { - let body = `
- Invalid public key. -
`; - mockDoPostRequest(body); - await rokuDeploy.rekeyDevice(options); - } catch (e) { - expect(e).to.be.instanceof(errors.FailedDeviceResponseError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should throw error if resulting Dev ID is not the one we are expecting', async () => { - try { - let body = `
- Success. -
`; - mockDoPostRequest(body); - - options.devId = '45fdc2019903ac333ff624b0b2cddd2c733c3e74'; - await rokuDeploy.rekeyDevice(options); - } catch (e) { - expect(e).to.be.instanceof(errors.UnknownDeviceResponseError); - return; - } - assert.fail('Exception should have been thrown'); - }); - }); - - describe('signExistingPackage', () => { - beforeEach(() => { - fsExtra.outputFileSync(`${stagingDir}/manifest`, ``); - }); - - it('should return our error if signingPassword is not supplied', async () => { - options.signingPassword = undefined; - await expectThrowsAsync(async () => { - await rokuDeploy.signExistingPackage(options); - }, 'Must supply signingPassword'); - }); - - it('should return an error if there is a problem with the network request', async () => { - let error = new Error('Network Error'); - try { - //intercept the post requests - sinon.stub(request, 'post').callsFake((_, callback) => { - process.nextTick(callback, error); - return {} as any; - }); - await rokuDeploy.signExistingPackage(options); - } catch (e) { - expect(e).to.equal(error); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should return our error if it received invalid data', async () => { - try { - mockDoPostRequest(null); - await rokuDeploy.signExistingPackage(options); - } catch (e) { - expect(e).to.be.instanceof(errors.UnparsableDeviceResponseError); - return; - } - assert.fail('Exception should have been thrown'); - }); - - it('should return an error if failure returned in response', async () => { - let body = `
- Failed: Invalid Password. - -
`; - mockDoPostRequest(body); - - await expectThrowsAsync( - rokuDeploy.signExistingPackage(options), - 'Invalid Password.' - ); - }); - - it('should return created pkg on success', async () => { - let body = `var pkgDiv = document.createElement('div'); - pkgDiv.innerHTML = '
P6953175d5df120c0069c53de12515b9a.pkg
package file (7360 bytes)
'; - node.appendChild(pkgDiv);`; - mockDoPostRequest(body); - - let pkgPath = await rokuDeploy.signExistingPackage(options); - expect(pkgPath).to.equal('pkgs//P6953175d5df120c0069c53de12515b9a.pkg'); - }); - - it('should return our fallback error if neither error or package link was detected', async () => { - mockDoPostRequest(); - await expectThrowsAsync( - rokuDeploy.signExistingPackage(options), - 'Unknown error signing package' - ); - }); - }); - - describe('prepublishToStaging', () => { - it('should use outDir for staging folder', async () => { - await rokuDeploy.prepublishToStaging({ - files: [ - 'manifest' - ] - }); - expectPathExists(`${stagingDir}`); - }); - - it('should support overriding the staging folder', async () => { - await rokuDeploy.prepublishToStaging({ - ...options, - files: ['manifest'], - stagingDir: `${tempDir}/custom-out-dir` - }); - expectPathExists(`${tempDir}/custom-out-dir`); - }); - - it('handles old glob-style', async () => { - writeFiles(rootDir, [ - 'manifest', - 'source/main.brs' - ]); - options.files = [ - 'manifest', - 'source/main.brs' - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/manifest`); - expectPathExists(`${stagingDir}/source/main.brs`); - }); - - it('handles copying a simple directory by name using src;dest;', async () => { - writeFiles(rootDir, [ - 'manifest', - 'source/main.brs' - ]); - options.files = [ - 'manifest', - { - src: 'source/**/*', - dest: 'source' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/source/main.brs`); - }); - - it('handles new src;dest style', async () => { - writeFiles(rootDir, [ - 'manifest', - 'source/main.brs' - ]); - options.files = [ - { - src: 'manifest', - dest: '' - }, - { - src: 'source/**/*', - dest: 'source/' - }, - { - src: 'source/main.brs', - dest: 'source/main.brs' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/manifest`); - expectPathExists(`${stagingDir}/source/main.brs`); - }); - - it('handles renaming files', async () => { - writeFiles(rootDir, [ - 'manifest', - 'source/main.brs' - ]); - options.files = [ - { - src: 'manifest', - dest: '' - }, - { - src: 'source/main.brs', - dest: 'source/renamed.brs' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/source/renamed.brs`); - }); - - it('handles absolute src paths', async () => { - writeFiles(rootDir, [ - 'manifest' - ]); - options.files = [ - { - src: `${rootDir}/manifest`, - dest: '' - }, - { - src: 'source/main.brs', - dest: 'source/renamed.brs' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/manifest`); - }); - - it('handles excluded folders in glob pattern', async () => { - writeFiles(rootDir, [ - 'manifest', - 'components/loader/loader.brs', - 'components/scenes/home/home.brs' - ]); - options.files = [ - 'manifest', - 'components/!(scenes)/**/*' - ]; - options.retainStagingFolder = true; - console.log('before'); - await rokuDeploy.prepublishToStaging(options); - console.log('after'); - expectPathExists(s`${stagingDir}/components/loader/loader.brs`); - expectPathNotExists(s`${stagingDir}/components/scenes/home/home.brs`); - }); - - it('handles multi-globs', async () => { - writeFiles(rootDir, [ - 'manifest', - 'components/Loader/Loader.brs', - 'components/scenes/Home/Home.brs' - ]); - options.retainStagingFolder = true; - await rokuDeploy.prepublishToStaging({ - ...options, files: [ - 'manifest', - 'source', - 'components/**/*', - '!components/scenes/**/*' - ] - }); - expectPathExists(`${stagingDir}/components/Loader/Loader.brs`); - expectPathNotExists(`${stagingDir}/components/scenes/Home/Home.brs`); - }); - - it('throws on invalid entries', async () => { - options.files = [ - 'manifest', - {} - ]; - options.retainStagingFolder = true; - try { - await rokuDeploy.prepublishToStaging(options); - expect(true).to.be.false; - } catch (e) { - expect(true).to.be.true; - } - }); - - it('retains subfolder structure when referencing a folder', async () => { - fsExtra.outputFileSync(`${rootDir}/flavors/shared/resources/images/fhd/image.jpg`, ''); - options.files = [ - 'manifest', - { - src: 'flavors/shared/resources/**/*', - dest: 'resources' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(`${stagingDir}/resources/images/fhd/image.jpg`); - }); - - it('handles multi-globs subfolder structure', async () => { - writeFiles(rootDir, [ - 'manifest', - 'flavors/shared/resources/images/fhd/image.jpg', - 'resources/image.jpg' - ]); - options.files = [ - 'manifest', - { - //the relative structure after /resources should be retained - src: 'flavors/shared/resources/**/*', - dest: 'resources' - } - ]; - await rokuDeploy.prepublishToStaging(options); - expectPathExists(s`${stagingDir}/resources/images/fhd/image.jpg`); - expectPathNotExists(s`${stagingDir}/resources/image.jpg`); - }); - - describe('symlinks', () => { - let sourcePath = s`${tempDir}/test.md`; - let symlinkPath = s`${rootDir}/renamed_test.md`; - - beforeEach(cleanUp); - afterEach(cleanUp); - - function cleanUp() { - try { - fsExtra.removeSync(sourcePath); - } catch (e) { } - //delete the symlink if it exists - try { - fsExtra.removeSync(symlinkPath); - } catch (e) { } - } - - let _isSymlinkingPermitted: boolean; - - /** - * Determine if we have permission to create symlinks - */ - function getIsSymlinksPermitted() { - if (_isSymlinkingPermitted === undefined) { - fsExtra.ensureDirSync(`${tempDir}/project`); - fsExtra.outputFileSync(`${tempDir}/a/alpha.txt`, 'alpha.txt'); - fsExtra.outputFileSync(`${tempDir}/a/b/c/charlie.txt`, 'charlie.txt'); - - try { - //make a file symlink - fsExtra.symlinkSync(`${tempDir}/a/alpha.txt`, `${tempDir}/project/alpha.txt`); - //create a folder symlink that also includes subfolders - fsExtra.symlinkSync(`${tempDir}/a`, `${tempDir}/project/a`); - //use glob to scan the directory recursively - glob.sync('**/*', { - cwd: s`${tempDir}/project`, - absolute: true, - follow: true - }); - _isSymlinkingPermitted = true; - } catch (e) { - _isSymlinkingPermitted = false; - return false; - } - } - return _isSymlinkingPermitted; - } - - function symlinkIt(name, callback) { - if (getIsSymlinksPermitted()) { - console.log(`symlinks are permitted for test "${name}"`); - it(name, callback); - } else { - console.log(`symlinks are not permitted for test "${name}"`); - it.skip(name, callback); - } - } - - symlinkIt('direct symlinked files are dereferenced properly', async () => { - //create the actual file - fsExtra.outputFileSync(sourcePath, 'hello symlink'); - - //the source file should exist - expectPathExists(sourcePath); - - //create the symlink in testProject - fsExtra.symlinkSync(sourcePath, symlinkPath); - - //the symlink file should exist - expectPathExists(symlinkPath); - let opts = { - ...options, - rootDir: rootDir, - files: [ - 'manifest', - 'renamed_test.md' - ] - }; - - let stagingDirValue = rokuDeploy.getOptions(opts).stagingDir; - //getFilePaths detects the file - expect(await rokuDeploy.getFilePaths(['renamed_test.md'], opts.rootDir)).to.eql([{ - src: s`${opts.rootDir}/renamed_test.md`, - dest: s`renamed_test.md` - }]); - - await rokuDeploy.prepublishToStaging(opts); - let stagedFilePath = s`${stagingDirValue}/renamed_test.md`; - expectPathExists(stagedFilePath); - let fileContents = await fsExtra.readFile(stagedFilePath); - expect(fileContents.toString()).to.equal('hello symlink'); - }); - - symlinkIt('copies files from subdirs of symlinked folders', async () => { - fsExtra.ensureDirSync(s`${tempDir}/baseProject/source/lib/promise`); - fsExtra.writeFileSync(s`${tempDir}/baseProject/source/lib/lib.brs`, `'lib.brs`); - fsExtra.writeFileSync(s`${tempDir}/baseProject/source/lib/promise/promise.brs`, `'q.brs`); - - fsExtra.ensureDirSync(s`${tempDir}/mainProject/source`); - fsExtra.writeFileSync(s`${tempDir}/mainProject/source/main.brs`, `'main.brs`); - - //symlink the baseProject lib folder into the mainProject - fsExtra.symlinkSync(s`${tempDir}/baseProject/source/lib`, s`${tempDir}/mainProject/source/lib`); - - //the symlinked file should exist in the main project - expect(fsExtra.pathExistsSync(s`${tempDir}/baseProject/source/lib/promise/promise.brs`)).to.be.true; - - let opts = { - ...options, - rootDir: s`${tempDir}/mainProject`, - files: [ - 'manifest', - 'source/**/*' - ] - }; - - let stagingPath = rokuDeploy.getOptions(opts).stagingDir; - //getFilePaths detects the file - expect( - (await rokuDeploy.getFilePaths(opts.files, opts.rootDir)).sort((a, b) => a.src.localeCompare(b.src)) - ).to.eql([{ - src: s`${tempDir}/mainProject/source/lib/lib.brs`, - dest: s`source/lib/lib.brs` - }, { - src: s`${tempDir}/mainProject/source/lib/promise/promise.brs`, - dest: s`source/lib/promise/promise.brs` - }, { - src: s`${tempDir}/mainProject/source/main.brs`, - dest: s`source/main.brs` - }]); - - await rokuDeploy.prepublishToStaging(opts); - expect(fsExtra.pathExistsSync(`${stagingPath}/source/lib/promise/promise.brs`)); - }); - }); - }); - - describe('normalizeFilesArray', () => { - it('catches invalid dest entries', () => { - expect(() => { - rokuDeploy['normalizeFilesArray']([{ - src: 'some/path', - dest: true - }]); - }).to.throw(); - - expect(() => { - rokuDeploy['normalizeFilesArray']([{ - src: 'some/path', - dest: false - }]); - }).to.throw(); - - expect(() => { - rokuDeploy['normalizeFilesArray']([{ - src: 'some/path', - dest: /asdf/gi - }]); - }).to.throw(); - - expect(() => { - rokuDeploy['normalizeFilesArray']([{ - src: 'some/path', - dest: {} - }]); - }).to.throw(); - - expect(() => { - rokuDeploy['normalizeFilesArray']([{ - src: 'some/path', - dest: [] - }]); - }).to.throw(); - }); - - it('normalizes directory separators paths', () => { - expect(rokuDeploy['normalizeFilesArray']([{ - src: `long/source/path`, - dest: `long/dest/path` - }])).to.eql([{ - src: s`long/source/path`, - dest: s`long/dest/path` - }]); - }); - - it('works for simple strings', () => { - expect(rokuDeploy['normalizeFilesArray']([ - 'manifest', - 'source/main.brs' - ])).to.eql([ - 'manifest', - 'source/main.brs' - ]); - }); - - it('works for negated strings', () => { - expect(rokuDeploy['normalizeFilesArray']([ - '!.git' - ])).to.eql([ - '!.git' - ]); - }); - - it('skips falsey and bogus entries', () => { - expect(rokuDeploy['normalizeFilesArray']([ - '', - 'manifest', - false, - undefined, - null - ])).to.eql([ - 'manifest' - ]); - }); - - it('works for {src:string} objects', () => { - expect(rokuDeploy['normalizeFilesArray']([ - { - src: 'manifest' - } - ])).to.eql([{ - src: 'manifest', - dest: undefined - }]); - }); - - it('works for {src:string[]} objects', () => { - expect(rokuDeploy['normalizeFilesArray']([ - { - src: [ - 'manifest', - 'source/main.brs' - ] - } - ])).to.eql([{ - src: 'manifest', - dest: undefined - }, { - src: s`source/main.brs`, - dest: undefined - }]); - }); - - it('retains dest option', () => { - expect(rokuDeploy['normalizeFilesArray']([ - { - src: 'source/config.dev.brs', - dest: 'source/config.brs' - } - ])).to.eql([{ - src: s`source/config.dev.brs`, - dest: s`source/config.brs` - }]); - }); - - it('throws when encountering invalid entries', () => { - expect(() => rokuDeploy['normalizeFilesArray']([true])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([/asdf/])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([new Date()])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([1])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: true }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: /asdf/ }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: new Date() }])).to.throw(); - expect(() => rokuDeploy['normalizeFilesArray']([{ src: 1 }])).to.throw(); - }); - }); - - describe('deploy', () => { - it('does the whole migration', async () => { - mockDoPostRequest(); - - writeFiles(rootDir, ['manifest']); - - let result = await rokuDeploy.deploy(options); - expect(result).not.to.be.undefined; - }); - - it('continues with deploy if deleteInstalledChannel fails', async () => { - sinon.stub(rokuDeploy, 'deleteInstalledChannel').returns( - Promise.reject( - new Error('failed') - ) - ); - mockDoPostRequest(); - let result = await rokuDeploy.deploy({ - ...options, - //something in the previous test is locking the default output zip file. We should fix that at some point... - outDir: s`${tempDir}/test1` - }); - expect(result).not.to.be.undefined; - }); - - it('should delete installed channel if requested', async () => { - const spy = sinon.spy(rokuDeploy, 'deleteInstalledChannel'); - options.deleteInstalledChannel = true; - mockDoPostRequest(); - - await rokuDeploy.deploy(options); - - expect(spy.called).to.equal(true); - }); - - it('should not delete installed channel if not requested', async () => { - const spy = sinon.spy(rokuDeploy, 'deleteInstalledChannel'); - options.deleteInstalledChannel = false; - mockDoPostRequest(); - - await rokuDeploy.deploy(options); - - expect(spy.notCalled).to.equal(true); - }); - }); - - describe('deleteInstalledChannel', () => { - it('attempts to delete any installed dev channel on the device', async () => { - mockDoPostRequest(); - - let result = await rokuDeploy.deleteInstalledChannel(options); - expect(result).not.to.be.undefined; - }); - }); - - describe('takeScreenshot', () => { - let onHandler: any; - let screenshotAddress: any; - - beforeEach(() => { - - //intercept the http request - sinon.stub(request, 'get').callsFake(() => { - let req: any = { - on: (event, callback) => { - process.nextTick(() => { - onHandler(event, callback); - }); - return req; - }, - pipe: async () => { - const writeStream = await writeStreamPromise; - writeStream.write(Buffer.from('test-content')); - writeStream.close(); - } - }; - return req; - }); - }); - - afterEach(() => { - if (screenshotAddress) { - fsExtra.removeSync(screenshotAddress); - } - onHandler = null; - screenshotAddress = null; - }); - - it('throws when there is no image returned', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = ''; - node.appendChild(screenshoot); - `); - - mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); - }); - - it('throws when there is no response body', async () => { - // missing body - mockDoPostRequest(null); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); - }); - - it('throws when there is an empty response body', async () => { - // empty body - mockDoPostRequest(); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); - }); - - it('throws when there is an error downloading the image from device', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 404 - }); - } - }; - - mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.takeScreenshot({ host: options.host, password: options.password })); - }); - - it('handles the device returning a png', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); - expect(result).not.to.be.undefined; - expect(path.extname(result)).to.equal('.png'); - expect(fsExtra.existsSync(result)); - }); - - it('handles the device returning a jpg', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); - expect(result).not.to.be.undefined; - expect(path.extname(result)).to.equal('.jpg'); - expect(fsExtra.existsSync(result)); - }); - - it('take a screenshot from the device and saves to supplied dir', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: `${tempDir}/myScreenShots` }); - expect(result).not.to.be.undefined; - expect(util.standardizePath(`${tempDir}/myScreenShots`)).to.equal(path.dirname(result)); - expect(fsExtra.existsSync(result)); - }); - - it('saves to specified file', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my' }); - expect(result).not.to.be.undefined; - expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); - expect(fsExtra.existsSync(path.join(tempDir, 'my.png'))); - }); - - it('saves to specified file ignoring supplied file extension', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outDir: tempDir, outFile: 'my.jpg' }); - expect(result).not.to.be.undefined; - expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); - expect(fsExtra.existsSync(path.join(tempDir, 'my.jpg.png'))); - }); - - it('take a screenshot from the device and saves to temp', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password }); - expect(result).not.to.be.undefined; - expect(fsExtra.existsSync(result)); - }); - - it('take a screenshot from the device and saves to temp but with the supplied file name', async () => { - let body = getFakeResponseBody(` - Shell.create('Roku.Message').trigger('Set message type', 'success').trigger('Set message content', 'Screenshot ok').trigger('Render', node); - - var screenshoot = document.createElement('div'); - screenshoot.innerHTML = '
'; - node.appendChild(screenshoot); - `); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - mockDoPostRequest(body); - let result = await rokuDeploy.takeScreenshot({ host: options.host, password: options.password, outFile: 'myFile' }); - expect(result).not.to.be.undefined; - expect(path.basename(result)).to.equal('myFile.jpg'); - expect(fsExtra.existsSync(result)); - }); - }); - - describe('zipFolder', () => { - //this is mainly done to hit 100% coverage, but why not ensure the errors are handled properly? :D - it('rejects the promise when an error occurs', async () => { - //zip path doesn't exist - await assertThrowsAsync(async () => { - await rokuDeploy.zipFolder('source', '.tmp/some/zip/path/that/does/not/exist'); - }); - }); - - it('allows modification of file contents with callback', async () => { - writeFiles(rootDir, [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]); - const stageFolder = path.join(tempDir, 'testProject'); - fsExtra.ensureDirSync(stageFolder); - const files = [ - 'components/components/Loader/Loader.brs', - 'images/splash_hd.jpg', - 'source/main.brs', - 'manifest' - ]; - for (const file of files) { - fsExtra.copySync(path.join(options.rootDir, file), path.join(stageFolder, file)); - } - - const outputZipPath = path.join(tempDir, 'output.zip'); - const addedManifestLine = 'bs_libs_required=roku_ads_lib'; - await rokuDeploy.zipFolder(stageFolder, outputZipPath, (file, data) => { - if (file.dest === 'manifest') { - let manifestContents = data.toString(); - manifestContents += addedManifestLine; - data = Buffer.from(manifestContents, 'utf8'); - } - return data; - }); - - const data = fsExtra.readFileSync(outputZipPath); - const zip = await JSZip.loadAsync(data); - for (const file of files) { - const zipFileContents = await zip.file(file.toString()).async('string'); - const sourcePath = path.join(options.rootDir, file); - const incomingContents = fsExtra.readFileSync(sourcePath, 'utf8'); - if (file === 'manifest') { - expect(zipFileContents).to.contain(addedManifestLine); - } else { - expect(zipFileContents).to.equal(incomingContents); - } - } - }); - - it('filters the folders before making the zip', async () => { - const files = [ - 'components/MainScene.brs', - 'components/MainScene.brs.map', - 'images/splash_hd.jpg', - 'source/main.brs', - 'source/main.brs.map', - 'manifest' - ]; - writeFiles(stagingDir, files); - - const outputZipPath = path.join(tempDir, 'output.zip'); - await rokuDeploy.zipFolder(stagingDir, outputZipPath, null, ['**/*', '!**/*.map']); - - const data = fsExtra.readFileSync(outputZipPath); - const zip = await JSZip.loadAsync(data); - //the .map files should be missing - expect( - Object.keys(zip.files).sort() - ).to.eql( - [ - 'source/', - 'images/', - 'components/', - ...files - ].sort().filter(x => !x.endsWith('.map')) - ); - }); - }); - - describe('parseManifest', () => { - it('correctly parses valid manifest', async () => { - fsExtra.outputFileSync(`${rootDir}/manifest`, `title=AwesomeApp`); - let parsedManifest = await rokuDeploy.parseManifest(`${rootDir}/manifest`); - expect(parsedManifest.title).to.equal('AwesomeApp'); - }); - - it('Throws our error message for a missing file', async () => { - await expectThrowsAsync( - rokuDeploy.parseManifest('invalid-path'), - `invalid-path does not exist` - ); - }); - }); - - describe('parseManifestFromString', () => { - it('correctly parses valid manifest', () => { - let parsedManifest = rokuDeploy.parseManifestFromString(` - title=RokuDeployTestChannel - major_version=1 - minor_version=0 - build_version=0 - splash_screen_hd=pkg:/images/splash_hd.jpg - ui_resolutions=hd - bs_const=IS_DEV_BUILD=false - splash_color=#000000 - `); - expect(parsedManifest.title).to.equal('RokuDeployTestChannel'); - expect(parsedManifest.major_version).to.equal('1'); - expect(parsedManifest.minor_version).to.equal('0'); - expect(parsedManifest.build_version).to.equal('0'); - expect(parsedManifest.splash_screen_hd).to.equal('pkg:/images/splash_hd.jpg'); - expect(parsedManifest.ui_resolutions).to.equal('hd'); - expect(parsedManifest.bs_const).to.equal('IS_DEV_BUILD=false'); - expect(parsedManifest.splash_color).to.equal('#000000'); - }); - }); - - describe('stringifyManifest', () => { - it('correctly converts back to a valid manifest when lineNumber and keyIndexes are provided', () => { - expect( - rokuDeploy.stringifyManifest( - rokuDeploy.parseManifestFromString('major_version=3\nminor_version=4') - ) - ).to.equal( - 'major_version=3\nminor_version=4' - ); - }); - - it('correctly converts back to a valid manifest when lineNumber and keyIndexes are not provided', () => { - const parsed = rokuDeploy.parseManifestFromString('title=App\nmajor_version=3'); - delete parsed.keyIndexes; - delete parsed.lineCount; - let outputParsedManifest = rokuDeploy.parseManifestFromString( - rokuDeploy.stringifyManifest(parsed) - ); - expect(outputParsedManifest.title).to.equal('App'); - expect(outputParsedManifest.major_version).to.equal('3'); - }); - }); - - describe('getFilePaths', () => { - const otherProjectName = 'otherProject'; - const otherProjectDir = s`${rootDir}/../${otherProjectName}`; - //create baseline project structure - beforeEach(() => { - fsExtra.ensureDirSync(`${rootDir}/components/emptyFolder`); - writeFiles(rootDir, [ - `manifest`, - `source/main.brs`, - `source/lib.brs`, - `components/component1.xml`, - `components/component1.brs`, - `components/screen1/screen1.xml`, - `components/screen1/screen1.brs` - ]); - }); - - async function getFilePaths(files: FileEntry[], rootDirOverride = rootDir) { - return (await rokuDeploy.getFilePaths(files, rootDirOverride)) - .sort((a, b) => a.src.localeCompare(b.src)); - } - - describe('top-level-patterns', () => { - it('excludes a file that is negated', async () => { - expect(await getFilePaths([ - 'source/**/*', - '!source/main.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('excludes file from non-rootdir top-level pattern', async () => { - writeFiles(rootDir, ['../externalDir/source/main.brs']); - expect(await getFilePaths([ - '../externalDir/**/*', - '!../externalDir/**/*' - ])).to.eql([]); - }); - - it('throws when using top-level string referencing file outside the root dir', async () => { - writeFiles(rootDir, [`../source/main.brs`]); - await expectThrowsAsync(async () => { - await getFilePaths([ - '../source/**/*' - ]); - }, 'Cannot reference a file outside of rootDir when using a top-level string. Please use a src;des; object instead'); - }); - - it('works for brighterscript files', async () => { - writeFiles(rootDir, ['src/source/main.bs']); - expect(await getFilePaths([ - 'manifest', - 'source/**/*.bs' - ], s`${rootDir}/src`)).to.eql([{ - src: s`${rootDir}/src/source/main.bs`, - dest: s`source/main.bs` - }]); - }); - - it('works for root-level double star in top-level pattern', async () => { - expect(await getFilePaths([ - '**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for multile entries', async () => { - expect(await getFilePaths([ - 'source/**/*', - 'components/**/*', - 'manifest' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies top-level-string single-star globs', async () => { - writeFiles(rootDir, [ - 'source/lib.brs', - 'source/main.brs' - ]); - expect(await getFilePaths([ - 'source/*.brs' - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('works for double-star globs', async () => { - expect(await getFilePaths([ - '**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('copies subdir-level relative double-star globs', async () => { - expect(await getFilePaths([ - 'components/**/*.brs' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }]); - }); - - it('throws exception when top-level strings reference files not under rootDir', async () => { - writeFiles(otherProjectDir, [ - 'manifest' - ]); - await expectThrowsAsync( - getFilePaths([ - `../${otherProjectName}/**/*` - ]) - ); - }); - - it('applies negated patterns', async () => { - expect(await getFilePaths([ - //include all components - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - - it('handles negated multi-globs', async () => { - expect((await getFilePaths([ - 'components/**/*', - '!components/screen1/**/*' - ])).map(x => x.dest)).to.eql([ - s`components/component1.brs`, - s`components/component1.xml` - ]); - }); - - it('allows negating paths outside rootDir without requiring src;dest; syntax', async () => { - fsExtra.outputFileSync(`${rootDir}/../externalLib/source/lib.brs`, ''); - const filePaths = await getFilePaths([ - 'source/**/*', - { src: '../externalLib/**/*', dest: 'source' }, - '!../externalLib/source/**/*' - ], rootDir); - expect( - filePaths.map(x => s`${x.src}`).sort() - ).to.eql([ - s`${rootDir}/source/lib.brs`, - s`${rootDir}/source/main.brs` - ]); - }); - - it('applies multi-glob paths relative to rootDir', async () => { - expect(await getFilePaths([ - 'manifest', - 'source/**/*', - 'components/**/*', - '!components/scenes/**/*' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('ignores non-glob folder paths', async () => { - expect(await getFilePaths([ - //this is the folder called "components" - 'components' - ])).to.eql([]); //there should be no matches because rokudeploy ignores folders - }); - - }); - - describe('{src;dest} objects', () => { - it('excludes a file that is negated in src;dest;', async () => { - expect(await getFilePaths([ - 'source/**/*', - { - src: '!source/main.brs' - } - ])).to.eql([{ - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }]); - }); - - it('works for root-level double star in {src;dest} object', async () => { - expect(await getFilePaths([{ - src: '**/*', - dest: '' - } - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`components/component1.xml` - }, - { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, - { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }, - { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }, - { - src: s`${rootDir}/source/lib.brs`, - dest: s`source/lib.brs` - }, - { - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('uses the root of staging folder for dest when not specified with star star', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/**/*` - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`source/thirdPartyLib.brs` - }]); - }); - - it('copies absolute path files to specified dest', async () => { - writeFiles(otherProjectDir, [ - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `${otherProjectDir}/source/thirdPartyLib.brs`, - dest: 'lib/thirdPartyLib.brs' - }])).to.eql([{ - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`lib/thirdPartyLib.brs` - }]); - }); - - it('copies relative path files to specified dest', async () => { - const rootDirName = path.basename(rootDir); - writeFiles(rootDir, [ - 'source/main.brs' - ]); - expect(await getFilePaths([{ - src: `../${rootDirName}/source/main.brs`, - dest: 'source/main.brs' - }])).to.eql([{ - src: s`${rootDir}/source/main.brs`, - dest: s`source/main.brs` - }]); - }); - - it('maintains relative path after **', async () => { - writeFiles(otherProjectDir, [ - 'components/component1/subComponent/screen.brs', - 'manifest', - 'source/thirdPartyLib.brs' - ]); - expect(await getFilePaths([{ - src: `../otherProject/**/*`, - dest: 'outFolder/' - }])).to.eql([{ - src: s`${otherProjectDir}/components/component1/subComponent/screen.brs`, - dest: s`outFolder/components/component1/subComponent/screen.brs` - }, { - src: s`${otherProjectDir}/manifest`, - dest: s`outFolder/manifest` - }, { - src: s`${otherProjectDir}/source/thirdPartyLib.brs`, - dest: s`outFolder/source/thirdPartyLib.brs` - }]); - }); - - it('works for other globs', async () => { - expect(await getFilePaths([{ - src: `components/screen1/*creen1.brs`, - dest: s`/source` - }])).to.eql([{ - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`source/screen1.brs` - }]); - }); - - it('works for other globs without dest', async () => { - expect(await getFilePaths([{ - src: `components/screen1/*creen1.brs` - }])).to.eql([{ - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`screen1.brs` - }]); - }); - - it('skips directory folder names for other globs without dest', async () => { - expect(await getFilePaths([{ - //straight wildcard matches folder names too - src: `components/*` - }])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`component1.brs` - }, { - src: s`${rootDir}/components/component1.xml`, - dest: s`component1.xml` - }]); - }); - - it('applies negated patterns', async () => { - writeFiles(rootDir, [ - 'components/component1.brs', - 'components/component1.xml', - 'components/screen1/screen1.brs', - 'components/screen1/screen1.xml' - ]); - expect(await getFilePaths([ - //include all component brs files - 'components/**/*.brs', - //exclude all xml files - '!components/**/*.xml', - //re-include a specific xml file - 'components/screen1/screen1.xml' - ])).to.eql([{ - src: s`${rootDir}/components/component1.brs`, - dest: s`components/component1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: s`components/screen1/screen1.brs` - }, { - src: s`${rootDir}/components/screen1/screen1.xml`, - dest: s`components/screen1/screen1.xml` - }]); - }); - }); - - it('converts relative rootDir path to absolute', async () => { - let stub = sinon.stub(rokuDeploy, 'getOptions').callThrough(); - await getFilePaths([ - 'source/main.brs' - ], './rootDir'); - expect(stub.callCount).to.be.greaterThan(0); - expect(stub.getCall(0).args[0].rootDir).to.eql('./rootDir'); - expect(stub.getCall(0).returnValue.rootDir).to.eql(s`${cwd}/rootDir`); - }); - - it('works when using a different current working directory than rootDir', async () => { - writeFiles(rootDir, [ - 'manifest', - 'images/splash_hd.jpg' - ]); - //sanity check, make sure it works without fiddling with cwd intact - let paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); - - //change the working directory and verify everything still works - - let wrongCwd = path.dirname(path.resolve(options.rootDir)); - process.chdir(wrongCwd); - - paths = await getFilePaths([ - 'manifest', - 'images/splash_hd.jpg' - ]); - - expect(paths).to.eql([{ - src: s`${rootDir}/images/splash_hd.jpg`, - dest: s`images/splash_hd.jpg` - }, { - src: s`${rootDir}/manifest`, - dest: s`manifest` - }]); - }); - - it('supports absolute paths from outside of the rootDir', async () => { - options = rokuDeploy.getOptions(options); - - //dest not specified - expect(await rokuDeploy.getFilePaths([{ - src: s`${cwd}/README.md` - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`README.md` - }]); - - //dest specified - expect(await rokuDeploy.getFilePaths([{ - src: path.join(cwd, 'README.md'), - dest: 'docs/README.md' - }], options.rootDir)).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - let paths: any[]; - - paths = await rokuDeploy.getFilePaths([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }], outDir); - - expect(paths).to.eql([{ - src: s`${cwd}/README.md`, - dest: s`docs/README.md` - }]); - - //top-level string paths pointing to files outside the root should thrown an exception - await expectThrowsAsync(async () => { - paths = await rokuDeploy.getFilePaths([ - s`${cwd}/README.md` - ], outDir); - }); - }); - - it('supports relative paths that grab files from outside of the rootDir', async () => { - writeFiles(`${rootDir}/../`, [ - 'README.md' - ]); - expect( - await rokuDeploy.getFilePaths([{ - src: path.join('..', 'README.md') - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`README.md` - }]); - - expect( - await rokuDeploy.getFilePaths([{ - src: path.join('..', 'README.md'), - dest: 'docs/README.md' - }], rootDir) - ).to.eql([{ - src: s`${rootDir}/../README.md`, - dest: s`docs/README.md` - }]); - }); - - it('should throw exception because we cannot have top-level string paths pointed to files outside the root', async () => { - writeFiles(rootDir, [ - '../README.md' - ]); - await expectThrowsAsync( - rokuDeploy.getFilePaths([ - path.join('..', 'README.md') - ], outDir) - ); - }); - - it('supports overriding paths', async () => { - let paths = await rokuDeploy.getFilePaths([{ - src: s`${rootDir}/components/component1.brs`, - dest: 'comp1.brs' - }, { - src: s`${rootDir}/components/screen1/screen1.brs`, - dest: 'comp1.brs' - }], rootDir); - expect(paths).to.be.lengthOf(1); - expect(s`${paths[0].src}`).to.equal(s`${rootDir}/components/screen1/screen1.brs`); - }); - - it('supports overriding paths from outside the root dir', async () => { - let thisRootDir = s`${tempDir}/tempTestOverrides/src`; - try { - - fsExtra.ensureDirSync(s`${thisRootDir}/source`); - fsExtra.ensureDirSync(s`${thisRootDir}/components`); - fsExtra.ensureDirSync(s`${thisRootDir}/../.tmp`); - - fsExtra.writeFileSync(s`${thisRootDir}/source/main.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.brs`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/components/MainScene.xml`, ''); - fsExtra.writeFileSync(s`${thisRootDir}/../.tmp/MainScene.brs`, ''); - - let files = [ - '**/*.xml', - '**/*.brs', - { - src: '../.tmp/MainScene.brs', - dest: 'components/MainScene.brs' - } - ]; - let paths = await rokuDeploy.getFilePaths(files, thisRootDir); - - //the MainScene.brs file from source should NOT be included - let mainSceneEntries = paths.filter(x => s`${x.dest}` === s`components/MainScene.brs`); - expect( - mainSceneEntries, - `Should only be one files entry for 'components/MainScene.brs'` - ).to.be.lengthOf(1); - expect(s`${mainSceneEntries[0].src}`).to.eql(s`${thisRootDir}/../.tmp/MainScene.brs`); - } finally { - //clean up - await fsExtra.remove(s`${thisRootDir}/../`); - } - }); - }); - - describe('computeFileDestPath', () => { - it('treats {src;dest} without dest as a top-level string', () => { - expect( - rokuDeploy['computeFileDestPath'](s`${rootDir}/source/main.brs`, { src: s`source/main.brs` } as any, rootDir) - ).to.eql(s`source/main.brs`); - }); - }); - - describe('getDestPath', () => { - it('handles unrelated exclusions properly', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/components/comp1/comp1.brs`, - [ - '**/*', - '!exclude.me' - ], - rootDir - ) - ).to.equal(s`components/comp1/comp1.brs`); - }); - - it('finds dest path for top-level path', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/components/comp1/comp1.brs`, - ['components/**/*'], - rootDir - ) - ).to.equal(s`components/comp1/comp1.brs`); - }); - - it('does not find dest path for non-matched top-level path', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - ['components/**/*'], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes a file that is negated', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - [ - 'source/**/*', - '!source/main.brs' - ], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes file from non-rootdir top-level pattern', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/../externalDir/source/main.brs`, - [ - '!../externalDir/**/*' - ], - rootDir - ) - ).to.be.undefined; - }); - - it('excludes a file that is negated in src;dest;', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/source/main.brs`, - [ - 'source/**/*', - { - src: '!source/main.brs' - } - ], - rootDir - ) - ).to.be.undefined; - }); - - it('works for brighterscript files', () => { - let destPath = rokuDeploy.getDestPath( - util.standardizePath(`${cwd}/src/source/main.bs`), - [ - 'manifest', - 'source/**/*.bs' - ], - s`${cwd}/src` - ); - expect(s`${destPath}`).to.equal(s`source/main.bs`); - }); - - it('excludes a file found outside the root dir', () => { - expect( - rokuDeploy.getDestPath( - s`${rootDir}/../source/main.brs`, - [ - '../source/**/*' - ], - rootDir - ) - ).to.be.undefined; - }); - }); - - describe('normalizeRootDir', () => { - it('handles falsey values', () => { - expect(rokuDeploy.normalizeRootDir(null)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir(undefined)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir(' ')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('\t')).to.equal(cwd); - }); - - it('handles non-falsey values', () => { - expect(rokuDeploy.normalizeRootDir(cwd)).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('./')).to.equal(cwd); - expect(rokuDeploy.normalizeRootDir('./testProject')).to.equal(path.join(cwd, 'testProject')); - }); - }); - - describe('retrieveSignedPackage', () => { - let onHandler: any; - beforeEach(() => { - sinon.stub(rokuDeploy.fsExtra, 'ensureDir').callsFake(((pth: string, callback: (err: Error) => void) => { - //do nothing, assume the dir gets created - }) as any); - - //intercept the http request - sinon.stub(request, 'get').callsFake(() => { - let req: any = { - on: (event, callback) => { - process.nextTick(() => { - onHandler(event, callback); - }); - return req; - }, - pipe: async () => { - //if a write stream gets created, write some stuff and close it - const writeStream = await writeStreamPromise; - writeStream.write('test'); - writeStream.close(); - } - }; - return req; - }); - }); - - it('returns a pkg file path on success', async () => { - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - let pkgFilePath = await rokuDeploy.retrieveSignedPackage('path_to_pkg', { - outFile: 'roku-deploy-test' - }); - expect(pkgFilePath).to.equal(path.join(process.cwd(), 'out', 'roku-deploy-test.pkg')); - }); - - it('returns a pkg file path on success', async () => { - //the write stream should return null, which causes a specific branch to be executed - createWriteStreamStub.callsFake(() => { - return null; - }); - - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 200 - }); - } - }; - - let error: Error; - try { - await rokuDeploy.retrieveSignedPackage('path_to_pkg', { - outFile: 'roku-deploy-test' - }); - } catch (e) { - error = e as any; - } - expect(error.message.startsWith('Unable to create write stream for')).to.be.true; - }); - - it('throws when error in request is encountered', async () => { - onHandler = (event, callback) => { - if (event === 'error') { - callback(new Error('Some error')); - } - }; - await expectThrowsAsync( - rokuDeploy.retrieveSignedPackage('path_to_pkg', { - outFile: 'roku-deploy-test' - }), - 'Some error' - ); - }); - - it('throws when status code is non 200', async () => { - onHandler = (event, callback) => { - if (event === 'response') { - callback({ - statusCode: 500 - }); - } - }; - await expectThrowsAsync( - rokuDeploy.retrieveSignedPackage('path_to_pkg', { - outFile: 'roku-deploy-test' - }), - 'Invalid response code: 500' - ); - }); - }); - - describe('prepublishToStaging', () => { - it('is resilient to file system errors', async () => { - let copy = rokuDeploy.fsExtra.copy; - let count = 0; - - //mock writeFile so we can throw a few errors during the test - sinon.stub(rokuDeploy.fsExtra, 'copy').callsFake((...args) => { - count += 1; - //fail a few times - if (count < 5) { - throw new Error('fake error thrown as part of the unit test'); - } else { - return copy.apply(rokuDeploy.fsExtra, args); - } - }); - - //override the retry milliseconds to make test run faster - let orig = util.tryRepeatAsync.bind(util); - sinon.stub(util, 'tryRepeatAsync').callsFake(async (...args) => { - return orig(args[0], args[1], 0); - }); - - fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); - - await rokuDeploy.prepublishToStaging({ - ...options, - files: [ - 'source/main.brs' - ] - }); - expectPathExists(s`${stagingDir}/source/main.brs`); - expect(count).to.be.greaterThan(4); - }); - - it('throws underlying error after the max fs error threshold is reached', async () => { - let copy = rokuDeploy.fsExtra.copy; - let count = 0; - - //mock writeFile so we can throw a few errors during the test - sinon.stub(rokuDeploy.fsExtra, 'copy').callsFake((...args) => { - count += 1; - //fail a few times - if (count < 15) { - throw new Error('fake error thrown as part of the unit test'); - } else { - return copy.apply(rokuDeploy.fsExtra, args); - } - }); - - //override the timeout for tryRepeatAsync so this test runs faster - let orig = util.tryRepeatAsync.bind(util); - sinon.stub(util, 'tryRepeatAsync').callsFake(async (...args) => { - return orig(args[0], args[1], 0); - }); - - fsExtra.outputFileSync(`${rootDir}/source/main.brs`, ''); - await expectThrowsAsync( - rokuDeploy.prepublishToStaging({ - rootDir: rootDir, - stagingDir: stagingDir, - files: [ - 'source/main.brs' - ] - }), - 'fake error thrown as part of the unit test' - ); - }); - }); - - describe('checkRequest', () => { - it('throws FailedDeviceResponseError when necessary', () => { - sinon.stub(rokuDeploy as any, 'getRokuMessagesFromResponseBody').returns({ - errors: ['a bad thing happened'] - } as any); - let ex; - try { - rokuDeploy['checkRequest']({ - response: {}, - body: 'something bad!' - }); - } catch (e) { - ex = e; - } - expect(ex).to.be.instanceof(errors.FailedDeviceResponseError); - }); - }); - - describe('getOptions', () => { - it('supports deprecated stagingFolderPath option', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path' }).stagingDir - ).to.eql(s`${cwd}/staging-folder-path`); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path', stagingDir: 'staging-dir' }).stagingDir - ).to.eql(s`${cwd}/staging-dir`); - expect( - rokuDeploy.getOptions({ stagingFolderPath: 'staging-folder-path' }).stagingFolderPath - ).to.eql(s`${cwd}/staging-folder-path`); - }); - - it('supports deprecated retainStagingFolder option', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - expect( - rokuDeploy.getOptions({ retainStagingFolder: true }).retainStagingDir - ).to.be.true; - expect( - rokuDeploy.getOptions({ retainStagingFolder: true, retainStagingDir: false }).retainStagingDir - ).to.be.false; - expect( - rokuDeploy.getOptions({ retainStagingFolder: true, retainStagingDir: false }).retainStagingFolder - ).to.be.false; - }); - - it('calling with no parameters works', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - options = rokuDeploy.getOptions(undefined); - expect(options.stagingDir).to.exist; - }); - - it('calling with empty param object', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - options = rokuDeploy.getOptions({}); - expect(options.stagingDir).to.exist; - }); - - it('works when passing in stagingDir', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return false; - }); - options = rokuDeploy.getOptions({ - stagingDir: './staging-dir' - }); - expect(options.stagingDir.endsWith('staging-dir')).to.be.true; - }); - - it('works when loading stagingDir from rokudeploy.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return true; - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "stagingDir": "./staging-dir" - } - `); - options = rokuDeploy.getOptions(); - expect(options.stagingDir.endsWith('staging-dir')).to.be.true; - }); - - it('supports jsonc for roku-deploy.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('rokudeploy.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - //leading comment - { - //inner comment - "rootDir": "src" //trailing comment - } - //trailing comment - `); - options = rokuDeploy.getOptions(undefined); - expect(options.rootDir).to.equal(path.join(process.cwd(), 'src')); - }); - - it('supports jsonc for bsconfig.json', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('bsconfig.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - //leading comment - { - //inner comment - "rootDir": "src" //trailing comment - } - //trailing comment - `); - options = rokuDeploy.getOptions(undefined); - expect(options.rootDir).to.equal(path.join(process.cwd(), 'src')); - }); - - it('catches invalid json with jsonc parser', () => { - sinon.stub(fsExtra, 'existsSync').callsFake((filePath) => { - return (filePath as string).endsWith('bsconfig.json'); - }); - sinon.stub(fsExtra, 'readFileSync').returns(` - { - "rootDir": "src" - `); - let ex; - try { - rokuDeploy.getOptions(undefined); - } catch (e) { - ex = e; - } - expect(ex).to.exist; - expect(ex.message.startsWith('Error parsing')).to.be.true; - }); - - it('does not error when no parameter provided', () => { - expect(rokuDeploy.getOptions(undefined)).to.exist; - }); - - describe('deleteInstalledChannel', () => { - it('defaults to true', () => { - expect(rokuDeploy.getOptions({}).deleteInstalledChannel).to.equal(true); - }); - - it('can be overridden', () => { - expect(rokuDeploy.getOptions({ deleteInstalledChannel: false }).deleteInstalledChannel).to.equal(false); - }); - }); - - describe('packagePort', () => { - - it('defaults to 80', () => { - expect(rokuDeploy.getOptions({}).packagePort).to.equal(80); - }); - - it('can be overridden', () => { - expect(rokuDeploy.getOptions({ packagePort: 95 }).packagePort).to.equal(95); - }); - - }); - - describe('remotePort', () => { - it('defaults to 8060', () => { - expect(rokuDeploy.getOptions({}).remotePort).to.equal(8060); - }); - - it('can be overridden', () => { - expect(rokuDeploy.getOptions({ remotePort: 1234 }).remotePort).to.equal(1234); - }); - }); - - describe('config file', () => { - beforeEach(() => { - process.chdir(rootDir); - }); - - it('if no config file is available it should use the default values', () => { - expect(rokuDeploy.getOptions().outFile).to.equal('roku-deploy'); - }); - - it('if rokudeploy.json config file is available it should use those values instead of the default', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('rokudeploy-outfile'); - }); - - it('if bsconfig.json config file is available it should use those values instead of the default', () => { - fsExtra.writeJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('bsconfig-outfile'); - }); - - it('if rokudeploy.json config file is available and bsconfig.json is also available it should use rokudeploy.json instead of bsconfig.json', () => { - fsExtra.outputJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); - fsExtra.outputJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - expect(rokuDeploy.getOptions().outFile).to.equal('rokudeploy-outfile'); - }); - - it('if runtime options are provided, they should override any existing config file options', () => { - fsExtra.writeJsonSync(`${rootDir}/bsconfig.json`, { outFile: 'bsconfig-outfile' }); - fsExtra.writeJsonSync(`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - expect(rokuDeploy.getOptions({ - outFile: 'runtime-outfile' - }).outFile).to.equal('runtime-outfile'); - }); - - it('if runtime config should override any existing config file options', () => { - fsExtra.writeJsonSync(s`${rootDir}/rokudeploy.json`, { outFile: 'rokudeploy-outfile' }); - fsExtra.writeJsonSync(s`${rootDir}/bsconfig`, { outFile: 'rokudeploy-outfile' }); - - fsExtra.writeJsonSync(s`${rootDir}/brsconfig.json`, { outFile: 'project-config-outfile' }); - options = { - project: 'brsconfig.json' - }; - expect(rokuDeploy.getOptions(options).outFile).to.equal('project-config-outfile'); - }); - }); - }); - - describe('getToFile', () => { - it('waits for the write stream to finish writing before resolving', async () => { - let getToFileIsResolved = false; - - let requestCalled = q.defer(); - let onResponse = q.defer<(res) => any>(); - - //intercept the http request - sinon.stub(request, 'get').callsFake(() => { - requestCalled.resolve(); - let req: any = { - on: (event, callback) => { - if (event === 'response') { - onResponse.resolve(callback); - } - return req; - }, - pipe: () => { - return req; - } - }; - return req; - }); - - const finalPromise = rokuDeploy['getToFile']({}, s`${tempDir}/out/something.txt`).then(() => { - getToFileIsResolved = true; - }); - - await requestCalled.promise; - expect(getToFileIsResolved).to.be.false; - - const callback = await onResponse.promise; - callback({ statusCode: 200 }); - await util.sleep(10); - - expect(getToFileIsResolved).to.be.false; - - const writeStream = await writeStreamPromise; - writeStream.write('test'); - writeStream.close(); - - await finalPromise; - expect(getToFileIsResolved).to.be.true; - }); - }); - - describe('deployAndSignPackage', () => { - beforeEach(() => { - //pretend the deploy worked - sinon.stub(rokuDeploy, 'deploy').returns(Promise.resolve(null)); - //pretend the sign worked - sinon.stub(rokuDeploy, 'signExistingPackage').returns(Promise.resolve(null)); - //pretend fetching the signed package worked - sinon.stub(rokuDeploy, 'retrieveSignedPackage').returns(Promise.resolve('some_local_path')); - }); - - it('succeeds and does proper things with staging folder', async () => { - let stub = sinon.stub(rokuDeploy['fsExtra'], 'remove').returns(Promise.resolve() as any); - - //this should not fail - let pkgFilePath = await rokuDeploy.deployAndSignPackage({ - retainStagingDir: false - }); - - //the return value should equal what retrieveSignedPackage returned. - expect(pkgFilePath).to.equal('some_local_path'); - - //fsExtra.remove should have been called - expect(stub.getCalls()).to.be.lengthOf(1); - - //call it again, but specify true for retainStagingDir - await rokuDeploy.deployAndSignPackage({ - retainStagingDir: true - }); - //call count should NOT increase - expect(stub.getCalls()).to.be.lengthOf(1); - - //call it again, but don't specify retainStagingDir at all (it should default to FALSE) - await rokuDeploy.deployAndSignPackage({}); - //call count should NOT increase - expect(stub.getCalls()).to.be.lengthOf(2); - }); - - it('converts to squashfs if we request it to', async () => { - options.convertToSquashfs = true; - let stub = sinon.stub(rokuDeploy, 'convertToSquashfs').returns(Promise.resolve(null)); - await rokuDeploy.deployAndSignPackage(options); - expect(stub.getCalls()).to.be.lengthOf(1); - }); - }); - - function mockDoGetRequest(body = '', statusCode = 200) { - return sinon.stub(rokuDeploy as any, 'doGetRequest').callsFake((params) => { - let results = { response: { statusCode: statusCode }, body: body }; - rokuDeploy['checkRequest'](results); - return Promise.resolve(results); - }); - } - - function mockDoPostRequest(body = '', statusCode = 200) { - return sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { - let results = { response: { statusCode: statusCode }, body: body }; - rokuDeploy['checkRequest'](results); - return Promise.resolve(results); - }); - } - - async function assertThrowsAsync(fn) { - let f = () => { }; - try { - await fn(); - } catch (e) { - f = () => { - throw e; - }; - } finally { - assert.throws(f); - } - } -}); - -function getFakeResponseBody(messages: string): string { - return ` - - - - Roku Development Kit - - - - -
- - -
- - -
- Failure: Form Error: "archive" Field Not Found - - -

f1338f071efb2ff0f50824a00be3402a
zip file in internal memory (3704254 bytes)

-
- - - - - - `; -} diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index 3a04b2e..a7874a6 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -106,25 +106,31 @@ export class RokuDeploy { * @param options */ public async zipPackage(options: RokuDeployOptions) { + console.log('zipPackage 1'); options = this.getOptions(options); //make sure the output folder exists await this.fsExtra.ensureDir(options.outDir); + console.log('zipPackage 2'); let zipFilePath = this.getOutputZipFilePath(options); //ensure the manifest file exists in the staging folder if (!await util.fileExistsCaseInsensitive(`${options.stagingDir}/manifest`)) { + console.log('zipPackage 3'); throw new Error(`Cannot zip package: missing manifest file in "${options.stagingDir}"`); } //create a zip of the staging folder + console.log('zipPackage 4'); await this.zipFolder(options.stagingDir, zipFilePath); //delete the staging folder unless told to retain it. if (options.retainStagingDir !== true) { + console.log('zipPackage 5'); await this.fsExtra.remove(options.stagingDir); } + console.log('zipPackage 6'); } /** @@ -132,20 +138,24 @@ export class RokuDeploy { * @param options */ public async createPackage(options: RokuDeployOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => Promise | void) { + console.log('createPackage 1'); options = this.getOptions(options); await this.prepublishToStaging(options); + console.log('createPackage 2'); let manifestPath = util.standardizePath(`${options.stagingDir}/manifest`); let parsedManifest = await this.parseManifest(manifestPath); if (options.incrementBuildNumber) { + console.log('createPackage 3'); let timestamp = dateformat(new Date(), 'yymmddHHMM'); parsedManifest.build_version = timestamp; //eslint-disable-line camelcase await this.fsExtra.writeFile(manifestPath, this.stringifyManifest(parsedManifest)); } if (beforeZipCallback) { + console.log('createPackage 4'); let info: BeforeZipCallbackInfo = { manifestData: parsedManifest, stagingFolderPath: options.stagingDir, @@ -154,7 +164,9 @@ export class RokuDeploy { await Promise.resolve(beforeZipCallback(info)); } + console.log('createPackage 5'); await this.zipPackage(options); + console.log('createPackage 6'); } /** @@ -414,21 +426,27 @@ export class RokuDeploy { * @param options */ public async publish(options: RokuDeployOptions): Promise<{ message: string; results: any }> { + console.log('publish 1'); options = this.getOptions(options); if (!options.host) { + console.log('publish 2'); throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); } //make sure the outDir exists + console.log('publish 3'); await this.fsExtra.ensureDir(options.outDir); let zipFilePath = this.getOutputZipFilePath(options); let readStream: _fsExtra.ReadStream; + console.log('publish 4'); try { if ((await this.fsExtra.pathExists(zipFilePath)) === false) { + console.log('publish 5'); throw new Error(`Cannot publish because file does not exist at '${zipFilePath}'`); } readStream = this.fsExtra.createReadStream(zipFilePath); //wait for the stream to open (no harm in doing this, and it helps solve an issue in the tests) + console.log('publish 6'); await new Promise((resolve) => { readStream.on('open', resolve); }); @@ -452,12 +470,16 @@ export class RokuDeploy { //try to "replace" the channel first since that usually works. let response: HttpResponse; try { + console.log('publish 7'); response = await this.doPostRequest(requestOptions); } catch (replaceError: any) { + console.log('publish 8'); //fail if this is a compile error if (this.isCompileError(replaceError.message) && options.failOnCompileError) { + console.log('publish 9'); throw new errors.CompileError('Compile error', replaceError, replaceError.results); } else { + console.log('publish 9.1'); requestOptions.formData.mysubmit = 'Install'; response = await this.doPostRequest(requestOptions); } @@ -465,23 +487,29 @@ export class RokuDeploy { if (options.failOnCompileError) { if (this.isCompileError(response.body)) { + console.log('publish 10'); throw new errors.CompileError('Compile error', response, this.getRokuMessagesFromResponseBody(response.body)); } } if (response.body.indexOf('Identical to previous version -- not replacing.') > -1) { + console.log('publish 11'); return { message: 'Identical to previous version -- not replacing', results: response }; } + console.log('publish 12'); return { message: 'Successful deploy', results: response }; } finally { //delete the zip file only if configured to do so if (options.retainDeploymentArchive === false) { + console.log('publish 13'); await this.fsExtra.remove(zipFilePath); } //try to close the read stream to prevent files becoming locked try { + console.log('publish 14'); readStream?.close(); } catch (e) { + console.log('publish 15'); this.logger.info('Error closing read stream', e); } } @@ -499,8 +527,10 @@ export class RokuDeploy { * @param options */ public async convertToSquashfs(options: RokuDeployOptions) { + console.log('convertToSquashfs 1'); options = this.getOptions(options); if (!options.host) { + console.log('convertToSquashfs 2'); throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); } let requestOptions = this.generateBaseRequestOptions('plugin_install', options, { @@ -508,10 +538,15 @@ export class RokuDeploy { mysubmit: 'Convert to squashfs' }); + console.log('convertToSquashfs 3'); + console.log(requestOptions); let results = await this.doPostRequest(requestOptions); + console.log('convertToSquashfs 4'); if (results.body.indexOf('Conversion succeeded') === -1) { + console.log('convertToSquashfs 5'); throw new errors.ConvertError('Squashfs conversion failed'); } + console.log('convertToSquashfs 6'); } /** @@ -762,16 +797,22 @@ export class RokuDeploy { * @param options */ public async deploy(options?: RokuDeployOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => void) { + console.log('deploy 1'); options = this.getOptions(options); + console.log('deploy 2'); await this.createPackage(options, beforeZipCallback); + console.log('deploy 3'); if (options.deleteInstalledChannel) { + console.log('deploy 4'); try { await this.deleteInstalledChannel(options); } catch (e) { // note we don't report the error; as we don't actually care that we could not deploy - it's just useless noise to log it. } } + console.log('deploy 5'); let result = await this.publish(options); + console.log('deploy 6'); return result; } @@ -862,20 +903,26 @@ export class RokuDeploy { * @param options */ public async deployAndSignPackage(options?: RokuDeployOptions, beforeZipCallback?: (info: BeforeZipCallbackInfo) => void): Promise { + console.log('deployAndSignPackage 1'); options = this.getOptions(options); + console.log('deployAndSignPackage 2'); let retainStagingDirInitialValue = options.retainStagingDir; options.retainStagingDir = true; + console.log('deployAndSignPackage 3'); await this.deploy(options, beforeZipCallback); if (options.convertToSquashfs) { + console.log('deployAndSignPackage 4'); await this.convertToSquashfs(options); } let remotePkgPath = await this.signExistingPackage(options); let localPkgFilePath = await this.retrieveSignedPackage(remotePkgPath, options); if (retainStagingDirInitialValue !== true) { + console.log('deployAndSignPackage 5'); await this.fsExtra.remove(options.stagingDir); } + console.log('deployAndSignPackage 6'); return localPkgFilePath; } @@ -887,6 +934,7 @@ export class RokuDeploy { let fileOptions: RokuDeployOptions = {}; const fileNames = ['rokudeploy.json', 'bsconfig.json']; if (options.project) { + console.log('getOptions 2'); fileNames.unshift(options.project); } @@ -965,7 +1013,8 @@ export class RokuDeploy { public getOutputZipFilePath(options: RokuDeployOptions) { options = this.getOptions(options); - let zipFileName = options.outFile; + //let zipFileName = options.outFile; + let zipFileName = 'a'; if (!zipFileName.toLowerCase().endsWith('.zip')) { zipFileName += '.zip'; } diff --git a/src/Stopwatch.spec.ts b/src/Stopwatch.spec.ts deleted file mode 100644 index f049846..0000000 --- a/src/Stopwatch.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { expect } from 'chai'; -import { Stopwatch } from './Stopwatch'; -import { util } from './util'; - -describe('Stopwatch', () => { - let stopwatch: Stopwatch; - beforeEach(() => { - stopwatch = new Stopwatch(); - }); - - it('constructs', () => { - stopwatch = new Stopwatch(); - }); - - it('starts', () => { - expect(stopwatch['startTime']).to.not.exist; - stopwatch.start(); - expect(stopwatch['startTime']).to.exist; - }); - - it('resets', () => { - stopwatch.start(); - expect(stopwatch['startTime']).to.exist; - stopwatch.reset(); - expect(stopwatch['startTime']).to.not.exist; - }); - - it('stops', async () => { - stopwatch.start(); - expect(stopwatch['startTime']).to.exist; - await util.sleep(3); - stopwatch.stop(); - expect(stopwatch['startTime']).to.not.exist; - expect(stopwatch['totalMilliseconds']).to.be.gte(2); - }); - - it('stop multiple times has no effect', () => { - stopwatch.start(); - stopwatch.stop(); - stopwatch.stop(); - }); - - it('breaks out hours, minutes, and seconds', () => { - stopwatch['totalMilliseconds'] = (17 * 60 * 1000) + (43 * 1000) + 30; - expect(stopwatch.getDurationText()).to.eql('17m43s30.0ms'); - }); - - it('returns only seconds and milliseconds', () => { - stopwatch['totalMilliseconds'] = (43 * 1000) + 30; - expect(stopwatch.getDurationText()).to.eql('43s30.0ms'); - }); - - it('returns only milliseconds', () => { - stopwatch['totalMilliseconds'] = 30; - expect(stopwatch.getDurationText()).to.eql('30.0ms'); - }); - - it('works for single run', async () => { - stopwatch = new Stopwatch(); - stopwatch.start(); - await new Promise((resolve) => { - setTimeout(resolve, 2); - }); - stopwatch.stop(); - expect(stopwatch.totalMilliseconds).to.be.greaterThan(1); - }); - - it('works for multiple start/stop', async () => { - stopwatch = new Stopwatch(); - stopwatch.start(); - stopwatch.stop(); - stopwatch.totalMilliseconds = 3; - stopwatch.start(); - await new Promise((resolve) => { - setTimeout(resolve, 4); - }); - stopwatch.stop(); - expect(stopwatch.totalMilliseconds).to.be.at.least(6); - }); - - it('pretty prints', () => { - stopwatch = new Stopwatch(); - stopwatch.totalMilliseconds = 45; - expect(stopwatch.getDurationText()).to.equal('45.0ms'); - stopwatch.totalMilliseconds = 2000 + 45; - expect(stopwatch.getDurationText()).to.equal('2s45.0ms'); - stopwatch.totalMilliseconds = 180000 + 2000 + 45; - expect(stopwatch.getDurationText()).to.equal('3m2s45.0ms'); - }); -}); diff --git a/src/device.spec.ts b/src/device.spec.ts deleted file mode 100644 index d7b3ebe..0000000 --- a/src/device.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as assert from 'assert'; -import * as fsExtra from 'fs-extra'; -import * as rokuDeploy from './index'; -import { cwd, expectPathExists, expectThrowsAsync, outDir, rootDir, tempDir, writeFiles } from './testUtils.spec'; -import * as dedent from 'dedent'; - -//these tests are run against an actual roku device. These cannot be enabled when run on the CI server -describe('device', function device() { - let options: rokuDeploy.RokuDeployOptions; - - beforeEach(() => { - fsExtra.emptyDirSync(tempDir); - fsExtra.ensureDirSync(rootDir); - process.chdir(rootDir); - options = rokuDeploy.getOptions({ - outDir: outDir, - host: '192.168.1.32', - retainDeploymentArchive: true, - password: 'aaaa', - devId: 'c6fdc2019903ac3332f624b0b2c2fe2c733c3e74', - rekeySignedPackage: `${cwd}/testSignedPackage.pkg`, - signingPassword: 'drRCEVWP/++K5TYnTtuAfQ==' - }); - - writeFiles(rootDir, [ - ['manifest', dedent` - title=RokuDeployTestChannel - major_version=1 - minor_version=0 - build_version=0 - splash_screen_hd=pkg:/images/splash_hd.jpg - ui_resolutions=hd - bs_const=IS_DEV_BUILD=false - splash_color=#000000 - `], - ['source/main.brs', dedent` - Sub RunUserInterface() - screen = CreateObject("roSGScreen") - m.scene = screen.CreateScene("HomeScene") - port = CreateObject("roMessagePort") - screen.SetMessagePort(port) - screen.Show() - - while(true) - msg = wait(0, port) - end while - - if screen <> invalid then - screen.Close() - screen = invalid - end if - End Sub - `] - ]); - }); - - afterEach(() => { - //restore the original working directory - process.chdir(cwd); - fsExtra.emptyDirSync(tempDir); - }); - - this.timeout(20000); - - describe('deploy', () => { - it('works', async () => { - options.retainDeploymentArchive = true; - let response = await rokuDeploy.deploy(options); - assert.equal(response.message, 'Successful deploy'); - }); - - it('Presents nice message for 401 unauthorized status code', async () => { - this.timeout(20000); - options.password = 'NOT_THE_PASSWORD'; - await expectThrowsAsync( - rokuDeploy.deploy(options), - 'Unauthorized. Please verify username and password for target Roku.' - ); - }); - }); - - describe('deployAndSignPackage', () => { - it('works', async () => { - await rokuDeploy.deleteInstalledChannel(options); - await rokuDeploy.rekeyDevice(options); - expectPathExists( - await rokuDeploy.deployAndSignPackage(options) - ); - }); - }); -}); diff --git a/src/testUtils.spec.ts b/src/testUtils.spec.ts deleted file mode 100644 index 544c1ac..0000000 --- a/src/testUtils.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'chai'; -import * as fsExtra from 'fs-extra'; -import * as path from 'path'; -import { standardizePath as s } from './util'; - -export const cwd = s`${__dirname}/..`; -export const tempDir = s`${cwd}/.tmp`; -export const rootDir = s`${tempDir}/rootDir`; -export const outDir = s`${tempDir}/outDir`; -export const stagingDir = s`${outDir}/.roku-deploy-staging`; - -export function expectPathExists(thePath: string) { - if (!fsExtra.pathExistsSync(thePath)) { - throw new Error(`Expected "${thePath}" to exist`); - } -} - -export function expectPathNotExists(thePath: string) { - expect( - fsExtra.pathExistsSync(thePath), - `Expected "${thePath}" not to exist` - ).to.be.false; -} - -export function writeFiles(baseDir: string, files: Array) { - const filePaths = []; - for (let entry of files) { - if (typeof entry === 'string') { - entry = [entry] as any; - } - let [filePath, contents] = entry as any; - filePaths.push(filePath); - filePath = path.resolve(baseDir, filePath); - fsExtra.outputFileSync(filePath, contents ?? ''); - } - return filePaths; -} - -export async function expectThrowsAsync(callback: Promise | (() => Promise), expectedMessage = undefined, failedTestMessage = 'Expected to throw but did not') { - let wasExceptionThrown = false; - let promise: Promise; - if (typeof callback === 'function') { - promise = callback(); - } else { - promise = callback; - } - try { - await promise; - } catch (e) { - wasExceptionThrown = true; - if (expectedMessage) { - expect((e as any).message).to.eql(expectedMessage); - } - } - if (wasExceptionThrown === false) { - throw new Error(failedTestMessage); - } -} diff --git a/src/util.spec.ts b/src/util.spec.ts deleted file mode 100644 index 15c4fc9..0000000 --- a/src/util.spec.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { util, standardizePath as s } from './util'; -import { expect } from 'chai'; -import * as fsExtra from 'fs-extra'; -import { tempDir } from './testUtils.spec'; -import * as path from 'path'; -import * as dns from 'dns'; -import { createSandbox } from 'sinon'; -const sinon = createSandbox(); - -describe('util', () => { - beforeEach(() => { - fsExtra.emptyDirSync(tempDir); - sinon.restore(); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe('isFile', () => { - it('recognizes valid files', async () => { - expect(await util.isFile(util.standardizePath(`${process.cwd()}/README.md`))).to.be.true; - }); - it('recognizes non-existant files', async () => { - expect(await util.isFile(util.standardizePath(`${process.cwd()}/FILE_THAT_DOES_NOT_EXIST.md`))).to.be.false; - }); - }); - - describe('toForwardSlashes', () => { - it('returns original value for non-strings', () => { - expect(util.toForwardSlashes(undefined)).to.be.undefined; - expect(util.toForwardSlashes(false)).to.be.false; - }); - - it('converts mixed slashes to forward', () => { - expect(util.toForwardSlashes('a\\b/c\\d/e')).to.eql('a/b/c/d/e'); - }); - }); - - describe('isChildOfPath', () => { - it('works for child path', () => { - let parentPath = `${process.cwd()}\\testProject`; - let childPath = `${process.cwd()}\\testProject\\manifest`; - expect(util.isParentOfPath(parentPath, childPath), `expected '${childPath}' to be child path of '${parentPath}'`).to.be.true; - //inverse is not true - expect(util.isParentOfPath(childPath, parentPath), `expected '${parentPath}' NOT to be child path of '${childPath}'`).to.be.false; - }); - - it('handles mixed path separators', () => { - let parentPath = `${process.cwd()}\\testProject`; - let childPath = `${process.cwd()}\\testProject/manifest`; - expect(util.isParentOfPath(parentPath, childPath), `expected '${childPath}' to be child path of '${parentPath}'`).to.be.true; - }); - - it('handles relative path traversals', () => { - let parentPath = `${process.cwd()}\\testProject`; - let childPath = `${process.cwd()}/testProject/../testProject/manifest`; - expect(util.isParentOfPath(parentPath, childPath), `expected '${childPath}' to be child path of '${parentPath}'`).to.be.true; - }); - - it('works with trailing slashes', () => { - let parentPath = `${process.cwd()}/testProject/`; - let childPath = `${process.cwd()}/testProject/../testProject/manifest`; - expect(util.isParentOfPath(parentPath, childPath), `expected '${childPath}' to be child path of '${parentPath}'`).to.be.true; - }); - - it('works with duplicate slashes', () => { - let parentPath = `${process.cwd()}///testProject/`; - let childPath = `${process.cwd()}/testProject///testProject//manifest`; - expect(util.isParentOfPath(parentPath, childPath), `expected '${childPath}' to be child path of '${parentPath}'`).to.be.true; - }); - }); - - describe('stringReplaceInsensitive', () => { - it('works for varying case', () => { - expect(util.stringReplaceInsensitive('aBcD', 'bCd', 'bcd')).to.equal('abcd'); - }); - - it('returns the original string if the needle was not found in the haystack', () => { - expect(util.stringReplaceInsensitive('abcd', 'efgh', 'EFGH')).to.equal('abcd'); - }); - }); - - describe('tryRepeatAsync', () => { - it('calls callback', async () => { - let count = 0; - await util.tryRepeatAsync(() => { - count++; - if (count < 3) { - throw new Error('test tryRepeatAsync'); - } - }, 10, 0); - expect(count).to.equal(3); - }); - - it('raises exception after max tries has been reached', async () => { - let error; - try { - await util.tryRepeatAsync(() => { - throw new Error('test tryRepeatAsync'); - }, 3, 1); - } catch (e) { - error = e; - } - expect(error).to.exist; - }); - }); - - describe('globAllByIndex', () => { - function writeFiles(filePaths: string[], cwd = tempDir) { - for (const filePath of filePaths) { - fsExtra.outputFileSync( - path.resolve(cwd, filePath), - '' - ); - } - } - - async function doTest(patterns: string[], expectedPaths: string[][]) { - const results = await util.globAllByIndex(patterns, tempDir); - for (let i = 0; i < results.length; i++) { - results[i] = results[i]?.map(x => s(x))?.sort(); - } - for (let i = 0; i < expectedPaths.length; i++) { - expectedPaths[i] = expectedPaths[i]?.map(x => { - return s`${path.resolve(tempDir, x)}`; - })?.sort(); - } - expect(results).to.eql(expectedPaths); - } - - it('finds direct file paths', async () => { - writeFiles([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ]); - await doTest([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ], [ - [ - 'manifest' - ], [ - 'source/main.brs' - ], [ - 'components/Component1/lib.brs' - ] - ]); - }); - - it('matches the wildcard glob', async () => { - writeFiles([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ]); - await doTest([ - '**/*' - ], [ - [ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ] - ]); - }); - - it('returns the same file path in multiple matches', async () => { - writeFiles([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ]); - await doTest([ - 'manifest', - 'source/main.brs', - 'manifest', - 'source/main.brs' - ], [ - [ - 'manifest' - ], [ - 'source/main.brs' - ], [ - 'manifest' - ], [ - 'source/main.brs' - ] - ]); - }); - - it('filters files', async () => { - writeFiles([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ]); - await doTest([ - '**/*', - //filter out brs files - '!**/*.brs' - ], [ - [ - 'manifest' - ], - null - ]); - }); - - it('filters files and adds them back in later', async () => { - writeFiles([ - 'manifest', - 'source/main.brs', - 'components/Component1/lib.brs' - ]); - await doTest([ - '**/*', - //filter out brs files - '!**/*.brs', - //re-add the main file - '**/main.brs' - ], [ - [ - 'manifest' - ], - undefined, - [ - 'source/main.brs' - ] - ]); - }); - }); - - describe('filterPaths', () => { - it('does not crash with bad params', () => { - //shouldn't crash - util['filterPaths']('*', [], '', 2); - }); - }); - - describe('dnsLookup', () => { - it('returns ip address for hostname', async () => { - sinon.stub(dns.promises, 'lookup').returns(Promise.resolve({ - address: '1.2.3.4', - family: undefined - })); - - expect( - await util.dnsLookup('some-host', true) - ).to.eql('1.2.3.4'); - }); - - it('returns ip address for ip address', async () => { - sinon.stub(dns.promises, 'lookup').returns(Promise.resolve({ - address: '1.2.3.4', - family: undefined - })); - - expect( - await util.dnsLookup('some-host', true) - ).to.eql('1.2.3.4'); - }); - - it('returns given value if the lookup failed', async () => { - sinon.stub(dns.promises, 'lookup').returns(Promise.resolve({ - address: undefined, - family: undefined - })); - - expect( - await util.dnsLookup('some-host', true) - ).to.eql('some-host'); - }); - }); - - describe('decodeHtmlEntities', () => { - it('decodes values properly', () => { - expect(util.decodeHtmlEntities(' ')).to.eql(' '); - expect(util.decodeHtmlEntities('&')).to.eql('&'); - expect(util.decodeHtmlEntities('"')).to.eql('"'); - expect(util.decodeHtmlEntities('<')).to.eql('<'); - expect(util.decodeHtmlEntities('>')).to.eql('>'); - expect(util.decodeHtmlEntities(''')).to.eql(`'`); - }); - }); -});