diff --git a/package.json b/package.json index 2567086..55d6fec 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "publish-coverage": "nyc report --reporter=text-lcov | coveralls" }, "dependencies": { + "@types/request": "^2.47.0", "chalk": "^2.4.2", "dateformat": "^3.0.3", "dayjs": "^1.11.0", @@ -42,7 +43,6 @@ "@types/mocha": "^9.0.0", "@types/node": "^16.11.3", "@types/q": "^1.5.8", - "@types/request": "^2.47.0", "@types/sinon": "^10.0.4", "@types/xml2js": "^0.4.5", "@typescript-eslint/eslint-plugin": "5.1.0", diff --git a/src/Errors.ts b/src/Errors.ts index 7f480aa..1ca78cc 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -1,4 +1,5 @@ -import type { RokuMessages } from './RokuDeploy'; +import type { HttpResponse, RokuMessages } from './RokuDeploy'; +import type * as requestType from 'request'; export class InvalidDeviceResponseCodeError extends Error { constructor(message: string, public results?: any) { @@ -56,16 +57,45 @@ export class MissingRequiredOptionError extends Error { } } +/** + * This error is thrown when a Roku device refuses to accept connections because it requires the user to check for updates (even if no updates are actually available). + */ export class UpdateCheckRequiredError extends Error { - results: any; - cause: Error; + static MESSAGE = `Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`; - constructor(originalError: Error) { + constructor( + public response: HttpResponse, + public requestOptions: requestType.OptionsWithUrl, + public cause?: Error + ) { super(); - this.message = `Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`; - this.results = { response: { statusCode: 577 } }; - this.cause = originalError; + this.message = UpdateCheckRequiredError.MESSAGE; Object.setPrototypeOf(this, UpdateCheckRequiredError.prototype); } } + +export function isUpdateCheckRequiredError(e: any): e is UpdateCheckRequiredError { + return e?.constructor?.name === 'UpdateCheckRequiredError'; +} + +/** + * This error is thrown when a Roku device ends the connection unexpectedly, causing an 'ECONNRESET' error. Typically this happens when the device needs to check for updates (even if no updates are available), but it can also happen for other reasons. + */ +export class ConnectionResetError extends Error { + + static MESSAGE = `The Roku device ended the connection unexpectedly and may need to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again.\n\nhttps://support.roku.com/article/208755668.`; + + constructor(error: Error, requestOptions: requestType.OptionsWithUrl) { + super(); + this.message = ConnectionResetError.MESSAGE; + this.cause = error; + Object.setPrototypeOf(this, ConnectionResetError.prototype); + } + + public cause?: Error; +} + +export function isConnectionResetError(e: any): e is ConnectionResetError { + return e?.constructor?.name === 'ConnectionResetError'; +} diff --git a/src/RokuDeploy.spec.ts b/src/RokuDeploy.spec.ts index dfe03fc..a05e50a 100644 --- a/src/RokuDeploy.spec.ts +++ b/src/RokuDeploy.spec.ts @@ -1103,17 +1103,18 @@ describe('index', () => { }); }); - it('rejects when response contains invalid password status code', () => { + it('rejects when response contains invalid password status code', async () => { options.failOnCompileError = true; - mockDoPostRequest('', 577); + mockDoPostRequest(`'Failed to check for software update'`, 200); - return rokuDeploy.publish(options).then(() => { + try { + await rokuDeploy.publish(options); 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(`Your device needs to check for updates before accepting connections. Please navigate to System Settings and check for updates and then try again. - -https://support.roku.com/article/208755668.`)); - }); + } catch (err) { + expect((err as any).message).to.eql( + errors.UpdateCheckRequiredError.MESSAGE + ); + } }); it('handles successful deploy', () => { @@ -1206,6 +1207,45 @@ https://support.roku.com/article/208755668.`)); } assert.fail('Should not have succeeded'); }); + + it('Should throw an excpetion', async () => { + options.failOnCompileError = true; + mockDoPostRequest('', 577); + + try { + await rokuDeploy.publish(options); + } catch (e) { + assert.ok('Exception was thrown as expected'); + expect(e).to.be.instanceof(errors.UpdateCheckRequiredError); + return; + } + assert.fail('Should not have succeeded'); + }); + + class ErrorWithConnectionResetCode extends Error { + code; + + constructor(code = 'ECONNRESET') { + super(); + this.code = code; + } + } + + it('Should throw an excpetion', async () => { + options.failOnCompileError = true; + sinon.stub(rokuDeploy as any, 'doPostRequest').callsFake((params) => { + throw new ErrorWithConnectionResetCode(); + }); + + try { + await rokuDeploy.publish(options); + } catch (e) { + assert.ok('Exception was thrown as expected'); + expect(e).to.be.instanceof(errors.ConnectionResetError); + return; + } + assert.fail('Should not have succeeded'); + }); }); describe('convertToSquashfs', () => { @@ -3642,6 +3682,22 @@ https://support.roku.com/article/208755668.`)); }); }); + describe('isUpdateCheckRequiredResponse', () => { + it('matches on actual response from device', () => { + const response = `\n
\n \n \n