From f56dfa9a195555626b9de054399f587ab342c4a7 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Thu, 10 Sep 2026 12:59:28 -0500 Subject: [PATCH] feat: update to use new smartapps endpoints --- .changeset/curvy-plants-end.md | 5 + .gitignore | 2 + README.md | 4 +- src/endpoint/installedapps.ts | 4 +- src/endpoint/{apps.ts => smartapps.ts} | 192 ++++++++++++------------- src/index.ts | 2 +- src/st-client.ts | 6 +- src/types.ts | 9 +- test/unit/apps.test.ts | 131 ----------------- test/unit/smartapps.test.ts | 175 ++++++++++++++++++++++ 10 files changed, 287 insertions(+), 243 deletions(-) create mode 100644 .changeset/curvy-plants-end.md rename src/endpoint/{apps.ts => smartapps.ts} (63%) delete mode 100644 test/unit/apps.test.ts create mode 100644 test/unit/smartapps.test.ts diff --git a/.changeset/curvy-plants-end.md b/.changeset/curvy-plants-end.md new file mode 100644 index 00000000..c542f914 --- /dev/null +++ b/.changeset/curvy-plants-end.md @@ -0,0 +1,5 @@ +--- +"@smartthings/core-sdk": major +--- + +Replace the /apps endpoints with the new /smartapps endpoints. diff --git a/.gitignore b/.gitignore index 21ae7ffb..c40a2459 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ dist .DS_Store .*.swp + +scratch.md diff --git a/README.md b/README.md index 0daf234a..0b222aa8 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,6 @@ and retry the original request. ### Endpoints -* apps -- A SmartApp can be an AWS lambda function or WebHook endpoint. Like to code interface [here](src/endpoint/apps.ts#L267), link to wiki page description [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/Apps) - * capabilities - Operations to read standard Capability definitions as well as create and modify custom Capabilities. Link to code interface [here](src/endpoint/capabilities.ts#L763), link to wiki page [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/Capabilities). * deviceProfiles - A Device Profile contains the Components, Capabilities, and metadata (ID, name, ownership, etc.) that define a SmartThings Device. Link to code interface [here](src/endpoint/deviceprofiles.ts#L93), link to wiki page [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/Device-Profiles) @@ -149,4 +147,6 @@ and retry the original request. * services - Operations to query for and subscribe to location service data, currently consisting of current weather conditions, weather forecast, and air quality data. Link to code interface [here](src/endpoint/services.ts#L499), link to wiki page description [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/Services) +* smartapps -- A SmartApp can be an AWS lambda function or WebHook endpoint. Like to code interface [here](src/endpoint/smartapps.ts#L267), link to wiki page description [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/SmartApps) + * subscriptions - Operations for subscribing to events, for use in SmartApps and API Access apps. Link to code interface [here](src/endpoint/subscriptions.ts#L213), link to wiki page description [here](https://github.com/SmartThingsCommunity/smartthings-core-sdk/wiki/Subscriptions) diff --git a/src/endpoint/installedapps.ts b/src/endpoint/installedapps.ts index 29b00a0a..d77c2d17 100644 --- a/src/endpoint/installedapps.ts +++ b/src/endpoint/installedapps.ts @@ -1,6 +1,6 @@ import { Endpoint } from '../endpoint' import { EndpointClient, EndpointClientConfig, HttpClientParams } from '../endpoint-client' -import { Count, Owner, PrincipalType, Status, SuccessStatusValue } from '../types' +import { Count, Owner, Status, SuccessStatusValue, ViewPrincipalType } from '../types' export interface StringConfig { @@ -183,7 +183,7 @@ export interface InstalledApp { /** * Denotes the principal type to be used with the app. Default is LOCATION. */ - principalType: PrincipalType + principalType: ViewPrincipalType /** * Inform the installation systems that the associated app can only be * installed once within a user's account. diff --git a/src/endpoint/apps.ts b/src/endpoint/smartapps.ts similarity index 63% rename from src/endpoint/apps.ts rename to src/endpoint/smartapps.ts index 4f5858d1..936be389 100644 --- a/src/endpoint/apps.ts +++ b/src/endpoint/smartapps.ts @@ -1,39 +1,31 @@ import { EndpointClient, EndpointClientConfig, HttpClientParams } from '../endpoint-client' import { Endpoint } from '../endpoint' -import { IconImage, Owner, PrincipalType, SuccessResponse } from '../types' +import { IconImage, Owner, PrincipalType, SuccessResponse, ViewPrincipalType } from '../types' -export enum AppType { - LAMBDA_SMART_APP = 'LAMBDA_SMART_APP', - WEBHOOK_SMART_APP = 'WEBHOOK_SMART_APP', - API_ONLY = 'API_ONLY' -} +// Pre-existing LAMBDA_SMART_APP and WEBHOOK_SMART_APP can be viewed but only API_ONLY can be created or edited. +export const smartAppTypeValues = ['API_ONLY'] as const +export const viewSmartAppTypeValues = [...smartAppTypeValues, 'LAMBDA_SMART_APP', 'WEBHOOK_SMART_APP'] as const +export type SmartAppType = typeof smartAppTypeValues[number] +export type ViewSmartAppType = typeof viewSmartAppTypeValues[number] -export enum AppClassification { - AUTOMATION = 'AUTOMATION', - SERVICE = 'SERVICE', - DEVICE = 'DEVICE', - CONNECTED_SERVICE = 'CONNECTED_SERVICE', -} +export const smartAppClassificationValues = ['AUTOMATION', 'SERVICE', 'DEVICE', 'CONNECTED_SERVICE'] as const +export type SmartAppClassification = typeof smartAppClassificationValues[number] -export enum AppTargetStatus { - PENDING = 'PENDING', - CONFIRMED = 'CONFIRMED', -} +export const smartAppTargetStatusValues = ['PENDING', 'CONFIRMED'] as const +export type SmartAppTargetStatus = typeof smartAppTargetStatusValues[number] -export enum SignatureType { - APP_RSA = 'APP_RSA', - ST_PADLOCK = 'ST_PADLOCK', -} +export const signatureTypeValues = ['APP_RSA', 'ST_PADLOCK'] as const +export type SignatureType = typeof signatureTypeValues[number] -export interface LambdaSmartApp { +export type LambdaSmartApp = { /** * A list of AWS ARNs referencing a Lambda function. */ functions: string[] } -export interface WebhookSmartApp { +export type WebhookSmartApp = { /** * A URL that should be invoked during execution. */ @@ -42,7 +34,7 @@ export interface WebhookSmartApp { /** * The registration status of a target url. */ - targetStatus?: AppTargetStatus + targetStatus?: SmartAppTargetStatus /** * The public half of an RSA key pair. Useful for verifying a Webhook @@ -59,16 +51,25 @@ export interface WebhookSmartApp { signatureType?: SignatureType } -export interface ApiOnlySubscription { +export type ApiOnlySubscription = { targetUrl: string - targetStatus: AppTargetStatus + targetStatus: SmartAppTargetStatus } -export interface ApiOnlyApp { +export type ApiOnlySmartApp = { subscription?: ApiOnlySubscription + + /** + * Link to the apps privacy policy. Url scheme must be https. + * + * This field is required for business plans. + * + * Must be <= 2048 + */ + privacyPolicyUrl?: string } -export interface ApiOnlyAppRequest { +export type ApiOnlySmartAppRequest = Omit & { /** * Optional target url to receive events. Url scheme must be https. * @@ -77,14 +78,14 @@ export interface ApiOnlyAppRequest { targetUrl?: string } -export interface AppUISettings { +export type SmartAppUISettings = { dashboardCardsEnabled: boolean preInstallDashboardCardsEnabled: boolean pluginId?: string pluginUri?: string } -export interface AppBase { +export type SmartAppBase = { /** * A user defined unique identifier for an app. It is alpha-numeric, may * contain dashes, underscores, periods, and be less then 250 characters @@ -95,7 +96,7 @@ export interface AppBase { /** * Denotes the type of app. */ - appType: AppType + appType: ViewSmartAppType /** * An App maybe associated to many classifications. A classification @@ -107,7 +108,7 @@ export interface AppBase { * DEVICE - Denotes an integration that should display under the "Device" tab in mobile clients. * CONNECTED_SERVICE - Denotes an integration that should display under the "Connected Services" menu in mobile clients. */ - classifications: AppClassification[] + classifications: SmartAppClassification[] /** * A default display name for an app. @@ -120,7 +121,9 @@ export interface AppBase { description: string } -export interface AppUpdateRequest extends AppBase { +export type SmartAppUpdateRequest = Omit & { + appType: SmartAppType + /** * Inform the installation systems that a particular app can only be * installed once within a user's account. @@ -132,32 +135,19 @@ export interface AppUpdateRequest extends AppBase { */ iconImage?: IconImage - /** - * Details related to a Lambda Smart App implementation. - * This model should only be specified for apps of type LAMBDA_SMART_APP. - */ - lambdaSmartApp?: LambdaSmartApp - - /** - * Details related to a Webhook Smart App implementation. - * This model should only be specified for apps of type WEBHOOK_SMART_APP. - */ - webhookSmartApp?: WebhookSmartApp - /** * Details related to an ApiOnly Smart App implementation. * This model should only be specified for apps of type API_ONLY. */ - apiOnly?: ApiOnlyAppRequest + apiOnly?: ApiOnlySmartAppRequest /** * A collection of settings to drive user interface in SmartThings clients. - * Currently, only applicable for LAMBDA_SMART_APP and WEBHOOK_SMART_APP app types. */ - ui?: AppUISettings + ui?: SmartAppUISettings } -export interface AppCreateRequest extends AppUpdateRequest { +export type SmartAppCreateRequest = SmartAppUpdateRequest & { /** * A globally unique, developer-defined identifier for an app. It is * alpha-numeric, may contain dashes, underscores, periods, and must @@ -174,10 +164,10 @@ export interface AppCreateRequest extends AppUpdateRequest { /** * App OAuth settings. */ - oauth?: Partial + oauth?: Partial } -export interface PagedApp extends AppBase { +export type PagedSmartApp = SmartAppBase & { /** * A globally unique identifier for an app. */ @@ -204,12 +194,11 @@ export interface PagedApp extends AppBase { lastUpdatedDate: string } -export interface AppResponse extends PagedApp { +export type SmartAppResponse = PagedSmartApp & { /** - * Denotes the principal type to be used with the app. - * Default is LOCATION. + * Denotes the principal type used with the app. */ - principalType: PrincipalType + principalType: ViewPrincipalType /** * Inform the installation systems that a particular app can only be @@ -225,17 +214,17 @@ export interface AppResponse extends PagedApp { lambdaSmartApp?: LambdaSmartApp webhookSmartApp?: WebhookSmartApp - apiOnly?: ApiOnlyApp - ui: AppUISettings + apiOnly?: ApiOnlySmartApp + ui: SmartAppUISettings } -export interface AppCreationResponse { - app: AppResponse +export type SmartAppCreationResponse = { + app: SmartAppResponse oauthClientId: string oauthClientSecret: string } -export interface GenerateAppOAuthRequest { +export type GenerateSmartAppOAuthRequest = { /** * A name given to the OAuth Client. */ @@ -248,44 +237,58 @@ export interface GenerateAppOAuthRequest { scope: string[] } -export interface AppOAuthRequest extends GenerateAppOAuthRequest { +export type SmartAppOAuthRequest = GenerateSmartAppOAuthRequest & { /** - * A list of redirect URIs. + * A list of redirect URIs. Maximum of 10 URIs. */ redirectUris: string[] + + /** + * A list of CORS domains. Maximum of 10 domains. + */ + corsDomains?: string[] } -export type AppOAuthResponse = AppOAuthRequest +export type SmartAppOAuthResponse = SmartAppOAuthRequest -export interface GenerateAppOAuthResponse { - oauthClientDetails: AppOAuthResponse +export type GenerateSmartAppOAuthResponse = { + oauthClientDetails: SmartAppOAuthResponse oauthClientId: string oauthClientSecret: string } -export interface AppSettingsRequest { +export type SmartAppSettingsRequest = { settings?: { [key: string]: string } } -export type AppSettingsResponse = Required +export type SmartAppSettingsResponse = Required -export interface AppListOptions { - appType?: AppType - classification?: AppClassification | AppClassification[] +export type SmartAppListOptions = { + /** + * account/organization id + * + * If not specified, Smart Apps for your default organization will be displayed. + */ + accountId?: string + appType?: ViewSmartAppType + classification?: SmartAppClassification | SmartAppClassification[] tag?: { [key: string]: string } } -export class AppsEndpoint extends Endpoint { +export class SmartAppsEndpoint extends Endpoint { constructor(config: EndpointClientConfig) { - super(new EndpointClient('apps', config)) + super(new EndpointClient('smartapps', config)) } /** * Returns a list of all apps belonging to the principal (i.e. the user) */ - public async list(options: AppListOptions = {}): Promise { + public async list(options: SmartAppListOptions = {}): Promise { const params: HttpClientParams = {} + if ('accountId' in options && options.accountId) { + params.accountId = options.accountId + } if ('appType' in options && options.appType) { params.appType = options.appType } @@ -297,38 +300,38 @@ export class AppsEndpoint extends Endpoint { params[`tag:${key}`] = options.tag[key] } } - return this.client.getPagedItems(undefined, params) + return this.client.getPagedItems(undefined, params) } /** * Returns a specific app + * * @param id either the appId UUID or the appName unique name */ - public get(id: string): Promise { + public get(id: string): Promise { return this.client.get(id) } /** - * Create a new app. For WEBHOOK_SMART_APPs the default SignatureType is ST_PADLOCK. + * Create a new app. + * * @param data the app definition */ - public create(data: AppCreateRequest): Promise { - // TODO -- use of query params might be temporary + public create(data: SmartAppCreateRequest, accountId?: string): Promise { const params: HttpClientParams = {} - if (data.webhookSmartApp) { - params.requireConfirmation = 'true' - params.signatureType = data.webhookSmartApp.signatureType || 'ST_PADLOCK' + if (accountId) { + params.accountId = accountId } - return this.client.post(undefined, data, params) } /** * Update an existing app + * * @param id either the appId UUID or the appName unique name * @param data the new app definition */ - public update(id: string, data: AppUpdateRequest): Promise { + public update(id: string, data: SmartAppUpdateRequest): Promise { return this.client.put(id, data) } @@ -336,7 +339,7 @@ export class AppsEndpoint extends Endpoint { * Get the settings of an app. Settings are string name/value pairs for optional use by the app developer. * @param id either the appId UUID or the appName unique name */ - public getSettings(id: string): Promise { + public getSettings(id: string): Promise { return this.client.get(`${id}/settings`) } @@ -345,28 +348,17 @@ export class AppsEndpoint extends Endpoint { * @param id either the appId UUID or the appName unique name * @param data the new app settings */ - public updateSettings(id: string, data: AppSettingsRequest): Promise { + public updateSettings(id: string, data: SmartAppSettingsRequest): Promise { return this.client.put(`${id}/settings`, data) } /** - * Update the signature type of an app. The signature type determines what mechanism is used to verify - * the identity of endpoint apps - * @param id either the appId UUID or the appName unique name - * @param signatureType the new signature type - */ - public async updateSignatureType(id: string, signatureType: SignatureType): SuccessResponse { - await this.client.put(`${id}/signature-type`, { signatureType }) - return Promise.resolve() - } - - /** - * Pings the targetUrl of the app to verify its existence. Endpoint apps and API Access apps must be registed + * Pings the targetUrl of the app to verify its existence. API Access apps must be registered * in order to receive events from SmartThings. * @param id either the appId UUID or the appName unique name */ public async register(id: string): SuccessResponse { - await this.client.put(`${id}/register`) + await this.client.put(`${id}/register`, {}) return Promise.resolve() } @@ -374,7 +366,7 @@ export class AppsEndpoint extends Endpoint { * Returns the OAuth information for this app, including the name, scopes, and redirect URLs, if any * @param id either the appId UUID or the appName unique name */ - public getOauth(id: string): Promise { + public getOauth(id: string): Promise { return this.client.get(`${id}/oauth`) } @@ -384,7 +376,7 @@ export class AppsEndpoint extends Endpoint { * @param id either the appId UUID or the appName unique name * @param data new OAuth definition */ - public updateOauth(id: string, data: AppOAuthRequest): Promise { + public updateOauth(id: string, data: SmartAppOAuthRequest): Promise { return this.client.put(`${id}/oauth`, data) } @@ -394,7 +386,7 @@ export class AppsEndpoint extends Endpoint { * @param id either the appId UUID or the appName unique name * @param data new OAuth definition */ - public regenerateOauth(id: string, data: GenerateAppOAuthRequest): Promise { + public regenerateOauth(id: string, data: GenerateSmartAppOAuthRequest): Promise { return this.client.post(`${id}/oauth/generate`, data) } diff --git a/src/index.ts b/src/index.ts index fa7a6787..e7ab4637 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ export * from './types' export * from './endpoint' export * from './pagination' -export * from './endpoint/apps' +export * from './endpoint/smartapps' export * from './endpoint/capabilities' export * from './endpoint/channels' export * from './endpoint/devicepreferences' diff --git a/src/st-client.ts b/src/st-client.ts index fada99e0..1a5453c8 100644 --- a/src/st-client.ts +++ b/src/st-client.ts @@ -2,7 +2,6 @@ import axios, { AxiosRequestConfig } from 'axios' import { Authenticator } from './authenticator' import { RESTClient, RESTClientConfig } from './rest-client' -import { AppsEndpoint } from './endpoint/apps' import { CapabilitiesEndpoint } from './endpoint/capabilities' import { DevicePreferencesEndpoint } from './endpoint/devicepreferences' import { DeviceProfilesEndpoint } from './endpoint/deviceprofiles' @@ -25,12 +24,12 @@ import { SubscriptionsEndpoint } from './endpoint/subscriptions' import { SchedulesEndpoint } from './endpoint/schedules' import { SchemaEndpoint } from './endpoint/schema' import { ServicesEndpoint } from './endpoint/services' +import { SmartAppsEndpoint } from './endpoint/smartapps' import { VirtualDevicesEndpoint } from './endpoint/virtualdevices' import { SmartThingsURLProvider, globalSmartThingsURLProvider, HttpClientHeaders } from './endpoint-client' export class SmartThingsClient extends RESTClient { - public readonly apps: AppsEndpoint public readonly capabilities: CapabilitiesEndpoint public readonly channels: ChannelsEndpoint public readonly devicePreferences: DevicePreferencesEndpoint @@ -53,12 +52,12 @@ export class SmartThingsClient extends RESTClient { public readonly schedules: SchedulesEndpoint public readonly schema: SchemaEndpoint public readonly services: ServicesEndpoint + public readonly smartapps: SmartAppsEndpoint public readonly virtualDevices: VirtualDevicesEndpoint constructor(authenticator: Authenticator, config?: RESTClientConfig) { super(authenticator, config) - this.apps = new AppsEndpoint(this.config) this.capabilities = new CapabilitiesEndpoint(this.config) this.channels = new ChannelsEndpoint(this.config) this.devicePreferences = new DevicePreferencesEndpoint(this.config) @@ -81,6 +80,7 @@ export class SmartThingsClient extends RESTClient { this.schedules = new SchedulesEndpoint(this.config) this.schema = new SchemaEndpoint(this.config) this.services = new ServicesEndpoint(this.config) + this.smartapps = new SmartAppsEndpoint(this.config) this.virtualDevices = new VirtualDevicesEndpoint(this.config) } diff --git a/src/types.ts b/src/types.ts index a3d00bdd..0c62f26e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,10 +28,11 @@ export interface Owner { ownerId: string } -export enum PrincipalType { - LOCATION = 'LOCATION', - USER_LEVEL = 'USER_LEVEL', -} +// New SmartApps can only be `LOCATION` but pre-existing SmartApps might have other values. +export const principalTypeValues = ['LOCATION'] as const +export type PrincipalType = typeof principalTypeValues[number] +export const viewPrincipalTypeValues = [...principalTypeValues, 'USER_LEVEL', 'ACCOUNT_LEVEL'] as const +export type ViewPrincipalType = typeof viewPrincipalTypeValues[number] export interface Status { status: string diff --git a/test/unit/apps.test.ts b/test/unit/apps.test.ts deleted file mode 100644 index 59ff5c99..00000000 --- a/test/unit/apps.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { NoOpAuthenticator } from '../../src/authenticator' -import { EndpointClient } from '../../src/endpoint-client' -import { AppClassification, AppCreateRequest, AppCreationResponse, AppOAuthRequest, AppOAuthResponse, AppResponse, AppsEndpoint, AppType, GenerateAppOAuthRequest, GenerateAppOAuthResponse, PagedApp, SignatureType } from '../../src/endpoint/apps' - - -const MOCK_APP_LIST = [{ appId: 'appId' }] as PagedApp[] -const MOCK_APP = { appId: 'appId', appType: AppType.WEBHOOK_SMART_APP } as AppResponse -const MOCK_APP_CREATE = { app: {} } as AppCreationResponse -const MOCK_APP_OAUTH = { clientName: 'clientName' } as AppOAuthResponse -const MOCK_APP_OAUTH_GENERATE = { oauthClientId: 'oauthClientId' } as GenerateAppOAuthResponse - -describe('AppsEndpoint', () => { - const authenticator = new NoOpAuthenticator() - const apps = new AppsEndpoint({ authenticator, urlProvider: { baseURL: 'https://example.com/baseURL' } }) - - const getSpy = jest.spyOn(EndpointClient.prototype, 'get') - const getPagedItemsSpy = jest.spyOn(EndpointClient.prototype, 'getPagedItems') - const postSpy = jest.spyOn(EndpointClient.prototype, 'post') - const putSpy = jest.spyOn(EndpointClient.prototype, 'put') - const deleteSpy = jest.spyOn(EndpointClient.prototype, 'delete') - - afterEach(() => { - jest.clearAllMocks() - }) - - test('List', async () => { - getPagedItemsSpy.mockResolvedValueOnce(MOCK_APP_LIST) - const response = await apps.list() - - expect(getPagedItemsSpy).toBeCalledWith(undefined, {}) - expect(response).toStrictEqual(MOCK_APP_LIST) - }) - - test('List Automations', async () => { - getPagedItemsSpy.mockResolvedValueOnce(MOCK_APP_LIST) - const response = await apps.list({ classification: AppClassification.AUTOMATION }) - - expect(getPagedItemsSpy).toBeCalledWith(undefined, { classification: 'AUTOMATION' }) - expect(response).toStrictEqual(MOCK_APP_LIST) - }) - - test('List Webhooks', async () => { - getPagedItemsSpy.mockResolvedValueOnce(MOCK_APP_LIST) - const response = await apps.list({ appType: AppType.WEBHOOK_SMART_APP }) - - expect(getPagedItemsSpy).toBeCalledWith(undefined, { appType: 'WEBHOOK_SMART_APP' }) - expect(response).toStrictEqual(MOCK_APP_LIST) - }) - - test('List Lambda Automations', async () => { - getPagedItemsSpy.mockResolvedValueOnce(MOCK_APP_LIST) - const response = await apps.list({ appType: AppType.LAMBDA_SMART_APP, classification: AppClassification.AUTOMATION }) - - expect(getPagedItemsSpy).toBeCalledWith(undefined, { appType: 'LAMBDA_SMART_APP', classification: 'AUTOMATION' }) - expect(response).toStrictEqual(MOCK_APP_LIST) - - }) - - test('List Tags', async () => { - getPagedItemsSpy.mockResolvedValueOnce(MOCK_APP_LIST) - const response = await apps.list({ tag: { industry: 'energy', region: 'North America' } }) - - expect(getPagedItemsSpy).toBeCalledWith(undefined, { 'tag:industry': 'energy', 'tag:region': 'North America' }) - expect(response).toStrictEqual(MOCK_APP_LIST) - }) - - test('Get', async () => { - getSpy.mockResolvedValueOnce(MOCK_APP) - const response = await apps.get('appName') - - expect(getSpy).toBeCalledWith('appName') - expect(response).toStrictEqual(MOCK_APP) - }) - - test('Create', async () => { - postSpy.mockResolvedValueOnce(MOCK_APP_CREATE) - const createRequest = { appName: 'app' } as AppCreateRequest - const response = await apps.create(createRequest) - - expect(postSpy).toBeCalledWith(undefined, createRequest, {}) - expect(response).toStrictEqual(MOCK_APP_CREATE) - }) - - test('Update signature type', async () => { - putSpy.mockResolvedValueOnce({}) - - await expect(apps.updateSignatureType('appId', SignatureType.ST_PADLOCK)).resolves.toBeUndefined() - expect(putSpy).toBeCalledWith('appId/signature-type', { signatureType: 'ST_PADLOCK' }) - }) - - test('Register', async () => { - putSpy.mockResolvedValueOnce({}) - - await expect(apps.register('appId')).resolves.toBeUndefined() - expect(putSpy).toBeCalledWith('appId/register') - }) - - test('Update OAuth', async () => { - putSpy.mockResolvedValueOnce(MOCK_APP_OAUTH) - const oauthRequest = { redirectUris: [] } as unknown as AppOAuthRequest - - const response = await apps.updateOauth('appId', oauthRequest) - - expect(putSpy).toBeCalledWith('appId/oauth', oauthRequest) - expect(response).toStrictEqual(MOCK_APP_OAUTH) - }) - - test('Regenerate OAuth', async () => { - postSpy.mockResolvedValueOnce(MOCK_APP_OAUTH_GENERATE) - const regenerateRequest = { clientName: 'clientName' } as GenerateAppOAuthRequest - - const response = await apps.regenerateOauth('appId', regenerateRequest) - - expect(postSpy).toBeCalledWith('appId/oauth/generate', regenerateRequest) - expect(response).toStrictEqual(MOCK_APP_OAUTH_GENERATE) - }) - - test('Delete', async () => { - deleteSpy.mockResolvedValueOnce({}) - - await expect(apps.delete('appId')).resolves.toBeUndefined() - expect(deleteSpy).toBeCalledWith('appId') - }) - - test('Delete Error', async () => { - const error = new Error('failed') - deleteSpy.mockRejectedValueOnce(error) - - await expect(apps.delete('appId')).rejects.toThrow(error) - }) -}) diff --git a/test/unit/smartapps.test.ts b/test/unit/smartapps.test.ts new file mode 100644 index 00000000..c48847d1 --- /dev/null +++ b/test/unit/smartapps.test.ts @@ -0,0 +1,175 @@ +import { NoOpAuthenticator } from '../../src/authenticator' +import { EndpointClient } from '../../src/endpoint-client' +import { + GenerateSmartAppOAuthRequest, + GenerateSmartAppOAuthResponse, + PagedSmartApp, + SmartAppCreateRequest, + SmartAppCreationResponse, + SmartAppOAuthRequest, + SmartAppOAuthResponse, + SmartAppResponse, + SmartAppsEndpoint, +} from '../../src/endpoint/smartapps' + + +const mockAppList = [{ appId: 'appId' }] as PagedSmartApp[] +const mockApp = { appId: 'appId', appType: 'API_ONLY' } as SmartAppResponse +const mockAppCreate = { app: {} } as SmartAppCreationResponse +const mockAppOAuth = { clientName: 'clientName' } as SmartAppOAuthResponse +const mockAppOAuthGenerate = { oauthClientId: 'oauthClientId' } as GenerateSmartAppOAuthResponse + +describe('SmartAppsEndpoint', () => { + const authenticator = new NoOpAuthenticator() + const apps = new SmartAppsEndpoint({ authenticator, urlProvider: { baseURL: 'https://example.com/baseURL' } }) + + const getSpy = jest.spyOn(EndpointClient.prototype, 'get') + const getPagedItemsSpy = jest.spyOn(EndpointClient.prototype, 'getPagedItems') + const postSpy = jest.spyOn(EndpointClient.prototype, 'post') + const putSpy = jest.spyOn(EndpointClient.prototype, 'put') + const deleteSpy = jest.spyOn(EndpointClient.prototype, 'delete') + + afterEach(() => { + jest.clearAllMocks() + }) + + test('List', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list() + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, {}) + expect(response).toStrictEqual(mockAppList) + }) + + test('List Automations', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ classification: 'AUTOMATION' }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { classification: 'AUTOMATION' }) + expect(response).toStrictEqual(mockAppList) + }) + + test('List Webhooks', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ appType: 'WEBHOOK_SMART_APP' }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { appType: 'WEBHOOK_SMART_APP' }) + expect(response).toStrictEqual(mockAppList) + }) + + test('List Lambda Automations', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ appType: 'LAMBDA_SMART_APP', classification: 'AUTOMATION' }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { appType: 'LAMBDA_SMART_APP', classification: 'AUTOMATION' }) + expect(response).toStrictEqual(mockAppList) + + }) + + test('List with accountId', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ accountId: 'accountId' }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { accountId: 'accountId' }) + expect(response).toStrictEqual(mockAppList) + }) + + test('List with accountId combined with other options', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ + accountId: 'accountId', + appType: 'API_ONLY', + classification: 'SERVICE', + }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { + accountId: 'accountId', + appType: 'API_ONLY', + classification: 'SERVICE', + }) + expect(response).toStrictEqual(mockAppList) + }) + + test.each([undefined, ''])('List omits falsy accountId %p', async accountId => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ accountId }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, {}) + expect(response).toStrictEqual(mockAppList) + }) + + test('List Tags', async () => { + getPagedItemsSpy.mockResolvedValueOnce(mockAppList) + const response = await apps.list({ tag: { industry: 'energy', region: 'North America' } }) + + expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { 'tag:industry': 'energy', 'tag:region': 'North America' }) + expect(response).toStrictEqual(mockAppList) + }) + + test('Get', async () => { + getSpy.mockResolvedValueOnce(mockApp) + const response = await apps.get('appName') + + expect(getSpy).toHaveBeenCalledWith('appName') + expect(response).toStrictEqual(mockApp) + }) + + test('Create passes accountId as a query parameter', async () => { + postSpy.mockResolvedValueOnce(mockAppCreate) + const createRequest = { appName: 'app' } as SmartAppCreateRequest + const response = await apps.create(createRequest, 'accountId') + + expect(postSpy).toHaveBeenCalledWith(undefined, createRequest, { accountId: 'accountId' }) + expect(response).toStrictEqual(mockAppCreate) + }) + + test('Create passes accountId in the body through unchanged', async () => { + postSpy.mockResolvedValueOnce(mockAppCreate) + const createRequest = { appName: 'app' } as SmartAppCreateRequest + const response = await apps.create(createRequest, 'paramAccountId') + + expect(postSpy).toHaveBeenCalledWith(undefined, createRequest, { accountId: 'paramAccountId' }) + expect(response).toStrictEqual(mockAppCreate) + }) + + test('Register', async () => { + putSpy.mockResolvedValueOnce({}) + + await expect(apps.register('appId')).resolves.toBeUndefined() + expect(putSpy).toHaveBeenCalledWith('appId/register', {}) + }) + + test('Update OAuth', async () => { + putSpy.mockResolvedValueOnce(mockAppOAuth) + const oauthRequest = { redirectUris: [] } as unknown as SmartAppOAuthRequest + + const response = await apps.updateOauth('appId', oauthRequest) + + expect(putSpy).toHaveBeenCalledWith('appId/oauth', oauthRequest) + expect(response).toStrictEqual(mockAppOAuth) + }) + + test('Regenerate OAuth', async () => { + postSpy.mockResolvedValueOnce(mockAppOAuthGenerate) + const regenerateRequest = { clientName: 'clientName' } as GenerateSmartAppOAuthRequest + + const response = await apps.regenerateOauth('appId', regenerateRequest) + + expect(postSpy).toHaveBeenCalledWith('appId/oauth/generate', regenerateRequest) + expect(response).toStrictEqual(mockAppOAuthGenerate) + }) + + test('Delete', async () => { + deleteSpy.mockResolvedValueOnce({}) + + await expect(apps.delete('appId')).resolves.toBeUndefined() + expect(deleteSpy).toHaveBeenCalledWith('appId') + }) + + test('Delete Error', async () => { + const error = new Error('failed') + deleteSpy.mockRejectedValueOnce(error) + + await expect(apps.delete('appId')).rejects.toThrow(error) + }) +})