Skip to content

Commit

Permalink
Add USB device host support
Browse files Browse the repository at this point in the history
Signed-off-by: Liam McLoughlin <lmcloughlin@google.com>
  • Loading branch information
Hexxeh committed Aug 1, 2022
1 parent aab38e0 commit 33f7abe
Show file tree
Hide file tree
Showing 19 changed files with 1,154 additions and 208 deletions.
1 change: 1 addition & 0 deletions packages/fdb-debugger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export class RemoteHost extends EventEmitter {
'app.launchComponent',
FDBTypes.LaunchComponentParams,
FDBTypes.AppComponent,
{ minTimeout: 15000 },
);

private changeSerialization = (serialization: FDBTypes.SerializationType) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@fitbit/fdb-protocol": "^1.8.0-pre.0",
"@fitbit/jsonrpc-ts": "^3.2.1",
"@fitbit/portable-pixmap": "^1.0.3",
"@fitbit/pulse": "^0.1.0",
"@moleculer/vorpal": "^1.11.5",
"@openid/appauth": "^1.2.8",
"chalk": "^4.1.0",
Expand All @@ -42,6 +43,7 @@
"tslib": "^2.0.1",
"untildify": "^4.0.0",
"update-notifier": "^4.1.1",
"usb": "^2.4.3",
"websocket-stream": "^5.5.2"
},
"devDependencies": {
Expand Down
57 changes: 20 additions & 37 deletions packages/sdk-cli/src/api/developerRelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import mockWithPromiseWaiter from '../testUtils/mockWithPromiseWaiter';
jest.mock('websocket-stream', () => jest.fn());
jest.mock('../auth');

const mockHostID = 'fakeHost';

const mockAppHost = {
id: 'apphostA',
displayName: 'App Host',
Expand All @@ -28,6 +26,15 @@ const mockCompanionHost = {

let endpointMock: nock.Scope;

function relayHostToHost(relayHost: typeof mockAppHost) {
return {
displayName: relayHost.displayName,
roles: relayHost.roles,
available: relayHost.state === 'available',
connect: expect.any(Function),
};
}

function mockHostsResponse(status: number, payload?: any) {
endpointMock
.get('/1/user/-/developer-relay/hosts.json')
Expand All @@ -45,48 +52,22 @@ function mockConnectionURLResponse(hostID: string, response: string) {
}

beforeEach(() => {
(auth.getAccessToken as jest.Mock).mockResolvedValueOnce('mockToken');
(auth.getAccessToken as jest.Mock).mockResolvedValue('mockToken');
endpointMock = nock(environment().config.apiUrl);
});

describe('hosts()', () => {
it('returns list of connected app hosts', async () => {
mockHostsSuccessResponse();
return expect(developerRelay.hosts()).resolves.toEqual(
expect.objectContaining({
appHost: [mockAppHost],
}),
);
});

it('returns list of connected companion hosts', async () => {
it('returns list of connected hosts', async () => {
mockHostsSuccessResponse();
return expect(developerRelay.hosts()).resolves.toEqual(
expect.objectContaining({
companionHost: [mockCompanionHost],
}),
);
return expect(developerRelay.hosts()).resolves.toEqual([
relayHostToHost(mockAppHost),
relayHostToHost(mockCompanionHost),
]);
});

it('returns empty lists if no hosts are connected', async () => {
mockHostsSuccessResponse([]);
return expect(developerRelay.hosts()).resolves.toEqual({
appHost: [],
companionHost: [],
});
});

it('ignores hosts with unknown roles', async () => {
mockHostsSuccessResponse([
{
...mockAppHost,
roles: ['__bad_role__'],
},
]);
return expect(developerRelay.hosts()).resolves.toEqual({
appHost: [],
companionHost: [],
});
return expect(developerRelay.hosts()).resolves.toEqual([]);
});

it('parses a 403 response for error reasons', async () => {
Expand Down Expand Up @@ -122,12 +103,14 @@ describe('connect()', () => {

beforeEach(async () => {
mockWebSocket = new stream.Duplex();
mockHostsSuccessResponse([mockAppHost]);
socketPromise = mockWithPromiseWaiter(
websocketStream as any,
mockWebSocket,
);
mockConnectionURLResponse(mockHostID, `${mockConnectionURL}\r\n`);
connectPromise = developerRelay.connect(mockHostID);
const host = (await developerRelay.hosts())[0];
mockConnectionURLResponse(mockAppHost.id, `${mockConnectionURL}\r\n`);
connectPromise = host.connect();
await socketPromise;
});

Expand Down
21 changes: 10 additions & 11 deletions packages/sdk-cli/src/api/developerRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { apiFetch, assertAPIResponseOK, decodeJSON } from './baseAPI';
import { assertContentType } from '../util/fetchUtil';

// tslint:disable-next-line:variable-name
export const Host = t.type(
export const RelayHost = t.type(
{
id: t.string,
displayName: t.string,
Expand All @@ -15,12 +15,12 @@ export const Host = t.type(
},
'Host',
);
export type Host = t.TypeOf<typeof Host>;
export type RelayHost = t.TypeOf<typeof RelayHost>;

// tslint:disable-next-line:variable-name
const HostsResponse = t.type(
{
hosts: t.array(Host),
hosts: t.array(RelayHost),
},
'HostsResponse',
);
Expand All @@ -45,7 +45,7 @@ function createWebSocket(uri: string) {
});
}

export async function connect(hostID: string) {
async function connect(hostID: string) {
const url = await getConnectionURL(hostID);
return createWebSocket(url);
}
Expand All @@ -55,11 +55,10 @@ export async function hosts() {
decodeJSON(HostsResponse),
);

const hostsWithRole = (role: string) =>
response.hosts.filter((host) => host.roles.includes(role));

return {
appHost: hostsWithRole('APP_HOST'),
companionHost: hostsWithRole('COMPANION_HOST'),
};
return response.hosts.map((host) => ({
available: host.state === 'available',
connect: () => connect(host.id),
displayName: host.displayName,
roles: host.roles,
}));
}
2 changes: 1 addition & 1 deletion packages/sdk-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ cli.use(logout);
cli.use(repl({ hostConnections }));

if (enableQACommands) {
cli.use(hosts);
cli.use(hosts({ hostConnections }));
cli.use(mockHost);
}

Expand Down
97 changes: 44 additions & 53 deletions packages/sdk-cli/src/commands/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,71 @@ import events from 'events';

import vorpal from '@moleculer/vorpal';

import connect, { DeviceType } from './connect';
import connect from './connect';
import commandTestHarness from '../testUtils/commandTestHarness';
import * as developerRelay from '../api/developerRelay';
import HostConnections, { HostType } from '../models/HostConnections';
import HostConnections, { Host } from '../models/HostConnections';
import { DeviceType, HostType } from '../models/HostTypes';

jest.mock('../models/HostConnections');

const mockAppHost: developerRelay.Host = {
id: 'apphost',
const mockAppHost: Host = {
connect: jest.fn(),
displayName: 'App Host',
roles: ['APP_HOST'],
state: 'available',
available: true,
};

const mockAppHost2: developerRelay.Host = {
id: 'apphost2',
const mockAppHost2: Host = {
connect: jest.fn(),
displayName: 'Another App Host',
roles: ['APP_HOST'],
state: 'available',
available: true,
};

const mockCompanionHost: developerRelay.Host = {
id: 'companionhost',
const mockCompanionHost: Host = {
connect: jest.fn(),
displayName: 'Companion Host',
roles: ['COMPANION_HOST'],
state: 'available',
available: true,
};

const mockCompanionHost2: developerRelay.Host = {
id: 'companionhost2',
const mockCompanionHost2: Host = {
connect: jest.fn(),
displayName: 'Another Companion Host',
roles: ['COMPANION_HOST'],
state: 'available',
available: true,
};

const mockBusyAppHost: developerRelay.Host = {
id: 'apphost3',
const mockBusyAppHost: Host = {
connect: jest.fn(),
displayName: 'Yet Another App Host',
roles: ['APP_HOST'],
state: 'busy',
available: false,
};

const mockRelayHosts = {
const mockRelayHosts: { [key in DeviceType]: Host[] } = {
device: [mockAppHost, mockAppHost2],
phone: [mockCompanionHost, mockCompanionHost2],
};

let cli: vorpal;
let mockLog: jest.Mock;
let mockPrompt: jest.Mock;
let mockWS: events.EventEmitter;
let mockStream: events.EventEmitter;

let hostConnections: HostConnections;
let relayHostsSpy: jest.SpyInstance;
let listOfTypeSpy: jest.SpyInstance;
let hostConnectSpy: jest.SpyInstance;

const mockRelayHostsResponse = {
device: (hosts: developerRelay.Host[]) =>
relayHostsSpy.mockResolvedValueOnce({ appHost: hosts, companionHost: [] }),
phone: (hosts: developerRelay.Host[]) =>
relayHostsSpy.mockResolvedValueOnce({ appHost: [], companionHost: hosts }),
};

beforeEach(() => {
hostConnections = new HostConnections();
({ cli, mockLog, mockPrompt } = commandTestHarness(
connect({ hostConnections }),
));
relayHostsSpy = jest.spyOn(developerRelay, 'hosts');
listOfTypeSpy = jest.spyOn(hostConnections, 'listOfType');
hostConnectSpy = jest.spyOn(hostConnections, 'connect');
mockWS = new events.EventEmitter();
hostConnectSpy.mockResolvedValueOnce({ ws: mockWS });
mockStream = new events.EventEmitter();
hostConnectSpy.mockResolvedValueOnce({ stream: mockStream });
});

function doConnect(type: DeviceType) {
Expand All @@ -85,17 +78,17 @@ describe.each<[DeviceType, HostType]>([
['phone', 'companionHost'],
])('when the device type argument is %s', (deviceType, hostType) => {
it(`logs an error if no ${deviceType}s are connected`, async () => {
mockRelayHostsResponse[deviceType]([]);
listOfTypeSpy.mockResolvedValueOnce([]);
await doConnect(deviceType);
expect(mockLog.mock.calls[0]).toMatchSnapshot();
});

describe(`when a single ${deviceType} is connected`, () => {
let mockHost: developerRelay.Host;
let mockHost: Host;

beforeEach(() => {
mockHost = mockRelayHosts[deviceType][0];
mockRelayHostsResponse[deviceType]([mockHost]);
listOfTypeSpy.mockResolvedValueOnce([mockHost]);
return doConnect(deviceType);
});

Expand All @@ -107,28 +100,25 @@ describe.each<[DeviceType, HostType]>([
expect(mockLog.mock.calls[0]).toMatchSnapshot();
});

it('acquires a developer relay connection for the given host type and ID', () => {
expect(hostConnectSpy).toBeCalledWith(hostType, mockHost.id);
it('acquires a connection for the selected host', () => {
expect(hostConnectSpy).toBeCalledWith(mockHost, deviceType);
});

it('logs a message when the host disconnects', () => {
mockWS.emit('finish');
mockStream.emit('finish');
expect(mockLog.mock.calls[1]).toMatchSnapshot();
});
});

describe(`when multiple ${deviceType}s are connected`, () => {
let mockSelectedHost: developerRelay.Host;
let mockSelectedHost: Host;

beforeEach(() => {
const mockHosts = mockRelayHosts[deviceType];
mockSelectedHost = mockHosts[1];
mockRelayHostsResponse[deviceType](mockHosts);
listOfTypeSpy.mockResolvedValueOnce(mockHosts);
mockPrompt.mockResolvedValueOnce({
hostID: {
id: mockSelectedHost.id,
displayName: mockSelectedHost.displayName,
},
host: mockSelectedHost,
});
return doConnect(deviceType);
});
Expand All @@ -137,41 +127,42 @@ describe.each<[DeviceType, HostType]>([
expect(mockPrompt).toBeCalled();
});

it('acquires a developer relay connection for the given host type and ID', () => {
expect(hostConnectSpy).toBeCalledWith(hostType, mockSelectedHost.id);
it('acquires a connection for the selected host', () => {
expect(hostConnectSpy).toBeCalledWith(mockSelectedHost, deviceType);
});
});

it('logs an error if the hosts call throws', async () => {
relayHostsSpy.mockRejectedValueOnce(new Error('some error'));
listOfTypeSpy.mockRejectedValueOnce(new Error('some error'));
await doConnect(deviceType);
expect(mockLog.mock.calls[0]).toMatchSnapshot();
});
});

it('does not show busy hosts', async () => {
mockRelayHostsResponse.device([mockAppHost, mockAppHost2, mockBusyAppHost]);
listOfTypeSpy.mockResolvedValueOnce([
mockAppHost,
mockAppHost2,
mockBusyAppHost,
]);

mockPrompt.mockResolvedValueOnce({
hostID: {
id: mockAppHost.id,
displayName: mockAppHost.displayName,
},
host: mockAppHost,
});

await doConnect('device');
expect(mockPrompt).toBeCalledWith(
expect.objectContaining({
choices: [mockAppHost, mockAppHost2].map((host) => ({
value: { id: host.id, displayName: host.displayName },
value: host,
name: host.displayName,
})),
}),
);
});

it('does not auto-connect a busy host', async () => {
mockRelayHostsResponse.device([mockBusyAppHost]);
listOfTypeSpy.mockResolvedValueOnce([mockBusyAppHost]);
await doConnect('device');
expect(mockLog.mock.calls[0]).toMatchSnapshot();
});
Loading

0 comments on commit 33f7abe

Please sign in to comment.