diff --git a/src/Errors.ts b/src/Errors.ts index e87ba2b..bec353e 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -48,10 +48,3 @@ export class ConvertError extends Error { Object.setPrototypeOf(this, ConvertError.prototype); } } - -export class MissingRequiredOptionError extends Error { - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, MissingRequiredOptionError.prototype); - } -} diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index 0b31958..ec2233a 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -16,6 +16,7 @@ import { cwd, expectPathExists, expectPathNotExists, expectThrowsAsync, outDir, import { createSandbox } from 'sinon'; import * as r from 'postman-request'; import type * as requestType from 'request'; +import type { CaptureScreenshotOptions, ConvertToSquashfsOptions, CreateSignedPackageOptions, DeleteDevChannelOptions, GetDevIdOptions, GetDeviceInfoOptions, RekeyDeviceOptions, SendKeyEventOptions, SideloadOptions } from './RokuDeploy'; const request = r as typeof requestType; const sinon = createSandbox(); @@ -646,11 +647,11 @@ describe('index', () => { describe('generateBaseRequestOptions', () => { it('uses default port', () => { - expect(rokuDeploy['generateBaseRequestOptions']('a_b_c', { host: '1.2.3.4', password: options.password }).url).to.equal('http://1.2.3.4:80/a_b_c'); + expect(rokuDeploy['generateBaseRequestOptions']('a_b_c', { host: '1.2.3.4', password: 'password' }).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, password: options.password }).url).to.equal('http://1.2.3.4:999/a_b_c'); + expect(rokuDeploy['generateBaseRequestOptions']('a_b_c', { host: '1.2.3.4', packagePort: 999, password: 'password' }).url).to.equal('http://1.2.3.4:999/a_b_c'); }); }); @@ -1058,21 +1059,6 @@ describe('index', () => { }); }); - it('should return MissingRequiredOptionError if host was not provided', async () => { - mockDoPostRequest(); - try { - options.host = undefined; - await rokuDeploy.convertToSquashfs({ - host: options.host, - password: 'password' - }); - } 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 { @@ -1181,40 +1167,6 @@ describe('index', () => { }); }); - it('should throw error if missing rekeySignedPackage option', async () => { - try { - await rokuDeploy.rekeyDevice({ - host: '1.2.3.4', - password: 'password', - rekeySignedPackage: null, - signingPassword: options.signingPassword, - rootDir: options.rootDir, - devId: options.devId - }); - } 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 { - await rokuDeploy.rekeyDevice({ - host: '1.2.3.4', - password: 'password', - rekeySignedPackage: options.rekeySignedPackage, - signingPassword: null, - rootDir: options.rootDir, - devId: options.devId - }); - } 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(); @@ -1303,17 +1255,6 @@ describe('index', () => { }); }); - it('should return our error if signingPassword is not supplied', async () => { - await expectThrowsAsync(async () => { - await rokuDeploy.createSignedPackage({ - host: '1.2.3.4', - password: 'password', - signingPassword: undefined, - stagingDir: stagingDir - }); - }, '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 { @@ -1401,7 +1342,7 @@ describe('index', () => { ); }); - it('should return our fallback error if neither error or package link was detected', async () => { + it('should return error if dev id does not match', async () => { mockDoGetRequest(` 789 @@ -2099,19 +2040,19 @@ describe('index', () => { `); mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: 'password' })); }); it('throws when there is no response body', async () => { // missing body mockDoPostRequest(null); - await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: 'password' })); }); it('throws when there is an empty response body', async () => { // empty body mockDoPostRequest(); - await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: 'password' })); }); it('throws when there is an error downloading the image from device', async () => { @@ -2132,7 +2073,7 @@ describe('index', () => { }; mockDoPostRequest(body); - await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: options.password })); + await expectThrowsAsync(rokuDeploy.captureScreenshot({ host: options.host, password: 'password' })); }); it('handles the device returning a png', async () => { @@ -2153,7 +2094,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password' }); expect(result).not.to.be.undefined; expect(path.extname(result)).to.equal('.png'); expect(fsExtra.existsSync(result)); @@ -2177,7 +2118,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password' }); expect(result).not.to.be.undefined; expect(path.extname(result)).to.equal('.jpg'); expect(fsExtra.existsSync(result)); @@ -2201,7 +2142,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: `${tempDir}/myScreenShots` }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password', screenshotDir: `${tempDir}/myScreenShots` }); expect(result).not.to.be.undefined; expect(util.standardizePath(`${tempDir}/myScreenShots`)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(result)); @@ -2225,7 +2166,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: tempDir, screenshotFile: 'my' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password', screenshotDir: tempDir, screenshotFile: 'my' }); expect(result).not.to.be.undefined; expect(util.standardizePath(tempDir)).to.equal(path.dirname(result)); expect(fsExtra.existsSync(path.join(tempDir, 'my.png'))); @@ -2249,7 +2190,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotDir: tempDir, screenshotFile: 'my.jpg' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password', screenshotDir: tempDir, screenshotFile: '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'))); @@ -2273,7 +2214,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password' }); expect(result).not.to.be.undefined; expect(fsExtra.existsSync(result)); }); @@ -2296,7 +2237,7 @@ describe('index', () => { }; mockDoPostRequest(body); - let result = await rokuDeploy.captureScreenshot({ host: options.host, password: options.password, screenshotFile: 'myFile' }); + let result = await rokuDeploy.captureScreenshot({ host: options.host, password: 'password', screenshotFile: 'myFile' }); expect(result).not.to.be.undefined; expect(path.basename(result)).to.equal('myFile.jpg'); expect(fsExtra.existsSync(result)); @@ -3175,6 +3116,71 @@ describe('index', () => { }); }); + describe('checkRequiredOptions', () => { + async function testRequiredOptions(action: string, requiredOptions: Partial, testedOption: string) { + const newOptions = { ...requiredOptions }; + delete newOptions[testedOption]; + await expectThrowsAsync(async () => { + await rokuDeploy[action](newOptions); + }, `Missing required option: ${testedOption}`); + } + + it('throws error when sendKeyEvent is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', key: 'string' }; + await testRequiredOptions('sendKeyEvent', requiredOptions, 'host'); + await testRequiredOptions('sendKeyEvent', requiredOptions, 'key'); + }); + + it('throws error when sideload is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd' }; + await testRequiredOptions('sideload', requiredOptions, 'host'); + await testRequiredOptions('sideload', requiredOptions, 'password'); + }); + + it('throws error when convertToSquashfs is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd' }; + await testRequiredOptions('convertToSquashfs', requiredOptions, 'host'); + await testRequiredOptions('convertToSquashfs', requiredOptions, 'password'); + }); + + it('throws error when rekeyDevice is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd', rekeySignedPackage: 'abcd', signingPassword: 'abcd' }; + await testRequiredOptions('rekeyDevice', requiredOptions, 'host'); + await testRequiredOptions('rekeyDevice', requiredOptions, 'password'); + await testRequiredOptions('rekeyDevice', requiredOptions, 'rekeySignedPackage'); + await testRequiredOptions('rekeyDevice', requiredOptions, 'signingPassword'); + }); + + it('throws error when createSignedPackage is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd', signingPassword: 'abcd' }; + await testRequiredOptions('createSignedPackage', requiredOptions, 'host'); + await testRequiredOptions('createSignedPackage', requiredOptions, 'password'); + await testRequiredOptions('createSignedPackage', requiredOptions, 'signingPassword'); + }); + + it('throws error when deleteDevChannel is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd' }; + await testRequiredOptions('deleteDevChannel', requiredOptions, 'host'); + await testRequiredOptions('deleteDevChannel', requiredOptions, 'password'); + }); + + it('throws error when captureScreenshot is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4', password: 'abcd' }; + await testRequiredOptions('captureScreenshot', requiredOptions, 'host'); + await testRequiredOptions('captureScreenshot', requiredOptions, 'password'); + }); + + it('throws error when getDeviceInfo is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4' }; + await testRequiredOptions('getDeviceInfo', requiredOptions, 'host'); + }); + + it('throws error when getDevId is missing required options', async () => { + const requiredOptions: Partial = { host: '1.2.3.4' }; + await testRequiredOptions('getDevId', requiredOptions, 'host'); + }); + }); + describe('downloadFile', () => { it('waits for the write stream to finish writing before resolving', async () => { let downloadFileIsResolved = false; diff --git a/src/RokuDeploy.ts b/src/RokuDeploy.ts index f874550..f797f99 100644 --- a/src/RokuDeploy.ts +++ b/src/RokuDeploy.ts @@ -37,9 +37,6 @@ export class RokuDeploy { //make sure the staging folder exists await fsExtra.ensureDir(options.stagingDir); - if (!options.stagingDir) { - throw new Error('stagingPath is required'); - } if (!await fsExtra.pathExists(options.rootDir)) { throw new Error(`rootDir does not exist at "${options.rootDir}"`); } @@ -215,6 +212,7 @@ export class RokuDeploy { * This makes the roku return to the home screen */ private async sendKeyEvent(options: SendKeyEventOptions) { + this.checkRequiredOptions(options, ['host', 'key']); let filledOptions = this.getOptions(options); // press the home button to return to the main screen return this.doPostRequest({ @@ -237,10 +235,8 @@ export class RokuDeploy { * @param options */ public async sideload(options: SideloadOptions): Promise<{ message: string; results: any }> { + this.checkRequiredOptions(options, ['host', 'password']); options = this.getOptions(options) as any; - if (!options.host) { - throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); - } //make sure the outDir exists await fsExtra.ensureDir(options.outDir); @@ -331,10 +327,8 @@ export class RokuDeploy { * @param options */ public async convertToSquashfs(options: ConvertToSquashfsOptions) { + this.checkRequiredOptions(options, ['host', 'password']); options = this.getOptions(options) as any; - if (!options.host) { - throw new errors.MissingRequiredOptionError('must specify the host for the Roku device'); - } let requestOptions = this.generateBaseRequestOptions('plugin_install', options as any, { archive: '', mysubmit: 'Convert to squashfs' @@ -351,14 +345,8 @@ export class RokuDeploy { * @param options */ public async rekeyDevice(options: RekeyDeviceOptions) { + this.checkRequiredOptions(options, ['host', 'password', 'rekeySignedPackage', 'signingPassword']); options = this.getOptions(options) as any; - if (!options.rekeySignedPackage) { - throw new errors.MissingRequiredOptionError('Must supply rekeySignedPackage'); - } - - if (!options.signingPassword) { - throw new errors.MissingRequiredOptionError('Must supply signingPassword'); - } let rekeySignedPackagePath = options.rekeySignedPackage; if (!path.isAbsolute(options.rekeySignedPackage)) { @@ -404,17 +392,15 @@ export class RokuDeploy { * @param options */ public async createSignedPackage(options: CreateSignedPackageOptions): Promise { + this.checkRequiredOptions(options, ['host', 'password', 'signingPassword']); options = this.getOptions(options) as any; - if (!options.signingPassword) { - throw new errors.MissingRequiredOptionError('Must supply signingPassword'); - } let manifestPath = path.join(options.stagingDir, 'manifest'); let parsedManifest = await this.parseManifest(manifestPath); let appName = parsedManifest.title + '/' + parsedManifest.major_version + '.' + parsedManifest.minor_version; //prevent devId mismatch (if devId is specified) if (options.devId) { - const deviceDevId = await this.getDevId(); + const deviceDevId = await this.getDevId(options); if (options.devId !== deviceDevId) { throw new Error(`Package signing cancelled: provided devId '${options.devId}' does not match on-device devId '${deviceDevId}'`); } @@ -593,6 +579,7 @@ export class RokuDeploy { * @param options */ public async deleteDevChannel(options?: DeleteDevChannelOptions) { + this.checkRequiredOptions(options, ['host', 'password']); options = this.getOptions(options) as any; let deleteOptions = this.generateBaseRequestOptions('plugin_install', options as any); @@ -607,6 +594,7 @@ export class RokuDeploy { * Gets a screenshot from the device. A side-loaded channel must be running or an error will be thrown. */ public async captureScreenshot(options: CaptureScreenshotOptions) { + this.checkRequiredOptions(options, ['host', 'password']); options = this.getOptions(options); options.screenshotFile = options.screenshotFile ?? `screenshot-${dayjs().format('YYYY-MM-DD-HH.mm.ss.SSS')}`; let saveFilePath: string; @@ -712,6 +700,14 @@ export class RokuDeploy { return options; } + public checkRequiredOptions>(options: T, requiredOptions: Array) { + for (let opt of requiredOptions as string[]) { + if (options[opt] === undefined) { + throw new Error('Missing required option: ' + opt); + } + } + } + /** * Centralizes getting output zip file path based on passed in options * @param options @@ -756,6 +752,7 @@ export class RokuDeploy { public async getDeviceInfo(options?: { enhance: true } & GetDeviceInfoOptions): Promise; public async getDeviceInfo(options?: GetDeviceInfoOptions): Promise public async getDeviceInfo(options: GetDeviceInfoOptions) { + this.checkRequiredOptions(options, ['host']); options = this.getOptions(options) as any; //if the host is a DNS name, look up the IP address @@ -822,6 +819,7 @@ export class RokuDeploy { * @returns */ public async getDevId(options?: GetDevIdOptions) { + this.checkRequiredOptions(options, ['host']); const deviceInfo = await this.getDeviceInfo(options); return deviceInfo['keyed-developer-id']; }