diff --git a/packages/edge/src/__tests__/commands/edge/channels/metainfo.test.ts b/packages/edge/src/__tests__/commands/edge/channels/metainfo.test.ts deleted file mode 100644 index 96115c60..00000000 --- a/packages/edge/src/__tests__/commands/edge/channels/metainfo.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ChannelsEndpoint, DriverChannelDetails, EdgeDriver } from '@smartthings/core-sdk' - -import { CustomCommonOutputProducer, defaultTableGenerator, outputItemOrList } from '@smartthings/cli-lib' - -import ChannelsMetaInfoCommand from '../../../../commands/edge/channels/metainfo.js' -import { buildTableOutput } from '../../../../lib/commands/drivers-util.js' -import { chooseChannel } from '../../../../lib/commands/channels-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('ChannelsMetaInfoCommand', () => { - const outputItemOrListMock = jest.mocked(outputItemOrList) - - const chooseChannelMock = jest.mocked(chooseChannel).mockResolvedValue('resolved-channel-id') - - const listAssignedDriversSpy = jest.spyOn(ChannelsEndpoint.prototype, 'listAssignedDrivers') - const getDriverChannelMetaInfoSpy = jest.spyOn(ChannelsEndpoint.prototype, 'getDriverChannelMetaInfo') - - it('uses outputItemOrList', async () => { - await expect(ChannelsMetaInfoCommand.run([])).resolves.not.toThrow() - - expect(chooseChannelMock).toHaveBeenCalledTimes(1) - expect(chooseChannelMock).toHaveBeenCalledWith(expect.any(ChannelsMetaInfoCommand), - 'Choose a channel to get meta info for.', undefined, { useConfigDefault: true }) - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - expect(outputItemOrListMock).toHaveBeenCalledWith( - expect.any(ChannelsMetaInfoCommand), - expect.objectContaining({ primaryKeyName: 'driverId' }), - undefined, - expect.any(Function), - expect.any(Function), - ) - }) - - it('passes channel from command line to chooseChannel', async () => { - await expect(ChannelsMetaInfoCommand.run(['--channel=channel-id-from-cmd-line'])) - .resolves.not.toThrow() - - expect(chooseChannelMock).toHaveBeenCalledTimes(1) - expect(chooseChannelMock).toHaveBeenCalledWith(expect.any(ChannelsMetaInfoCommand), - 'Choose a channel to get meta info for.', 'channel-id-from-cmd-line', { useConfigDefault: true }) - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - }) - - describe('listDriversMetaInfo', () => { - it('returns empty list when no drivers assigned to channel', async () => { - await expect(ChannelsMetaInfoCommand.run([])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const listDriversMetaInfo = outputItemOrListMock.mock.calls[0][3] as () => Promise - listAssignedDriversSpy.mockResolvedValueOnce([]) - - expect(await listDriversMetaInfo()).toEqual([]) - - expect(listAssignedDriversSpy).toHaveBeenCalledTimes(1) - expect(listAssignedDriversSpy).toHaveBeenCalledWith('resolved-channel-id') - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledTimes(0) - }) - - it('combines meta info for all drivers in a channel', async () => { - await expect(ChannelsMetaInfoCommand.run([])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const listDriversMetaInfo = outputItemOrListMock.mock.calls[0][3] as () => Promise - const assignedDrivers = [ - { driverId: 'assigned-driver-1' }, - { driverId: 'assigned-driver-2' }, - ] as DriverChannelDetails[] - listAssignedDriversSpy.mockResolvedValueOnce(assignedDrivers) - const metaInfo1 = { driverId: 'driver-1-with-meta-info-id' } as EdgeDriver - const metaInfo2 = { driverId: 'driver-2-with-meta-info-id' } as EdgeDriver - getDriverChannelMetaInfoSpy.mockResolvedValueOnce(metaInfo1) - getDriverChannelMetaInfoSpy.mockResolvedValueOnce(metaInfo2) - - expect(await listDriversMetaInfo()).toStrictEqual([metaInfo1, metaInfo2]) - - expect(listAssignedDriversSpy).toHaveBeenCalledTimes(1) - expect(listAssignedDriversSpy).toHaveBeenCalledWith('resolved-channel-id') - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledTimes(2) - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledWith('resolved-channel-id', 'assigned-driver-1') - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledWith('resolved-channel-id', 'assigned-driver-2') - }) - }) - - test('get item function uses channels.getDriverChannelMetaInfo', async () => { - await expect(ChannelsMetaInfoCommand.run(['id-from-command-line'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const getFunction = outputItemOrListMock.mock.calls[0][4] - - const driver = { driverId: 'driver-id' } as EdgeDriver - getDriverChannelMetaInfoSpy.mockResolvedValueOnce(driver) - - expect(await getFunction('resolved-driver-id')).toBe(driver) - - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledTimes(1) - expect(getDriverChannelMetaInfoSpy).toHaveBeenCalledWith('resolved-channel-id', 'resolved-driver-id') - }) - - it('uses buildTableOutput from drivers-util', async () => { - await expect(ChannelsMetaInfoCommand.run(['id-from-command-line'])).resolves.not.toThrow() - - expect(outputItemOrListMock).toHaveBeenCalledTimes(1) - - const config = outputItemOrListMock.mock.calls[0][1] as CustomCommonOutputProducer - const driver = { driverId: 'driver-id' } as EdgeDriver - - const buildTableOutputMock = jest.mocked(buildTableOutput).mockReturnValueOnce('table output') - - expect(config.buildTableOutput(driver)).toBe('table output') - - expect(buildTableOutputMock).toHaveBeenCalledTimes(1) - expect(buildTableOutputMock).toHaveBeenCalledWith(expect.any(defaultTableGenerator), driver) - }) -}) diff --git a/packages/edge/src/commands/edge/channels/metainfo.ts b/packages/edge/src/commands/edge/channels/metainfo.ts deleted file mode 100644 index 7958607c..00000000 --- a/packages/edge/src/commands/edge/channels/metainfo.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Flags } from '@oclif/core' - -import { EdgeDriver } from '@smartthings/core-sdk' - -import { outputItemOrList, OutputItemOrListConfig } from '@smartthings/cli-lib' - -import { EdgeCommand } from '../../../lib/edge-command.js' -import { chooseChannel } from '../../../lib/commands/channels-util.js' -import { buildTableOutput, listTableFieldDefinitions } from '../../../lib/commands/drivers-util.js' - - -export default class ChannelsMetaInfoCommand extends EdgeCommand { - static description = 'list all channels owned by you or retrieve a single channel' - - static flags = { - ...EdgeCommand.flags, - ...outputItemOrList.flags, - channel: Flags.string({ - char: 'C', - description: 'channel id', - exclusive: ['input'], - helpValue: '', - }), - } - - static args = [{ - name: 'idOrIndex', - description: 'the channel id or number in list', - }] - - static examples = [`# summarize metainfo for all drivers in a channel -$ smartthings edge:channels:metainfo - -# summarize metainfo for all drivers in the specified channel -$ smartthings edge:channels:metainfo -C b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2 - -# display metainfo about the third driver listed in the above command -$ smartthings edge:channels:metainfo -C b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2 3`, - ` -# display metainfo about a driver by using its id -$ smartthings edge:channels:metainfo -C b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2 699c7308-8c72-4363-9571-880d0f5cc725`] - - async run(): Promise { - const channelId = await chooseChannel(this, 'Choose a channel to get meta info for.', - this.flags.channel, { useConfigDefault: true }) - - const config: OutputItemOrListConfig = { - primaryKeyName: 'driverId', - sortKeyName: 'name', - buildTableOutput: (driver: EdgeDriver) => buildTableOutput(this.tableGenerator, driver), - listTableFieldDefinitions, - } - - const listDriversMetaInfo = async (): Promise => { - const drivers = await this.client.channels.listAssignedDrivers(channelId) - return Promise.all(drivers.map(async driver => - await this.client.channels.getDriverChannelMetaInfo(channelId, driver.driverId))) - } - - await outputItemOrList(this, config, this.args.idOrIndex, - listDriversMetaInfo, - driverId => this.client.channels.getDriverChannelMetaInfo(channelId, driverId)) - } -} diff --git a/src/__tests__/commands/edge/channels/metainfo.test.ts b/src/__tests__/commands/edge/channels/metainfo.test.ts new file mode 100644 index 00000000..fb032d46 --- /dev/null +++ b/src/__tests__/commands/edge/channels/metainfo.test.ts @@ -0,0 +1,153 @@ +import { jest } from '@jest/globals' + +import type { ArgumentsCamelCase, Argv } from 'yargs' + +import type { CommandArgs } from '../../../../commands/edge/channels/metainfo.js' + +import { ChannelsEndpoint, DriverChannelDetails, EdgeDriver } from '@smartthings/core-sdk' + +import type { APICommand, APICommandFlags } from '../../../../lib/command/api-command.js' +import type { CustomCommonOutputProducer } from '../../../../lib/command/format.js' +import type { outputItemOrList, outputItemOrListBuilder } from '../../../../lib/command/listing-io.js' +import { type buildTableOutput, listTableFieldDefinitions } from '../../../../lib/command/util/edge-drivers.js' +import type { chooseChannel } from '../../../../lib/command/util/edge/channels-choose.js' +import { apiCommandMocks } from '../../../test-lib/api-command-mock.js' +import { buildArgvMock, buildArgvMockStub } from '../../../test-lib/builder-mock.js' +import { tableGeneratorMock } from '../../../test-lib/table-mock.js' + + +const { apiCommandMock, apiCommandBuilderMock } = apiCommandMocks('../../../..') + +const outputItemOrListMock = jest.fn>() +const outputItemOrListBuilderMock = jest.fn() +jest.unstable_mockModule('../../../../lib/command/listing-io.js', () => ({ + outputItemOrList: outputItemOrListMock, + outputItemOrListBuilder: outputItemOrListBuilderMock, +})) + +const buildTableOutputMock = jest.fn() +jest.unstable_mockModule('../../../../lib/command/util/edge-drivers.js', () => ({ + buildTableOutput: buildTableOutputMock, + listTableFieldDefinitions, +})) + +const chooseChannelMock = jest.fn().mockResolvedValue('chosen-channel-id') +jest.unstable_mockModule('../../../../lib/command/util/edge/channels-choose.js', () => ({ + chooseChannel: chooseChannelMock, +})) + + +const { default: cmd } = await import('../../../../commands/edge/channels/metainfo.js') + + +test('builder', async () => { + const yargsMock = buildArgvMockStub() + const { + yargsMock: apiCommandBuilderArgvMock, + positionalMock, + optionMock, + exampleMock, + 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(1) + expect(exampleMock).toHaveBeenCalledTimes(1) +}) + +describe('handler', () => { + const driver1 = { driverId: 'driver-id-1', name: 'Driver 1' } as EdgeDriver + const driver2 = { driverId: 'driver-id-2', name: 'Driver 2' } as EdgeDriver + const driver1ChannelDetails = { driverId: 'driver-id-1', channelId: 'channel-id-1' } as DriverChannelDetails + const driver2ChannelDetails = { driverId: 'driver-id-2', channelId: 'channel-id-1' } as DriverChannelDetails + + const apiChannelsListAssignedDriversMock = + jest.fn() + .mockResolvedValue([driver1ChannelDetails, driver2ChannelDetails]) + const apiChannelsGetDriverChannelMetaInfoMock = + jest.fn() + const command = { + client: { + channels: { + getDriverChannelMetaInfo: apiChannelsGetDriverChannelMetaInfoMock, + listAssignedDrivers: apiChannelsListAssignedDriversMock, + }, + }, + tableGenerator: tableGeneratorMock, + } as unknown as APICommand + apiCommandMock.mockResolvedValue(command) + + const baseInputArgv = { profile: 'default' } as ArgumentsCamelCase + it('prompts for a channel and lists metadata', async () => { + await expect(cmd.handler(baseInputArgv)).resolves.not.toThrow() + + expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(baseInputArgv) + expect(chooseChannelMock).toHaveBeenCalledExactlyOnceWith( + command, + undefined, + expect.objectContaining({ useConfigDefault: true }), + ) + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ listTableFieldDefinitions }), + undefined, + expect.any(Function), + expect.any(Function), + ) + + const listFunction = outputItemOrListMock.mock.calls[0][3] + apiChannelsGetDriverChannelMetaInfoMock.mockResolvedValueOnce(driver1) + apiChannelsGetDriverChannelMetaInfoMock.mockResolvedValueOnce(driver2) + + expect(await listFunction()).toStrictEqual([driver1, driver2]) + + expect(apiChannelsListAssignedDriversMock).toHaveBeenCalledExactlyOnceWith('chosen-channel-id') + expect(apiChannelsGetDriverChannelMetaInfoMock).toHaveBeenCalledTimes(2) + expect(apiChannelsGetDriverChannelMetaInfoMock).toHaveBeenCalledWith('chosen-channel-id', 'driver-id-1') + expect(apiChannelsGetDriverChannelMetaInfoMock).toHaveBeenCalledWith('chosen-channel-id', 'driver-id-2') + }) + + it('displays details for a specific driver', async () => { + const inputArgv = { ...baseInputArgv, driverIdOrIndex: 'cmd-line-driver-id', channel: 'cmd-line-channel-id' } + await expect(cmd.handler(inputArgv)).resolves.not.toThrow() + + expect(chooseChannelMock).toHaveBeenCalledExactlyOnceWith( + command, + 'cmd-line-channel-id', + expect.objectContaining({ useConfigDefault: true }), + ) + expect(outputItemOrListMock).toHaveBeenCalledExactlyOnceWith( + command, + expect.objectContaining({ listTableFieldDefinitions }), + 'cmd-line-driver-id', + expect.any(Function), + expect.any(Function), + ) + + const getFunction = outputItemOrListMock.mock.calls[0][4] + apiChannelsGetDriverChannelMetaInfoMock.mockResolvedValueOnce(driver1) + + expect(await getFunction('driver-id')).toBe(driver1) + + expect(apiChannelsGetDriverChannelMetaInfoMock) + .toHaveBeenCalledExactlyOnceWith('chosen-channel-id', 'driver-id') + + buildTableOutputMock.mockReturnValueOnce('table output') + + const buildTableOutput = + (outputItemOrListMock.mock.calls[0][1] as CustomCommonOutputProducer).buildTableOutput + + expect(buildTableOutput(driver1)).toBe('table output') + + expect(buildTableOutputMock).toHaveBeenCalledExactlyOnceWith(tableGeneratorMock, driver1) + }) +}) diff --git a/src/commands/edge/channels/metainfo.ts b/src/commands/edge/channels/metainfo.ts new file mode 100644 index 00000000..91e1ee7d --- /dev/null +++ b/src/commands/edge/channels/metainfo.ts @@ -0,0 +1,84 @@ +import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' + +import type { EdgeDriver } from '@smartthings/core-sdk' + +import { apiCommand, apiCommandBuilder, type APICommandFlags } from '../../../lib/command/api-command.js' +import { + outputItemOrList, + outputItemOrListBuilder, + type OutputItemOrListConfig, + type OutputItemOrListFlags, +} from '../../../lib/command/listing-io.js' +import { buildTableOutput, listTableFieldDefinitions } from '../../../lib/command/util/edge-drivers.js' +import { chooseChannel } from '../../../lib/command/util/edge/channels-choose.js' + + +export type CommandArgs = + & APICommandFlags + & OutputItemOrListFlags + & { + driverIdOrIndex?: string + channel?: string + } + +const command = 'edge:channels:metainfo [driver-id-or-index]' + +const describe = 'display metadata about drivers assigned to channels' + +const builder = (yargs: Argv): Argv => + outputItemOrListBuilder(apiCommandBuilder(yargs)) + .positional('id-or-index', { describe: 'driver id or number in list', type: 'string' }) + .option('channel', { alias: 'C', describe: 'channel id', type: 'string' }) + .example([ + [ + '$0 edge:channels:metainfo', + 'prompt for a channel and summarize metainfo for drivers assigned to it', + ], + [ + '$0 edge:channels:metainfo --channel b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2', + 'summarize metainfo for drivers assigned to the specified channel', + ], + [ + '$0 edge:channels:metainfo -C b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2 3', + 'display metainfo about the third driver listed in the above command', + ], + [ + '$0 edge:channels:metainfo --channel b50c0aa1-d9ea-4005-8db8-0cf9c2d9d7b2' + + ' 699c7308-8c72-4363-9571-880d0f5cc725', + 'display metainfo about a driver by using its channel and id', + ], + ]) + +const handler = async (argv: ArgumentsCamelCase): Promise => { + const command = await apiCommand(argv) + + const channelId = await chooseChannel( + command, + argv.channel, + { useConfigDefault: true, promptMessage: 'Choose a channel to get meta info for.' }, + ) + + const config: OutputItemOrListConfig = { + primaryKeyName: 'driverId', + sortKeyName: 'name', + buildTableOutput: (driver: EdgeDriver) => buildTableOutput(command.tableGenerator, driver), + listTableFieldDefinitions, + } + + const listDriversMetaInfo = async (): Promise => { + const drivers = await command.client.channels.listAssignedDrivers(channelId) + return Promise.all(drivers.map(async driver => + await command.client.channels.getDriverChannelMetaInfo(channelId, driver.driverId))) + } + + await outputItemOrList( + command, + config, + argv.driverIdOrIndex, + listDriversMetaInfo, + driverId => command.client.channels.getDriverChannelMetaInfo(channelId, driverId), + ) +} + +const cmd: CommandModule = { command, describe, builder, handler } +export default cmd diff --git a/src/commands/index.ts b/src/commands/index.ts index 584e8ffa..3044073a 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -65,6 +65,7 @@ import edgeChannelsInvitesCommand from './edge/channels/invites.js' import edgeChannelsInvitesAcceptCommand from './edge/channels/invites/accept.js' import edgeChannelsInvitesCreateCommand from './edge/channels/invites/create.js' import edgeChannelsInvitesDeleteCommand from './edge/channels/invites/delete.js' +import edgeChannelsMetaInfoCommand from './edge/channels/metainfo.js' import edgeChannelsUnassignCommand from './edge/channels/unassign.js' import edgeDriversCommand from './edge/drivers.js' import edgeDriversDefaultCommand from './edge/drivers/default.js' @@ -190,6 +191,7 @@ export const commands: CommandModule[] = [ edgeChannelsInvitesAcceptCommand, edgeChannelsInvitesCreateCommand, edgeChannelsInvitesDeleteCommand, + edgeChannelsMetaInfoCommand, edgeChannelsUnassignCommand, edgeDriversCommand, edgeDriversDefaultCommand,