From 967318d140074af6df83b3a9fcae47c00fcb32f2 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Thu, 12 Jun 2025 13:37:19 -0500 Subject: [PATCH] refactor: convert edge:drivers:installed command to yargs --- .../commands/edge/drivers/installed.test.ts | 147 ------------- .../src/commands/edge/drivers/installed.ts | 82 -------- .../commands/edge/drivers/installed.test.ts | 199 ++++++++++++++++++ src/commands/edge/drivers/installed.ts | 88 ++++++++ src/commands/index.ts | 2 + 5 files changed, 289 insertions(+), 229 deletions(-) delete mode 100644 packages/edge/src/__tests__/commands/edge/drivers/installed.test.ts delete mode 100644 packages/edge/src/commands/edge/drivers/installed.ts create mode 100644 src/__tests__/commands/edge/drivers/installed.test.ts create mode 100644 src/commands/edge/drivers/installed.ts diff --git a/packages/edge/src/__tests__/commands/edge/drivers/installed.test.ts b/packages/edge/src/__tests__/commands/edge/drivers/installed.test.ts deleted file mode 100644 index fbd15c6f..00000000 --- a/packages/edge/src/__tests__/commands/edge/drivers/installed.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { HubdevicesEndpoint, InstalledDriver, SmartThingsClient } from '@smartthings/core-sdk' - -import { outputItemOrList } from '@smartthings/cli-lib' - -import DriversInstalledCommand from '../../../../commands/edge/drivers/installed.js' -import { withChannelNames, WithNamedChannel } from '../../../../lib/commands/channels-util.js' -import { chooseHub } from '../../../../lib/commands/drivers-util.js' - - -jest.mock('@smartthings/cli-lib', () => { - const originalLib = jest.requireActual('@smartthings/cli-lib') - - return { - ...originalLib, - outputItemOrList: jest.fn(), - } -}) -jest.mock('../../../../../src/lib/commands/channels-util') -jest.mock('../../../../../src/lib/commands/drivers-util') - -describe('DriversInstalledCommand', () => { - const driver1 = { name: 'Driver 1' } as InstalledDriver - const driver2 = { name: 'Driver 2' } as InstalledDriver - const driver1WithChannelName = { ...driver1, channelName: 'Channel 1' } as InstalledDriver & WithNamedChannel - const driver2WithChannelName = { ...driver2, channelName: 'Channel 2' } as InstalledDriver & WithNamedChannel - const chooseHubMock = jest.mocked(chooseHub).mockResolvedValue('chosen-hub-id') - const apiListInstalledSpy = jest.spyOn(HubdevicesEndpoint.prototype, 'listInstalled').mockResolvedValue([driver1]) - const apiGetInstalledSpy = jest.spyOn(HubdevicesEndpoint.prototype, 'getInstalled').mockResolvedValue(driver1) - const outputItemOrListMock = jest.mocked(outputItemOrList) - - it('uses outputItemOrList', async () => { - await expect(DriversInstalledCommand.run([])).resolves.not.toThrow() - - expect(chooseHubMock).toHaveBeenCalledTimes(1) - expect(chooseHubMock).toHaveBeenCalledWith(expect.any(DriversInstalledCommand), - 'Select a hub.', undefined, { allowIndex: true, useConfigDefault: true }) - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - expect(outputItemOrListMock).toHaveBeenCalledWith( - expect.any(DriversInstalledCommand), - expect.objectContaining({ primaryKeyName: 'driverId' }), - undefined, - expect.any(Function), - expect.any(Function), - ) - }) - - it('uses hub id from command line', async () => { - await expect(DriversInstalledCommand.run(['--hub', 'cmd-line-hub-id'])).resolves.not.toThrow() - - expect(chooseHubMock).toHaveBeenCalledTimes(1) - expect(chooseHubMock).toHaveBeenCalledWith(expect.any(DriversInstalledCommand), - 'Select a hub.', 'cmd-line-hub-id', { allowIndex: true, useConfigDefault: true }) - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - }) - - it('includes channel name with verbose flag', async () => { - await expect(DriversInstalledCommand.run(['--verbose'])).resolves.not.toThrow() - - expect(chooseHubMock).toHaveBeenCalledTimes(1) - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - expect(outputItemOrListMock).toHaveBeenCalledWith( - expect.any(DriversInstalledCommand), - expect.objectContaining({ - primaryKeyName: 'driverId', - tableFieldDefinitions: expect.arrayContaining(['channelName']), - listTableFieldDefinitions: expect.arrayContaining(['channelName']), - }), - undefined, - expect.any(Function), - expect.any(Function), - ) - }) - - describe('list function', () => { - const withChannelNamesMock = jest.mocked(withChannelNames) - - it('calls listInstalled', async () => { - await expect(DriversInstalledCommand.run(['--device', 'cmd-line-device-id'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const listFunction = outputItemOrListMock.mock.calls[0][3] - - expect(await listFunction()).toStrictEqual([driver1]) - - expect(withChannelNamesMock).toHaveBeenCalledTimes(0) - expect(apiListInstalledSpy).toHaveBeenCalledTimes(1) - expect(apiListInstalledSpy).toHaveBeenCalledWith('chosen-hub-id', 'cmd-line-device-id') - }) - - it('uses withChannelNames in verbose mode', async () => { - await expect(DriversInstalledCommand.run(['--verbose'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const listFunction = outputItemOrListMock.mock.calls[0][3] - - apiListInstalledSpy.mockResolvedValueOnce([driver1, driver2]) - withChannelNamesMock.mockResolvedValueOnce([driver1WithChannelName, driver2WithChannelName]) - - expect(await listFunction()).toStrictEqual([driver1WithChannelName, driver2WithChannelName]) - - expect(apiListInstalledSpy).toHaveBeenCalledTimes(1) - expect(apiListInstalledSpy).toHaveBeenCalledWith('chosen-hub-id', undefined) - expect(withChannelNamesMock).toHaveBeenCalledTimes(1) - expect(withChannelNamesMock).toHaveBeenCalledWith(expect.any(SmartThingsClient), [driver1, driver2]) - }) - }) - - describe('get function', () => { - const withChannelNamesMock = jest.mocked(withChannelNames) as unknown as - jest.Mock, [SmartThingsClient, InstalledDriver]> - - it('calls getInstalled', async () => { - await expect(DriversInstalledCommand.run(['cmd-line-driver-id'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const getFunction = outputItemOrListMock.mock.calls[0][4] - - expect(await getFunction('chosen-device-id')).toBe(driver1) - - expect(withChannelNamesMock).toHaveBeenCalledTimes(0) - expect(apiGetInstalledSpy).toHaveBeenCalledTimes(1) - expect(apiGetInstalledSpy).toHaveBeenCalledWith('chosen-hub-id', 'chosen-device-id') - }) - - it('uses withChannelNames in verbose mode', async () => { - await expect(DriversInstalledCommand.run(['cmd-line-driver-id', '--verbose'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const getFunction = outputItemOrListMock.mock.calls[0][4] - withChannelNamesMock.mockResolvedValueOnce(driver1WithChannelName) - - expect(await getFunction('chosen-device-id')).toBe(driver1WithChannelName) - - expect(apiGetInstalledSpy).toHaveBeenCalledTimes(1) - expect(apiGetInstalledSpy).toHaveBeenCalledWith('chosen-hub-id', 'chosen-device-id') - expect(withChannelNamesMock).toHaveBeenCalledTimes(1) - expect(withChannelNamesMock).toHaveBeenCalledWith(expect.any(SmartThingsClient), driver1) - }) - }) -}) diff --git a/packages/edge/src/commands/edge/drivers/installed.ts b/packages/edge/src/commands/edge/drivers/installed.ts deleted file mode 100644 index 09bc7943..00000000 --- a/packages/edge/src/commands/edge/drivers/installed.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Flags } from '@oclif/core' - -import { InstalledDriver } from '@smartthings/core-sdk' - -import { outputItemOrList, OutputItemOrListConfig } from '@smartthings/cli-lib' - -import { chooseHub } from '../../../lib/commands/drivers-util.js' -import { EdgeCommand } from '../../../lib/edge-command.js' -import { WithNamedChannel, withChannelNames } from '../../../lib/commands/channels-util.js' - - -export default class DriversInstalledCommand extends EdgeCommand { - static description = 'list all drivers installed on a given hub' + - this.apiDocsURL('listHubInstalledDrivers', 'getHubDeviceDriver') - - static flags = { - ...EdgeCommand.flags, - ...outputItemOrList.flags, - hub: Flags.string({ - char: 'H', - description: 'hub id', - helpValue: '', - }), - device: Flags.string({ - description: 'return drivers matching the specified device', - helpValue: '', - }), - verbose: Flags.boolean({ - description: 'include channel name in output', - char: 'v', - }), - } - - static args = [{ - name: 'idOrIndex', - description: 'the driver id or number in list', - }] - - static examples = [ - { - description: 'list all installed drivers', - command: 'smartthings edge:drivers:installed', - }, - { - description: 'list all installed drivers and include the channel name in the output', - command: 'smartthings edge:drivers:installed --verbose', - }, - { - description: 'list the first driver in the list retrieved by running "smartthings edge:drivers:installed"', - command: 'smartthings edge:drivers:installed 1', - }, - { - description: 'list an installed driver by id', - command: 'smartthings edge:drivers:installed ', - }, - ] - - async run(): Promise { - const config: OutputItemOrListConfig = { - primaryKeyName: 'driverId', - sortKeyName: 'name', - tableFieldDefinitions: ['name', 'driverId', 'description', 'version', 'channelId', - 'developer', 'vendorSupportInformation'], - listTableFieldDefinitions: ['name', 'driverId', 'version', 'channelId'], - } - if (this.flags.verbose) { - config.tableFieldDefinitions.splice(4, 0, 'channelName') - config.listTableFieldDefinitions.splice(3, 0, 'channelName') - } - const listInstalledWrapper: (drivers: Promise) => Promise<(InstalledDriver & WithNamedChannel)[]> = - this.flags.verbose ? async drivers => withChannelNames(this.client, await drivers) : drivers => drivers - const getInstalledWrapper: (driver: Promise) => Promise = - this.flags.verbose ? async driver => withChannelNames(this.client, await driver) : driver => driver - - const hubId = await chooseHub(this, 'Select a hub.', this.flags.hub, - { allowIndex: true, useConfigDefault: true }) - - await outputItemOrList(this, config, this.args.idOrIndex, - () => listInstalledWrapper(this.client.hubdevices.listInstalled(hubId, this.flags.device)), - id => getInstalledWrapper(this.client.hubdevices.getInstalled(hubId, id))) - } -} diff --git a/src/__tests__/commands/edge/drivers/installed.test.ts b/src/__tests__/commands/edge/drivers/installed.test.ts new file mode 100644 index 00000000..fac4a786 --- /dev/null +++ b/src/__tests__/commands/edge/drivers/installed.test.ts @@ -0,0 +1,199 @@ +import { jest } from '@jest/globals' + +import type { ArgumentsCamelCase, Argv } from 'yargs' + +import type { HubdevicesEndpoint, InstalledDriver } from '@smartthings/core-sdk' + +import type { CommandArgs } from '../../../../commands/edge/drivers/installed.js' +import type { APICommand, APICommandFlags } from '../../../../lib/command/api-command.js' +import type { TableCommonOutputProducer } from '../../../../lib/command/format.js' +import type { outputItemOrList, outputItemOrListBuilder } from '../../../../lib/command/listing-io.js' +import { chooseHub } from '../../../../lib/command/util/hubs-choose.js' +import type { withChannelNames, WithNamedChannel } from '../../../../lib/command/util/edge/channels.js' +import { apiCommandMocks } from '../../../test-lib/api-command-mock.js' +import { buildArgvMock, buildArgvMockStub } from '../../../test-lib/builder-mock.js' + + +const { apiCommandMock, apiCommandBuilderMock, apiDocsURLMock } = apiCommandMocks('../../../..') + +const outputItemOrListMock = jest.fn>() +const outputItemOrListBuilderMock = jest.fn() +jest.unstable_mockModule('../../../../lib/command/listing-io.js', () => ({ + outputItemOrList: outputItemOrListMock, + outputItemOrListBuilder: outputItemOrListBuilderMock, +})) + +const chooseHubMock = jest.fn().mockResolvedValue('chosen-hub-id') +jest.unstable_mockModule('../../../../lib/command/util/hubs-choose.js', () => ({ + chooseHub: chooseHubMock, +})) + +const withChannelNamesMock = jest.fn>() +jest.unstable_mockModule('../../../../lib/command/util/edge/channels.js', () => ({ + withChannelNames: withChannelNamesMock, +})) + + +const { default: cmd } = await import('../../../../commands/edge/drivers/installed.js') + + +test('builder', async () => { + const yargsMock = buildArgvMockStub() + const { + yargsMock: apiCommandBuilderArgvMock, + positionalMock, + optionMock, + exampleMock, + epilogMock, + argvMock, + } = buildArgvMock() + + apiCommandBuilderMock.mockReturnValue(apiCommandBuilderArgvMock) + outputItemOrListBuilderMock.mockReturnValue(argvMock) + + const builder = cmd.builder as (yargs: Argv) => Argv + + expect(builder(yargsMock)).toBe(argvMock) + + expect(apiCommandBuilderMock).toHaveBeenCalledExactlyOnceWith(yargsMock) + expect(outputItemOrListBuilderMock).toHaveBeenCalledExactlyOnceWith(apiCommandBuilderArgvMock) + expect(positionalMock).toHaveBeenCalledTimes(1) + expect(optionMock).toHaveBeenCalledTimes(3) + expect(exampleMock).toHaveBeenCalledTimes(1) + expect(apiDocsURLMock).toHaveBeenCalledTimes(1) + expect(epilogMock).toHaveBeenCalledTimes(1) +}) + + +describe('handler', () => { + const driver1 = { driverId: 'driver-id-1' } as InstalledDriver + const driver2 = { driverId: 'driver-id-2' } as InstalledDriver + const drivers = [driver1, driver2] + + const apiHubDevicesGetInstalledMock = jest.fn() + .mockResolvedValue(driver1) + const apiHubDevicesListInstalledMock = jest.fn() + .mockResolvedValue(drivers) + const command = { + client: { + hubdevices: { + getInstalled: apiHubDevicesGetInstalledMock, + listInstalled: apiHubDevicesListInstalledMock, + }, + }, + } as unknown as APICommand> + apiCommandMock.mockResolvedValue(command) + + const driver1WithChannel = { ...driver1, channelName: 'Channel Name' } as InstalledDriver & WithNamedChannel + const driver2WithChannel = { ...driver2, channelName: 'Channel Name' } as InstalledDriver & WithNamedChannel + const driversWithChannels = [driver1WithChannel, driver2WithChannel] + withChannelNamesMock.mockResolvedValue(driversWithChannels) + + const inputArgv = { + profile: 'default', + verbose: false, + } as ArgumentsCamelCase + + it('lists installed drivers by default', async () => { + await expect(cmd.handler(inputArgv)).resolves.not.toThrow() + + expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(inputArgv) + expect(chooseHubMock).toHaveBeenCalledExactlyOnceWith( + command, + undefined, + expect.objectContaining({ useConfigDefault: true }), + ) + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ + primaryKeyName: 'driverId', + tableFieldDefinitions: expect.arrayContaining(['name', 'driverId']), + }), + undefined, + expect.any(Function), + expect.any(Function), + ) + const config = outputItemOrListMock.mock.calls[0][1] as TableCommonOutputProducer + expect(config.tableFieldDefinitions).not.toContain('channelName') + + const listFunction = outputItemOrListMock.mock.calls[0][3] + + expect(await listFunction()).toBe(drivers) + + expect(apiHubDevicesListInstalledMock).toHaveBeenCalledExactlyOnceWith('chosen-hub-id', undefined) + + expect(withChannelNamesMock).not.toHaveBeenCalled() + expect(apiHubDevicesGetInstalledMock).not.toHaveBeenCalled() + }) + + it('includes channel id in verbose mode', async () => { + await expect(cmd.handler({ ...inputArgv, verbose: true })).resolves.not.toThrow() + + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ + primaryKeyName: 'driverId', + tableFieldDefinitions: expect.arrayContaining(['channelName']), + }), + undefined, + expect.any(Function), + expect.any(Function), + ) + + const listFunction = outputItemOrListMock.mock.calls[0][3] + + expect(await listFunction()).toBe(driversWithChannels) + + expect(apiHubDevicesListInstalledMock).toHaveBeenCalledExactlyOnceWith('chosen-hub-id', undefined) + expect(withChannelNamesMock).toHaveBeenCalledExactlyOnceWith(command.client, drivers) + + expect(apiHubDevicesGetInstalledMock).not.toHaveBeenCalled() + }) + + it('displays details of a specified driver', async () => { + await expect(cmd.handler({ ...inputArgv, idOrIndex: 'cmd-line-id' })).resolves.not.toThrow() + + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ + tableFieldDefinitions: expect.not.arrayContaining(['channelName']), + }), + 'cmd-line-id', + expect.any(Function), + expect.any(Function), + ) + + const getFunction = outputItemOrListMock.mock.calls[0][4] + + expect(await getFunction('driver-id')).toBe(driver1) + + expect(apiHubDevicesGetInstalledMock).toHaveBeenCalledExactlyOnceWith('chosen-hub-id', 'driver-id') + + expect(withChannelNamesMock).not.toHaveBeenCalled() + expect(apiHubDevicesListInstalledMock).not.toHaveBeenCalled() + }) + + it('includes channel name in details in verbose mode', async () => { + await expect(cmd.handler({ ...inputArgv, idOrIndex: 'cmd-line-id', verbose: true })).resolves.not.toThrow() + + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ + tableFieldDefinitions: expect.arrayContaining(['channelName']), + }), + 'cmd-line-id', + expect.any(Function), + expect.any(Function), + ) + + const getFunction = outputItemOrListMock.mock.calls[0][4] + withChannelNamesMock.mockResolvedValueOnce(driver1WithChannel as unknown as (InstalledDriver & WithNamedChannel)[]) + + expect(await getFunction('driver-id')).toBe(driver1WithChannel) + + expect(apiHubDevicesGetInstalledMock).toHaveBeenCalledExactlyOnceWith('chosen-hub-id', 'driver-id') + expect(withChannelNamesMock).toHaveBeenCalledExactlyOnceWith(command.client, driver1) + + expect(apiHubDevicesListInstalledMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/commands/edge/drivers/installed.ts b/src/commands/edge/drivers/installed.ts new file mode 100644 index 00000000..1e732aa0 --- /dev/null +++ b/src/commands/edge/drivers/installed.ts @@ -0,0 +1,88 @@ +import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' + +import { type InstalledDriver } from '@smartthings/core-sdk' + +import { apiCommand, apiCommandBuilder, type APICommandFlags, apiDocsURL } from '../../../lib/command/api-command.js' +import { + outputItemOrList, + outputItemOrListBuilder, + type OutputItemOrListConfig, + type OutputItemOrListFlags, +} from '../../../lib/command/listing-io.js' +import { chooseHub } from '../../../lib/command/util/hubs-choose.js' +import { withChannelNames, type WithNamedChannel } from '../../../lib/command/util/edge/channels.js' + + +export type CommandArgs = + & APICommandFlags + & OutputItemOrListFlags + & { + hub?: string + device?: string + verbose: boolean + idOrIndex?: string + } + +const command = 'edge:drivers:installed [id-or-index]' + +const describe = 'list all drivers installed on a given hub' + +const builder = (yargs: Argv): Argv => + outputItemOrListBuilder(apiCommandBuilder(yargs)) + .positional('id-or-index', { describe: 'driver id or number in list', type: 'string' }) + .option('hub', { alias: 'H', describe: 'hub id', type: 'string' }) + .option('device', { describe: 'include only drivers matching the device', type: 'string' }) + .option('verbose', { alias: 'v', describe: 'include channel name in output', type: 'boolean', default: false }) + .example([ + ['$0 smartthings edge:drivers:installed', 'list all installed drivers'], + [ + '$0 smartthings edge:drivers:installed --verbose', + 'list all installed drivers and include the channel name in the output', + ], + [ + '$0 smartthings edge:drivers:installed 1', + 'list the first driver in the list retrieved by running "smartthings edge:drivers:installed', + ], + [ + '$0 smartthings edge:drivers:installed 3f9f151d-3022-4b9f-814a-afcbf69b650f', + 'display details for an installed driver by id', + ], + ]) + .epilog(apiDocsURL('listHubInstalledDrivers', 'getHubDeviceDriver')) + +const handler = async (argv: ArgumentsCamelCase): Promise => { + const command = await apiCommand(argv) + + const config: OutputItemOrListConfig = { + primaryKeyName: 'driverId', + sortKeyName: 'name', + tableFieldDefinitions: ['name', 'driverId', 'description', 'version', 'channelId', + 'developer', 'vendorSupportInformation'], + listTableFieldDefinitions: ['name', 'driverId', 'version', 'channelId'], + } + if (argv.verbose) { + config.tableFieldDefinitions.splice(4, 0, 'channelName') + config.listTableFieldDefinitions.splice(3, 0, 'channelName') + } + const listInstalledWrapper: (drivers: Promise) => Promise<(InstalledDriver & WithNamedChannel)[]> = + argv.verbose ? async drivers => withChannelNames(command.client, await drivers) : drivers => drivers + const getInstalledWrapper: (driver: Promise) => Promise = + argv.verbose ? async driver => withChannelNames(command.client, await driver) : driver => driver + + const hubId = await chooseHub( + command, + argv.hub, + { promptMessage: 'Select a hub.', allowIndex: true, useConfigDefault: true }, + ) + + await outputItemOrList( + command, + config, + argv.idOrIndex, + () => listInstalledWrapper(command.client.hubdevices.listInstalled(hubId, argv.device)), + id => getInstalledWrapper(command.client.hubdevices.getInstalled(hubId, id)), + ) +} + +const cmd: CommandModule = { command, describe, builder, handler } +export default cmd diff --git a/src/commands/index.ts b/src/commands/index.ts index b4e93cd4..d3e7d2c6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -68,6 +68,7 @@ import edgeDriversCommand from './edge/drivers.js' import edgeDriversDefaultCommand from './edge/drivers/default.js' import edgeDriversDevicesCommand from './edge/drivers/devices.js' import edgeDriversInstallCommand from './edge/drivers/install.js' +import edgeDriversInstalledCommand from './edge/drivers/installed.js' import edgeDriversLogcatCommand from './edge/drivers/logcat.js' import installedappsCommand from './installedapps.js' import installedappsDeleteCommand from './installedapps/delete.js' @@ -190,6 +191,7 @@ export const commands: CommandModule[] = [ edgeDriversDefaultCommand, edgeDriversDevicesCommand, edgeDriversInstallCommand, + edgeDriversInstalledCommand, edgeDriversLogcatCommand, installedappsCommand, installedappsDeleteCommand,