From fa15f3ed39815205aee8dd2a23f6161b1c9a308a Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Tue, 12 Sep 2023 10:53:39 -0500 Subject: [PATCH] feat: add support for viewing edge driver usage and pruning unused drivers --- .changeset/calm-lizards-leave.md | 6 + packages/cli/README.md | 86 ++ packages/cli/src/commands/schema/update.ts | 2 +- packages/edge/README.md | 86 ++ .../lib/commands/drivers-util.test.ts | 1222 +++++++++-------- .../edge/src/commands/edge/drivers/devices.ts | 80 ++ .../edge/src/commands/edge/drivers/prune.ts | 75 + .../edge/src/lib/commands/drivers-util.ts | 54 + 8 files changed, 1067 insertions(+), 544 deletions(-) create mode 100644 .changeset/calm-lizards-leave.md create mode 100644 packages/edge/src/commands/edge/drivers/devices.ts create mode 100644 packages/edge/src/commands/edge/drivers/prune.ts diff --git a/.changeset/calm-lizards-leave.md b/.changeset/calm-lizards-leave.md new file mode 100644 index 00000000..4bcce972 --- /dev/null +++ b/.changeset/calm-lizards-leave.md @@ -0,0 +1,6 @@ +--- +"@smartthings/cli": minor +"@smartthings/plugin-cli-edge": minor +--- + +added support for viewing edge driver usage and pruning unused drivers diff --git a/packages/cli/README.md b/packages/cli/README.md index e5c77356..bb1f813a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -192,10 +192,12 @@ that map to the API spec. * [`smartthings edge:drivers [IDORINDEX]`](#smartthings-edgedrivers-idorindex) * [`smartthings edge:drivers:default [IDORINDEX]`](#smartthings-edgedriversdefault-idorindex) * [`smartthings edge:drivers:delete [ID]`](#smartthings-edgedriversdelete-id) +* [`smartthings edge:drivers:devices [IDORINDEX]`](#smartthings-edgedriversdevices-idorindex) * [`smartthings edge:drivers:install [DRIVERID]`](#smartthings-edgedriversinstall-driverid) * [`smartthings edge:drivers:installed [IDORINDEX]`](#smartthings-edgedriversinstalled-idorindex) * [`smartthings edge:drivers:logcat [DRIVERID]`](#smartthings-edgedriverslogcat-driverid) * [`smartthings edge:drivers:package [PROJECTDIRECTORY]`](#smartthings-edgedriverspackage-projectdirectory) +* [`smartthings edge:drivers:prune [DRIVERID]`](#smartthings-edgedriversprune-driverid) * [`smartthings edge:drivers:switch [DEVICEID]`](#smartthings-edgedriversswitch-deviceid) * [`smartthings edge:drivers:uninstall [DRIVERID]`](#smartthings-edgedriversuninstall-driverid) * [`smartthings help [COMMAND]`](#smartthings-help-command) @@ -3571,6 +3573,58 @@ DESCRIPTION _See code: [@smartthings/plugin-cli-edge](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/delete.ts)_ +## `smartthings edge:drivers:devices [IDORINDEX]` + +list devices using edge drivers + +``` +USAGE + $ smartthings edge:drivers:devices [IDORINDEX] [-h] [-p ] [-t ] [--language ] [-O ] [-j] + [-y] [-o ] [-H ] [-D ] + +ARGUMENTS + IDORINDEX the device id or number in list + +FLAGS + -D, --driver= driver id + -H, --hub= hub id + -O, --organization= the organization ID to use for this command + +COMMON FLAGS + -h, --help Show CLI help. + -j, --json use JSON format of input and/or output + -o, --output= specify output file + -p, --profile= [default: default] configuration profile + -t, --token= the auth token to use + -y, --yaml use YAML format of input and/or output + --language= ISO language code or "NONE" to not specify a language. Defaults to the OS locale + +DESCRIPTION + list devices using edge drivers + + For API information, see: + + https://developer.smartthings.com/docs/api/public/#operation/getDevices, + https://developer.smartthings.com/docs/api/public/#operation/listDrivers, + https://developer.smartthings.com/docs/api/public/#operation/getDriver, + https://developer.smartthings.com/docs/api/public/#operation/getDriverRevision + +EXAMPLES + # list all devices using edge drivers + $ smartthings edge:drivers:devices + # display details about the third device listed in the above command + $ smartthings edge:drivers:devices 3 + + # display details about a device by using its id + $ smartthings edge:drivers:devices dfda0a8e-55d6-445b-ace5-db828679bcb3 + # list all devices using edge drivers on the specified hub + $ smartthings edge:drivers:devices --hub a9108ab1-7087-4c10-9781-a0627b084fce + # list devices that use a specific driver + $ smartthings edge:drivers:devices --driver b67a134c-ace8-4b8d-9a0e-444ad78b4455 +``` + +_See code: [@smartthings/plugin-cli-edge](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/devices.ts)_ + ## `smartthings edge:drivers:install [DRIVERID]` install an edge driver onto a hub @@ -3757,6 +3811,38 @@ EXAMPLES _See code: [@smartthings/plugin-cli-edge](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/package.ts)_ +## `smartthings edge:drivers:prune [DRIVERID]` + +uninstall unused edge drivers from a hub + +``` +USAGE + $ smartthings edge:drivers:prune [DRIVERID] [-h] [-p ] [-t ] [--language ] [-O ] [-H + ] + +ARGUMENTS + DRIVERID id of driver to uninstall + +FLAGS + -H, --hub= hub id + -O, --organization= the organization ID to use for this command + +COMMON FLAGS + -h, --help Show CLI help. + -p, --profile= [default: default] configuration profile + -t, --token= the auth token to use + --language= ISO language code or "NONE" to not specify a language. Defaults to the OS locale + +DESCRIPTION + uninstall unused edge drivers from a hub + + For API information, see: + + https://developer.smartthings.com/docs/api/public/#operation/uninstallDriver +``` + +_See code: [@smartthings/plugin-cli-edge](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/prune.ts)_ + ## `smartthings edge:drivers:switch [DEVICEID]` change the driver used by an installed device diff --git a/packages/cli/src/commands/schema/update.ts b/packages/cli/src/commands/schema/update.ts index ead03548..dc432b20 100644 --- a/packages/cli/src/commands/schema/update.ts +++ b/packages/cli/src/commands/schema/update.ts @@ -2,7 +2,7 @@ import { Flags, Errors } from '@oclif/core' import { SchemaApp, SchemaAppRequest } from '@smartthings/core-sdk' -import { APICommand, inputItem, selectFromList, lambdaAuthFlags, SelectFromListConfig, userInputProcessor, inputAndOutputItem } from '@smartthings/cli-lib' +import { APICommand, inputItem, selectFromList, lambdaAuthFlags, SelectFromListConfig, userInputProcessor } from '@smartthings/cli-lib' import { addSchemaPermission } from '../../lib/aws-utils' import { getSchemaAppUpdateFromUser } from '../../lib/commands/schema-util' diff --git a/packages/edge/README.md b/packages/edge/README.md index e8e62e7f..2fa021e0 100644 --- a/packages/edge/README.md +++ b/packages/edge/README.md @@ -38,10 +38,12 @@ for information on running the CLI. * [`smartthings edge:drivers [IDORINDEX]`](#smartthings-edgedrivers-idorindex) * [`smartthings edge:drivers:default [IDORINDEX]`](#smartthings-edgedriversdefault-idorindex) * [`smartthings edge:drivers:delete [ID]`](#smartthings-edgedriversdelete-id) +* [`smartthings edge:drivers:devices [IDORINDEX]`](#smartthings-edgedriversdevices-idorindex) * [`smartthings edge:drivers:install [DRIVERID]`](#smartthings-edgedriversinstall-driverid) * [`smartthings edge:drivers:installed [IDORINDEX]`](#smartthings-edgedriversinstalled-idorindex) * [`smartthings edge:drivers:logcat [DRIVERID]`](#smartthings-edgedriverslogcat-driverid) * [`smartthings edge:drivers:package [PROJECTDIRECTORY]`](#smartthings-edgedriverspackage-projectdirectory) +* [`smartthings edge:drivers:prune [DRIVERID]`](#smartthings-edgedriversprune-driverid) * [`smartthings edge:drivers:switch [DEVICEID]`](#smartthings-edgedriversswitch-deviceid) * [`smartthings edge:drivers:uninstall [DRIVERID]`](#smartthings-edgedriversuninstall-driverid) @@ -694,6 +696,58 @@ DESCRIPTION _See code: [src/commands/edge/drivers/delete.ts](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/delete.ts)_ +## `smartthings edge:drivers:devices [IDORINDEX]` + +list devices using edge drivers + +``` +USAGE + $ smartthings edge:drivers:devices [IDORINDEX] [-h] [-p ] [-t ] [--language ] [-O ] [-j] + [-y] [-o ] [-H ] [-D ] + +ARGUMENTS + IDORINDEX the device id or number in list + +FLAGS + -D, --driver= driver id + -H, --hub= hub id + -O, --organization= the organization ID to use for this command + +COMMON FLAGS + -h, --help Show CLI help. + -j, --json use JSON format of input and/or output + -o, --output= specify output file + -p, --profile= [default: default] configuration profile + -t, --token= the auth token to use + -y, --yaml use YAML format of input and/or output + --language= ISO language code or "NONE" to not specify a language. Defaults to the OS locale + +DESCRIPTION + list devices using edge drivers + + For API information, see: + + https://developer.smartthings.com/docs/api/public/#operation/getDevices, + https://developer.smartthings.com/docs/api/public/#operation/listDrivers, + https://developer.smartthings.com/docs/api/public/#operation/getDriver, + https://developer.smartthings.com/docs/api/public/#operation/getDriverRevision + +EXAMPLES + # list all devices using edge drivers + $ smartthings edge:drivers:devices + # display details about the third device listed in the above command + $ smartthings edge:drivers:devices 3 + + # display details about a device by using its id + $ smartthings edge:drivers:devices dfda0a8e-55d6-445b-ace5-db828679bcb3 + # list all devices using edge drivers on the specified hub + $ smartthings edge:drivers:devices --hub a9108ab1-7087-4c10-9781-a0627b084fce + # list devices that use a specific driver + $ smartthings edge:drivers:devices --driver b67a134c-ace8-4b8d-9a0e-444ad78b4455 +``` + +_See code: [src/commands/edge/drivers/devices.ts](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/devices.ts)_ + ## `smartthings edge:drivers:install [DRIVERID]` install an edge driver onto a hub @@ -880,6 +934,38 @@ EXAMPLES _See code: [src/commands/edge/drivers/package.ts](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/package.ts)_ +## `smartthings edge:drivers:prune [DRIVERID]` + +uninstall unused edge drivers from a hub + +``` +USAGE + $ smartthings edge:drivers:prune [DRIVERID] [-h] [-p ] [-t ] [--language ] [-O ] [-H + ] + +ARGUMENTS + DRIVERID id of driver to uninstall + +FLAGS + -H, --hub= hub id + -O, --organization= the organization ID to use for this command + +COMMON FLAGS + -h, --help Show CLI help. + -p, --profile= [default: default] configuration profile + -t, --token= the auth token to use + --language= ISO language code or "NONE" to not specify a language. Defaults to the OS locale + +DESCRIPTION + uninstall unused edge drivers from a hub + + For API information, see: + + https://developer.smartthings.com/docs/api/public/#operation/uninstallDriver +``` + +_See code: [src/commands/edge/drivers/prune.ts](https://github.com/SmartThingsCommunity/smartthings-cli/blob/@smartthings/plugin-cli-edge@3.2.1/packages/edge/src/commands/edge/drivers/prune.ts)_ + ## `smartthings edge:drivers:switch [DEVICEID]` change the driver used by an installed device diff --git a/packages/edge/src/__tests__/lib/commands/drivers-util.test.ts b/packages/edge/src/__tests__/lib/commands/drivers-util.test.ts index f1a59cc7..7a392152 100644 --- a/packages/edge/src/__tests__/lib/commands/drivers-util.test.ts +++ b/packages/edge/src/__tests__/lib/commands/drivers-util.test.ts @@ -1,14 +1,42 @@ -import { Device, DeviceIntegrationType, DriverChannelDetails, EdgeDeviceIntegrationProfileKey, - EdgeDriver, EdgeDriverPermissions, EdgeDriverSummary, InstalledDriver, Location, - OrganizationResponse, SmartThingsClient } from '@smartthings/core-sdk' - -import { APICommand, ChooseOptions, chooseOptionsWithDefaults, forAllOrganizations, selectFromList, - stringTranslateToId, TableGenerator, WithOrganization } from '@smartthings/cli-lib' - -import { buildTableOutput, chooseDriver, chooseDriverFromChannel, chooseHub, - DriverChannelDetailsWithName, listAllAvailableDrivers, listAssignedDriversWithNames, - listDrivers, listMatchingDrivers, permissionsValue, withoutCurrentDriver } - from '../../../lib/commands/drivers-util' +import { + Device, + DeviceIntegrationType, + DriverChannelDetails, + EdgeDeviceIntegrationProfileKey, + EdgeDriver, + EdgeDriverPermissions, + EdgeDriverSummary, + InstalledDriver, + Location, + OrganizationResponse, + SmartThingsClient, +} from '@smartthings/core-sdk' + +import { + APICommand, + ChooseOptions, + chooseOptionsWithDefaults, + forAllOrganizations, + selectFromList, + stringTranslateToId, + TableGenerator, + WithOrganization, +} from '@smartthings/cli-lib' + +import { + buildTableOutput, + chooseDriver, + chooseDriverFromChannel, + chooseHub, + DriverChannelDetailsWithName, + getDriverDevices, + listAllAvailableDrivers, + listAssignedDriversWithNames, + listDrivers, + listMatchingDrivers, + permissionsValue, + withoutCurrentDriver, +} from '../../../lib/commands/drivers-util' import * as driversUtil from '../../../lib/commands/drivers-util' @@ -20,693 +48,801 @@ jest.mock('@smartthings/cli-lib', () => ({ forAllOrganizations: jest.fn(), })) -describe('drivers-util', () => { - const selectFromListMock = jest.mocked(selectFromList) - const stringTranslateToIdMock = jest.mocked(stringTranslateToId) +const selectFromListMock = jest.mocked(selectFromList) +const stringTranslateToIdMock = jest.mocked(stringTranslateToId) - describe('permissionsValue', () => { - it('returns none with no permissions at all', () => { - expect(permissionsValue({} as EdgeDriver)).toBe('none') - }) +describe('permissionsValue', () => { + it('returns none with no permissions at all', () => { + expect(permissionsValue({} as EdgeDriver)).toBe('none') + }) - it('returns none with empty permissions array', () => { - expect(permissionsValue({ permissions: [] as EdgeDriverPermissions[] } as EdgeDriver)).toBe('none') - }) + it('returns none with empty permissions array', () => { + expect(permissionsValue({ permissions: [] as EdgeDriverPermissions[] } as EdgeDriver)).toBe('none') + }) - it('combines permissions names', () => { - expect(permissionsValue({ permissions: [ - { name: 'r:locations' }, - { name: 'r:devices' }, - ] } as EdgeDriver)).toBe('r:locations\nr:devices') - }) + it('combines permissions names', () => { + expect(permissionsValue({ permissions: [ + { name: 'r:locations' }, + { name: 'r:devices' }, + ] } as EdgeDriver)).toBe('r:locations\nr:devices') }) +}) - describe('buildTableOutput', () => { - const buildTableFromItem = jest.fn().mockReturnValue('basic info') - const buildTableFromList = jest.fn() - const tableGenerator = { - buildTableFromItem, - buildTableFromList, - } as unknown as TableGenerator - const minimalDriver: EdgeDriver = { - driverId: 'driver-id', - name: 'Driver Name', - version: 'driver-version', - packageKey: 'package key', - deviceIntegrationProfiles: [{ id: 'profile-id' } as EdgeDeviceIntegrationProfileKey], - } - - it('works with minimal fields', () => { - buildTableFromList.mockReturnValueOnce('profiles table') - - expect(buildTableOutput(tableGenerator, minimalDriver)) - .toBe('Basic Information\nbasic info\n\n' + - 'Device Integration Profiles\nprofiles table\n\n' + - 'No fingerprints specified.') - - expect(buildTableFromItem).toHaveBeenCalledTimes(1) - expect(buildTableFromItem).toHaveBeenCalledWith(minimalDriver, - expect.arrayContaining(['driverId', 'name', 'version', 'packageKey'])) - expect(buildTableFromList).toHaveBeenCalledTimes(1) - expect(buildTableFromList).toHaveBeenCalledWith(minimalDriver.deviceIntegrationProfiles, - ['id', 'majorVersion']) - }) +describe('buildTableOutput', () => { + const buildTableFromItem = jest.fn().mockReturnValue('basic info') + const buildTableFromList = jest.fn() + const tableGenerator = { + buildTableFromItem, + buildTableFromList, + } as unknown as TableGenerator + const minimalDriver: EdgeDriver = { + driverId: 'driver-id', + name: 'Driver Name', + version: 'driver-version', + packageKey: 'package key', + deviceIntegrationProfiles: [{ id: 'profile-id' } as EdgeDeviceIntegrationProfileKey], + } + + it('works with minimal fields', () => { + buildTableFromList.mockReturnValueOnce('profiles table') + + expect(buildTableOutput(tableGenerator, minimalDriver)) + .toBe('Basic Information\nbasic info\n\n' + + 'Device Integration Profiles\nprofiles table\n\n' + + 'No fingerprints specified.') + + expect(buildTableFromItem).toHaveBeenCalledTimes(1) + expect(buildTableFromItem).toHaveBeenCalledWith(minimalDriver, + expect.arrayContaining(['driverId', 'name', 'version', 'packageKey'])) + expect(buildTableFromList).toHaveBeenCalledTimes(1) + expect(buildTableFromList).toHaveBeenCalledWith(minimalDriver.deviceIntegrationProfiles, + ['id', 'majorVersion']) + }) - it('includes fingerprints when specified', () => { - const driver = { ...minimalDriver, fingerprints: [{ id: 'fingerprint-id' }] } as EdgeDriver - buildTableFromList.mockReturnValueOnce('profiles table') - buildTableFromList.mockReturnValueOnce('fingerprints table') - - expect(buildTableOutput(tableGenerator, driver)) - .toBe('Basic Information\nbasic info\n\n' + - 'Device Integration Profiles\nprofiles table\n\n' + - 'Fingerprints\nfingerprints table') - - expect(buildTableFromItem).toHaveBeenCalledTimes(1) - expect(buildTableFromItem).toHaveBeenCalledWith(driver, - expect.arrayContaining(['driverId', 'name', 'version', 'packageKey'])) - expect(buildTableFromList).toHaveBeenCalledTimes(2) - expect(buildTableFromList).toHaveBeenCalledWith(driver.deviceIntegrationProfiles, - ['id', 'majorVersion']) - expect(buildTableFromList).toHaveBeenCalledWith(driver.fingerprints, - ['id', 'type', 'deviceLabel']) - }) + it('includes fingerprints when specified', () => { + const driver = { ...minimalDriver, fingerprints: [{ id: 'fingerprint-id' }] } as EdgeDriver + buildTableFromList.mockReturnValueOnce('profiles table') + buildTableFromList.mockReturnValueOnce('fingerprints table') + + expect(buildTableOutput(tableGenerator, driver)) + .toBe('Basic Information\nbasic info\n\n' + + 'Device Integration Profiles\nprofiles table\n\n' + + 'Fingerprints\nfingerprints table') + + expect(buildTableFromItem).toHaveBeenCalledTimes(1) + expect(buildTableFromItem).toHaveBeenCalledWith(driver, + expect.arrayContaining(['driverId', 'name', 'version', 'packageKey'])) + expect(buildTableFromList).toHaveBeenCalledTimes(2) + expect(buildTableFromList).toHaveBeenCalledWith(driver.deviceIntegrationProfiles, + ['id', 'majorVersion']) + expect(buildTableFromList).toHaveBeenCalledWith(driver.fingerprints, + ['id', 'type', 'deviceLabel']) }) +}) - const client = { - drivers: { list: jest.fn(), listDefault: jest.fn() }, - devices: { get: jest.fn() }, - hubdevices: { listInstalled: jest.fn() }, - } as unknown as SmartThingsClient - const apiDriversListMock = jest.mocked(client.drivers.list) - const apiDriversListDefaultMock = jest.mocked(client.drivers.listDefault) - const apiDevicesGetMock = jest.mocked(client.devices.get) - const apiHubdevicesListInstalledMock = jest.mocked(client.hubdevices.listInstalled) +const client = { + drivers: { list: jest.fn(), listDefault: jest.fn() }, + devices: { get: jest.fn(), list: jest.fn() }, + hubdevices: { listInstalled: jest.fn() }, +} as unknown as SmartThingsClient +const apiDriversListMock = jest.mocked(client.drivers.list) +const apiDriversListDefaultMock = jest.mocked(client.drivers.listDefault) +const apiDevicesGetMock = jest.mocked(client.devices.get) +const apiDevicesListMock = jest.mocked(client.devices.list) +const apiHubdevicesListInstalledMock = jest.mocked(client.hubdevices.listInstalled) - const driverList = [{ name: 'Driver' }] as EdgeDriverSummary[] +const driverList = [{ name: 'Driver' }] as EdgeDriverSummary[] - describe('listDrivers', () => { - const forAllOrganizationsMock = jest.mocked(forAllOrganizations) +describe('listDrivers', () => { + const forAllOrganizationsMock = jest.mocked(forAllOrganizations) - it('normally uses drivers.list', async () => { - apiDriversListMock.mockResolvedValueOnce(driverList) + it('normally uses drivers.list', async () => { + apiDriversListMock.mockResolvedValueOnce(driverList) - expect(await listDrivers(client)).toBe(driverList) + expect(await listDrivers(client)).toBe(driverList) - expect(apiDriversListMock).toHaveBeenCalledTimes(1) - expect(apiDriversListMock).toHaveBeenCalledWith() - expect(forAllOrganizationsMock).toHaveBeenCalledTimes(0) - }) + expect(apiDriversListMock).toHaveBeenCalledTimes(1) + expect(apiDriversListMock).toHaveBeenCalledWith() + expect(forAllOrganizationsMock).toHaveBeenCalledTimes(0) + }) - it('lists drivers for all organizations when requested', async () => { - const withOrg = [{ name: 'driver', organization: 'organization-name' }] as (EdgeDriverSummary & WithOrganization)[] - forAllOrganizationsMock.mockResolvedValueOnce(withOrg) + it('lists drivers for all organizations when requested', async () => { + const withOrg = [{ name: 'driver', organization: 'organization-name' }] as (EdgeDriverSummary & WithOrganization)[] + forAllOrganizationsMock.mockResolvedValueOnce(withOrg) - expect(await listDrivers(client, true)).toBe(withOrg) + expect(await listDrivers(client, true)).toBe(withOrg) - expect(apiDriversListMock).toHaveBeenCalledTimes(0) - expect(forAllOrganizationsMock).toHaveBeenCalledTimes(1) - expect(forAllOrganizationsMock).toHaveBeenCalledWith(client, expect.any(Function)) + expect(apiDriversListMock).toHaveBeenCalledTimes(0) + expect(forAllOrganizationsMock).toHaveBeenCalledTimes(1) + expect(forAllOrganizationsMock).toHaveBeenCalledWith(client, expect.any(Function)) - const listDriversFunction = forAllOrganizationsMock.mock.calls[0][1] - apiDriversListMock.mockResolvedValueOnce(driverList) + const listDriversFunction = forAllOrganizationsMock.mock.calls[0][1] + apiDriversListMock.mockResolvedValueOnce(driverList) - expect(await listDriversFunction(client, { organizationId: 'unused' } as OrganizationResponse)).toBe(driverList) - expect(apiDriversListMock).toHaveBeenCalledTimes(1) - expect(apiDriversListMock).toHaveBeenCalledWith() - }) + expect(await listDriversFunction(client, { organizationId: 'unused' } as OrganizationResponse)).toBe(driverList) + expect(apiDriversListMock).toHaveBeenCalledTimes(1) + expect(apiDriversListMock).toHaveBeenCalledWith() }) +}) - test.each` - deviceType | expectedCount - ${'lan'} | ${3} - ${'matter'} | ${3} - ${'zigbee'} | ${3} - ${'zwave'} | ${3} - ${'other'} | ${4} - `('withoutCurrentDriver filters ', async ({ deviceType, expectedCount }) => { - const drivers = [ - { name: 'lan', driverId: 'lan-driver-id' }, - { name: 'matter', driverId: 'matter-driver-id' }, - { name: 'zigbee', driverId: 'zigbee-driver-id' }, - { name: 'zwave', driverId: 'zwave-driver-id' }, - ] +test.each` + deviceType | expectedCount + ${'lan'} | ${3} + ${'matter'} | ${3} + ${'zigbee'} | ${3} + ${'zwave'} | ${3} + ${'other'} | ${4} +`('withoutCurrentDriver filters ', async ({ deviceType, expectedCount }) => { + const drivers = [ + { name: 'lan', driverId: 'lan-driver-id' }, + { name: 'matter', driverId: 'matter-driver-id' }, + { name: 'zigbee', driverId: 'zigbee-driver-id' }, + { name: 'zwave', driverId: 'zwave-driver-id' }, + ] + + const driverId = `${deviceType}-driver-id` + const device = { [deviceType]: { driverId } } as unknown as Device + apiDevicesGetMock.mockResolvedValueOnce(device) + + const result = await withoutCurrentDriver(client, 'device-id', drivers) + expect(result.length).toBe(expectedCount) + expect(result.find(driver => driver.driverId === driverId)).toBeUndefined() + + expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) + expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') +}) - const driverId = `${deviceType}-driver-id` - const device = { [deviceType]: { driverId } } as unknown as Device +const installedDriver = { driverId: 'installed-driver-id', name: 'Installed Driver' } as InstalledDriver +const defaultDriver = { driverId: 'default-driver-id', name: 'Default Driver' } as EdgeDriverSummary +const currentDriver = { driverId: 'current-driver-id', name: 'Current Driver' } as InstalledDriver +const device = { zigbee: { driverId: 'current-driver-id' } } as Device + +describe('listAllAvailableDrivers', () => { + it('combines default and installed drivers lists', async () => { apiDevicesGetMock.mockResolvedValueOnce(device) - const result = await withoutCurrentDriver(client, 'device-id', drivers) - expect(result.length).toBe(expectedCount) - expect(result.find(driver => driver.driverId === driverId)).toBeUndefined() + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) + apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) + + const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') + expect(result.length).toBe(2) + expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') + expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) + expect(apiDriversListDefaultMock).toHaveBeenCalledWith() expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') }) - const installedDriver = { driverId: 'installed-driver-id', name: 'Installed Driver' } as InstalledDriver - const defaultDriver = { driverId: 'default-driver-id', name: 'Default Driver' } as EdgeDriverSummary - const currentDriver = { driverId: 'current-driver-id', name: 'Current Driver' } as InstalledDriver - const device = { zigbee: { driverId: 'current-driver-id' } } as Device + it('filters out current driver', async () => { + apiDevicesGetMock.mockResolvedValueOnce(device) - describe('listAllAvailableDrivers', () => { - it('combines default and installed drivers lists', async () => { - apiDevicesGetMock.mockResolvedValueOnce(device) + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, currentDriver]) + apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) - apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) + const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') + expect(result.length).toBe(2) + expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) - const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') - expect(result.length).toBe(2) - expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') + expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) + expect(apiDriversListDefaultMock).toHaveBeenCalledWith() + expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) + expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') + }) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') - expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) - expect(apiDriversListDefaultMock).toHaveBeenCalledWith() - expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) - expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') - }) + it('filters out duplicates', async () => { + apiDevicesGetMock.mockResolvedValueOnce(device) + + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, defaultDriver as unknown as InstalledDriver]) + apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) - it('filters out current driver', async () => { - apiDevicesGetMock.mockResolvedValueOnce(device) + const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') + expect(result.length).toBe(2) + expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, currentDriver]) - apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') + expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) + expect(apiDriversListDefaultMock).toHaveBeenCalledWith() + expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) + expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') + }) +}) - const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') - expect(result.length).toBe(2) - expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) +describe('listMatchingDrivers', () => { + it('lists matching drivers', async () => { + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) + apiDevicesGetMock.mockResolvedValueOnce(device) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') - expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) - expect(apiDriversListDefaultMock).toHaveBeenCalledWith() - expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) - expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') - }) + expect(await listMatchingDrivers(client, 'device-id', 'hub-id')) + .toStrictEqual([installedDriver]) - it('filters out duplicates', async () => { - apiDevicesGetMock.mockResolvedValueOnce(device) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id', 'device-id') + expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) + expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') + }) - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, defaultDriver as unknown as InstalledDriver]) - apiDriversListDefaultMock.mockResolvedValueOnce([defaultDriver]) + it('filters out current driver', async () => { + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, currentDriver]) + apiDevicesGetMock.mockResolvedValueOnce(device) - const result = await listAllAvailableDrivers(client, 'device-id', 'hub-id') - expect(result.length).toBe(2) - expect(result).toEqual(expect.arrayContaining([defaultDriver, installedDriver])) + expect(await listMatchingDrivers(client, 'device-id', 'hub-id')) + .toStrictEqual([installedDriver]) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') - expect(apiDriversListDefaultMock).toHaveBeenCalledTimes(1) - expect(apiDriversListDefaultMock).toHaveBeenCalledWith() - expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) - expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') - }) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id', 'device-id') + expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) + expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') }) +}) - describe('listMatchingDrivers', () => { - it('lists matching drivers', async () => { - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) - apiDevicesGetMock.mockResolvedValueOnce(device) +describe('chooseDriver', () => { + const command = { client } as APICommand - expect(await listMatchingDrivers(client, 'device-id', 'hub-id')) - .toStrictEqual([installedDriver]) + it('presents user with list of drivers', async () => { + selectFromListMock.mockImplementation(async () => 'chosen-driver-id') - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id', 'device-id') - expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) - expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') - }) + expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id')).toBe('chosen-driver-id') - it('filters out current driver', async () => { - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver, currentDriver]) - apiDevicesGetMock.mockResolvedValueOnce(device) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) + }) - expect(await listMatchingDrivers(client, 'device-id', 'hub-id')) - .toStrictEqual([installedDriver]) + it('translates id from index if allowed', async () => { + stringTranslateToIdMock.mockResolvedValueOnce('translated-id') + selectFromListMock.mockImplementation(async () => 'chosen-driver-id') - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id', 'device-id') - expect(apiDevicesGetMock).toHaveBeenCalledTimes(1) - expect(apiDevicesGetMock).toHaveBeenCalledWith('device-id') - }) + expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id', + { allowIndex: true })).toBe('chosen-driver-id') + + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) + expect(stringTranslateToIdMock).toHaveBeenCalledWith( + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + 'command-line-driver-id', expect.any(Function)) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'translated-id', promptMessage: 'prompt message' })) }) - describe('chooseDriver', () => { - const command = { client } as APICommand + it('uses list function that lists drivers', async () => { + selectFromListMock.mockImplementation(async () => 'chosen-driver-id') - it('presents user with list of drivers', async () => { - selectFromListMock.mockImplementation(async () => 'chosen-driver-id') + expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id')) + .toBe('chosen-driver-id') - expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id')).toBe('chosen-driver-id') + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) - }) + const listItems = selectFromListMock.mock.calls[0][2].listItems - it('translates id from index if allowed', async () => { - stringTranslateToIdMock.mockResolvedValueOnce('translated-id') - selectFromListMock.mockImplementation(async () => 'chosen-driver-id') - - expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id', - { allowIndex: true })).toBe('chosen-driver-id') - - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) - expect(stringTranslateToIdMock).toHaveBeenCalledWith( - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - 'command-line-driver-id', expect.any(Function)) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'translated-id', promptMessage: 'prompt message' })) - }) + apiDriversListMock.mockResolvedValueOnce(driverList) - it('uses list function that lists drivers', async () => { - selectFromListMock.mockImplementation(async () => 'chosen-driver-id') + expect(await listItems()).toBe(driverList) - expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id')) - .toBe('chosen-driver-id') + expect(apiDriversListMock).toHaveBeenCalledTimes(1) + expect(apiDriversListMock).toHaveBeenCalledWith() + }) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) + it('uses supplied list function', async () => { + selectFromListMock.mockImplementation(async () => 'chosen-driver-id') - const listItems = selectFromListMock.mock.calls[0][2].listItems + const listItemsMock = jest.fn() + expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id', { listItems: listItemsMock })) + .toBe('chosen-driver-id') + + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) - apiDriversListMock.mockResolvedValueOnce(driverList) + const listItems = selectFromListMock.mock.calls[0][2].listItems - expect(await listItems()).toBe(driverList) + listItemsMock.mockResolvedValueOnce(driverList) - expect(apiDriversListMock).toHaveBeenCalledTimes(1) - expect(apiDriversListMock).toHaveBeenCalledWith() - }) + expect(await listItems()).toBe(driverList) - it('uses supplied list function', async () => { - selectFromListMock.mockImplementation(async () => 'chosen-driver-id') + expect(apiDriversListMock).toHaveBeenCalledTimes(0) + expect(listItemsMock).toHaveBeenCalledTimes(1) + expect(listItemsMock).toHaveBeenCalledWith() + }) +}) - const listItemsMock = jest.fn() - expect(await chooseDriver(command, 'prompt message', 'command-line-driver-id', { listItems: listItemsMock })) - .toBe('chosen-driver-id') +describe('chooseHub', () => { + const hub = { deviceId: 'hub-device-id', label: 'hub label' } as Device + + const listDevicesMock = jest.fn() + const getDevicesMock = jest.fn() + const getLocationsMock = jest.fn() + const client = { devices: { list: listDevicesMock, get: getDevicesMock }, locations: { get: getLocationsMock } } + const logToStderrMock = jest.fn() + const command = { + client, + logToStderr: logToStderrMock, + logger: { warn: jest.fn() }, + } as unknown as APICommand + + const chooseOptionsWithDefaultsMock = jest.mocked(chooseOptionsWithDefaults) + const stringTranslateToIdMock = jest.mocked(stringTranslateToId) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-driver-id', promptMessage: 'prompt message' })) + it('uses default hub if specified', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const listItems = selectFromListMock.mock.calls[0][2].listItems + expect(await chooseHub(command, 'prompt message', undefined, + { useConfigDefault: true })).toBe('chosen-hub-id') - listItemsMock.mockResolvedValueOnce(driverList) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ useConfigDefault: true }) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + expect.objectContaining({ + defaultValue: { configKey: 'defaultHub', getItem: expect.any(Function), userMessage: expect.any(Function) }, + promptMessage: 'prompt message', + })) + }) - expect(await listItems()).toBe(driverList) + it('prefers command line over default', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - expect(apiDriversListMock).toHaveBeenCalledTimes(0) - expect(listItemsMock).toHaveBeenCalledTimes(1) - expect(listItemsMock).toHaveBeenCalledWith() - }) + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', + { useConfigDefault: true })).toBe('chosen-hub-id') + + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ useConfigDefault: true }) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + expect.objectContaining({ + preselectedId: 'command-line-hub-id', + defaultValue: { configKey: 'defaultHub', getItem: expect.any(Function), userMessage: expect.any(Function) }, + promptMessage: 'prompt message', + })) }) - describe('chooseHub', () => { - const hub = { deviceId: 'hub-device-id', label: 'hub label' } as Device + it('translates id from index if allowed', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: true } as ChooseOptions) + stringTranslateToIdMock.mockResolvedValueOnce('translated-id') + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const listDevicesMock = jest.fn() - const getDevicesMock = jest.fn() - const getLocationsMock = jest.fn() - const client = { devices: { list: listDevicesMock, get: getDevicesMock }, locations: { get: getLocationsMock } } - const logToStderrMock = jest.fn() - const command = { - client, - logToStderr: logToStderrMock, - logger: { warn: jest.fn() }, - } as unknown as APICommand + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', + { useConfigDefault: true, allowIndex: true })).toBe('chosen-hub-id') - const chooseOptionsWithDefaultsMock = jest.mocked(chooseOptionsWithDefaults) - const stringTranslateToIdMock = jest.mocked(stringTranslateToId) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) + expect(chooseOptionsWithDefaultsMock) + .toHaveBeenCalledWith({ allowIndex: true, useConfigDefault: true }) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) + expect(stringTranslateToIdMock).toHaveBeenCalledWith( + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + 'command-line-hub-id', expect.any(Function)) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'translated-id', promptMessage: 'prompt message' })) + }) - it('uses default hub if specified', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + it('uses list function that specifies hubs', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - expect(await chooseHub(command, 'prompt message', undefined, - { useConfigDefault: true })).toBe('chosen-hub-id') + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') + + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith(undefined) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + expect.objectContaining({ preselectedId: 'command-line-hub-id', promptMessage: 'prompt message' })) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ useConfigDefault: true }) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - expect.objectContaining({ - defaultValue: { configKey: 'defaultHub', getItem: expect.any(Function), userMessage: expect.any(Function) }, - promptMessage: 'prompt message', - })) + const listItems = selectFromListMock.mock.calls[0][2].listItems + + const list = [{ name: 'Hub', locationId: 'locationId' }] as Device[] + listDevicesMock.mockResolvedValueOnce(list) + getLocationsMock.mockResolvedValueOnce({ + 'allowed': [ + 'd:locations', + ], + 'locationId': 'locationId', }) - it('prefers command line over default', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + expect(await listItems()).toStrictEqual(list) - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', - { useConfigDefault: true })).toBe('chosen-hub-id') + expect(listDevicesMock).toHaveBeenCalledTimes(1) + expect(listDevicesMock).toHaveBeenCalledWith({ type: DeviceIntegrationType.HUB }) + }) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ useConfigDefault: true }) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - expect.objectContaining({ - preselectedId: 'command-line-hub-id', - defaultValue: { configKey: 'defaultHub', getItem: expect.any(Function), userMessage: expect.any(Function) }, - promptMessage: 'prompt message', - })) - }) + it('uses listItems from options', async () => { + const listItemsMock = jest.fn() - it('translates id from index if allowed', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: true } as ChooseOptions) - stringTranslateToIdMock.mockResolvedValueOnce('translated-id') - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + chooseOptionsWithDefaultsMock.mockReturnValueOnce({} as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', - { useConfigDefault: true, allowIndex: true })).toBe('chosen-hub-id') - - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock) - .toHaveBeenCalledWith({ allowIndex: true, useConfigDefault: true }) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) - expect(stringTranslateToIdMock).toHaveBeenCalledWith( - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - 'command-line-hub-id', expect.any(Function)) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'translated-id', promptMessage: 'prompt message' })) - }) + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', + { listItems: listItemsMock })).toBe('chosen-hub-id') - it('uses list function that specifies hubs', async () => { + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) + expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ listItems: listItemsMock }) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), + expect.objectContaining({ listItems: listItemsMock })) + }) + + describe('listItems', () => { + it('checks hub locations for ownership', async () => { chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) selectFromListMock.mockImplementation(async () => 'chosen-hub-id') expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith(undefined) - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - expect.objectContaining({ preselectedId: 'command-line-hub-id', promptMessage: 'prompt message' })) - - const listItems = selectFromListMock.mock.calls[0][2].listItems + const hubList = [ + { name: 'Hub', locationId: 'locationId' }, + { name: 'AnotherHub', locationId: 'locationId' }, + { name: 'SecondLocationHub', locationId: 'secondLocationId' }, + ] as Device[] - const list = [{ name: 'Hub', locationId: 'locationId' }] as Device[] - listDevicesMock.mockResolvedValueOnce(list) - getLocationsMock.mockResolvedValueOnce({ + const location = { 'allowed': [ 'd:locations', ], - 'locationId': 'locationId', - }) - - expect(await listItems()).toStrictEqual(list) - - expect(listDevicesMock).toHaveBeenCalledTimes(1) - expect(listDevicesMock).toHaveBeenCalledWith({ type: DeviceIntegrationType.HUB }) - }) + } as Location - it('uses listItems from options', async () => { - const listItemsMock = jest.fn() + listDevicesMock.mockResolvedValueOnce(hubList) + getLocationsMock.mockResolvedValue(location) - chooseOptionsWithDefaultsMock.mockReturnValueOnce({} as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + const listItems = selectFromListMock.mock.calls[0][2].listItems - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id', - { listItems: listItemsMock })).toBe('chosen-hub-id') + expect(await listItems()).toStrictEqual(hubList) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith({ listItems: listItemsMock }) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'deviceId', sortKeyName: 'name' }), - expect.objectContaining({ listItems: listItemsMock })) + expect(getLocationsMock).toBeCalledTimes(2) + expect(getLocationsMock).toBeCalledWith('locationId', { allowed: true }) + expect(getLocationsMock).toBeCalledWith('secondLocationId', { allowed: true }) }) - describe('listItems', () => { - it('checks hub locations for ownership', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') - - const hubList = [ - { name: 'Hub', locationId: 'locationId' }, - { name: 'AnotherHub', locationId: 'locationId' }, - { name: 'SecondLocationHub', locationId: 'secondLocationId' }, - ] as Device[] + it('filters out devices on shared locations or when allowed is null, undefined', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const location = { - 'allowed': [ - 'd:locations', - ], - } as Location + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') - listDevicesMock.mockResolvedValueOnce(hubList) - getLocationsMock.mockResolvedValue(location) + const ownedHub = { name: 'Hub', locationId: 'locationId' } + const hubList = [ + ownedHub, + { name: 'SharedHub', locationId: 'sharedLocationId' }, + { name: 'NullAllowedHub', locationId: 'nullAllowedLocationId' }, + { name: 'UndefinedAllowedHub', locationId: 'undefinedAllowedLocationId' }, + ] as Device[] - const listItems = selectFromListMock.mock.calls[0][2].listItems + listDevicesMock.mockResolvedValueOnce(hubList) - expect(await listItems()).toStrictEqual(hubList) + const location = { + 'allowed': [ + 'd:locations', + ], + 'locationId': 'locationId', + } - expect(getLocationsMock).toBeCalledTimes(2) - expect(getLocationsMock).toBeCalledWith('locationId', { allowed: true }) - expect(getLocationsMock).toBeCalledWith('secondLocationId', { allowed: true }) - }) + const sharedLocation = { + 'allowed': [ + ], + 'locationId': 'sharedLocationId', + } - it('filters out devices on shared locations or when allowed is null, undefined', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + const nullAllowedLocation = { + 'allowed': null, + 'locationId': 'nullAllowedLocationId', + } - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') + const undefinedAllowedLocation = { + 'locationId': 'undefinedAllowedLocationId', + } - const ownedHub = { name: 'Hub', locationId: 'locationId' } - const hubList = [ - ownedHub, - { name: 'SharedHub', locationId: 'sharedLocationId' }, - { name: 'NullAllowedHub', locationId: 'nullAllowedLocationId' }, - { name: 'UndefinedAllowedHub', locationId: 'undefinedAllowedLocationId' }, - ] as Device[] + getLocationsMock + .mockResolvedValueOnce(location) + .mockResolvedValueOnce(sharedLocation) + .mockResolvedValueOnce(nullAllowedLocation) + .mockResolvedValueOnce(undefinedAllowedLocation) - listDevicesMock.mockResolvedValueOnce(hubList) + const listItems = selectFromListMock.mock.calls[0][2].listItems - const location = { - 'allowed': [ - 'd:locations', - ], - 'locationId': 'locationId', - } + expect(await listItems()).toStrictEqual([ownedHub]) - const sharedLocation = { - 'allowed': [ - ], - 'locationId': 'sharedLocationId', - } + expect(getLocationsMock).toBeCalledTimes(4) + expect(command.logger.warn).toBeCalledWith('filtering out location', sharedLocation) + expect(command.logger.warn).toBeCalledWith('filtering out location', nullAllowedLocation) + expect(command.logger.warn).toBeCalledWith('filtering out location', undefinedAllowedLocation) + }) - const nullAllowedLocation = { - 'allowed': null, - 'locationId': 'nullAllowedLocationId', - } + it('warns when hub does not have locationId', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const undefinedAllowedLocation = { - 'locationId': 'undefinedAllowedLocationId', - } + expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') - getLocationsMock - .mockResolvedValueOnce(location) - .mockResolvedValueOnce(sharedLocation) - .mockResolvedValueOnce(nullAllowedLocation) - .mockResolvedValueOnce(undefinedAllowedLocation) + const hub = { name: 'Hub' } as Device - const listItems = selectFromListMock.mock.calls[0][2].listItems + listDevicesMock.mockResolvedValueOnce([hub]) - expect(await listItems()).toStrictEqual([ownedHub]) + const listItems = selectFromListMock.mock.calls[0][2].listItems - expect(getLocationsMock).toBeCalledTimes(4) - expect(command.logger.warn).toBeCalledWith('filtering out location', sharedLocation) - expect(command.logger.warn).toBeCalledWith('filtering out location', nullAllowedLocation) - expect(command.logger.warn).toBeCalledWith('filtering out location', undefinedAllowedLocation) - }) + expect(await listItems()).toStrictEqual([]) - it('warns when hub does not have locationId', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') + expect(command.logger.warn).toBeCalledWith('hub record found without locationId', hub) + }) + }) - expect(await chooseHub(command, 'prompt message', 'command-line-hub-id')).toBe('chosen-hub-id') + describe('defaultConfig', () => { + test('getItem uses devices.get', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const hub = { name: 'Hub' } as Device + expect(await chooseHub(command, 'prompt message', undefined, + { useConfigDefault: true })).toBe('chosen-hub-id') - listDevicesMock.mockResolvedValueOnce([hub]) + const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue - const listItems = selectFromListMock.mock.calls[0][2].listItems + expect(defaultValue).toBeDefined() + const getItem = defaultValue?.getItem as (id: string) => Promise + expect(getItem).toBeDefined() + getDevicesMock.mockResolvedValueOnce(hub) - expect(await listItems()).toStrictEqual([]) + expect(await getItem('id-to-check')).toBe(hub) - expect(command.logger.warn).toBeCalledWith('hub record found without locationId', hub) - }) + expect(getDevicesMock).toHaveBeenCalledTimes(1) + expect(getDevicesMock).toHaveBeenCalledWith('id-to-check') }) - describe('defaultConfig', () => { - test('getItem uses devices.get', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - - expect(await chooseHub(command, 'prompt message', undefined, - { useConfigDefault: true })).toBe('chosen-hub-id') + test('userMessage returns expected message', async () => { + chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) + selectFromListMock.mockImplementation(async () => 'chosen-hub-id') - const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue + expect(await chooseHub(command, 'prompt message', undefined, + { useConfigDefault: true })).toBe('chosen-hub-id') - expect(defaultValue).toBeDefined() - const getItem = defaultValue?.getItem as (id: string) => Promise - expect(getItem).toBeDefined() - getDevicesMock.mockResolvedValueOnce(hub) + const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue - expect(await getItem('id-to-check')).toBe(hub) + expect(defaultValue).toBeDefined() + const userMessage = defaultValue?.userMessage as (hub: Device) => string + expect(userMessage).toBeDefined() - expect(getDevicesMock).toHaveBeenCalledTimes(1) - expect(getDevicesMock).toHaveBeenCalledWith('id-to-check') - }) + expect(userMessage(hub)).toBe('using previously specified default hub labeled "hub label" (hub-device-id)') + }) + }) +}) - test('userMessage returns expected message', async () => { - chooseOptionsWithDefaultsMock.mockReturnValueOnce({ allowIndex: false, useConfigDefault: true } as ChooseOptions) - selectFromListMock.mockImplementation(async () => 'chosen-hub-id') +describe('listAssignedDriversWithNames', () => { + const driverChannelDetailsList = [{ channelId: 'channel-id', driverId: 'driver-id' }] as + unknown as DriverChannelDetails[] + const listAssignedDriversMock = jest.fn() + const getDriverChannelMetaInfoMock = jest.fn() + const client = { channels: { + listAssignedDrivers: listAssignedDriversMock, + getDriverChannelMetaInfo: getDriverChannelMetaInfoMock, + } } as unknown as SmartThingsClient - expect(await chooseHub(command, 'prompt message', undefined, - { useConfigDefault: true })).toBe('chosen-hub-id') + it('lists drivers with their names', async () => { + listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) + getDriverChannelMetaInfoMock.mockResolvedValueOnce({ name: 'driver name' }) - const defaultValue = selectFromListMock.mock.calls[0][2].defaultValue + const result = await listAssignedDriversWithNames(client, 'channel-id') - expect(defaultValue).toBeDefined() - const userMessage = defaultValue?.userMessage as (hub: Device) => string - expect(userMessage).toBeDefined() + expect(result).toEqual([{ channelId: 'channel-id', driverId: 'driver-id', name: 'driver name' }]) - expect(userMessage(hub)).toBe('using previously specified default hub labeled "hub label" (hub-device-id)') - }) - }) + expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) + expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') + expect(getDriverChannelMetaInfoMock).toHaveBeenCalledTimes(1) + expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'driver-id') }) - describe('listAssignedDriversWithNames', () => { - const driverChannelDetailsList = [{ channelId: 'channel-id', driverId: 'driver-id' }] as - unknown as DriverChannelDetails[] - const listAssignedDriversMock = jest.fn() - const getDriverChannelMetaInfoMock = jest.fn() - const client = { channels: { - listAssignedDrivers: listAssignedDriversMock, - getDriverChannelMetaInfo: getDriverChannelMetaInfoMock, - } } as unknown as SmartThingsClient + it('skips deleted drivers', async () => { + const driverChannelDetailsList = [ + { channelId: 'channel-id', driverId: 'driver-id' }, + { channelId: 'channel-id', driverId: 'deleted-driver-id' }, + ] as unknown as DriverChannelDetails[] + listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) + getDriverChannelMetaInfoMock.mockResolvedValueOnce({ name: 'driver name' }) + getDriverChannelMetaInfoMock.mockRejectedValueOnce({ response: { status: 404 } }) - it('lists drivers with their names', async () => { - listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) - getDriverChannelMetaInfoMock.mockResolvedValueOnce({ name: 'driver name' }) + const result = await listAssignedDriversWithNames(client, 'channel-id') - const result = await listAssignedDriversWithNames(client, 'channel-id') + expect(result).toEqual([{ channelId: 'channel-id', driverId: 'driver-id', name: 'driver name' }]) - expect(result).toEqual([{ channelId: 'channel-id', driverId: 'driver-id', name: 'driver name' }]) + expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) + expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') + expect(getDriverChannelMetaInfoMock).toHaveBeenCalledTimes(2) + expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'driver-id') + expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'deleted-driver-id') + }) - expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) - expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') - expect(getDriverChannelMetaInfoMock).toHaveBeenCalledTimes(1) - expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'driver-id') - }) + it('passes on other errors from getDriverChannelMetaInfo', async () => { + listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) + getDriverChannelMetaInfoMock.mockRejectedValueOnce(Error('random error')) - it('skips deleted drivers', async () => { - const driverChannelDetailsList = [ - { channelId: 'channel-id', driverId: 'driver-id' }, - { channelId: 'channel-id', driverId: 'deleted-driver-id' }, - ] as unknown as DriverChannelDetails[] - listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) - getDriverChannelMetaInfoMock.mockResolvedValueOnce({ name: 'driver name' }) - getDriverChannelMetaInfoMock.mockRejectedValueOnce({ response: { status: 404 } }) + await expect(listAssignedDriversWithNames(client, 'channel-id')).rejects.toThrow(Error('random error')) - const result = await listAssignedDriversWithNames(client, 'channel-id') + expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) + expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') + }) +}) - expect(result).toEqual([{ channelId: 'channel-id', driverId: 'driver-id', name: 'driver name' }]) +test('chooseDriverFromChannel presents user with list of drivers with names', async () => { + const command = { client } as APICommand + selectFromListMock.mockResolvedValueOnce('chosen-driver-id') - expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) - expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') - expect(getDriverChannelMetaInfoMock).toHaveBeenCalledTimes(2) - expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'driver-id') - expect(getDriverChannelMetaInfoMock).toHaveBeenCalledWith('channel-id', 'deleted-driver-id') - }) + expect(await chooseDriverFromChannel(command, 'channel-id', 'preselected-driver-id')).toBe('chosen-driver-id') - it('passes on other errors from getDriverChannelMetaInfo', async () => { - listAssignedDriversMock.mockReturnValueOnce(driverChannelDetailsList) - getDriverChannelMetaInfoMock.mockRejectedValueOnce(Error('random error')) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ + preselectedId: 'preselected-driver-id', + promptMessage: 'Select a driver to install.', + })) - await expect(listAssignedDriversWithNames(client, 'channel-id')).rejects.toThrow(Error('random error')) + const drivers = [{ name: 'driver' }] as DriverChannelDetailsWithName[] + const listAssignedDriversWithNamesSpy = jest.spyOn(driversUtil, 'listAssignedDriversWithNames') + .mockResolvedValueOnce(drivers) - expect(listAssignedDriversMock).toHaveBeenCalledTimes(1) - expect(listAssignedDriversMock).toHaveBeenCalledWith('channel-id') - }) - }) + const listItems = selectFromListMock.mock.calls[0][2].listItems - test('chooseDriverFromChannel presents user with list of drivers with names', async () => { - const command = { client } as APICommand - selectFromListMock.mockResolvedValueOnce('chosen-driver-id') + expect(await listItems()).toBe(drivers) - expect(await chooseDriverFromChannel(command, 'channel-id', 'preselected-driver-id')).toBe('chosen-driver-id') + expect(listAssignedDriversWithNamesSpy).toHaveBeenCalledTimes(1) + expect(listAssignedDriversWithNamesSpy).toHaveBeenCalledWith(client, 'channel-id') +}) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ - preselectedId: 'preselected-driver-id', - promptMessage: 'Select a driver to install.', - })) +test('chooseInstalledDriver presents user with list of drivers with names', async () => { + const command = { client } as APICommand + selectFromListMock.mockResolvedValueOnce('chosen-driver-id') + stringTranslateToIdMock.mockResolvedValueOnce('preselected-driver-id') + + expect(await driversUtil.chooseInstalledDriver(command, 'hub-id', 'prompt message', 'command-line-driver-id')) + .toBe('chosen-driver-id') + + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) + expect(stringTranslateToIdMock).toHaveBeenCalledWith( + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + 'command-line-driver-id', expect.any(Function)) + expect(selectFromListMock).toHaveBeenCalledTimes(1) + expect(selectFromListMock).toHaveBeenCalledWith(command, + expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), + expect.objectContaining({ + preselectedId: 'preselected-driver-id', + promptMessage: 'prompt message', + })) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(0) + + const listItems = stringTranslateToIdMock.mock.calls[0][2] + apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) + + expect(await listItems()).toStrictEqual([installedDriver]) + + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) + expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') +}) - const drivers = [{ name: 'driver' }] as DriverChannelDetailsWithName[] - const listAssignedDriversWithNamesSpy = jest.spyOn(driversUtil, 'listAssignedDriversWithNames') - .mockResolvedValueOnce(drivers) +describe('getDriverDevices', () => { + it('includes only edge device types', async () => { + const hubDevices = [ + { deviceId: 'hub-device-id', label: 'Hub Label' }, + ] as Device[] + const edgeDevices = [ + { + type: DeviceIntegrationType.LAN, + deviceId: 'lan-device-id', + label: 'LAN Device', + lan: { + driverId: 'lan-driver-id', + hubId: 'hub-device-id', + }, + }, + { + type: DeviceIntegrationType.MATTER, + deviceId: 'matter-device-id', + label: 'Matter Device', + matter: { + driverId: 'matter-driver-id', + hubId: 'hub-device-id', + }, + }, + { + type: DeviceIntegrationType.ZIGBEE, + deviceId: 'zigbee-device-id', + label: 'Zigbee Device', + zigbee: { + driverId: 'zigbee-driver-id', + hubId: 'hub-device-id', + }, + }, + { + type: DeviceIntegrationType.ZWAVE, + deviceId: 'zwave-device-id', + label: 'Z-Wave Device', + zwave: { + driverId: 'zwave-driver-id', + hubId: 'bad-hub-device-id', + }, + }, + ] as Device[] + const driverDevices: driversUtil.DeviceDriverInfo[] = [ + { + type: DeviceIntegrationType.LAN, + label: 'LAN Device', + deviceId: 'lan-device-id', + driverId: 'lan-driver-id', + hubId: 'hub-device-id', + hubLabel: 'Hub Label', + }, + { + type: DeviceIntegrationType.MATTER, + label: 'Matter Device', + deviceId: 'matter-device-id', + driverId: 'matter-driver-id', + hubId: 'hub-device-id', + hubLabel: 'Hub Label', + }, + { + type: DeviceIntegrationType.ZIGBEE, + label: 'Zigbee Device', + deviceId: 'zigbee-device-id', + driverId: 'zigbee-driver-id', + hubId: 'hub-device-id', + hubLabel: 'Hub Label', + }, + { + type: DeviceIntegrationType.ZWAVE, + label: 'Z-Wave Device', + deviceId: 'zwave-device-id', + driverId: 'zwave-driver-id', + hubId: 'bad-hub-device-id', + hubLabel: undefined, + }, + ] - const listItems = selectFromListMock.mock.calls[0][2].listItems + apiDevicesListMock.mockResolvedValueOnce(hubDevices) + apiDevicesListMock.mockResolvedValueOnce(edgeDevices) - expect(await listItems()).toBe(drivers) + expect(await getDriverDevices(client)).toStrictEqual(driverDevices) - expect(listAssignedDriversWithNamesSpy).toHaveBeenCalledTimes(1) - expect(listAssignedDriversWithNamesSpy).toHaveBeenCalledWith(client, 'channel-id') + expect(apiDevicesListMock).toHaveBeenCalledTimes(2) + expect(apiDevicesListMock).toHaveBeenCalledWith({ type: DeviceIntegrationType.HUB }) + expect(apiDevicesListMock).toHaveBeenCalledWith({ type: driversUtil.edgeDeviceTypes }) }) - test('chooseInstalledDriver presents user with list of drivers with names', async () => { - const command = { client } as APICommand - selectFromListMock.mockResolvedValueOnce('chosen-driver-id') - stringTranslateToIdMock.mockResolvedValueOnce('preselected-driver-id') - - expect(await driversUtil.chooseInstalledDriver(command, 'hub-id', 'prompt message', 'command-line-driver-id')) - .toBe('chosen-driver-id') - - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) - expect(stringTranslateToIdMock).toHaveBeenCalledWith( - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - 'command-line-driver-id', expect.any(Function)) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith(command, - expect.objectContaining({ primaryKeyName: 'driverId', sortKeyName: 'name' }), - expect.objectContaining({ - preselectedId: 'preselected-driver-id', - promptMessage: 'prompt message', - })) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(0) + it('throws exception for invalid device input', async () => { + const hubDevices = [{ deviceId: 'hub-device-id', label: 'Hub Label' }] as Device[] + const edgeDevices = [ + { + type: DeviceIntegrationType.LAN, + deviceId: 'lan-device-id', + label: 'LAN Device', + }, + ] as Device[] - const listItems = stringTranslateToIdMock.mock.calls[0][2] - apiHubdevicesListInstalledMock.mockResolvedValueOnce([installedDriver]) + apiDevicesListMock.mockResolvedValueOnce(hubDevices) + apiDevicesListMock.mockResolvedValueOnce(edgeDevices) - expect(await listItems()).toStrictEqual([installedDriver]) + await expect(getDriverDevices(client)).rejects.toThrow('unexpected device type LAN or missing type info') - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledTimes(1) - expect(apiHubdevicesListInstalledMock).toHaveBeenCalledWith('hub-id') + expect(apiDevicesListMock).toHaveBeenCalledTimes(2) + expect(apiDevicesListMock).toHaveBeenCalledWith({ type: DeviceIntegrationType.HUB }) + expect(apiDevicesListMock).toHaveBeenCalledWith({ type: driversUtil.edgeDeviceTypes }) }) }) diff --git a/packages/edge/src/commands/edge/drivers/devices.ts b/packages/edge/src/commands/edge/drivers/devices.ts new file mode 100644 index 00000000..8ba2235d --- /dev/null +++ b/packages/edge/src/commands/edge/drivers/devices.ts @@ -0,0 +1,80 @@ +import { Flags } from '@oclif/core' + +import { OutputItemOrListConfig, outputItemOrList } from '@smartthings/cli-lib' + +import { EdgeCommand } from '../../../lib/edge-command' +import { DeviceDriverInfo, getDriverDevices } from '../../../lib/commands/drivers-util' + + +export default class DriversDevicesCommand extends EdgeCommand { + static description = 'list devices using edge drivers' + + this.apiDocsURL('getDevices', 'listDrivers', 'getDriver', 'getDriverRevision') // TODO: update this list + + static flags = { + ...EdgeCommand.flags, + ...outputItemOrList.flags, + hub: Flags.string({ + char: 'H', + description: 'hub id', + helpValue: '', + }), + driver: Flags.string({ + char: 'D', + description: 'driver id', + helpValue: '', + }), + } + + static args = [{ + name: 'idOrIndex', + description: 'the device id or number in list', + }] + + static examples = [`# list all devices using edge drivers +$ smartthings edge:drivers:devices + +# display details about the third device listed in the above command +$ smartthings edge:drivers:devices 3`, + ` +# display details about a device by using its id +$ smartthings edge:drivers:devices dfda0a8e-55d6-445b-ace5-db828679bcb3 + +# list all devices using edge drivers on the specified hub +$ smartthings edge:drivers:devices --hub a9108ab1-7087-4c10-9781-a0627b084fce + +# list devices that use a specific driver +$ smartthings edge:drivers:devices --driver b67a134c-ace8-4b8d-9a0e-444ad78b4455`] + + async run(): Promise { + const config: OutputItemOrListConfig = { + primaryKeyName: 'deviceId', + sortKeyName: 'label', + tableFieldDefinitions: ['label', 'type', 'deviceId', 'driverId', 'hubId', 'hubLabel'], + listTableFieldDefinitions: ['label', 'type', 'deviceId', 'driverId'], + } + + const matchesUserFilters = (device: DeviceDriverInfo): boolean => { + if (this.flags.hub && device.hubId !== this.flags.hub) { + return false + } + if (this.flags.driver && device.driverId !== this.flags.driver) { + return false + } + return true + } + + const matchingDevices = (await getDriverDevices(this.client)).filter(matchesUserFilters) + + const list = async (): Promise => matchingDevices + + const get = async (id: string): Promise => { + const match = matchingDevices.find(device => device.deviceId === id) + if (match) { + return match + } + throw Error(`Could not find device with id ${id}`) + } + + await outputItemOrList(this, config, this.args.idOrIndex, list, get) + } +} diff --git a/packages/edge/src/commands/edge/drivers/prune.ts b/packages/edge/src/commands/edge/drivers/prune.ts new file mode 100644 index 00000000..a309924b --- /dev/null +++ b/packages/edge/src/commands/edge/drivers/prune.ts @@ -0,0 +1,75 @@ +import { Flags } from '@oclif/core' + +import { EdgeCommand } from '../../../lib/edge-command' +import { chooseHub, getDriverDevices } from '../../../lib/commands/drivers-util' +import { FormatAndWriteItemConfig, askForBoolean, formatAndWriteItem } from '@smartthings/cli-lib' +import { InstalledDriver } from '@smartthings/core-sdk' + + +export default class DriversPruneCommand extends EdgeCommand { + static description = 'uninstall unused edge drivers from a hub' + + this.apiDocsURL('uninstallDriver') // TODO: update + + static flags = { + ...EdgeCommand.flags, + hub: Flags.string({ + char: 'H', + description: 'hub id', + helpValue: '', + }), + } + + static args = [{ + name: 'driverId', + description: 'id of driver to uninstall', + }] + + async run(): Promise { + const hubId = await chooseHub(this, 'Select a hub to uninstall from.', this.flags.hub, + { useConfigDefault: true }) + + const installedDrivers = await this.client.hubdevices.listInstalled(hubId) + const isDefined = (item?: string): item is string => item != null + const inUseDriverIds = (await getDriverDevices(this.client)) + .map(info => info.driverId) + .filter(isDefined) + .flat() + + // LAN drivers can be automatically re-installed so we don't allow pruning them. + // no ocean currents here :-) + const defaultLANDrivers = (await this.client.drivers.listDefault()) + .filter(driver => driver.permissions?.find(permission => permission.name === 'lan')) + .map(driver => driver.driverId) + const prunableDrivers = installedDrivers + .filter(driver => !inUseDriverIds.includes(driver.driverId)) + .filter(driver => !defaultLANDrivers.includes(driver.driverId)) + + if (prunableDrivers.length === 0) { + console.log('No unused drivers found.') + } else { + console.log(`Found ${prunableDrivers.length} unused drivers.`) + const config: FormatAndWriteItemConfig = { + tableFieldDefinitions: [ + 'driverId', + 'name', + { prop: 'description', skipEmpty: true }, + 'version', + 'channelId', + 'developer', + { prop: 'vendorSupportInformation', skipEmpty: true }, + ], + } + for (const prunableDriver of prunableDrivers) { + console.log('\n\nFound unused driver:') + await formatAndWriteItem(this, config, prunableDriver) + const doUninstall = await askForBoolean(`Uninstall ${prunableDriver.name} driver?`, { default: false }) + if (doUninstall) { + await this.client.hubdevices.uninstallDriver(prunableDriver.driverId, hubId) + this.log(`driver ${prunableDriver.driverId} uninstalled from hub ${hubId}`) + } else { + console.log(`Left driver ${prunableDriver.name} installed.`) + } + } + } + } +} diff --git a/packages/edge/src/lib/commands/drivers-util.ts b/packages/edge/src/lib/commands/drivers-util.ts index 7f4efaec..460507f5 100644 --- a/packages/edge/src/lib/commands/drivers-util.ts +++ b/packages/edge/src/lib/commands/drivers-util.ts @@ -5,6 +5,7 @@ import { EdgeDriver, EdgeDriverSummary, InstalledDriver, + LanDeviceDetails, SmartThingsClient, } from '@smartthings/core-sdk' @@ -207,3 +208,56 @@ export const chooseInstalledDriver = async (command: APICommand + +const deviceTypeInfo = (device: Device): DeviceTypeInfo => { + if (device.type === DeviceIntegrationType.LAN && device.lan) { + return device.lan + } + if (device.type === DeviceIntegrationType.MATTER && device.matter) { + return device.matter + } + if (device.type === DeviceIntegrationType.ZIGBEE && device.zigbee) { + return device.zigbee + } + if (device.type === DeviceIntegrationType.ZWAVE && device.zwave) { + return device.zwave + } + throw Error(`unexpected device type ${device.type} or missing type info`) +} + +const deviceToDeviceDriverInfo = (device: Device, hubDevices: Device[]): DeviceDriverInfo => { + const typeInfo = deviceTypeInfo(device) + const hubDevice = hubDevices.find(hub => typeInfo.hubId && hub.deviceId === typeInfo.hubId) + return { + type: device.type, + label: device.label, + deviceId: device.deviceId, + driverId: typeInfo.driverId, + hubId: typeInfo.hubId, + hubLabel: hubDevice?.label, + } +} + +export const getDriverDevices = async (client: SmartThingsClient): Promise => { + const hubDevices = await client.devices.list({ type: DeviceIntegrationType.HUB }) + return (await client.devices.list({ type: edgeDeviceTypes })) + .map(device => deviceToDeviceDriverInfo(device, hubDevices)) +}