Skip to content

Commit cfebf99

Browse files
committed
refactor: move organization handling into APIOrganizationCommand class
1 parent 8fd3e98 commit cfebf99

File tree

7 files changed

+130
-74
lines changed

7 files changed

+130
-74
lines changed

.changeset/quick-brooms-develop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smartthings/cli-lib": patch
3+
"@smartthings/cli-testlib": patch
4+
---
5+
6+
refactor handling of headers on initialization

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/lib/src/__tests__/api-command.test.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -91,28 +91,6 @@ describe('api-command', () => {
9191
expect(configUsed?.headers).toContainEntry(['Accept-Language', 'es-US'])
9292
})
9393

94-
it('passes organization flag on to client', async () => {
95-
parseSpy.mockResolvedValueOnce({ args: {}, flags: { organization: 'organization-id-from-flag' } } as ParserOutputType)
96-
await apiCommand.init()
97-
98-
expect(stClientSpy).toHaveBeenCalledTimes(1)
99-
100-
const configUsed = stClientSpy.mock.calls[0][1]
101-
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-flag'])
102-
})
103-
104-
it('passes organization config on to client', async () => {
105-
const profile: Profile = { organization: 'organization-id-from-config' }
106-
loadConfigMock.mockResolvedValueOnce({ profile } as CLIConfig)
107-
108-
await apiCommand.init()
109-
110-
expect(stClientSpy).toHaveBeenCalledTimes(1)
111-
112-
const configUsed = stClientSpy.mock.calls[0][1]
113-
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-config'])
114-
})
115-
11694
it('returns oclif config User-Agent and default if undefined', () => {
11795
expect(apiCommand.userAgent).toBe('@smartthings/cli')
11896

@@ -146,19 +124,6 @@ describe('api-command', () => {
146124
expect(LoginAuthenticator).toBeCalledWith(expect.anything(), expect.anything(), expect.any(String))
147125
})
148126

149-
it('prefers organization flag over config', async () => {
150-
const profile: Profile = { organization: 'organization-id-from-config' }
151-
loadConfigMock.mockResolvedValueOnce({ profile } as CLIConfig)
152-
parseSpy.mockResolvedValueOnce({ args: {}, flags: { organization: 'organization-id-from-flag' } } as ParserOutputType)
153-
154-
await apiCommand.init()
155-
156-
expect(stClientSpy).toHaveBeenCalledTimes(1)
157-
158-
const configUsed = stClientSpy.mock.calls[0][1]
159-
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-flag'])
160-
})
161-
162127
describe('warningLogger', () => {
163128
it('uses string as-is', async () => {
164129
parseSpy.mockResolvedValueOnce({ args: {}, flags: { language: 'es-US' } } as ParserOutputType)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Config, Interfaces } from '@oclif/core'
2+
3+
import * as coreSDK from '@smartthings/core-sdk'
4+
5+
import { APIOrganizationCommand } from '../api-organization-command'
6+
import { CLIConfig, loadConfig, Profile } from '../cli-config'
7+
8+
9+
jest.mock('@smartthings/core-sdk')
10+
jest.mock('../cli-config')
11+
jest.mock('../login-authenticator')
12+
13+
describe('APIOrganizationCommand', () => {
14+
const stClientSpy = jest.spyOn(coreSDK, 'SmartThingsClient')
15+
16+
class TestCommand extends APIOrganizationCommand<typeof TestCommand.flags> {
17+
async run(): Promise<void> {
18+
// eslint-disable-line @typescript-eslint/no-empty-function
19+
}
20+
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
22+
async parse(options?: Interfaces.Input<any>, argv?: string[]): Promise<Interfaces.ParserOutput<any, any>> {
23+
return {
24+
flags: {},
25+
args: {},
26+
argv: [],
27+
raw: [],
28+
metadata: { flags: {} },
29+
}
30+
}
31+
}
32+
33+
const loadConfigMock = jest.mocked(loadConfig)
34+
const parseSpy = jest.spyOn(TestCommand.prototype, 'parse')
35+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36+
type ParserOutputType = Interfaces.ParserOutput<any, any>
37+
38+
let apiCommand: TestCommand
39+
40+
beforeEach(() => {
41+
apiCommand = new TestCommand([], {} as Config)
42+
apiCommand.warn = jest.fn()
43+
})
44+
45+
it('passes organization flag on to client', async () => {
46+
parseSpy.mockResolvedValueOnce({ args: {}, flags: { organization: 'organization-id-from-flag' } } as ParserOutputType)
47+
await apiCommand.init()
48+
49+
expect(stClientSpy).toHaveBeenCalledTimes(1)
50+
51+
const configUsed = stClientSpy.mock.calls[0][1]
52+
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-flag'])
53+
})
54+
55+
it('passes organization config on to client', async () => {
56+
const profile: Profile = { organization: 'organization-id-from-config' }
57+
loadConfigMock.mockResolvedValueOnce({ profile } as CLIConfig)
58+
59+
await apiCommand.init()
60+
61+
expect(stClientSpy).toHaveBeenCalledTimes(1)
62+
63+
const configUsed = stClientSpy.mock.calls[0][1]
64+
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-config'])
65+
})
66+
67+
it('prefers organization flag over config', async () => {
68+
const profile: Profile = { organization: 'organization-id-from-config' }
69+
loadConfigMock.mockResolvedValueOnce({ profile } as CLIConfig)
70+
parseSpy.mockResolvedValueOnce({ args: {}, flags: { organization: 'organization-id-from-flag' } } as ParserOutputType)
71+
72+
await apiCommand.init()
73+
74+
expect(stClientSpy).toHaveBeenCalledTimes(1)
75+
76+
const configUsed = stClientSpy.mock.calls[0][1]
77+
expect(configUsed?.headers).toContainEntry(['X-ST-Organization', 'organization-id-from-flag'])
78+
})
79+
})

packages/lib/src/api-command.ts

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
1+
import log4js from '@log4js-node/log4js-api'
12
import { Flags } from '@oclif/core'
23
import osLocale from 'os-locale'
4+
35
import { Authenticator, BearerTokenAuthenticator, HttpClientHeaders, SmartThingsClient, WarningFromHeader } from '@smartthings/core-sdk'
6+
47
import { ClientIdProvider, defaultClientIdProvider, LoginAuthenticator } from './login-authenticator'
58
import { SmartThingsCommand } from './smartthings-command'
6-
import log4js from '@log4js-node/log4js-api'
7-
import { APIOrganizationCommand } from './api-organization-command'
89

910

1011
const LANGUAGE_HEADER = 'Accept-Language'
11-
const ORGANIZATION_HEADER = 'X-ST-Organization'
12-
13-
/**
14-
* The command being parsed will not always have {@link APIOrganizationCommand.flags}.
15-
* Therefore, we make them all optional to be safely accessible in init below.
16-
*/
17-
type InputFlags = typeof APICommand.flags & Partial<typeof APIOrganizationCommand.flags>
1812

1913
/**
2014
* Base class for commands that need to use Rest API via the SmartThings Core SDK.
2115
*/
22-
export abstract class APICommand<T extends InputFlags> extends SmartThingsCommand<T> {
16+
export abstract class APICommand<T extends typeof APICommand.flags> extends SmartThingsCommand<T> {
2317
static flags = {
2418
...SmartThingsCommand.flags,
2519
token: Flags.string({
@@ -36,7 +30,6 @@ export abstract class APICommand<T extends InputFlags> extends SmartThingsComman
3630
protected token?: string
3731
private _authenticator!: Authenticator
3832
private _client!: SmartThingsClient
39-
private _headers!: HttpClientHeaders
4033

4134
get authenticator(): Authenticator {
4235
return this._authenticator
@@ -46,14 +39,25 @@ export abstract class APICommand<T extends InputFlags> extends SmartThingsComman
4639
return this._client
4740
}
4841

49-
protected get headers(): HttpClientHeaders {
50-
return this._headers
51-
}
52-
5342
get userAgent(): string {
5443
return this.config.userAgent ?? '@smartthings/cli'
5544
}
5645

46+
async initHeaders(): Promise<HttpClientHeaders> {
47+
// eslint-disable-next-line @typescript-eslint/naming-convention
48+
const headers: HttpClientHeaders = { 'User-Agent': this.userAgent }
49+
50+
if (this.flags.language) {
51+
if (this.flags.language !== 'NONE') {
52+
headers[LANGUAGE_HEADER] = this.flags.language
53+
}
54+
} else {
55+
headers[LANGUAGE_HEADER] = await osLocale()
56+
}
57+
58+
return headers
59+
}
60+
5761
async init(): Promise<void> {
5862
await super.init()
5963

@@ -77,25 +81,7 @@ export abstract class APICommand<T extends InputFlags> extends SmartThingsComman
7781

7882
const logger = log4js.getLogger('rest-client')
7983

80-
// eslint-disable-next-line @typescript-eslint/naming-convention
81-
this._headers = { 'User-Agent': this.userAgent }
82-
83-
if (this.flags.language) {
84-
if (this.flags.language !== 'NONE') {
85-
this._headers[LANGUAGE_HEADER] = this.flags.language
86-
}
87-
} else {
88-
this._headers[LANGUAGE_HEADER] = await osLocale()
89-
}
90-
91-
if (this.flags.organization) {
92-
this._headers[ORGANIZATION_HEADER] = this.flags.organization
93-
} else {
94-
const configOrganization = this.stringConfigValue('organization')
95-
if (configOrganization) {
96-
this._headers[ORGANIZATION_HEADER] = configOrganization
97-
}
98-
}
84+
const headers = await this.initHeaders()
9985

10086
this._authenticator = this.token
10187
? new BearerTokenAuthenticator(this.token)
@@ -109,6 +95,6 @@ export abstract class APICommand<T extends InputFlags> extends SmartThingsComman
10995
this.warn(message)
11096
}
11197
this._client = new SmartThingsClient(this._authenticator,
112-
{ urlProvider: this.clientIdProvider, logger, headers: this.headers, warningLogger })
98+
{ urlProvider: this.clientIdProvider, logger, headers, warningLogger })
11399
}
114100
}

packages/lib/src/api-organization-command.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { Flags } from '@oclif/core'
2+
3+
import { HttpClientHeaders } from '@smartthings/core-sdk'
4+
25
import { APICommand } from './api-command'
36

47

8+
const ORGANIZATION_HEADER = 'X-ST-Organization'
9+
510
/**
611
* Base class for commands that need to use Rest API commands via the
712
* SmartThings Core SDK and can act on the behalf of different organizations.
@@ -11,7 +16,22 @@ export abstract class APIOrganizationCommand<T extends typeof APIOrganizationCom
1116
...APICommand.flags,
1217
organization: Flags.string({
1318
char: 'O',
14-
description: 'The organization ID to use for this command',
19+
description: 'the organization ID to use for this command',
1520
}),
1621
}
22+
23+
async initHeaders(): Promise<HttpClientHeaders> {
24+
const headers = await super.initHeaders()
25+
26+
if (this.flags.organization) {
27+
headers[ORGANIZATION_HEADER] = this.flags.organization
28+
} else {
29+
const configOrganization = this.stringConfigValue('organization')
30+
if (configOrganization) {
31+
headers[ORGANIZATION_HEADER] = configOrganization
32+
}
33+
}
34+
35+
return headers
36+
}
1737
}

packages/testlib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"dependencies": {
3131
"@smartthings/cli-lib": "^1.0.0-beta.10",
32-
"@smartthings/core-sdk": "^5.0.0"
32+
"@smartthings/core-sdk": "^5.1.0"
3333
},
3434
"devDependencies": {
3535
"@types/jest": "^28.1.5",

0 commit comments

Comments
 (0)