Skip to content

Commit

Permalink
chore(test): add RokuDebugClient unit tests
Browse files Browse the repository at this point in the history
- add default connect timeout
- do not connect when already connected
- move setEncoding call (may be wrong)
  • Loading branch information
boneskull committed Nov 2, 2022
1 parent b4c992d commit 99519b8
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 6 deletions.
25 changes: 19 additions & 6 deletions lib/debug/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const DEFAULT_TELNET_OPTS = Object.freeze(
/** @type {TelnetOptions} */ ({
negotiationMandatory: false,
encoding: 'utf8',
timeout: 2000
})
);

Expand Down Expand Up @@ -61,11 +62,14 @@ export class RokuDebugClient extends EventEmitter {

/**
* Assigns defaults & computes host slug
* @param {string} host
* @param {RokuDebugClientOpts} [opts]
* @param {string} host - Hostname or IP address of Roku device
* @param {RokuDebugClientOpts} [opts] - Options
*/
constructor(host, opts = {}) {
super();
if (!host) {
throw new Error('host is required');
}
this.#host = host;
this.#opts = _.defaultsDeep(opts, DEFAULT_DEBUG_CLIENT_OPTS);
this.#hostSlug = RokuDebugClient.slugify(host);
Expand Down Expand Up @@ -100,6 +104,10 @@ export class RokuDebugClient extends EventEmitter {
* @returns {Promise<void>}
*/
async connect() {
if (this.isConnected) {
return;
}

const client = (this.#telnetClient = new Telnet());
await client.connect({
host: this.#host,
Expand All @@ -108,7 +116,7 @@ export class RokuDebugClient extends EventEmitter {
...this.#opts.telnet,
});
log.debug(`Connected to ${this.#host}`);

client.getSocket().setEncoding('utf-8');
client
.on('error', (err) => {
log.error(err);
Expand All @@ -127,8 +135,14 @@ export class RokuDebugClient extends EventEmitter {
}
})
.on('timeout', () => {
// inactivity timeout. 'close' will also be emitted (I think), so don't unlock here
this.emit('timeout');
// connection timeout
const timeout = this.#opts.telnet.timeout;
log.error(`Connection to host ${this.#host} timed out (${timeout} ms)`);
try {
this.emit('timeout');
} finally {
this.#cleanup();
}
});
}

Expand All @@ -154,7 +168,6 @@ export class RokuDebugClient extends EventEmitter {
async *[Symbol.asyncIterator]() {
const sock = this.#telnetClient?.getSocket();
if (sock) {
sock.setEncoding('utf8');
for await (const chunk of sock) {
yield chunk;
}
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"type-fest": "2.19.0",
"typescript": "^4.7.4",
"unexpected": "13.0.1",
"unexpected-eventemitter": "2.4.0",
"unexpected-sinon": "11.1.0",
"webdriverio": "7.25.1"
},
Expand Down
225 changes: 225 additions & 0 deletions test/unit/debug/client.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import rewiremock from 'rewiremock/node';
import {createSandbox} from 'sinon';
import {EventEmitter} from 'node:events';
import unexpected from 'unexpected';
import unexpectedSinon from 'unexpected-sinon';
import unexpectedEventemitter from 'unexpected-eventemitter';

const expect = unexpected.clone().use(unexpectedSinon).use(unexpectedEventemitter);

describe('RokuDebugClient', function () {
/** @type {typeof import('../../../lib/debug/client').RokuDebugClient} */
let RokuDebugClient;

/** @type {sinon.SinonSandbox} */
let sandbox;

let MockTelnet;

let MockSlug;

let telnet;

beforeEach(function () {
sandbox = createSandbox();

telnet = new EventEmitter();

const socket = ['chunk', 'another chunk', 'yet another chunk'];
socket.setEncoding = sandbox.stub();

Object.assign(telnet, {
connect: sandbox.stub().resolves(),
end: sandbox.stub().resolves(),
destroy: sandbox.stub().resolves(),
getSocket: sandbox.stub().returns(socket),
});

MockTelnet = {
Telnet: sandbox.stub().returns(telnet),
};

MockSlug = sandbox.stub().returnsArg(0);

({RokuDebugClient} = rewiremock.proxy(() => require('../../../lib/debug/client'), {
'telnet-client': MockTelnet,
slug: MockSlug,
}));
});

describe('constructor', function () {
describe('when provided a host parameter', function () {
it('should instantiate a RokuDebugClient', function () {
expect(new RokuDebugClient('localhost'), 'to be a', RokuDebugClient);
});
});

describe('when not provided a host parameter', function () {
it('should throw', function () {
expect(() => new RokuDebugClient(), 'to throw', 'host is required');
});
});
});

describe('instance property', function () {
let client;

beforeEach(function () {
client = new RokuDebugClient('localhost');
});

describe('hostSlug', function () {
it('should return a slugified host', function () {
expect(client.hostSlug, 'to be a', 'string');
});
});

describe('isConnected', function () {
describe('when connected to the host', function () {
it('should be `true`', async function () {
await client.connect();
expect(client.isConnected, 'to be true');
});
});

describe('when disconnected from the host', function () {
it('should be `false`', function () {
expect(client.isConnected, 'to be false');
});
});
});
});

describe('instance method', function () {
let client;

beforeEach(function () {
client = new RokuDebugClient('localhost');
});

describe('connect()', function () {
it('should connect to the host', async function () {
await client.connect();
expect(telnet.connect, 'was called once');
});

it('should set the encoding to utf-8', async function () {
await client.connect();
expect(telnet.getSocket().setEncoding, 'to have a call satisfying', ['utf-8']);
});

describe('when already connected', function () {
it('should not attempt to reconnect', async function () {
await client.connect();
await client.connect();
expect(telnet.connect, 'was called once');
});
});

describe('event handling', function () {
beforeEach(async function () {
await client.connect();
});

describe('when the client emits event `error`', function () {
it('should emit event `error`', function () {
const error = new Error('some error');

expect(() => telnet.emit('error', error), 'to emit from', client, 'error', error);
});

it('should clean up', function () {
const error = new Error('some error');
client.on('error', () => {}); // eat error
telnet.emit('error', error);
expect(client.isConnected, 'to be false');
});
});

describe('when the client emits event `close`', function () {
it('should emit event `close`', function () {
expect(() => telnet.emit('close'), 'to emit from', client, 'close');
});

it('should clean up', function () {
telnet.emit('close');
expect(client.isConnected, 'to be false');
});
});

describe('when the client emits event `timeout`', function () {
it('should emit event `timeout`', function () {
expect(() => telnet.emit('timeout'), 'to emit from', client, 'timeout');
});

it('should clean up', function () {
telnet.emit('timeout');
expect(client.isConnected, 'to be false');
});
});
});
});

describe('disconnect()', function () {
describe('when connected to the host', function () {
it('should disconnect', async function () {
await client.connect();
await client.disconnect();
expect(telnet.end, 'was called once');
});

it('should clean up', async function () {
await client.connect();
await client.disconnect();
expect(client.isConnected, 'to be false');
});

describe('when disconnecting fails', function () {
it('should destroy the socket', async function () {
await client.connect();
telnet.end.rejects(new Error('some error'));
await client.disconnect();
expect(telnet.destroy, 'was called once');
});
});
});
});
});

describe('AsyncIterable behavior', function () {
// https://tc39.es/proposal-array-from-async/ would be handy here

/** @type {RokuDebugClient} */
let client;
beforeEach(function () {
client = new RokuDebugClient('localhost');
});

describe('when connected', function () {
beforeEach(async function () {
await client.connect();
});

it('should be async iterable', async function () {
const chunks = [];
for await (const chunk of client) {
chunks.push(chunk);
}
expect(chunks, 'to equal', Array.from(telnet.getSocket()));
});
});
});

describe('static method', function () {
describe('slugify', function () {
it('should delegate to `slug` package', function () {
RokuDebugClient.slugify('localhost');
expect(MockSlug, 'was called once');
});
});
});

afterEach(function () {
sandbox.restore();
});
});

0 comments on commit 99519b8

Please sign in to comment.