Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/purple-steaks-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@smartthings/cli": minor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to stick with patch for all commits so no versions get bumped while we're still considering the CLI beta. @john-u is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it will matter while in prerelease, it should not touch the major.minor.patch version number until we promote out of beta.

"@smartthings/cli-lib": patch
"@smartthings/cli-testlib": patch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes don't modify testlib, so it doesn't need a release in the changeset.

---

Added commands to create virtual devices and generate events on their behalf
17 changes: 17 additions & 0 deletions packages/cli/src/__tests__/commands/devices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ describe('DevicesCommand', () => {
}))
expect(withLocationsAndRoomsMock).toHaveBeenCalledTimes(0)
})

it('uses type flag in devices.list', async () => {
await expect(DevicesCommand.run(['--type', 'VIRTUAL'])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions)
.toEqual(['label', 'name', 'type', 'deviceId'])

const listDevices = outputListingMock.mock.calls[0][3]

expect(await listDevices()).toBe(devices)

expect(listSpy).toHaveBeenCalledTimes(1)
expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'VIRTUAL' }))
expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({ capability: undefined }))
expect(withLocationsAndRoomsMock).toHaveBeenCalledTimes(0)
})
})

it('uses devices.get to get device', async () => {
Expand Down
230 changes: 230 additions & 0 deletions packages/cli/src/__tests__/commands/devices/virtual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import inquirer from 'inquirer'
import {
APICommand,
APIOrganizationCommand,
FileInputProcessor,
outputListing,
selectFromList,
} from '@smartthings/cli-lib'
import VirtualDevicesCommand, {
chooseDeviceName,
chooseDeviceProfileDefinition,
chooseDevicePrototype,
chooseRoom,
} from '../../../commands/devices/virtual'
import { chooseDeviceProfile } from '../../../commands/deviceprofiles'
import { Device, DeviceIntegrationType, DeviceProfile, DeviceProfileStatus, DevicesEndpoint } from '@smartthings/core-sdk'


jest.mock('../../../commands/deviceprofiles')

describe('chooseDeviceName function', () => {
const command = { } as unknown as APICommand<typeof APICommand.flags>

it('choose with from prompt', async () => {
const promptSpy = jest.spyOn(inquirer, 'prompt')
promptSpy.mockResolvedValue({ deviceName: 'Device Name' })

const value = await chooseDeviceName(command)
expect(promptSpy).toHaveBeenCalledTimes(1)
expect(promptSpy).toHaveBeenCalledWith({
type: 'input', name: 'deviceName',
message: 'Device Name:',
})
expect(value).toBeDefined()
expect(value).toBe('Device Name')
})

it('choose with default', async () => {
const promptSpy = jest.spyOn(inquirer, 'prompt')
promptSpy.mockResolvedValue({ deviceName: 'Another Device Name' })

const value = await chooseDeviceName(command, 'Device Name')
expect(promptSpy).toHaveBeenCalledTimes(0)
expect(value).toBeDefined()
expect(value).toBe('Device Name')
})
})

describe('chooseRoom function', () => {
const selectFromListMock = jest.mocked(selectFromList)
const listRoomsMock = jest.fn()
const client = { rooms: { list: listRoomsMock } }
const command = { client } as unknown as APICommand<typeof APICommand.flags>

it('choose room from prompt', async () => {
selectFromListMock.mockResolvedValueOnce('room-id')

const value = await chooseRoom(command, 'location-id', undefined, true)

expect(selectFromListMock).toHaveBeenCalledTimes(1)
expect(selectFromListMock).toHaveBeenCalledWith(command,
expect.objectContaining({ primaryKeyName: 'roomId', sortKeyName: 'name' }),
expect.objectContaining({ autoChoose: true }))
expect(value).toBeDefined()
expect(value).toBe('room-id')
})

it('choose room with default', async () => {
selectFromListMock.mockResolvedValueOnce('room-id')

const value = await chooseRoom(command, 'location-id', 'room-id', true)

expect(selectFromListMock).toHaveBeenCalledTimes(1)
expect(selectFromListMock).toHaveBeenCalledWith(command,
expect.objectContaining({ primaryKeyName: 'roomId', sortKeyName: 'name' }),
expect.objectContaining({ autoChoose: true, preselectedId: 'room-id' }))
expect(value).toBeDefined()
expect(value).toBe('room-id')
})
})

describe('chooseDeviceProfileDefinition function', () => {
const chooseDeviceProfileMock = jest.mocked(chooseDeviceProfile)
const command = { } as unknown as APIOrganizationCommand<typeof APIOrganizationCommand.flags>

it('choose profile ID from prompt', async() => {
chooseDeviceProfileMock.mockResolvedValueOnce('device-profile-id')

const value = await chooseDeviceProfileDefinition(command)

expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(1)
expect(chooseDeviceProfileMock).toHaveBeenCalledWith(command,
undefined,
expect.objectContaining({ allowIndex: true}))
expect(value).toBeDefined()
expect(value).toEqual({deviceProfileId: 'device-profile-id', deviceProfile: undefined})
})

it('choose profile ID from default', async() => {
const value = await chooseDeviceProfileDefinition(command, 'device-profile-id')

expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(0)
expect(value).toBeDefined()
expect(value).toEqual({deviceProfileId: 'device-profile-id', deviceProfile: undefined})
})

it('choose definition from file argument', async() => {
const deviceProfile: DeviceProfile = {
id: 'device-profile-id',
name: 'name',
components: [],
status: DeviceProfileStatus.PUBLISHED,
}

const fileSpy = jest.spyOn(FileInputProcessor.prototype, 'read').mockResolvedValueOnce(deviceProfile)

const value = await chooseDeviceProfileDefinition(command, undefined, 'device-profile-file')

expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(0)
expect(fileSpy).toHaveBeenCalledTimes(1)
expect(value).toBeDefined()
expect(value).toEqual({deviceProfileId: undefined, deviceProfile})
})
})

describe('chooseDevicePrototype function', () => {
const selectFromListMock = jest.mocked(selectFromList)
const command = {} as unknown as APICommand<typeof APICommand.flags>

it('choose from default list prompt', async () => {
selectFromListMock.mockResolvedValueOnce('VIRTUAL_SWITCH')

const value = await chooseDevicePrototype(command)

expect(selectFromListMock).toHaveBeenCalledTimes(1)
expect(selectFromListMock).toHaveBeenCalledWith(command,
expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}),
expect.not.objectContaining({preselectedId: 'VIRTUAL_SWITCH'}))
expect(value).toBeDefined()
expect(value).toBe('VIRTUAL_SWITCH')
})

it('choose with command line value', async () => {
selectFromListMock.mockResolvedValueOnce('VIRTUAL_SWITCH')

const value = await chooseDevicePrototype(command, 'VIRTUAL_SWITCH')

expect(selectFromListMock).toHaveBeenCalledTimes(1)
expect(selectFromListMock).toHaveBeenCalledWith(command,
expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}),
expect.objectContaining({preselectedId: 'VIRTUAL_SWITCH'}))
expect(value).toBeDefined()
expect(value).toBe('VIRTUAL_SWITCH')
})

it('choose from extended list prompt', async () => {
selectFromListMock.mockResolvedValueOnce('more')
selectFromListMock.mockResolvedValueOnce('VIRTUAL_CONTACT_SENSOR')

const value = await chooseDevicePrototype(command)

expect(selectFromListMock).toHaveBeenCalledTimes(2)
expect(selectFromListMock).toHaveBeenNthCalledWith(1, command,
expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}),
expect.toBeObject())
expect(selectFromListMock).toHaveBeenNthCalledWith(2, command,
expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}),
expect.toBeObject())
expect(value).toBeDefined()
expect(value).toBe('VIRTUAL_CONTACT_SENSOR')
})
})

describe('VirtualDevicesCommand', () => {
const outputListingMock = jest.mocked(outputListing)
const devices = [{ deviceId: 'device-id' }] as Device[]
const listSpy = jest.spyOn(DevicesEndpoint.prototype, 'list').mockResolvedValue(devices)

it('virtual devices in all locations', async() => {
await expect(VirtualDevicesCommand.run([])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
})

it('use simple fields by default', async() => {
await expect(VirtualDevicesCommand.run([])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions)
.toEqual(['label', 'deviceId'])
})

it('include location and room with verbose flag', async() => {
await expect(VirtualDevicesCommand.run(['--verbose'])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions)
.toEqual(['label', 'deviceId', 'location', 'room'])
})

it('virtual devices uses location id in list', async() => {
outputListingMock.mockImplementationOnce(async (_command, _config, _idOrIndex, listFunction) => {
await listFunction()
})

await expect(VirtualDevicesCommand.run(['--location-id=location-id'])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
expect(listSpy).toHaveBeenCalledTimes(1)
expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({
type: DeviceIntegrationType.VIRTUAL,
locationId: ['location-id'],
}))
})

it('virtual devices uses installed app id in list', async() => {
outputListingMock.mockImplementationOnce(async (_command, _config, _idOrIndex, listFunction) => {
await listFunction()
})

await expect(VirtualDevicesCommand.run(['--installed-app-id=installed-app-id'])).resolves.not.toThrow()

expect(outputListingMock).toHaveBeenCalledTimes(1)
expect(listSpy).toHaveBeenCalledTimes(1)
expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({
type: DeviceIntegrationType.VIRTUAL,
installedAppId: 'installed-app-id',
}))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { inputAndOutputItem } from '@smartthings/cli-lib'
import {
VirtualDeviceStandardCreateRequest,
VirtualDevicesEndpoint,
} from '@smartthings/core-sdk'
import VirtualDeviceCreateStandardCommand from '../../../../commands/devices/virtual/create-standard'
import { chooseDeviceName, chooseDevicePrototype, chooseRoom } from '../../../../commands/devices/virtual'
import { chooseLocation } from '../../../../commands/locations'


jest.mock('../../../../commands/locations')
jest.mock('../../../../commands/devices/virtual')

describe('VirtualDeviceStandardCreateCommand', () => {
const mockInputAndOutputItem = jest.mocked(inputAndOutputItem)
const createSpy = jest.spyOn(VirtualDevicesEndpoint.prototype, 'createStandard').mockImplementation()

it('calls correct endpoint', async () => {
const createRequest: VirtualDeviceStandardCreateRequest = {
name: 'Device Name',
owner: {
ownerType: 'LOCATION',
ownerId: 'location-id',
},
prototype: 'VIRTUAL_SWITCH',
}

mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction) => {
await actionFunction(undefined, createRequest)
})

await expect(VirtualDeviceCreateStandardCommand.run([])).resolves.not.toThrow()

expect(createSpy).toBeCalledWith(createRequest)
})

it('overwrites name, location, and room from command line', async () => {
const createRequest: VirtualDeviceStandardCreateRequest = {
name: 'Device Name',
owner: {
ownerType: 'LOCATION',
ownerId: 'location-id',
},
prototype: 'VIRTUAL_SWITCH',
}

const expectedCreateRequest: VirtualDeviceStandardCreateRequest = {
name: 'NewDeviceName',
owner: {
ownerType: 'LOCATION',
ownerId: 'new-location-id',
},
roomId: 'new-room-id',
prototype: 'VIRTUAL_SWITCH',
}

mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction) => {
await actionFunction(undefined, createRequest)
})

await expect(VirtualDeviceCreateStandardCommand.run([
'--name=NewDeviceName',
'--location-id=new-location-id',
'--room-id=new-room-id',
])).resolves.not.toThrow()

expect(createSpy).toBeCalledWith(expectedCreateRequest)
})

it('command line flag input', async () => {
const mockChooseDeviceName = jest.mocked(chooseDeviceName)
const mockChooseRoom = jest.mocked(chooseRoom)
const mockChooseLocation = jest.mocked(chooseLocation)
const mockChooseDevicePrototype = jest.mocked(chooseDevicePrototype)

const expectedCreateRequest: VirtualDeviceStandardCreateRequest = {
name: 'DeviceName',
owner: {
ownerType: 'LOCATION',
ownerId: 'location-id',
},
roomId: 'room-id',
prototype: 'VIRTUAL_SWITCH',
}

mockChooseDeviceName.mockResolvedValueOnce('DeviceName')
mockChooseLocation.mockResolvedValueOnce('location-id')
mockChooseRoom.mockResolvedValueOnce('room-id')
mockChooseDevicePrototype.mockResolvedValueOnce('VIRTUAL_SWITCH')

mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction, inputProcessor) => {
const data = await inputProcessor.read()
await actionFunction(undefined, data)
})

await expect(VirtualDeviceCreateStandardCommand.run([
'--name=DeviceName',
'--location-id=location-id',
'--room-id=room-id',
'--prototype=VIRTUAL_SWITCH',
])).resolves.not.toThrow()

expect(mockChooseDeviceName).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'DeviceName')
expect(mockChooseLocation).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'location-id', true)
expect(mockChooseRoom).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'location-id', 'room-id', true)
expect(mockChooseDevicePrototype).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'VIRTUAL_SWITCH')
expect(createSpy).toBeCalledWith(expectedCreateRequest)
})
})
Loading