diff --git a/config/CLIConfiguration.ts b/config/CLIConfiguration.ts deleted file mode 100644 index 75c331dd..00000000 --- a/config/CLIConfiguration.ts +++ /dev/null @@ -1,693 +0,0 @@ -import fs from 'fs'; -import findup from 'findup-sync'; -import { getCwd } from '../lib/path'; -import { logger } from '../lib/logger'; -import { loadConfigFromEnvironment } from './environment'; -import { getValidEnv } from '../lib/environment'; -import { - loadConfigFromFile, - writeConfigToFile, - configFileExists, - configFileIsBlank, - deleteConfigFile, -} from './configFile'; -import { commaSeparatedValues } from '../lib/text'; -import { ENVIRONMENTS } from '../constants/environments'; -import { API_KEY_AUTH_METHOD } from '../constants/auth'; -import { - HUBSPOT_ACCOUNT_TYPES, - MIN_HTTP_TIMEOUT, - DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME, - DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID, - DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND, -} from '../constants/config'; -import { CMS_PUBLISH_MODE } from '../constants/files'; -import { CLIConfig_NEW, Environment } from '../types/Config'; -import { - CLIAccount_NEW, - OAuthAccount_NEW, - FlatAccountFields_NEW, - AccountType, -} from '../types/Accounts'; -import { CLIOptions } from '../types/CLIOptions'; -import { i18n } from '../utils/lang'; -import { CmsPublishMode } from '../types/Files'; - -const i18nKey = 'config.cliConfiguration'; - -class _CLIConfiguration { - options: CLIOptions; - useEnvConfig: boolean; - config: CLIConfig_NEW | null; - active: boolean; - - constructor() { - this.options = {}; - this.useEnvConfig = false; - this.config = null; - this.active = false; - } - - setActive(isActive: boolean): void { - this.active = isActive; - } - - isActive(): boolean { - return this.active; - } - - init(options: CLIOptions = {}): CLIConfig_NEW | null { - this.options = options; - this.load(); - this.setActive(true); - return this.config; - } - - load(): CLIConfig_NEW | null { - if (this.options.useEnv) { - const configFromEnv = loadConfigFromEnvironment(); - if (configFromEnv) { - logger.debug( - i18n(`${i18nKey}.load.configFromEnv`, { - accountId: configFromEnv.accounts[0].accountId, - }) - ); - this.useEnvConfig = true; - this.config = this.handleLegacyCmsPublishMode(configFromEnv); - } - } else { - const configFromFile = loadConfigFromFile(); - logger.debug(i18n(`${i18nKey}.load.configFromFile`)); - - if (!configFromFile) { - logger.debug(i18n(`${i18nKey}.load.empty`)); - this.config = { accounts: [] }; - } - this.useEnvConfig = false; - this.config = this.handleLegacyCmsPublishMode(configFromFile); - } - - return this.config; - } - - configIsEmpty(): boolean { - if (!configFileExists() || configFileIsBlank()) { - return true; - } else { - this.load(); - if ( - !!this.config && - Object.keys(this.config).length === 1 && - !!this.config.accounts - ) { - return true; - } - } - return false; - } - - delete(): void { - if (!this.useEnvConfig && this.configIsEmpty()) { - deleteConfigFile(); - this.config = null; - } - } - - write(updatedConfig?: CLIConfig_NEW): CLIConfig_NEW | null { - if (!this.useEnvConfig) { - if (updatedConfig) { - this.config = updatedConfig; - } - if (this.config) { - writeConfigToFile(this.config); - } - } - return this.config; - } - - validate(): boolean { - if (!this.config) { - logger.log(i18n(`${i18nKey}.validate.noConfig`)); - return false; - } - if (!Array.isArray(this.config.accounts)) { - logger.log(i18n(`${i18nKey}.validate.noConfigAccounts`)); - return false; - } - - const accountIdsMap: { [key: number]: boolean } = {}; - const accountNamesMap: { [key: string]: boolean } = {}; - - return this.config.accounts.every(accountConfig => { - if (!accountConfig) { - logger.log(i18n(`${i18nKey}.validate.emptyAccountConfig`)); - return false; - } - if (!accountConfig.accountId) { - logger.log(i18n(`${i18nKey}.validate.noAccountId`)); - return false; - } - if (accountIdsMap[accountConfig.accountId]) { - logger.log( - i18n(`${i18nKey}.validate.duplicateAccountIds`, { - accountId: accountConfig.accountId, - }) - ); - return false; - } - if (accountConfig.name) { - if (accountNamesMap[accountConfig.name.toLowerCase()]) { - logger.log( - i18n(`${i18nKey}.validate.duplicateAccountNames`, { - accountName: accountConfig.name, - }) - ); - return false; - } - if (/\s+/.test(accountConfig.name)) { - logger.log( - i18n(`${i18nKey}.validate.nameContainsSpaces`, { - accountName: accountConfig.name, - }) - ); - return false; - } - accountNamesMap[accountConfig.name] = true; - } - if (!accountConfig.accountType) { - this.addOrUpdateAccount({ - ...accountConfig, - accountId: accountConfig.accountId, - accountType: this.getAccountType( - undefined, - accountConfig.sandboxAccountType - ), - }); - } - - accountIdsMap[accountConfig.accountId] = true; - return true; - }); - } - - getAccount(nameOrId: string | number | undefined): CLIAccount_NEW | null { - let name: string | null = null; - let accountId: number | null = null; - - if (!this.config) { - return null; - } - - const nameOrIdToCheck = nameOrId ? nameOrId : this.getDefaultAccount(); - - if (!nameOrIdToCheck) { - return null; - } - - if (typeof nameOrIdToCheck === 'number') { - accountId = nameOrIdToCheck; - } else if (/^\d+$/.test(nameOrIdToCheck)) { - accountId = parseInt(nameOrIdToCheck, 10); - } else { - name = nameOrIdToCheck; - } - - if (name) { - return this.config.accounts.find(a => a.name === name) || null; - } else if (accountId) { - return this.config.accounts.find(a => accountId === a.accountId) || null; - } - - return null; - } - - isConfigFlagEnabled( - flag: keyof CLIConfig_NEW, - defaultValue = false - ): boolean { - if (this.config && typeof this.config[flag] !== 'undefined') { - return Boolean(this.config[flag]); - } - return defaultValue; - } - - getAccountId(nameOrId?: string | number): number | null { - const account = this.getAccount(nameOrId); - return account ? account.accountId : null; - } - - getDefaultAccount(): string | number | null { - return this.getCWDAccountOverride() || this.config?.defaultAccount || null; - } - - getDefaultAccountOverrideFilePath(): string | null { - return findup([DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME], { - cwd: getCwd(), - }); - } - - getCWDAccountOverride(): string | number | null { - const defaultOverrideFile = this.getDefaultAccountOverrideFilePath(); - if (!defaultOverrideFile) { - return null; - } - - let source: string; - try { - source = fs.readFileSync(defaultOverrideFile, 'utf8'); - } catch (e) { - if (e instanceof Error) { - logger.error( - i18n(`${i18nKey}.getCWDAccountOverride.readFileError`, { - error: e.message, - }) - ); - } - return null; - } - - const accountId = Number(source); - - if (isNaN(accountId)) { - throw new Error( - i18n(`${i18nKey}.getCWDAccountOverride.errorHeader`, { - hsAccountFile: defaultOverrideFile, - }), - { - cause: DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID, - } - ); - } - - const account = this.config?.accounts?.find( - account => account.accountId === accountId - ); - if (!account) { - throw new Error( - i18n(`${i18nKey}.getCWDAccountOverride.errorHeader`, { - hsAccountFile: defaultOverrideFile, - }), - { - cause: DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND, - } - ); - } - - return account.name || account.accountId; - } - - getAccountIndex(accountId: number): number { - return this.config - ? this.config.accounts.findIndex( - account => account.accountId === accountId - ) - : -1; - } - - getConfigForAccount(accountId?: number): CLIAccount_NEW | null { - if (this.config) { - return ( - this.config.accounts.find(account => account.accountId === accountId) || - null - ); - } - return null; - } - - getConfigAccounts(): Array | null { - if (this.config) { - return this.config.accounts || null; - } - return null; - } - - isAccountInConfig(nameOrId: string | number): boolean { - if (typeof nameOrId === 'string') { - return ( - !!this.config && - this.config.accounts && - !!this.getAccountId(nameOrId.toLowerCase()) - ); - } - return ( - !!this.config && this.config.accounts && !!this.getAccountId(nameOrId) - ); - } - - getAndLoadConfigIfNeeded(options?: CLIOptions): CLIConfig_NEW { - if (!this.config) { - this.init(options); - } - return this.config!; - } - - getEnv(nameOrId?: string | number): Environment { - const accountConfig = this.getAccount(nameOrId); - - if (accountConfig && accountConfig.accountId && accountConfig.env) { - return accountConfig.env; - } - if (this.config && this.config.env) { - return this.config.env; - } - return ENVIRONMENTS.PROD; - } - - // Deprecating sandboxAccountType in favor of accountType - getAccountType( - accountType?: AccountType | null, - sandboxAccountType?: string | null - ): AccountType { - if (accountType) { - return accountType; - } - if (typeof sandboxAccountType === 'string') { - if (sandboxAccountType.toUpperCase() === 'DEVELOPER') { - return HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX; - } - if (sandboxAccountType.toUpperCase() === 'STANDARD') { - return HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX; - } - } - return HUBSPOT_ACCOUNT_TYPES.STANDARD; - } - - /* - * Config Update Utils - */ - - /** - * @throws {Error} - */ - addOrUpdateAccount( - updatedAccountFields: Partial, - writeUpdate = true - ): FlatAccountFields_NEW | null { - const { - accountId, - accountType, - apiKey, - authType, - clientId, - clientSecret, - defaultCmsPublishMode, - env, - name, - parentAccountId, - personalAccessKey, - sandboxAccountType, - scopes, - tokenInfo, - } = updatedAccountFields; - - if (!accountId) { - throw new Error( - i18n(`${i18nKey}.updateAccount.errors.accountIdRequired`) - ); - } - if (!this.config) { - logger.debug(i18n(`${i18nKey}.updateAccount.noConfigToUpdate`)); - return null; - } - - // Check whether the account is already listed in the config.yml file. - const currentAccountConfig = this.getAccount(accountId); - - // For accounts that are already in the config.yml file, sets the auth property. - let auth: OAuthAccount_NEW['auth'] = - (currentAccountConfig && currentAccountConfig.auth) || {}; - // For accounts not already in the config.yml file, sets the auth property. - if (clientId || clientSecret || scopes || tokenInfo) { - auth = { - ...(currentAccountConfig ? currentAccountConfig.auth : {}), - clientId, - clientSecret, - scopes, - tokenInfo, - }; - } - - const nextAccountConfig: Partial = { - ...(currentAccountConfig ? currentAccountConfig : {}), - }; - - // Allow everything except for 'undefined' values to override the existing values - function safelyApplyUpdates( - fieldName: T, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - newValue: FlatAccountFields_NEW[T] - ) { - if (typeof newValue !== 'undefined') { - nextAccountConfig[fieldName] = newValue; - } - } - - const updatedEnv = getValidEnv( - env || (currentAccountConfig && currentAccountConfig.env) - ); - const updatedDefaultCmsPublishMode: CmsPublishMode | undefined = - defaultCmsPublishMode && - (defaultCmsPublishMode.toLowerCase() as CmsPublishMode); - const updatedAccountType = - accountType || (currentAccountConfig && currentAccountConfig.accountType); - - safelyApplyUpdates('name', name); - safelyApplyUpdates('env', updatedEnv); - safelyApplyUpdates('accountId', accountId); - safelyApplyUpdates('authType', authType); - safelyApplyUpdates('auth', auth); - if (nextAccountConfig.authType === API_KEY_AUTH_METHOD.value) { - safelyApplyUpdates('apiKey', apiKey); - } - if (typeof updatedDefaultCmsPublishMode !== 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - safelyApplyUpdates( - 'defaultCmsPublishMode', - CMS_PUBLISH_MODE[updatedDefaultCmsPublishMode] - ); - } - safelyApplyUpdates('personalAccessKey', personalAccessKey); - - // Deprecating sandboxAccountType in favor of the more generic accountType - safelyApplyUpdates('sandboxAccountType', sandboxAccountType); - safelyApplyUpdates( - 'accountType', - this.getAccountType(updatedAccountType, sandboxAccountType) - ); - - safelyApplyUpdates('parentAccountId', parentAccountId); - - const completedAccountConfig = nextAccountConfig as FlatAccountFields_NEW; - if (!Object.hasOwn(this.config, 'accounts')) { - this.config.accounts = []; - } - if (currentAccountConfig) { - logger.debug( - i18n(`${i18nKey}.updateAccount.updating`, { - accountId, - }) - ); - const index = this.getAccountIndex(accountId); - if (index < 0) { - this.config.accounts.push(completedAccountConfig); - } else { - this.config.accounts[index] = completedAccountConfig; - } - logger.debug( - i18n(`${i18nKey}.updateAccount.addingConfigEntry`, { - accountId, - }) - ); - } else { - this.config.accounts.push(completedAccountConfig); - } - - if (writeUpdate) { - this.write(); - } - - return completedAccountConfig; - } - - /** - * @throws {Error} - */ - updateDefaultAccount(defaultAccount: string | number): CLIConfig_NEW | null { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - if ( - !defaultAccount || - (typeof defaultAccount !== 'number' && typeof defaultAccount !== 'string') - ) { - throw new Error( - i18n(`${i18nKey}.updateDefaultAccount.errors.invalidInput`) - ); - } - - this.config.defaultAccount = defaultAccount; - return this.write(); - } - - /** - * @throws {Error} - */ - renameAccount(currentName: string, newName: string): void { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - const accountId = this.getAccountId(currentName); - let accountConfigToRename: CLIAccount_NEW | null = null; - - if (accountId) { - accountConfigToRename = this.getAccount(accountId); - } - - if (!accountConfigToRename) { - throw new Error( - i18n(`${i18nKey}.renameAccount.errors.invalidName`, { - currentName, - }) - ); - } - - if (accountId) { - this.addOrUpdateAccount({ - accountId, - name: newName, - env: this.getEnv(), - accountType: accountConfigToRename.accountType, - }); - } - - if (accountConfigToRename.name === this.getDefaultAccount()) { - this.updateDefaultAccount(newName); - } - } - - /** - * @throws {Error} - * TODO: this does not account for the special handling of sandbox account deletes - */ - removeAccountFromConfig(nameOrId: string | number): boolean { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - const accountId = this.getAccountId(nameOrId); - - if (!accountId) { - throw new Error( - i18n(`${i18nKey}.removeAccountFromConfig.errors.invalidId`, { - nameOrId, - }) - ); - } - - let removedAccountIsDefault = false; - const accountConfig = this.getAccount(accountId); - - if (accountConfig) { - logger.debug( - i18n(`${i18nKey}.removeAccountFromConfig.deleting`, { accountId }) - ); - const index = this.getAccountIndex(accountId); - this.config.accounts.splice(index, 1); - - if (this.getDefaultAccount() === accountConfig.name) { - removedAccountIsDefault = true; - } - - this.write(); - } - - return removedAccountIsDefault; - } - - /** - * @throws {Error} - */ - updateDefaultCmsPublishMode( - defaultCmsPublishMode: CmsPublishMode - ): CLIConfig_NEW | null { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - const ALL_CMS_PUBLISH_MODES = Object.values(CMS_PUBLISH_MODE); - if ( - !defaultCmsPublishMode || - !ALL_CMS_PUBLISH_MODES.find(m => m === defaultCmsPublishMode) - ) { - throw new Error( - i18n( - `${i18nKey}.updateDefaultCmsPublishMode.errors.invalidCmsPublishMode`, - { - defaultCmsPublishMode, - validCmsPublishModes: commaSeparatedValues(ALL_CMS_PUBLISH_MODES), - } - ) - ); - } - - this.config.defaultCmsPublishMode = defaultCmsPublishMode; - return this.write(); - } - - /** - * @throws {Error} - */ - updateHttpTimeout(timeout: string): CLIConfig_NEW | null { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - const parsedTimeout = parseInt(timeout); - if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { - throw new Error( - i18n(`${i18nKey}.updateHttpTimeout.errors.invalidTimeout`, { - timeout, - minTimeout: MIN_HTTP_TIMEOUT, - }) - ); - } - - this.config.httpTimeout = parsedTimeout; - return this.write(); - } - - /** - * @throws {Error} - */ - updateAllowUsageTracking(isEnabled: boolean): CLIConfig_NEW | null { - if (!this.config) { - throw new Error(i18n(`${i18nKey}.errors.noConfigLoaded`)); - } - if (typeof isEnabled !== 'boolean') { - throw new Error( - i18n(`${i18nKey}.updateAllowUsageTracking.errors.invalidInput`, { - isEnabled: `${isEnabled}`, - }) - ); - } - - this.config.allowUsageTracking = isEnabled; - return this.write(); - } - - isTrackingAllowed(): boolean { - if (!this.config) { - return true; - } - return this.config.allowUsageTracking !== false; - } - - handleLegacyCmsPublishMode( - config: CLIConfig_NEW | null - ): CLIConfig_NEW | null { - if (config?.defaultMode) { - config.defaultCmsPublishMode = config.defaultMode; - delete config.defaultMode; - } - return config; - } -} - -export const CLIConfiguration = new _CLIConfiguration(); diff --git a/config/README.md b/config/README.md index a5b94685..017212fc 100644 --- a/config/README.md +++ b/config/README.md @@ -10,17 +10,20 @@ The config file is named `huspot.config.yml`. There are a handful of standard config utils that anyone working in this library should be familiar with. -#### getAndLoadConfigIfNeeded() +#### getConfig() -Locates, parses, and stores the `hubspot.config.yml` file in memory. This should be the first thing that you do if you plan to access any of the config file values. If the config has already been loaded, this function will simply return the already-parsed config values. +Locates and parses the hubspot config file. This function will automatically find the correct config file. Typically, it defaults to the nearest config file by working up the direcotry tree. Custom config locations can be set using the following environment variables -#### updateAccountConfig() +- `USE_ENVIRONTMENT_CONFIG` - load config account from environment variables +- `HUBSPOT_CONFIG_PATH` - specify a path to a specific config file -Safely writes updated values to the `hubspot.config.yml` file. This will also refresh the in-memory values that have been stored for the targeted account. +#### updateConfigAccount() -#### getAccountConfig() +Safely writes updated values to the `hubspot.config.yml` file. -Returns config data for a specific account, given the account's ID. +#### getConfigAccountById() and getConfigAccountByName() + +Returns config data for a specific account, given the account's ID or name. ## Example config @@ -39,7 +42,3 @@ portals: accountType: STANDARD personalAccessKey: 'my-personal-access-key' ``` - -## config_DEPRECATED.ts explained - -You may notice that we have a few configuration files in our `config/` folder. This is because we are in the middle of exploring a new method for storing account information. Despite its naming, config_DEPRECATED.ts is still the configuration file that handles all of our config logic. We have a proxy file named `config/index.ts` that will always choose to use the soon-to-be deprecated configuration file. This proxy file will enable us to slowly port config functionality over to the new pattern (i.e. `config/CLIConfiguration.ts`). For now, it is recommended to use config_DEPRECATED.ts and the utils it provides. We ask that any updates made to config_DEPRECATED.ts are also made to the newer CLIConfiguration.ts file whenever applicable. diff --git a/config/__tests__/CLIConfiguration.test.ts b/config/__tests__/CLIConfiguration.test.ts deleted file mode 100644 index 22727dc1..00000000 --- a/config/__tests__/CLIConfiguration.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { HUBSPOT_ACCOUNT_TYPES } from '../../constants/config'; -import { ENVIRONMENTS } from '../../constants/environments'; -import { CLIConfiguration as config } from '../CLIConfiguration'; - -describe('config/CLIConfiguration', () => { - afterAll(() => { - config.setActive(false); - }); - - describe('constructor()', () => { - it('initializes correctly', () => { - expect(config).toBeDefined(); - expect(config.options).toBeDefined(); - expect(config.useEnvConfig).toBe(false); - expect(config.config).toBe(null); - expect(config.active).toBe(false); - }); - }); - - describe('isActive()', () => { - it('returns true when the class is being used', () => { - expect(config.isActive()).toBe(false); - config.setActive(true); - expect(config.isActive()).toBe(true); - }); - }); - - describe('getAccount()', () => { - it('returns null when no config is loaded', () => { - expect(config.getAccount('account-name')).toBe(null); - }); - }); - - describe('isConfigFlagEnabled()', () => { - it('returns default value when no config is loaded', () => { - expect(config.isConfigFlagEnabled('allowUsageTracking', false)).toBe( - false - ); - }); - }); - - describe('getAccountId()', () => { - it('returns null when it cannot find the account in the config', () => { - expect(config.getAccountId('account-name')).toBe(null); - }); - }); - - describe('getDefaultAccount()', () => { - it('returns null when no config is loaded', () => { - expect(config.getDefaultAccount()).toBe(null); - }); - }); - - describe('getAccountIndex()', () => { - it('returns -1 when no config is loaded', () => { - expect(config.getAccountIndex(123)).toBe(-1); - }); - }); - - describe('isAccountInConfig()', () => { - it('returns false when no config is loaded', () => { - expect(config.isAccountInConfig(123)).toBe(false); - }); - }); - - describe('getConfigForAccount()', () => { - it('returns null when no config is loaded', () => { - expect(config.getConfigForAccount(123)).toBe(null); - }); - }); - - describe('getEnv()', () => { - it('returns PROD when no config is loaded', () => { - expect(config.getEnv(123)).toBe(ENVIRONMENTS.PROD); - }); - }); - - describe('getAccountType()', () => { - it('returns STANDARD when no accountType or sandboxAccountType is specified', () => { - expect(config.getAccountType()).toBe(HUBSPOT_ACCOUNT_TYPES.STANDARD); - }); - it('handles sandboxAccountType transforms correctly', () => { - expect(config.getAccountType(undefined, 'DEVELOPER')).toBe( - HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX - ); - expect(config.getAccountType(undefined, 'STANDARD')).toBe( - HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX - ); - }); - it('handles accountType arg correctly', () => { - expect( - config.getAccountType(HUBSPOT_ACCOUNT_TYPES.STANDARD, 'DEVELOPER') - ).toBe(HUBSPOT_ACCOUNT_TYPES.STANDARD); - }); - }); - - describe('updateDefaultAccount()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.updateDefaultAccount('account-name'); - }).toThrow(); - }); - }); - - describe('renameAccount()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.renameAccount('account-name', 'new-account-name'); - }).toThrow(); - }); - }); - - describe('removeAccountFromConfig()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.removeAccountFromConfig('account-name'); - }).toThrow(); - }); - }); - - describe('updateDefaultCmsPublishMode()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.updateDefaultCmsPublishMode('draft'); - }).toThrow(); - }); - }); - - describe('updateHttpTimeout()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.updateHttpTimeout('1000'); - }).toThrow(); - }); - }); - - describe('updateAllowUsageTracking()', () => { - it('throws when no config is loaded', () => { - expect(() => { - config.updateAllowUsageTracking(true); - }).toThrow(); - }); - }); - - describe('isTrackingAllowed()', () => { - it('returns true when no config is loaded', () => { - expect(config.isTrackingAllowed()).toBe(true); - }); - }); -}); diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index eca433d9..237d193f 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -1,808 +1,547 @@ +import findup from 'findup-sync'; import fs from 'fs-extra'; +import yaml from 'js-yaml'; + import { - setConfig, - getAndLoadConfigIfNeeded, + localConfigFileExists, + globalConfigFileExists, + getConfigFilePath, getConfig, - getAccountType, - getConfigPath, - getAccountConfig, - getAccountId, - updateDefaultAccount, - updateAccountConfig, - validateConfig, - deleteEmptyConfigFile, - setConfigPath, + isConfigValid, createEmptyConfigFile, - configFileExists, + deleteConfigFile, + getConfigAccountById, + getConfigAccountByName, + getConfigDefaultAccount, + getAllConfigAccounts, + getConfigAccountEnvironment, + addConfigAccount, + updateConfigAccount, + setConfigAccountAsDefault, + renameConfigAccount, + removeAccountFromConfig, + updateHttpTimeout, + updateAllowUsageTracking, + updateDefaultCmsPublishMode, + isConfigFlagEnabled, } from '../index'; -import { getAccountIdentifier } from '../getAccountIdentifier'; -import { getAccounts, getDefaultAccount } from '../../utils/accounts'; -import { ENVIRONMENTS } from '../../constants/environments'; -import { HUBSPOT_ACCOUNT_TYPES } from '../../constants/config'; -import { CLIConfig, CLIConfig_DEPRECATED } from '../../types/Config'; +import { HubSpotConfigAccount } from '../../types/Accounts'; +import { HubSpotConfig } from '../../types/Config'; import { - APIKeyAccount_DEPRECATED, - AuthType, - CLIAccount, - OAuthAccount, - OAuthAccount_DEPRECATED, - APIKeyAccount, - PersonalAccessKeyAccount, - PersonalAccessKeyAccount_DEPRECATED, + PersonalAccessKeyConfigAccount, + OAuthConfigAccount, + APIKeyConfigAccount, } from '../../types/Accounts'; -import * as configFile from '../configFile'; -import * as config_DEPRECATED from '../config_DEPRECATED'; - -const CONFIG_PATHS = { - none: null, - default: '/Users/fakeuser/hubspot.config.yml', - nonStandard: '/Some/non-standard.config.yml', - cwd: `${process.cwd()}/hubspot.config.yml`, - hidden: '/Users/fakeuser/config.yml', -}; - -let mockedConfigPath: string | null = CONFIG_PATHS.default; - -jest.mock('findup-sync', () => { - return jest.fn(() => mockedConfigPath); -}); - -jest.mock('../../lib/logger'); - -const fsReadFileSyncSpy = jest.spyOn(fs, 'readFileSync'); -const fsWriteFileSyncSpy = jest.spyOn(fs, 'writeFileSync'); - -jest.mock('../configFile', () => ({ - getConfigFilePath: jest.fn(), - configFileExists: jest.fn(), -})); - -const API_KEY_CONFIG: APIKeyAccount_DEPRECATED = { - portalId: 1111, - name: 'API', - authType: 'apikey', - apiKey: 'secret', - env: ENVIRONMENTS.QA, -}; - -const OAUTH2_CONFIG: OAuthAccount_DEPRECATED = { - name: 'OAUTH2', - portalId: 2222, - authType: 'oauth2', +import { + PERSONAL_ACCESS_KEY_AUTH_METHOD, + OAUTH_AUTH_METHOD, + API_KEY_AUTH_METHOD, +} from '../../constants/auth'; +import { + getGlobalConfigFilePath, + getLocalConfigDefaultFilePath, + formatConfigForWrite, +} from '../utils'; +import { getDefaultAccountOverrideAccountId } from '../defaultAccountOverride'; +import { CONFIG_FLAGS, ENVIRONMENT_VARIABLES } from '../../constants/config'; +import * as utils from '../utils'; +import { CmsPublishMode } from '../../types/Files'; + +jest.mock('findup-sync'); +jest.mock('../../lib/path'); +jest.mock('fs-extra'); +jest.mock('../defaultAccountOverride'); + +const mockFindup = findup as jest.MockedFunction; +const mockFs = fs as jest.Mocked; +const mockGetDefaultAccountOverrideAccountId = + getDefaultAccountOverrideAccountId as jest.MockedFunction< + typeof getDefaultAccountOverrideAccountId + >; + +const PAK_ACCOUNT: PersonalAccessKeyConfigAccount = { + name: 'test-account', + accountId: 123, + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'test-key', + env: 'qa', auth: { - clientId: 'fakeClientId', - clientSecret: 'fakeClientSecret', - scopes: ['content'], - tokenInfo: { - expiresAt: '2020-01-01T00:00:00.000Z', - refreshToken: 'fakeOauthRefreshToken', - accessToken: 'fakeOauthAccessToken', - }, + tokenInfo: {}, }, - env: ENVIRONMENTS.QA, + accountType: 'STANDARD', }; -const PERSONAL_ACCESS_KEY_CONFIG: PersonalAccessKeyAccount_DEPRECATED = { - name: 'PERSONALACCESSKEY', - authType: 'personalaccesskey', +const OAUTH_ACCOUNT: OAuthConfigAccount = { + accountId: 234, + env: 'qa', + name: '234', + authType: OAUTH_AUTH_METHOD.value, + accountType: undefined, auth: { + clientId: 'test-client-id', + clientSecret: 'test-client-secret', tokenInfo: { - expiresAt: '2020-01-01T00:00:00.000Z', - accessToken: 'fakePersonalAccessKeyAccessToken', + refreshToken: 'test-refresh-token', }, + scopes: ['content', 'hubdb', 'files'], }, - personalAccessKey: 'fakePersonalAccessKey', - env: ENVIRONMENTS.QA, - portalId: 1, }; -const PORTALS = [API_KEY_CONFIG, OAUTH2_CONFIG, PERSONAL_ACCESS_KEY_CONFIG]; +const API_KEY_ACCOUNT: APIKeyConfigAccount = { + accountId: 345, + env: 'qa', + name: 'api-key-account', + authType: API_KEY_AUTH_METHOD.value, + apiKey: 'test-api-key', + accountType: 'STANDARD', +}; -const CONFIG: CLIConfig_DEPRECATED = { - defaultPortal: PORTALS[0].name, - portals: PORTALS, +const CONFIG: HubSpotConfig = { + defaultAccount: PAK_ACCOUNT.accountId, + accounts: [PAK_ACCOUNT], + defaultCmsPublishMode: 'publish', + httpTimeout: 1000, + httpUseLocalhost: true, + allowUsageTracking: true, }; -function getAccountByAuthType( - config: CLIConfig | undefined | null, - authType: AuthType -): CLIAccount { - return getAccounts(config).filter(portal => portal.authType === authType)[0]; +function cleanup() { + Object.keys(ENVIRONMENT_VARIABLES).forEach(key => { + delete process.env[key]; + }); + mockFs.existsSync.mockReset(); + mockFs.readFileSync.mockReset(); + mockFs.writeFileSync.mockReset(); + mockFs.unlinkSync.mockReset(); + mockFindup.mockReset(); + jest.restoreAllMocks(); } -describe('config/config', () => { - const globalConsole = global.console; - beforeAll(() => { - global.console.error = jest.fn(); - global.console.debug = jest.fn(); - }); - afterAll(() => { - global.console = globalConsole; +function mockConfig(config = CONFIG) { + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + jest.spyOn(utils, 'parseConfig').mockReturnValueOnce(structuredClone(config)); +} + +describe('config/index', () => { + afterEach(() => { + cleanup(); }); - describe('setConfig()', () => { - beforeEach(() => { - setConfig(CONFIG); + describe('localConfigFileExists()', () => { + it('returns true when local config exists', () => { + mockFindup.mockReturnValueOnce(getLocalConfigDefaultFilePath()); + expect(localConfigFileExists()).toBe(true); }); - it('sets the config properly', () => { - expect(getConfig()).toEqual(CONFIG); + it('returns false when local config does not exist', () => { + mockFindup.mockReturnValueOnce(null); + expect(localConfigFileExists()).toBe(false); }); }); - describe('getAccountId()', () => { - beforeEach(() => { - process.env = {}; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: PORTALS, - }); + describe('globalConfigFileExists()', () => { + it('returns true when global config exists', () => { + mockFs.existsSync.mockReturnValueOnce(true); + expect(globalConfigFileExists()).toBe(true); }); - it('returns portalId from config when a name is passed', () => { - expect(getAccountId(OAUTH2_CONFIG.name)).toEqual(OAUTH2_CONFIG.portalId); + it('returns false when global config does not exist', () => { + mockFs.existsSync.mockReturnValueOnce(false); + expect(globalConfigFileExists()).toBe(false); }); + }); - it('returns portalId from config when a string id is passed', () => { - expect(getAccountId((OAUTH2_CONFIG.portalId || '').toString())).toEqual( - OAUTH2_CONFIG.portalId - ); + describe('getConfigFilePath()', () => { + it('returns environment path when set', () => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH] = + 'test-environment-path'; + expect(getConfigFilePath()).toBe('test-environment-path'); }); - it('returns portalId from config when a numeric id is passed', () => { - expect(getAccountId(OAUTH2_CONFIG.portalId)).toEqual( - OAUTH2_CONFIG.portalId - ); + it('returns global path when exists', () => { + mockFs.existsSync.mockReturnValueOnce(true); + expect(getConfigFilePath()).toBe(getGlobalConfigFilePath()); }); - it('returns defaultPortal from config', () => { - expect(getAccountId() || undefined).toEqual( - PERSONAL_ACCESS_KEY_CONFIG.portalId - ); - }); - - describe('when defaultPortal is a portalId', () => { - beforeEach(() => { - process.env = {}; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.portalId, - portals: PORTALS, - }); - }); - - it('returns defaultPortal from config', () => { - expect(getAccountId() || undefined).toEqual( - PERSONAL_ACCESS_KEY_CONFIG.portalId - ); - }); + it('returns local path when global does not exist', () => { + mockFs.existsSync.mockReturnValueOnce(false); + mockFindup.mockReturnValueOnce(getLocalConfigDefaultFilePath()); + expect(getConfigFilePath()).toBe(getLocalConfigDefaultFilePath()); }); }); - describe('updateDefaultAccount()', () => { - const myPortalName = 'Foo'; - - beforeEach(() => { - updateDefaultAccount(myPortalName); + describe('getConfig()', () => { + it('returns environment config when enabled', () => { + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] = + 'true'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '234'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_API_KEY] = 'test-api-key'; + expect(getConfig()).toEqual({ + defaultAccount: 234, + accounts: [ + { + accountId: 234, + name: '234', + env: 'qa', + apiKey: 'test-api-key', + authType: API_KEY_AUTH_METHOD.value, + }, + ], + }); }); - it('sets the defaultPortal in the config', () => { - const config = getConfig(); - expect(config ? getDefaultAccount(config) : null).toEqual(myPortalName); + it('returns parsed config from file', () => { + mockConfig(); + expect(getConfig()).toEqual(CONFIG); }); }); - describe('deleteEmptyConfigFile()', () => { - it('does not delete config file if there are contents', () => { - jest - .spyOn(fs, 'readFileSync') - .mockImplementation(() => 'defaultPortal: "test"'); - jest.spyOn(fs, 'existsSync').mockImplementation(() => true); - fs.unlinkSync = jest.fn(); + describe('isConfigValid()', () => { + it('returns true for valid config', () => { + mockConfig(); - deleteEmptyConfigFile(); - expect(fs.unlinkSync).not.toHaveBeenCalled(); + expect(isConfigValid()).toBe(true); }); - it('deletes config file if empty', () => { - jest.spyOn(fs, 'readFileSync').mockImplementation(() => ''); - jest.spyOn(fs, 'existsSync').mockImplementation(() => true); - fs.unlinkSync = jest.fn(); + it('returns false for config with no accounts', () => { + mockConfig({ accounts: [] }); - deleteEmptyConfigFile(); - expect(fs.unlinkSync).toHaveBeenCalled(); + expect(isConfigValid()).toBe(false); }); - }); - describe('updateAccountConfig()', () => { - const CONFIG = { - defaultPortal: PORTALS[0].name, - portals: PORTALS, - }; + it('returns false for config with duplicate account ids', () => { + mockConfig({ accounts: [PAK_ACCOUNT, PAK_ACCOUNT] }); - beforeEach(() => { - setConfig(CONFIG); + expect(isConfigValid()).toBe(false); }); + }); - it('sets the env in the config if specified', () => { - const environment = ENVIRONMENTS.QA; - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - environment, - }; - updateAccountConfig(modifiedPersonalAccessKeyConfig); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(environment); - }); - - it('sets the env in the config if it was preexisting', () => { - const env = ENVIRONMENTS.QA; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: [{ ...PERSONAL_ACCESS_KEY_CONFIG, env }], - }); - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - env: undefined, - }; - - updateAccountConfig(modifiedPersonalAccessKeyConfig); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(env); - }); - - it('overwrites the existing env in the config if specified as environment', () => { - // NOTE: the config now uses "env", but this is to support legacy behavior - const previousEnv = ENVIRONMENTS.PROD; - const newEnv = ENVIRONMENTS.QA; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: [{ ...PERSONAL_ACCESS_KEY_CONFIG, env: previousEnv }], - }); - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - environment: newEnv, - }; - updateAccountConfig(modifiedPersonalAccessKeyConfig); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(newEnv); - }); - - it('overwrites the existing env in the config if specified as env', () => { - const previousEnv = ENVIRONMENTS.PROD; - const newEnv = ENVIRONMENTS.QA; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: [{ ...PERSONAL_ACCESS_KEY_CONFIG, env: previousEnv }], - }); - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - env: newEnv, - }; - updateAccountConfig(modifiedPersonalAccessKeyConfig); + describe('createEmptyConfigFile()', () => { + it('creates global config when specified', () => { + mockFs.existsSync.mockReturnValueOnce(true); + createEmptyConfigFile(true); - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(newEnv); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getGlobalConfigFilePath(), + yaml.dump({ accounts: [] }) + ); }); - it('sets the name in the config if specified', () => { - const name = 'MYNAME'; - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - name, - }; - updateAccountConfig(modifiedPersonalAccessKeyConfig); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).name - ).toEqual(name); - }); - - it('sets the name in the config if it was preexisting', () => { - const name = 'PREEXISTING'; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: [{ ...PERSONAL_ACCESS_KEY_CONFIG, name }], - }); - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - }; - delete modifiedPersonalAccessKeyConfig.name; - updateAccountConfig(modifiedPersonalAccessKeyConfig); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).name - ).toEqual(name); - }); - - it('overwrites the existing name in the config if specified', () => { - const previousName = 'PREVIOUSNAME'; - const newName = 'NEWNAME'; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: [{ ...PERSONAL_ACCESS_KEY_CONFIG, name: previousName }], - }); - const modifiedPersonalAccessKeyConfig = { - ...PERSONAL_ACCESS_KEY_CONFIG, - name: newName, - }; - updateAccountConfig(modifiedPersonalAccessKeyConfig); + it('creates local config by default', () => { + mockFs.existsSync.mockReturnValueOnce(true); + createEmptyConfigFile(false); - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).name - ).toEqual(newName); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getLocalConfigDefaultFilePath(), + yaml.dump({ accounts: [] }) + ); }); }); - describe('validateConfig()', () => { - const DEFAULT_PORTAL = PORTALS[0].name; + describe('deleteConfigFile()', () => { + it('deletes the config file', () => { + mockFs.existsSync.mockReturnValue(true); + deleteConfigFile(); - it('allows valid config', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: PORTALS, - }); - expect(validateConfig()).toEqual(true); + expect(mockFs.unlinkSync).toHaveBeenCalledWith(getConfigFilePath()); }); + }); - it('does not allow duplicate portalIds', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [...PORTALS, PORTALS[0]], - }); - expect(validateConfig()).toEqual(false); - }); + describe('getConfigAccountById()', () => { + it('returns account when found', () => { + mockConfig(); - it('does not allow duplicate names', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - ...PORTALS, - { - ...PORTALS[0], - portalId: 123456789, - }, - ], - }); - expect(validateConfig()).toEqual(false); + expect(getConfigAccountById(123)).toEqual(PAK_ACCOUNT); }); - it('does not allow names with spaces', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: 'A NAME WITH SPACES', - }, - ], - }); - expect(validateConfig()).toEqual(false); - }); + it('throws when account not found', () => { + mockConfig(); - it('allows multiple portals with no name', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: undefined, - }, - { - ...PORTALS[1], - name: undefined, - }, - ], - }); - expect(validateConfig()).toEqual(true); + expect(() => getConfigAccountById(456)).toThrow(); }); }); - describe('getAndLoadConfigIfNeeded()', () => { - beforeEach(() => { - setConfig(undefined); - process.env = {}; - }); - - it('loads a config from file if no combination of environment variables is sufficient', () => { - const readFileSyncSpy = jest.spyOn(fs, 'readFileSync'); - - getAndLoadConfigIfNeeded(); - expect(fs.readFileSync).toHaveBeenCalled(); - readFileSyncSpy.mockReset(); - }); - - describe('oauth environment variable config', () => { - const { - portalId, - auth: { clientId, clientSecret }, - } = OAUTH2_CONFIG; - const refreshToken = OAUTH2_CONFIG.auth.tokenInfo?.refreshToken || ''; - let portalConfig: OAuthAccount | null; - - beforeEach(() => { - process.env = { - HUBSPOT_ACCOUNT_ID: `${portalId}`, - HUBSPOT_CLIENT_ID: clientId, - HUBSPOT_CLIENT_SECRET: clientSecret, - HUBSPOT_REFRESH_TOKEN: refreshToken, - }; - getAndLoadConfigIfNeeded({ useEnv: true }); - portalConfig = getAccountConfig(portalId) as OAuthAccount; - fsReadFileSyncSpy.mockReset(); - }); - - afterEach(() => { - // Clean up environment variable config so subsequent tests don't break - process.env = {}; - setConfig(undefined); - getAndLoadConfigIfNeeded(); - }); - - it('does not load a config from file', () => { - expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); - }); - - it('creates a portal config', () => { - expect(portalConfig).toBeTruthy(); - }); + describe('getConfigAccountByName()', () => { + it('returns account when found', () => { + mockConfig(); - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads client id value', () => { - expect(portalConfig?.auth.clientId).toEqual(clientId); - }); + expect(getConfigAccountByName('test-account')).toEqual(PAK_ACCOUNT); + }); - it('properly loads client secret value', () => { - expect(portalConfig?.auth.clientSecret).toEqual(clientSecret); - }); + it('throws when account not found', () => { + mockConfig(); - it('properly loads refresh token value', () => { - expect(portalConfig?.auth?.tokenInfo?.refreshToken).toEqual( - refreshToken - ); - }); + expect(() => getConfigAccountByName('non-existent-account')).toThrow(); }); + }); - describe('apikey environment variable config', () => { - const { portalId, apiKey } = API_KEY_CONFIG; - let portalConfig: APIKeyAccount; - - beforeEach(() => { - process.env = { - HUBSPOT_ACCOUNT_ID: `${portalId}`, - HUBSPOT_API_KEY: apiKey, - }; - getAndLoadConfigIfNeeded({ useEnv: true }); - portalConfig = getAccountConfig(portalId) as APIKeyAccount; - fsReadFileSyncSpy.mockReset(); - }); + describe('getConfigDefaultAccount()', () => { + it('returns default account when set', () => { + mockConfig(); - afterEach(() => { - // Clean up environment variable config so subsequent tests don't break - process.env = {}; - setConfig(undefined); - getAndLoadConfigIfNeeded(); - }); + expect(getConfigDefaultAccount()).toEqual(PAK_ACCOUNT); + }); - it('does not load a config from file', () => { - expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); - }); + it('throws when no default account', () => { + mockConfig({ accounts: [] }); - it('creates a portal config', () => { - expect(portalConfig).toBeTruthy(); - }); + expect(() => getConfigDefaultAccount()).toThrow(); + }); - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); + it('returns the correct account when default account override is set', () => { + mockConfig({ accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT] }); + mockGetDefaultAccountOverrideAccountId.mockReturnValueOnce( + OAUTH_ACCOUNT.accountId + ); - it('properly loads api key value', () => { - expect(portalConfig.apiKey).toEqual(apiKey); - }); + expect(getConfigDefaultAccount()).toEqual(OAUTH_ACCOUNT); }); + }); - describe('personalaccesskey environment variable config', () => { - const { portalId, personalAccessKey } = PERSONAL_ACCESS_KEY_CONFIG; - let portalConfig: PersonalAccessKeyAccount | null; - - beforeEach(() => { - process.env = { - HUBSPOT_ACCOUNT_ID: `${portalId}`, - HUBSPOT_PERSONAL_ACCESS_KEY: personalAccessKey, - }; - getAndLoadConfigIfNeeded({ useEnv: true }); - portalConfig = getAccountConfig(portalId) as PersonalAccessKeyAccount; - fsReadFileSyncSpy.mockReset(); - }); + describe('getAllConfigAccounts()', () => { + it('returns all accounts', () => { + mockConfig(); - afterEach(() => { - // Clean up environment variable config so subsequent tests don't break - process.env = {}; - setConfig(undefined); - getAndLoadConfigIfNeeded(); - }); + expect(getAllConfigAccounts()).toEqual([PAK_ACCOUNT]); + }); + }); - it('does not load a config from file', () => { - expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); - }); + describe('getConfigAccountEnvironment()', () => { + it('returns environment for specified account', () => { + mockConfig(); - it('creates a portal config', () => { - expect(portalConfig).toBeTruthy(); - }); + expect(getConfigAccountEnvironment(123)).toEqual('qa'); + }); - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); + it('returns default account environment when no identifier', () => { + mockConfig(); - it('properly loads personal access key value', () => { - expect(portalConfig?.personalAccessKey).toEqual(personalAccessKey); - }); + expect(getConfigAccountEnvironment()).toEqual('qa'); }); }); - describe('getAccountType()', () => { - it('returns STANDARD when no accountType or sandboxAccountType is specified', () => { - expect(getAccountType()).toBe(HUBSPOT_ACCOUNT_TYPES.STANDARD); - }); - it('handles sandboxAccountType transforms correctly', () => { - expect(getAccountType(undefined, 'DEVELOPER')).toBe( - HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX - ); - expect(getAccountType(undefined, 'STANDARD')).toBe( - HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX + describe('addConfigAccount()', () => { + it('adds valid account to config', () => { + mockConfig(); + mockFs.writeFileSync.mockImplementationOnce(() => undefined); + addConfigAccount(OAUTH_ACCOUNT); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT], + }) + ) ); }); - it('handles accountType arg correctly', () => { - expect(getAccountType(HUBSPOT_ACCOUNT_TYPES.STANDARD, 'DEVELOPER')).toBe( - HUBSPOT_ACCOUNT_TYPES.STANDARD - ); - }); - }); - describe('getConfigPath()', () => { - let fsExistsSyncSpy: jest.SpyInstance; - - beforeAll(() => { - fsExistsSyncSpy = jest.spyOn(fs, 'existsSync').mockImplementation(() => { - return false; - }); + it('throws for invalid account', () => { + expect(() => + addConfigAccount({ + ...PAK_ACCOUNT, + personalAccessKey: null, + } as unknown as HubSpotConfigAccount) + ).toThrow(); }); - afterAll(() => { - fsExistsSyncSpy.mockRestore(); - }); - - describe('when a standard config is present', () => { - it('returns the standard config path when useHiddenConfig is false', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.default - ); - const configPath = getConfigPath('', false); - expect(configPath).toBe(CONFIG_PATHS.default); - }); + it('throws when account already exists', () => { + mockConfig(); - it('returns the hidden config path when useHiddenConfig is true', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.hidden - ); - const hiddenConfigPath = getConfigPath(undefined, true); - expect(hiddenConfigPath).toBe(CONFIG_PATHS.hidden); - }); + expect(() => addConfigAccount(PAK_ACCOUNT)).toThrow(); }); + }); - describe('when passed a path', () => { - it('returns the path when useHiddenConfig is false', () => { - const randomConfigPath = '/some/random/path.config.yml'; - const configPath = getConfigPath(randomConfigPath, false); - expect(configPath).toBe(randomConfigPath); - }); + describe('updateConfigAccount()', () => { + it('updates existing account', () => { + mockConfig(); - it('returns the hidden config path when useHiddenConfig is true, ignoring the passed path', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.hidden - ); - const hiddenConfigPath = getConfigPath( - '/some/random/path.config.yml', - true - ); - expect(hiddenConfigPath).toBe(CONFIG_PATHS.hidden); - }); - }); + const newAccount = { ...PAK_ACCOUNT, name: 'new-name' }; - describe('when no config is present', () => { - beforeAll(() => { - fsExistsSyncSpy.mockReturnValue(false); - }); - - it('returns default directory when useHiddenConfig is false', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue(null); - const configPath = getConfigPath(undefined, false); - expect(configPath).toBe(CONFIG_PATHS.default); - }); + updateConfigAccount(newAccount); - it('returns null when useHiddenConfig is true and no hidden config exists', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue(null); - const hiddenConfigPath = getConfigPath(undefined, true); - expect(hiddenConfigPath).toBeNull(); - }); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...CONFIG, accounts: [newAccount] })) + ); + }); + it('throws for invalid account', () => { + expect(() => + updateConfigAccount({ + ...PAK_ACCOUNT, + personalAccessKey: null, + } as unknown as HubSpotConfigAccount) + ).toThrow(); }); - describe('when a non-standard config is present', () => { - beforeAll(() => { - fsExistsSyncSpy.mockReturnValue(true); - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.nonStandard - ); - }); + it('throws when account not found', () => { + mockConfig(); - it('returns the hidden config path when useHiddenConfig is true', () => { - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.hidden - ); - const hiddenConfigPath = getConfigPath(undefined, true); - expect(hiddenConfigPath).toBe(CONFIG_PATHS.hidden); - }); + expect(() => updateConfigAccount(OAUTH_ACCOUNT)).toThrow(); }); }); - describe('createEmptyConfigFile()', () => { - describe('when no config is present', () => { - let fsExistsSyncSpy: jest.SpyInstance; - - beforeEach(() => { - setConfigPath(CONFIG_PATHS.none); - mockedConfigPath = CONFIG_PATHS.none; - fsExistsSyncSpy = jest - .spyOn(fs, 'existsSync') - .mockImplementation(() => { - return false; - }); - }); + describe('setConfigAccountAsDefault()', () => { + it('sets account as default by id', () => { + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); - afterAll(() => { - setConfigPath(CONFIG_PATHS.default); - mockedConfigPath = CONFIG_PATHS.default; - fsExistsSyncSpy.mockRestore(); - }); + setConfigAccountAsDefault(345); - it('writes a new config file', () => { - createEmptyConfigFile(); - - expect(fsWriteFileSyncSpy).toHaveBeenCalled(); - }); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...config, defaultAccount: 345 })) + ); }); - describe('when a config is present', () => { - let fsExistsSyncAndReturnTrueSpy: jest.SpyInstance; + it('sets account as default by name', () => { + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); - beforeAll(() => { - setConfigPath(CONFIG_PATHS.cwd); - mockedConfigPath = CONFIG_PATHS.cwd; - fsExistsSyncAndReturnTrueSpy = jest - .spyOn(fs, 'existsSync') - .mockImplementation(pathToCheck => { - if (pathToCheck === CONFIG_PATHS.cwd) { - return true; - } + setConfigAccountAsDefault('api-key-account'); - return false; - }); - }); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...config, defaultAccount: 345 })) + ); + }); - afterAll(() => { - fsExistsSyncAndReturnTrueSpy.mockRestore(); - }); + it('throws when account not found', () => { + expect(() => setConfigAccountAsDefault('non-existent-account')).toThrow(); + }); + }); - it('does nothing', () => { - createEmptyConfigFile(); + describe('renameConfigAccount()', () => { + it('renames existing account', () => { + mockConfig(); - expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); - }); + renameConfigAccount('test-account', 'new-name'); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [{ ...PAK_ACCOUNT, name: 'new-name' }], + }) + ) + ); }); - describe('when passed a path', () => { - beforeAll(() => { - setConfigPath(CONFIG_PATHS.none); - mockedConfigPath = CONFIG_PATHS.none; - }); + it('throws when account not found', () => { + expect(() => + renameConfigAccount('non-existent-account', 'new-name') + ).toThrow(); + }); - it('creates a config at the specified path', () => { - const specifiedPath = '/some/path/that/has/never/been/used.config.yml'; - createEmptyConfigFile({ path: specifiedPath }); + it('throws when new name already exists', () => { + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); - expect(fsWriteFileSyncSpy).not.toHaveBeenCalledWith(specifiedPath); - }); + expect(() => + renameConfigAccount('test-account', 'api-key-account') + ).toThrow(); }); }); - describe('configFileExists', () => { - let getConfigPathSpy: jest.SpyInstance; - - beforeAll(() => { - getConfigPathSpy = jest.spyOn(config_DEPRECATED, 'getConfigPath'); + describe('removeAccountFromConfig()', () => { + it('removes existing account', () => { + mockConfig(); + + removeAccountFromConfig(123); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [], + defaultAccount: undefined, + }) + ) + ); }); - beforeEach(() => { - jest.clearAllMocks(); - }); + it('throws when account not found', () => { + mockConfig(); - afterAll(() => { - getConfigPathSpy.mockRestore(); + expect(() => removeAccountFromConfig(456)).toThrow(); }); + }); - it('returns true when useHiddenConfig is true and newConfigFileExists returns true', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(true); + describe('updateHttpTimeout()', () => { + it('updates timeout value', () => { + mockConfig(); - const result = configFileExists(true); + updateHttpTimeout(4000); - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(true); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...CONFIG, httpTimeout: 4000 })) + ); }); - it('returns false when useHiddenConfig is true and newConfigFileExists returns false', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(false); + it('throws for invalid timeout', () => { + expect(() => updateHttpTimeout('invalid-timeout')).toThrow(); + }); + }); - const result = configFileExists(true); + describe('updateAllowUsageTracking()', () => { + it('updates tracking setting', () => { + mockConfig(); + updateAllowUsageTracking(false); - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(false); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ ...CONFIG, allowUsageTracking: false }) + ) + ); }); + }); - it('returns true when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns a valid path', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); + describe('updateDefaultCmsPublishMode()', () => { + it('updates publish mode', () => { + mockConfig(); - const result = configFileExists(false); + updateDefaultCmsPublishMode('draft'); - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ ...CONFIG, defaultCmsPublishMode: 'draft' }) + ) + ); }); - it('returns false when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns an empty path', () => { - getConfigPathSpy.mockReturnValue(''); + it('throws for invalid mode', () => { + expect(() => + updateDefaultCmsPublishMode('invalid-mode' as unknown as CmsPublishMode) + ).toThrow(); + }); + }); - const result = configFileExists(false); + describe('isConfigFlagEnabled()', () => { + it('returns flag value when set', () => { + mockConfig({ + ...CONFIG, + [CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE]: true, + }); - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(false); + expect(isConfigFlagEnabled(CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE)).toBe( + true + ); }); - it('defaults to useHiddenConfig as false when not provided', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); + it('returns default value when not set', () => { + mockConfig(); - const result = configFileExists(); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); + expect( + isConfigFlagEnabled(CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE, true) + ).toBe(true); }); }); }); diff --git a/config/__tests__/configFile.test.ts b/config/__tests__/configFile.test.ts deleted file mode 100644 index 32e6f67b..00000000 --- a/config/__tests__/configFile.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import fs from 'fs-extra'; -import os from 'os'; -import yaml from 'js-yaml'; -import { - getConfigFilePath, - configFileExists, - configFileIsBlank, - deleteConfigFile, - readConfigFile, - parseConfig, - loadConfigFromFile, - writeConfigToFile, -} from '../configFile'; -import { - HUBSPOT_CONFIGURATION_FILE, - HUBSPOT_CONFIGURATION_FOLDER, -} from '../../constants/config'; -import { CLIConfig_NEW } from '../../types/Config'; - -// fs spy -const existsSyncSpy = jest.spyOn(fs, 'existsSync'); -const readFileSyncSpy = jest.spyOn(fs, 'readFileSync'); -const unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); -const ensureFileSyncSpy = jest.spyOn(fs, 'ensureFileSync'); -const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync'); - -// yamp spy -const loadSpy = jest.spyOn(yaml, 'load'); -const dumpSpy = jest.spyOn(yaml, 'dump'); - -const CONFIG = { - defaultAccount: '', - accounts: [], -} as CLIConfig_NEW; - -describe('config/configFile', () => { - describe('getConfigFilePath()', () => { - it('returns the config file path', () => { - const configFilePath = getConfigFilePath(); - const homeDir = os.homedir(); - - const homeDirIndex = configFilePath.indexOf(homeDir); - const folderIndex = configFilePath.indexOf(HUBSPOT_CONFIGURATION_FOLDER); - const fileIndex = configFilePath.indexOf(HUBSPOT_CONFIGURATION_FILE); - - expect(homeDirIndex).toBeGreaterThan(-1); - expect(folderIndex).toBeGreaterThan(-1); - expect(fileIndex).toBeGreaterThan(-1); - expect(folderIndex).toBeGreaterThan(homeDirIndex); - expect(fileIndex).toBeGreaterThan(folderIndex); - }); - }); - - describe('configFileExists()', () => { - it('returns true if config file exists', () => { - existsSyncSpy.mockImplementation(() => true); - const exists = configFileExists(); - - expect(existsSyncSpy).toHaveBeenCalled(); - expect(exists).toBe(true); - }); - }); - - describe('configFileIsBlank()', () => { - it('returns true if config file is blank', () => { - readFileSyncSpy.mockImplementation(() => Buffer.from('')); - const isBlank = configFileIsBlank(); - - expect(readFileSyncSpy).toHaveBeenCalled(); - expect(isBlank).toBe(true); - }); - it('returns false if config file is not blank', () => { - readFileSyncSpy.mockImplementation(() => Buffer.from('content')); - const isBlank = configFileIsBlank(); - - expect(readFileSyncSpy).toHaveBeenCalled(); - expect(isBlank).toBe(false); - }); - }); - - describe('deleteConfigFile()', () => { - it('deletes a file', () => { - unlinkSyncSpy.mockImplementation(() => null); - deleteConfigFile(); - - expect(unlinkSyncSpy).toHaveBeenLastCalledWith(getConfigFilePath()); - }); - }); - - describe('readConfigFile()', () => { - it('reads the config file', () => { - readFileSyncSpy.mockImplementation(() => Buffer.from('content')); - const result = readConfigFile('path/to/config/file'); - - expect(result).toBeDefined(); - }); - it('throws error if it fails to read the config file', () => { - readFileSyncSpy.mockImplementation(() => { - throw new Error('failed to do the thing'); - }); - - expect(() => readConfigFile('path/to/config/file')).toThrow(); - }); - }); - - describe('parseConfig()', () => { - it('parses the config file', () => { - loadSpy.mockImplementation(() => ({})); - const result = parseConfig('config-source'); - - expect(result).toBeDefined(); - }); - it('throws error if it fails to parse the config file', () => { - loadSpy.mockImplementation(() => { - throw new Error('failed to do the thing'); - }); - - expect(() => parseConfig('config-source')).toThrow(); - }); - }); - - describe('loadConfigFromFile()', () => { - it('loads the config from file', () => { - readFileSyncSpy.mockImplementation(() => Buffer.from('content')); - loadSpy.mockImplementation(() => ({})); - const result = loadConfigFromFile(); - - expect(result).toBeDefined(); - }); - it('throws error if it fails to load the config file', () => { - loadSpy.mockImplementation(() => { - throw new Error('Config file could not be read: /testpath'); - }); - - expect(() => loadConfigFromFile()).toThrow(); - }); - }); - - describe('writeConfigToFile()', () => { - it('writes the config to a file', () => { - ensureFileSyncSpy.mockImplementation(() => null); - writeFileSyncSpy.mockImplementation(() => null); - readFileSyncSpy.mockImplementation(() => Buffer.from('content')); - loadSpy.mockImplementation(() => ({})); - - writeConfigToFile(CONFIG); - - expect(ensureFileSyncSpy).toHaveBeenCalled(); - expect(writeFileSyncSpy).toHaveBeenCalled(); - }); - it('throws error if it fails to parse the config json', () => { - dumpSpy.mockImplementation(() => { - throw new Error('failed to do the thing'); - }); - - expect(() => writeConfigToFile(CONFIG)).toThrow(); - }); - it('throws error if it fails to write the config to a file', () => { - ensureFileSyncSpy.mockImplementation(() => null); - writeFileSyncSpy.mockImplementation(() => { - throw new Error('failed to do the thing'); - }); - - expect(() => writeConfigToFile(CONFIG)).toThrow(); - }); - }); -}); diff --git a/config/__tests__/configUtils.test.ts b/config/__tests__/configUtils.test.ts deleted file mode 100644 index bbd6df57..00000000 --- a/config/__tests__/configUtils.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - generateConfig, - getOrderedAccount, - getOrderedConfig, -} from '../configUtils'; -import { CLIConfig } from '../../types/Config'; -import { - CLIAccount, - OAuthAccount, - PersonalAccessKeyAccount, -} from '../../types/Accounts'; - -const PAK_ACCOUNT: PersonalAccessKeyAccount = { - accountId: 111, - authType: 'personalaccesskey', - name: 'pak-account-1', - auth: { - tokenInfo: { - accessToken: 'pak-access-token', - expiresAt: '', - }, - }, - personalAccessKey: 'pak-12345', - env: '', -}; - -const OAUTH_ACCOUNT: OAuthAccount = { - accountId: 222, - authType: 'oauth2', - name: 'oauth-account-1', - auth: { - clientId: 'oauth-client-id', - clientSecret: 'oauth-client-secret', - scopes: [], - tokenInfo: { - refreshToken: 'oauth-refresh-token', - }, - }, - env: '', -}; - -const APIKEY_ACCOUNT: CLIAccount = { - accountId: 333, - name: 'apikey-account-1', - authType: 'apikey', - apiKey: 'api-key', - env: '', -}; - -const CONFIG: CLIConfig = { - defaultAccount: PAK_ACCOUNT.name, - accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT, APIKEY_ACCOUNT], -}; - -describe('config/configUtils', () => { - describe('getOrderedAccount()', () => { - it('returns an ordered account', () => { - const orderedAccount = getOrderedAccount(PAK_ACCOUNT); - const keys = Object.keys(orderedAccount); - - expect(keys[0]).toBe('name'); - expect(keys[1]).toBe('accountId'); - }); - }); - - describe('getOrderedConfig()', () => { - it('returns an ordered config', () => { - const orderedConfig = getOrderedConfig(CONFIG); - const keys = Object.keys(orderedConfig); - - expect(keys[0]).toBe('defaultAccount'); - expect(keys[keys.length - 1]).toBe('accounts'); - }); - it('returns a config with accounts ordered', () => { - const orderedConfig = getOrderedConfig(CONFIG); - const accountKeys = Object.keys(orderedConfig.accounts[0]); - - expect(accountKeys[0]).toBe('name'); - expect(accountKeys[1]).toBe('accountId'); - }); - }); - - describe('generateConfig()', () => { - it('returns a personal access key auth account', () => { - const pakConfig = generateConfig('personalaccesskey', { - accountId: 111, - personalAccessKey: 'pak-12345', - env: 'prod', - }); - - expect(pakConfig).toBeDefined(); - if (pakConfig) { - expect(pakConfig.accounts).toBeDefined(); - expect(pakConfig.accounts[0].authType).toBe('personalaccesskey'); - } - }); - it('returns an oauth auth account', () => { - const oauthConfig = generateConfig('oauth2', { - accountId: 111, - clientId: 'client-id', - clientSecret: 'client-secret', - refreshToken: 'refresh-token', - scopes: [], - env: 'prod', - }); - - expect(oauthConfig).toBeDefined(); - if (oauthConfig) { - expect(oauthConfig.accounts).toBeDefined(); - expect(oauthConfig.accounts[0].authType).toBe('oauth2'); - } - }); - it('returns an apikey account', () => { - const apikeyConfig = generateConfig('apikey', { - accountId: 111, - apiKey: 'api-key', - env: 'prod', - }); - - expect(apikeyConfig).toBeDefined(); - if (apikeyConfig) { - expect(apikeyConfig.accounts).toBeDefined(); - expect(apikeyConfig.accounts[0].authType).toBe('apikey'); - } - }); - }); -}); diff --git a/config/__tests__/defaultAccountOverride.test.ts b/config/__tests__/defaultAccountOverride.test.ts new file mode 100644 index 00000000..c6f1db93 --- /dev/null +++ b/config/__tests__/defaultAccountOverride.test.ts @@ -0,0 +1,73 @@ +import fs from 'fs-extra'; +import findup from 'findup-sync'; +import { + getDefaultAccountOverrideAccountId, + getDefaultAccountOverrideFilePath, +} from '../defaultAccountOverride'; +import * as config from '../index'; +import { PersonalAccessKeyConfigAccount } from '../../types/Accounts'; +import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; + +jest.mock('fs-extra'); +jest.mock('findup-sync'); +jest.mock('../index'); + +const mockFs = fs as jest.Mocked; +const mockFindup = findup as jest.MockedFunction; +const mockGetAllConfigAccounts = + config.getAllConfigAccounts as jest.MockedFunction< + typeof config.getAllConfigAccounts + >; + +const PAK_ACCOUNT: PersonalAccessKeyConfigAccount = { + name: 'test-account', + accountId: 123, + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'test-key', + env: 'qa', + auth: { + tokenInfo: {}, + }, + accountType: 'STANDARD', +}; + +describe('defaultAccountOverride', () => { + describe('getDefaultAccountOverrideAccountId()', () => { + it('returns null when override file does not exist', () => { + mockFindup.mockReturnValueOnce(null); + expect(getDefaultAccountOverrideAccountId()).toBeNull(); + }); + + it('throws an error when override file exists but is not a number', () => { + mockFindup.mockReturnValueOnce('.hsaccount'); + mockFs.readFileSync.mockReturnValueOnce('string'); + expect(() => getDefaultAccountOverrideAccountId()).toThrow(); + }); + + it('throws an error when account specified in override file does not exist in config', () => { + mockFindup.mockReturnValueOnce('.hsaccount'); + mockFs.readFileSync.mockReturnValueOnce('234'); + mockGetAllConfigAccounts.mockReturnValueOnce([PAK_ACCOUNT]); + expect(() => getDefaultAccountOverrideAccountId()).toThrow(); + }); + + it('returns the account ID when an account with that ID exists in config', () => { + mockFindup.mockReturnValueOnce('.hsaccount'); + mockFs.readFileSync.mockReturnValueOnce('123'); + mockGetAllConfigAccounts.mockReturnValueOnce([PAK_ACCOUNT]); + expect(getDefaultAccountOverrideAccountId()).toBe(123); + }); + }); + + describe('getDefaultAccountOverrideFilePath()', () => { + it('returns the path to the override file if one exists', () => { + mockFindup.mockReturnValueOnce('.hsaccount'); + expect(getDefaultAccountOverrideFilePath()).toBe('.hsaccount'); + }); + + it('returns null if no override file exists', () => { + mockFindup.mockReturnValueOnce(null); + expect(getDefaultAccountOverrideFilePath()).toBeNull(); + }); + }); +}); diff --git a/config/__tests__/environment.test.ts b/config/__tests__/environment.test.ts deleted file mode 100644 index 62b8015f..00000000 --- a/config/__tests__/environment.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { loadConfigFromEnvironment } from '../environment'; -import { ENVIRONMENT_VARIABLES } from '../../constants/environments'; -import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; - -describe('config/environment', () => { - describe('loadConfigFromEnvironment()', () => { - const INITIAL_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...INITIAL_ENV }; - }); - - afterAll(() => { - process.env = INITIAL_ENV; - }); - - it('returns null when no accountId exists', () => { - const config = loadConfigFromEnvironment(); - expect(config).toBe(null); - }); - - it('returns null when no env exists', () => { - process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '1234'; - - const config = loadConfigFromEnvironment(); - expect(config).toBe(null); - }); - - it('generates a personal access key config from the env', () => { - process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '1234'; - process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; - process.env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY] = - 'personal-access-key'; - - const config = loadConfigFromEnvironment(); - expect(config).toMatchObject({ - accounts: [ - { - authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, - accountId: 1234, - env: 'qa', - personalAccessKey: 'personal-access-key', - }, - ], - }); - }); - }); -}); diff --git a/config/__tests__/migrate.test.ts b/config/__tests__/migrate.test.ts new file mode 100644 index 00000000..db9e2832 --- /dev/null +++ b/config/__tests__/migrate.test.ts @@ -0,0 +1,392 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +import { + getConfigAtPath, + migrateConfigAtPath, + mergeConfigProperties, + mergeConfigAccounts, +} from '../migrate'; +import { HubSpotConfig } from '../../types/Config'; +import { + getGlobalConfigFilePath, + readConfigFile, + writeConfigFile, +} from '../utils'; +import { + DEFAULT_CMS_PUBLISH_MODE, + HTTP_TIMEOUT, + ENV, + HTTP_USE_LOCALHOST, + ALLOW_USAGE_TRACKING, + DEFAULT_ACCOUNT, +} from '../../constants/config'; +import { ENVIRONMENTS } from '../../constants/environments'; +import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; +import { PersonalAccessKeyConfigAccount } from '../../types/Accounts'; +import { createEmptyConfigFile } from '../index'; + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + unlinkSync: jest.fn(), +})); + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + readConfigFile: jest.fn(), + writeConfigFile: jest.fn(), + getGlobalConfigFilePath: jest.fn(), +})); + +jest.mock('../index', () => ({ + ...jest.requireActual('../index'), + createEmptyConfigFile: jest.fn(), +})); + +describe('config/migrate', () => { + let mockConfig: HubSpotConfig; + let mockConfigSource: string; + let mockConfigPath: string; + let mockGlobalConfigPath: string; + + beforeEach(() => { + jest.clearAllMocks(); + + mockConfig = { + accounts: [ + { + accountId: 123456, + name: 'Test Account', + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'test-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, + } as PersonalAccessKeyConfigAccount, + ], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }; + + mockConfigSource = JSON.stringify(mockConfig); + mockConfigPath = '/path/to/config.yml'; + mockGlobalConfigPath = path.join(os.homedir(), '.hscli', 'config.yml'); + + (readConfigFile as jest.Mock).mockReturnValue(mockConfigSource); + (getGlobalConfigFilePath as jest.Mock).mockReturnValue( + mockGlobalConfigPath + ); + }); + + describe('getConfigAtPath', () => { + it('should read and parse config from the given path', () => { + const result = getConfigAtPath(mockConfigPath); + + expect(readConfigFile).toHaveBeenCalledWith(mockConfigPath); + expect(result).toEqual(mockConfig); + }); + }); + + describe('migrateConfigAtPath', () => { + it('should migrate config from the given path to the global config path', () => { + (createEmptyConfigFile as jest.Mock).mockImplementation(() => undefined); + migrateConfigAtPath(mockConfigPath); + + expect(createEmptyConfigFile).toHaveBeenCalledWith(true); + expect(readConfigFile).toHaveBeenCalledWith(mockConfigPath); + expect(writeConfigFile).toHaveBeenCalledWith( + mockConfig, + mockGlobalConfigPath + ); + expect(fs.unlinkSync).toHaveBeenCalledWith(mockConfigPath); + }); + }); + + describe('mergeConfigProperties', () => { + it('should merge properties from fromConfig to toConfig without conflicts when force is false', () => { + // Arrange + const toConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'publish', + httpTimeout: 3000, + env: ENVIRONMENTS.QA, + httpUseLocalhost: true, + allowUsageTracking: false, + defaultAccount: 654321, + }; + + const fromConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }; + + const result = mergeConfigProperties(toConfig, fromConfig); + + expect(result.configWithMergedProperties).toEqual(toConfig); + expect(result.conflicts).toHaveLength(6); + expect(result.conflicts).toContainEqual({ + property: DEFAULT_CMS_PUBLISH_MODE, + oldValue: 'draft', + newValue: 'publish', + }); + expect(result.conflicts).toContainEqual({ + property: HTTP_TIMEOUT, + oldValue: 5000, + newValue: 3000, + }); + expect(result.conflicts).toContainEqual({ + property: ENV, + oldValue: ENVIRONMENTS.PROD, + newValue: ENVIRONMENTS.QA, + }); + expect(result.conflicts).toContainEqual({ + property: HTTP_USE_LOCALHOST, + oldValue: false, + newValue: true, + }); + expect(result.conflicts).toContainEqual({ + property: ALLOW_USAGE_TRACKING, + oldValue: true, + newValue: false, + }); + expect(result.conflicts).toContainEqual({ + property: DEFAULT_ACCOUNT, + oldValue: 123456, + newValue: 654321, + }); + }); + + it('should merge properties from fromConfig to toConfig without conflicts when force is true', () => { + const toConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'publish', + httpTimeout: 3000, + env: ENVIRONMENTS.QA, + httpUseLocalhost: true, + allowUsageTracking: false, + defaultAccount: 654321, + }; + + const fromConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }; + + const result = mergeConfigProperties(toConfig, fromConfig, true); + + expect(result.configWithMergedProperties).toEqual({ + ...toConfig, + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }); + expect(result.conflicts).toHaveLength(0); + }); + + it('should merge properties from fromConfig to toConfig when toConfig has missing properties', () => { + const toConfig: HubSpotConfig = { + accounts: [], + }; + + const fromConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }; + + const result = mergeConfigProperties(toConfig, fromConfig); + + expect(result.configWithMergedProperties).toEqual({ + ...toConfig, + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + httpUseLocalhost: false, + allowUsageTracking: true, + defaultAccount: 123456, + }); + expect(result.conflicts).toHaveLength(0); + }); + }); + + describe('mergeConfigAccounts', () => { + it('should merge accounts from fromConfig to toConfig and skip existing accounts', () => { + const existingAccount = { + accountId: 123456, + name: 'Existing Account', + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'existing-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, + } as PersonalAccessKeyConfigAccount; + + const newAccount = { + accountId: 789012, + name: 'New Account', + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'new-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, + } as PersonalAccessKeyConfigAccount; + + const toConfig: HubSpotConfig = { + accounts: [existingAccount], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const fromConfig: HubSpotConfig = { + accounts: [existingAccount, newAccount], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const result = mergeConfigAccounts(toConfig, fromConfig); + + expect(result.configWithMergedAccounts.accounts).toHaveLength(2); + expect(result.configWithMergedAccounts.accounts).toContainEqual( + existingAccount + ); + expect(result.configWithMergedAccounts.accounts).toContainEqual( + newAccount + ); + expect(result.skippedAccountIds).toEqual([123456]); + expect(writeConfigFile).toHaveBeenCalledWith( + result.configWithMergedAccounts, + mockGlobalConfigPath + ); + }); + + it('should handle empty accounts arrays', () => { + const toConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const fromConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const result = mergeConfigAccounts(toConfig, fromConfig); + + expect(result.configWithMergedAccounts.accounts).toHaveLength(0); + expect(result.skippedAccountIds).toEqual([]); + expect(writeConfigFile).toHaveBeenCalledWith( + result.configWithMergedAccounts, + mockGlobalConfigPath + ); + }); + + it('should handle case when fromConfig has no accounts', () => { + const existingAccount = { + accountId: 123456, + name: 'Existing Account', + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'existing-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, + } as PersonalAccessKeyConfigAccount; + + const toConfig: HubSpotConfig = { + accounts: [existingAccount], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const fromConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const result = mergeConfigAccounts(toConfig, fromConfig); + + expect(result.configWithMergedAccounts.accounts).toHaveLength(1); + expect(result.configWithMergedAccounts.accounts).toContainEqual( + existingAccount + ); + expect(result.skippedAccountIds).toEqual([]); + expect(writeConfigFile).toHaveBeenCalledWith( + result.configWithMergedAccounts, + mockGlobalConfigPath + ); + }); + + it('should handle case when toConfig has no accounts', () => { + const newAccount = { + accountId: 789012, + name: 'New Account', + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'new-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, + } as PersonalAccessKeyConfigAccount; + + const toConfig: HubSpotConfig = { + accounts: [], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const fromConfig: HubSpotConfig = { + accounts: [newAccount], + defaultCmsPublishMode: 'draft', + httpTimeout: 5000, + env: ENVIRONMENTS.PROD, + }; + + const result = mergeConfigAccounts(toConfig, fromConfig); + + expect(result.configWithMergedAccounts.accounts).toHaveLength(1); + expect(result.configWithMergedAccounts.accounts).toContainEqual( + newAccount + ); + expect(result.skippedAccountIds).toEqual([]); + expect(writeConfigFile).toHaveBeenCalledWith( + result.configWithMergedAccounts, + mockGlobalConfigPath + ); + }); + }); +}); diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts new file mode 100644 index 00000000..65a9715e --- /dev/null +++ b/config/__tests__/utils.test.ts @@ -0,0 +1,398 @@ +import findup from 'findup-sync'; +import fs from 'fs-extra'; +import { + getGlobalConfigFilePath, + getLocalConfigFilePath, + getLocalConfigDefaultFilePath, + getConfigPathEnvironmentVariables, + readConfigFile, + removeUndefinedFieldsFromConfigAccount, + writeConfigFile, + normalizeParsedConfig, + buildConfigFromEnvironment, + getConfigAccountByIdentifier, + getConfigAccountIndexById, + isConfigAccountValid, + getAccountIdentifierAndType, +} from '../utils'; +import { getCwd } from '../../lib/path'; +import { + DeprecatedHubSpotConfigAccountFields, + HubSpotConfigAccount, + PersonalAccessKeyConfigAccount, + OAuthConfigAccount, + APIKeyConfigAccount, +} from '../../types/Accounts'; +import { + DeprecatedHubSpotConfigFields, + HubSpotConfig, +} from '../../types/Config'; +import { + ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIGURATION_FOLDER, +} from '../../constants/config'; +import { FileSystemError } from '../../models/FileSystemError'; +import { + PERSONAL_ACCESS_KEY_AUTH_METHOD, + OAUTH_AUTH_METHOD, + API_KEY_AUTH_METHOD, +} from '../../constants/auth'; + +jest.mock('findup-sync'); +jest.mock('../../lib/path'); +jest.mock('fs-extra'); + +const mockFindup = findup as jest.MockedFunction; +const mockCwd = getCwd as jest.MockedFunction; +const mockFs = fs as jest.Mocked; + +const PAK_ACCOUNT: PersonalAccessKeyConfigAccount = { + name: 'test-account', + accountId: 123, + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'test-key', + env: 'qa', + auth: { + tokenInfo: {}, + }, + accountType: 'STANDARD', +}; + +const OAUTH_ACCOUNT: OAuthConfigAccount = { + accountId: 123, + env: 'qa', + name: '123', + authType: OAUTH_AUTH_METHOD.value, + accountType: undefined, + auth: { + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + tokenInfo: { + refreshToken: 'test-refresh-token', + }, + scopes: ['content', 'hubdb', 'files'], + }, +}; + +const API_KEY_ACCOUNT: APIKeyConfigAccount = { + accountId: 123, + env: 'qa', + name: '123', + authType: API_KEY_AUTH_METHOD.value, + accountType: undefined, + apiKey: 'test-api-key', +}; + +const DEPRECATED_ACCOUNT: HubSpotConfigAccount & + DeprecatedHubSpotConfigAccountFields = { + name: 'test-account', + portalId: 123, + accountId: 1, + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + personalAccessKey: 'test-key', + env: 'qa', + auth: { + tokenInfo: {}, + }, + accountType: undefined, +}; + +const CONFIG: HubSpotConfig = { + defaultAccount: PAK_ACCOUNT.accountId, + accounts: [PAK_ACCOUNT], + defaultCmsPublishMode: 'publish', + httpTimeout: 1000, + httpUseLocalhost: true, + allowUsageTracking: true, +}; + +const DEPRECATED_CONFIG: HubSpotConfig & DeprecatedHubSpotConfigFields = { + accounts: [], + defaultAccount: 1, + portals: [DEPRECATED_ACCOUNT], + defaultPortal: DEPRECATED_ACCOUNT.name, + defaultCmsPublishMode: undefined, + defaultMode: 'publish', + httpTimeout: 1000, + httpUseLocalhost: true, + allowUsageTracking: true, +}; + +function cleanupEnvironmentVariables() { + Object.keys(ENVIRONMENT_VARIABLES).forEach(key => { + delete process.env[key]; + }); +} + +describe('config/utils', () => { + beforeEach(() => { + cleanupEnvironmentVariables(); + }); + + afterEach(() => { + cleanupEnvironmentVariables(); + }); + + describe('getGlobalConfigFilePath()', () => { + it('returns the global config file path', () => { + const globalConfigFilePath = getGlobalConfigFilePath(); + expect(globalConfigFilePath).toBeDefined(); + expect(globalConfigFilePath).toContain( + `${HUBSPOT_CONFIGURATION_FOLDER}/config.yml` + ); + }); + }); + + describe('getLocalConfigFilePath()', () => { + it('returns the nearest config file path', () => { + const mockConfigPath = '/mock/path/hubspot.config.yml'; + mockFindup.mockReturnValue(mockConfigPath); + + const localConfigPath = getLocalConfigFilePath(); + expect(localConfigPath).toBe(mockConfigPath); + }); + + it('returns null if no config file found', () => { + mockFindup.mockReturnValue(null); + const localConfigPath = getLocalConfigFilePath(); + expect(localConfigPath).toBeNull(); + }); + }); + + describe('getLocalConfigDefaultFilePath()', () => { + it('returns the default config path in current directory', () => { + const mockCwdPath = '/mock/cwd'; + mockCwd.mockReturnValue(mockCwdPath); + + const defaultPath = getLocalConfigDefaultFilePath(); + expect(defaultPath).toBe(`${mockCwdPath}/hubspot.config.yml`); + }); + }); + + describe('getConfigPathEnvironmentVariables()', () => { + it('returns environment config settings', () => { + const configPath = 'config/path'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH] = configPath; + + const result = getConfigPathEnvironmentVariables(); + expect(result.useEnvironmentConfig).toBe(false); + expect(result.configFilePathFromEnvironment).toBe(configPath); + }); + + it('throws when both environment variables are set', () => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH] = 'path'; + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] = + 'true'; + + expect(() => getConfigPathEnvironmentVariables()).toThrow(); + }); + }); + + describe('readConfigFile()', () => { + it('reads and returns file contents', () => { + mockFs.readFileSync.mockReturnValue('config contents'); + const result = readConfigFile('test'); + expect(result).toBe('config contents'); + }); + + it('throws FileSystemError on read failure', () => { + mockFs.readFileSync.mockImplementation(() => { + throw new Error('Read error'); + }); + + expect(() => readConfigFile('test')).toThrow(FileSystemError); + }); + }); + + describe('removeUndefinedFieldsFromConfigAccount()', () => { + it('removes undefined fields from account', () => { + const accountWithUndefinedFields = { + ...PAK_ACCOUNT, + defaultCmsPublishMode: undefined, + auth: { + ...PAK_ACCOUNT.auth, + tokenInfo: { + refreshToken: undefined, + }, + }, + }; + + const withFieldsRemoved = removeUndefinedFieldsFromConfigAccount( + accountWithUndefinedFields + ); + + expect(withFieldsRemoved).toEqual(PAK_ACCOUNT); + }); + }); + + describe('writeConfigFile()', () => { + it('writes formatted config to file', () => { + mockFs.ensureFileSync.mockImplementation(() => undefined); + mockFs.writeFileSync.mockImplementation(() => undefined); + + writeConfigFile(CONFIG, 'test.yml'); + + expect(mockFs.writeFileSync).toHaveBeenCalled(); + }); + + it('throws FileSystemError on write failure', () => { + mockFs.writeFileSync.mockImplementation(() => { + throw new Error('Write error'); + }); + + expect(() => writeConfigFile(CONFIG, 'test.yml')).toThrow( + FileSystemError + ); + }); + }); + + describe('normalizeParsedConfig()', () => { + it('converts portal fields to account fields', () => { + const normalizedConfig = normalizeParsedConfig(DEPRECATED_CONFIG); + expect(normalizedConfig).toEqual(CONFIG); + }); + }); + + describe('buildConfigFromEnvironment()', () => { + it('builds personal access key config', () => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY] = + 'test-key'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '123'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; + process.env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] = '1000'; + process.env[ENVIRONMENT_VARIABLES.HTTP_USE_LOCALHOST] = 'true'; + process.env[ENVIRONMENT_VARIABLES.ALLOW_USAGE_TRACKING] = 'true'; + process.env[ENVIRONMENT_VARIABLES.DEFAULT_CMS_PUBLISH_MODE] = 'publish'; + + const config = buildConfigFromEnvironment(); + + expect(config).toEqual({ + ...CONFIG, + accounts: [{ ...PAK_ACCOUNT, name: '123', accountType: undefined }], + }); + }); + + it('builds OAuth config', () => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_ID] = 'test-client-id'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_SECRET] = + 'test-client-secret'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_REFRESH_TOKEN] = + 'test-refresh-token'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '123'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; + process.env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] = '1000'; + process.env[ENVIRONMENT_VARIABLES.HTTP_USE_LOCALHOST] = 'true'; + process.env[ENVIRONMENT_VARIABLES.ALLOW_USAGE_TRACKING] = 'true'; + process.env[ENVIRONMENT_VARIABLES.DEFAULT_CMS_PUBLISH_MODE] = 'publish'; + + const config = buildConfigFromEnvironment(); + + expect(config).toEqual({ + ...CONFIG, + accounts: [OAUTH_ACCOUNT], + }); + }); + + it('throws when required variables missing', () => { + expect(() => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '123'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; + buildConfigFromEnvironment(); + }).toThrow(); + }); + }); + + describe('getConfigAccountByIdentifier()', () => { + it('finds account by name', () => { + const account = getConfigAccountByIdentifier( + CONFIG.accounts, + 'name', + 'test-account' + ); + + expect(account).toEqual(PAK_ACCOUNT); + }); + + it('finds account by accountId', () => { + const account = getConfigAccountByIdentifier( + CONFIG.accounts, + 'accountId', + 123 + ); + + expect(account).toEqual(PAK_ACCOUNT); + }); + + it('returns undefined when account not found', () => { + const account = getConfigAccountByIdentifier( + CONFIG.accounts, + 'accountId', + 1234 + ); + + expect(account).toBeUndefined(); + }); + }); + + describe('getConfigAccountIndexById()', () => { + it('returns correct index for existing account', () => { + const index = getConfigAccountIndexById(CONFIG.accounts, 123); + expect(index).toBe(0); + }); + + it('returns -1 when account not found', () => { + const index = getConfigAccountIndexById(CONFIG.accounts, 1234); + expect(index).toBe(-1); + }); + }); + + describe('isConfigAccountValid()', () => { + it('validates personal access key account', () => { + expect(isConfigAccountValid(PAK_ACCOUNT)).toBe(true); + }); + + it('validates OAuth account', () => { + expect(isConfigAccountValid(OAUTH_ACCOUNT)).toBe(true); + }); + + it('validates API key account', () => { + expect(isConfigAccountValid(API_KEY_ACCOUNT)).toBe(true); + }); + + it('returns false for invalid account', () => { + expect( + isConfigAccountValid({ + ...PAK_ACCOUNT, + personalAccessKey: undefined, + }) + ).toBe(false); + expect( + isConfigAccountValid({ + ...PAK_ACCOUNT, + accountId: undefined, + }) + ).toBe(false); + }); + }); + + describe('getAccountIdentifierAndType()', () => { + it('returns name identifier for string', () => { + const { identifier, identifierType } = + getAccountIdentifierAndType('test-account'); + expect(identifier).toBe('test-account'); + expect(identifierType).toBe('name'); + }); + + it('returns accountId identifier for number', () => { + const { identifier, identifierType } = getAccountIdentifierAndType(123); + expect(identifier).toBe(123); + expect(identifierType).toBe('accountId'); + }); + + it('returns accountId identifier for numeric string', () => { + const { identifier, identifierType } = getAccountIdentifierAndType('123'); + expect(identifier).toBe(123); + expect(identifierType).toBe('accountId'); + }); + }); +}); diff --git a/config/configFile.ts b/config/configFile.ts deleted file mode 100644 index 89d884e7..00000000 --- a/config/configFile.ts +++ /dev/null @@ -1,111 +0,0 @@ -import fs from 'fs-extra'; -import yaml from 'js-yaml'; -import { logger } from '../lib/logger'; -import { GLOBAL_CONFIG_PATH } from '../constants/config'; -import { getOrderedConfig } from './configUtils'; -import { CLIConfig_NEW } from '../types/Config'; -import { i18n } from '../utils/lang'; -import { FileSystemError } from '../models/FileSystemError'; - -const i18nKey = 'config.configFile'; - -export function getConfigFilePath(): string { - return GLOBAL_CONFIG_PATH; -} - -export function configFileExists(): boolean { - const configPath = getConfigFilePath(); - return !!configPath && fs.existsSync(configPath); -} - -export function configFileIsBlank(): boolean { - const configPath = getConfigFilePath(); - return !!configPath && fs.readFileSync(configPath).length === 0; -} - -export function deleteConfigFile(): void { - const configPath = getConfigFilePath(); - fs.unlinkSync(configPath); -} - -/** - * @throws {Error} - */ -export function readConfigFile(configPath: string): string { - let source = ''; - - try { - source = fs.readFileSync(configPath).toString(); - } catch (err) { - logger.debug(i18n(`${i18nKey}.errorReading`, { configPath })); - throw new FileSystemError( - { cause: err }, - { - filepath: configPath, - operation: 'read', - } - ); - } - - return source; -} - -/** - * @throws {Error} - */ -export function parseConfig(configSource: string): CLIConfig_NEW { - let parsed: CLIConfig_NEW; - - try { - parsed = yaml.load(configSource) as CLIConfig_NEW; - } catch (err) { - throw new Error(i18n(`${i18nKey}.errors.parsing`), { cause: err }); - } - - return parsed; -} - -/** - * @throws {Error} - */ -export function loadConfigFromFile(): CLIConfig_NEW | null { - const configPath = getConfigFilePath(); - - if (configPath) { - const source = readConfigFile(configPath); - - if (!source) { - return null; - } - - return parseConfig(source); - } - - logger.debug(i18n(`${i18nKey}.errorLoading`, { configPath })); - - return null; -} - -/** - * @throws {Error} - */ -export function writeConfigToFile(config: CLIConfig_NEW): void { - const source = yaml.dump( - JSON.parse(JSON.stringify(getOrderedConfig(config), null, 2)) - ); - const configPath = getConfigFilePath(); - - try { - fs.ensureFileSync(configPath); - fs.writeFileSync(configPath, source); - logger.debug(i18n(`${i18nKey}.writeSuccess`, { configPath })); - } catch (err) { - throw new FileSystemError( - { cause: err }, - { - filepath: configPath, - operation: 'write', - } - ); - } -} diff --git a/config/configUtils.ts b/config/configUtils.ts deleted file mode 100644 index 04c42265..00000000 --- a/config/configUtils.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { logger } from '../lib/logger'; -import { - API_KEY_AUTH_METHOD, - OAUTH_AUTH_METHOD, - PERSONAL_ACCESS_KEY_AUTH_METHOD, -} from '../constants/auth'; -import { CLIConfig_NEW } from '../types/Config'; -import { - AuthType, - CLIAccount_NEW, - APIKeyAccount_NEW, - OAuthAccount_NEW, - PersonalAccessKeyAccount_NEW, - PersonalAccessKeyOptions, - OAuthOptions, - APIKeyOptions, -} from '../types/Accounts'; -import { i18n } from '../utils/lang'; - -const i18nKey = 'config.configUtils'; - -export function getOrderedAccount( - unorderedAccount: CLIAccount_NEW -): CLIAccount_NEW { - const { name, accountId, env, authType, ...rest } = unorderedAccount; - - return { - name, - accountId, - env, - authType, - ...rest, - }; -} - -export function getOrderedConfig( - unorderedConfig: CLIConfig_NEW -): CLIConfig_NEW { - const { - defaultAccount, - defaultCmsPublishMode, - httpTimeout, - allowUsageTracking, - accounts, - ...rest - } = unorderedConfig; - - return { - ...(defaultAccount && { defaultAccount }), - defaultCmsPublishMode, - httpTimeout, - allowUsageTracking, - ...rest, - accounts: accounts.map(getOrderedAccount), - }; -} - -function generatePersonalAccessKeyAccountConfig({ - accountId, - personalAccessKey, - env, -}: PersonalAccessKeyOptions): PersonalAccessKeyAccount_NEW { - return { - authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, - accountId, - personalAccessKey, - env, - }; -} - -function generateOauthAccountConfig({ - accountId, - clientId, - clientSecret, - refreshToken, - scopes, - env, -}: OAuthOptions): OAuthAccount_NEW { - return { - authType: OAUTH_AUTH_METHOD.value, - accountId, - auth: { - clientId, - clientSecret, - scopes, - tokenInfo: { - refreshToken, - }, - }, - env, - }; -} - -function generateApiKeyAccountConfig({ - accountId, - apiKey, - env, -}: APIKeyOptions): APIKeyAccount_NEW { - return { - authType: API_KEY_AUTH_METHOD.value, - accountId, - apiKey, - env, - }; -} - -export function generateConfig( - type: AuthType, - options: PersonalAccessKeyOptions | OAuthOptions | APIKeyOptions -): CLIConfig_NEW | null { - if (!options) { - return null; - } - const config: CLIConfig_NEW = { accounts: [] }; - let configAccount: CLIAccount_NEW; - - switch (type) { - case API_KEY_AUTH_METHOD.value: - configAccount = generateApiKeyAccountConfig(options as APIKeyOptions); - break; - case PERSONAL_ACCESS_KEY_AUTH_METHOD.value: - configAccount = generatePersonalAccessKeyAccountConfig( - options as PersonalAccessKeyOptions - ); - break; - case OAUTH_AUTH_METHOD.value: - configAccount = generateOauthAccountConfig(options as OAuthOptions); - break; - default: - logger.debug(i18n(`${i18nKey}.unknownType`, { type })); - return null; - } - - if (configAccount) { - config.accounts.push(configAccount); - } - - return config; -} diff --git a/config/config_DEPRECATED.ts b/config/config_DEPRECATED.ts deleted file mode 100644 index 94bfaa0f..00000000 --- a/config/config_DEPRECATED.ts +++ /dev/null @@ -1,918 +0,0 @@ -import fs from 'fs-extra'; -import yaml from 'js-yaml'; -import findup from 'findup-sync'; -import { getCwd } from '../lib/path'; -import { - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, - MIN_HTTP_TIMEOUT, - HUBSPOT_ACCOUNT_TYPES, -} from '../constants/config'; -import { ENVIRONMENTS, ENVIRONMENT_VARIABLES } from '../constants/environments'; -import { - API_KEY_AUTH_METHOD, - OAUTH_AUTH_METHOD, - PERSONAL_ACCESS_KEY_AUTH_METHOD, - OAUTH_SCOPES, -} from '../constants/auth'; -import { CMS_PUBLISH_MODE } from '../constants/files'; -import { getValidEnv } from '../lib/environment'; -import { logger } from '../lib/logger'; -import { isConfigPathInGitRepo } from '../utils/git'; -import { - logErrorInstance, - logFileSystemErrorInstance, -} from '../errors/errors_DEPRECATED'; -import { CLIConfig_DEPRECATED, Environment } from '../types/Config'; -import { - APIKeyAccount_DEPRECATED, - AccountType, - CLIAccount_DEPRECATED, - FlatAccountFields_DEPRECATED, - OAuthAccount_DEPRECATED, - UpdateAccountConfigOptions, -} from '../types/Accounts'; -import { BaseError } from '../types/Error'; -import { CmsPublishMode } from '../types/Files'; -import { CLIOptions, WriteConfigOptions } from '../types/CLIOptions'; - -const ALL_CMS_PUBLISH_MODES = Object.values(CMS_PUBLISH_MODE); -let _config: CLIConfig_DEPRECATED | null; -let _configPath: string | null; -let environmentVariableConfigLoaded = false; - -const commaSeparatedValues = ( - arr: Array, - conjunction = 'and', - ifempty = '' -): string => { - const l = arr.length; - if (!l) return ifempty; - if (l < 2) return arr[0]; - if (l < 3) return arr.join(` ${conjunction} `); - arr = arr.slice(); - arr[l - 1] = `${conjunction} ${arr[l - 1]}`; - return arr.join(', '); -}; - -export const getConfig = () => _config; - -export function setConfig( - updatedConfig?: CLIConfig_DEPRECATED -): CLIConfig_DEPRECATED | null { - _config = updatedConfig || null; - return _config; -} - -export function getConfigAccounts( - config?: CLIConfig_DEPRECATED -): Array | undefined { - const __config = config || getConfig(); - if (!__config) return; - return __config.portals; -} - -export function getConfigDefaultAccount( - config?: CLIConfig_DEPRECATED -): string | number | undefined { - const __config = config || getConfig(); - if (!__config) return; - return __config.defaultPortal; -} - -export function getConfigAccountId( - account: CLIAccount_DEPRECATED -): number | undefined { - if (!account) return; - return account.portalId; -} - -export function setConfigPath(path: string | null) { - return (_configPath = path); -} - -export function getConfigPath(path?: string | null): string | null { - return path || (configFileExists() && _configPath) || findConfig(getCwd()); -} - -export function validateConfig(): boolean { - const config = getConfig(); - if (!config) { - logger.error('No config was found'); - return false; - } - const accounts = getConfigAccounts(); - if (!Array.isArray(accounts)) { - logger.error('config.portals[] is not defined'); - return false; - } - - if (accounts.length === 0) { - logger.error('There are no accounts defined in the configuration file'); - return false; - } - - const accountIdsHash: { [id: number]: CLIAccount_DEPRECATED } = {}; - const accountNamesHash: { [name: string]: CLIAccount_DEPRECATED } = {}; - - return accounts.every(cfg => { - if (!cfg) { - logger.error('config.portals[] has an empty entry'); - return false; - } - - const accountId = getConfigAccountId(cfg); - if (!accountId) { - logger.error('config.portals[] has an entry missing portalId'); - return false; - } - if (accountIdsHash[accountId]) { - logger.error( - `config.portals[] has multiple entries with portalId=${accountId}` - ); - return false; - } - - if (cfg.name) { - if (accountNamesHash[cfg.name]) { - logger.error( - `config.name has multiple entries with portalId=${accountId}` - ); - return false; - } - if (/\s+/.test(cfg.name)) { - logger.error(`config.name '${cfg.name}' cannot contain spaces`); - return false; - } - accountNamesHash[cfg.name] = cfg; - } - - if (!cfg.accountType) { - updateAccountConfig({ - ...cfg, - portalId: accountId, - accountType: getAccountType(undefined, cfg.sandboxAccountType), - }); - writeConfig(); - } - - accountIdsHash[accountId] = cfg; - return true; - }); -} - -export function accountNameExistsInConfig(name: string): boolean { - const config = getConfig(); - const accounts = getConfigAccounts(); - - if (!config || !Array.isArray(accounts)) { - return false; - } - - return accounts.some(cfg => cfg.name && cfg.name === name); -} - -export function getOrderedAccount( - unorderedAccount: CLIAccount_DEPRECATED -): CLIAccount_DEPRECATED { - const { name, portalId, env, authType, ...rest } = unorderedAccount; - - return { - name, - ...(portalId && { portalId }), - env, - authType, - ...rest, - }; -} - -export function getOrderedConfig(unorderedConfig: CLIConfig_DEPRECATED) { - const { - defaultPortal, - defaultCmsPublishMode, - httpTimeout, - allowUsageTracking, - portals, - ...rest - } = unorderedConfig; - - return { - ...(defaultPortal && { defaultPortal }), - defaultCmsPublishMode, - httpTimeout, - allowUsageTracking, - ...rest, - portals: portals.map(getOrderedAccount), - }; -} - -export function writeConfig(options: WriteConfigOptions = {}): void { - if (environmentVariableConfigLoaded) { - return; - } - let source; - try { - source = - typeof options.source === 'string' - ? options.source - : yaml.dump( - JSON.parse(JSON.stringify(getOrderedConfig(getConfig()!), null, 2)) - ); - } catch (err) { - logErrorInstance(err as BaseError); - return; - } - const configPath = options.path || _configPath; - try { - logger.debug(`Writing current config to ${configPath}`); - fs.ensureFileSync(configPath || ''); - fs.writeFileSync(configPath || '', source); - setConfig(parseConfig(source).parsed); - } catch (err) { - logFileSystemErrorInstance(err as BaseError, { - filepath: configPath || '', - operation: 'write', - }); - } -} - -function readConfigFile(): { source?: string; error?: BaseError } { - let source; - let error; - if (!_configPath) { - return { source, error }; - } - try { - isConfigPathInGitRepo(_configPath); - source = fs.readFileSync(_configPath); - } catch (err) { - error = err as BaseError; - logger.error('Config file could not be read "%s"', _configPath); - logFileSystemErrorInstance(error, { - filepath: _configPath, - operation: 'read', - }); - } - return { source: source && source.toString(), error }; -} - -function parseConfig(configSource?: string): { - parsed?: CLIConfig_DEPRECATED; - error?: BaseError; -} { - let parsed: CLIConfig_DEPRECATED | undefined = undefined; - let error: BaseError | undefined = undefined; - if (!configSource) { - return { parsed, error }; - } - try { - parsed = yaml.load(configSource) as CLIConfig_DEPRECATED; - } catch (err) { - error = err as BaseError; - logger.error('Config file could not be parsed "%s"', _configPath); - logErrorInstance(err as BaseError); - } - return { parsed, error }; -} - -function loadConfigFromFile(path?: string, options: CLIOptions = {}) { - setConfigPath(getConfigPath(path)); - if (!_configPath) { - if (!options.silenceErrors) { - logger.error( - `A ${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME} file could not be found. To create a new config file, use the "hs init" command.` - ); - } else { - logger.debug( - `A ${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME} file could not be found` - ); - } - return; - } - - logger.debug(`Reading config from ${_configPath}`); - const { source, error: sourceError } = readConfigFile(); - if (sourceError) return; - const { parsed, error: parseError } = parseConfig(source); - if (parseError) return; - setConfig(handleLegacyCmsPublishMode(parsed)); - - if (!getConfig()) { - logger.debug('The config file was empty config'); - logger.debug('Initializing an empty config'); - setConfig({ portals: [] }); - } - - return getConfig(); -} - -export function loadConfig( - path?: string, - options: CLIOptions = { - useEnv: false, - } -): CLIConfig_DEPRECATED | null { - if (options.useEnv && loadEnvironmentVariableConfig(options)) { - logger.debug('Loaded environment variable config'); - environmentVariableConfigLoaded = true; - } else { - path && logger.debug(`Loading config from ${path}`); - loadConfigFromFile(path, options); - environmentVariableConfigLoaded = false; - } - - return getConfig(); -} - -export function isTrackingAllowed(): boolean { - if (!configFileExists() || configFileIsBlank()) { - return true; - } - const { allowUsageTracking } = getAndLoadConfigIfNeeded(); - return allowUsageTracking !== false; -} - -export function getAndLoadConfigIfNeeded( - options = {} -): Partial { - if (!getConfig()) { - loadConfig('', { - silenceErrors: true, - ...options, - }); - } - return getConfig() || { allowUsageTracking: undefined }; -} - -export function findConfig(directory: string): string | null { - return findup( - [ - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), - ], - { cwd: directory } - ); -} - -export function getEnv(nameOrId?: string | number): Environment { - let env: Environment = ENVIRONMENTS.PROD; - const config = getAndLoadConfigIfNeeded(); - const accountId = getAccountId(nameOrId); - - if (accountId) { - const accountConfig = getAccountConfig(accountId); - if (accountConfig && accountConfig.env) { - env = accountConfig.env; - } - } else if (config && config.env) { - env = config.env; - } - return env; -} - -// Deprecating sandboxAccountType in favor of accountType -export function getAccountType( - accountType?: AccountType, - sandboxAccountType?: string | null -): AccountType { - if (accountType) { - return accountType; - } - if (typeof sandboxAccountType === 'string') { - if (sandboxAccountType.toUpperCase() === 'DEVELOPER') { - return HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX; - } - if (sandboxAccountType.toUpperCase() === 'STANDARD') { - return HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX; - } - } - return HUBSPOT_ACCOUNT_TYPES.STANDARD; -} - -export function getAccountConfig( - accountId: number | undefined -): CLIAccount_DEPRECATED | undefined { - return getConfigAccounts( - getAndLoadConfigIfNeeded() as CLIConfig_DEPRECATED - )?.find(account => account.portalId === accountId); -} - -/* - * Returns a portalId from the config if it exists, else returns null - */ -export function getAccountId(nameOrId?: string | number): number | undefined { - const config = getAndLoadConfigIfNeeded() as CLIConfig_DEPRECATED; - let name: string | undefined = undefined; - let accountId: number | undefined = undefined; - let account: CLIAccount_DEPRECATED | undefined = undefined; - - function setNameOrAccountFromSuppliedValue( - suppliedValue: string | number - ): void { - if (typeof suppliedValue === 'number') { - accountId = suppliedValue; - } else if (/^\d+$/.test(suppliedValue)) { - accountId = parseInt(suppliedValue, 10); - } else { - name = suppliedValue; - } - } - - if (!nameOrId) { - const defaultAccount = getConfigDefaultAccount(config); - - if (defaultAccount) { - setNameOrAccountFromSuppliedValue(defaultAccount); - } - } else { - setNameOrAccountFromSuppliedValue(nameOrId); - } - - const accounts = getConfigAccounts(config); - if (name && accounts) { - account = accounts.find(p => p.name === name); - } else if (accountId && accounts) { - account = accounts.find(p => accountId === p.portalId); - } - - if (account) { - return account.portalId; - } - - return undefined; -} - -/** - * @throws {Error} - */ -export function removeSandboxAccountFromConfig( - nameOrId: string | number -): boolean { - const config = getAndLoadConfigIfNeeded(); - const accountId = getAccountId(nameOrId); - let promptDefaultAccount = false; - - if (!accountId) { - throw new Error(`Unable to find account for ${nameOrId}.`); - } - - const accountConfig = getAccountConfig(accountId); - - const accountType = getAccountType( - accountConfig?.accountType, - accountConfig?.sandboxAccountType - ); - - const isSandboxAccount = - accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX || - accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX; - - if (!isSandboxAccount) return promptDefaultAccount; - - if (config.defaultPortal === accountConfig?.name) { - promptDefaultAccount = true; - } - - const accounts = getConfigAccounts(config as CLIConfig_DEPRECATED); - - if (accountConfig && accounts) { - logger.debug(`Deleting config for ${accountId}`); - const index = accounts.indexOf(accountConfig); - accounts.splice(index, 1); - } - - writeConfig(); - - return promptDefaultAccount; -} - -/** - * @throws {Error} - */ -export function updateAccountConfig( - configOptions: UpdateAccountConfigOptions -): FlatAccountFields_DEPRECATED { - const { - accountType, - apiKey, - authType, - clientId, - clientSecret, - defaultCmsPublishMode, - environment, - name, - parentAccountId, - personalAccessKey, - portalId, - sandboxAccountType, - scopes, - tokenInfo, - } = configOptions; - - if (!portalId) { - throw new Error('A portalId is required to update the config'); - } - - const config = getAndLoadConfigIfNeeded() as CLIConfig_DEPRECATED; - const accountConfig = getAccountConfig(portalId); - - let auth: OAuthAccount_DEPRECATED['auth'] | undefined = - accountConfig && accountConfig.auth; - if (clientId || clientSecret || scopes || tokenInfo) { - auth = { - ...(accountConfig ? accountConfig.auth : {}), - clientId, - clientSecret, - scopes, - tokenInfo, - }; - } - - const env = getValidEnv( - environment || - (configOptions && configOptions.env) || - (accountConfig && accountConfig.env) - ); - const cmsPublishMode: CmsPublishMode | undefined = - defaultCmsPublishMode?.toLowerCase() as CmsPublishMode; - const nextAccountConfig: FlatAccountFields_DEPRECATED = { - ...accountConfig, - name: name || (accountConfig && accountConfig.name), - env, - ...(portalId && { portalId }), - authType, - auth, - accountType: getAccountType(accountType, sandboxAccountType), - apiKey, - defaultCmsPublishMode: - cmsPublishMode && Object.hasOwn(CMS_PUBLISH_MODE, cmsPublishMode) - ? cmsPublishMode - : undefined, - personalAccessKey, - sandboxAccountType, - parentAccountId, - }; - - let accounts = getConfigAccounts(config); - if (accountConfig && accounts) { - logger.debug(`Updating config for ${portalId}`); - const index = accounts.indexOf(accountConfig); - accounts[index] = nextAccountConfig; - } else { - logger.debug(`Adding config entry for ${portalId}`); - if (accounts) { - accounts.push(nextAccountConfig); - } else { - accounts = [nextAccountConfig]; - } - } - - return nextAccountConfig; -} - -/** - * @throws {Error} - */ -export function updateDefaultAccount(defaultAccount: string | number): void { - if ( - !defaultAccount || - (typeof defaultAccount !== 'number' && typeof defaultAccount !== 'string') - ) { - throw new Error( - `A 'defaultPortal' with value of number or string is required to update the config` - ); - } - - const config = getAndLoadConfigIfNeeded(); - config.defaultPortal = defaultAccount; - - setDefaultConfigPathIfUnset(); - writeConfig(); -} - -/** - * @throws {Error} - */ -export function updateDefaultCmsPublishMode( - defaultCmsPublishMode: CmsPublishMode -): void { - if ( - !defaultCmsPublishMode || - !ALL_CMS_PUBLISH_MODES.find(m => m === defaultCmsPublishMode) - ) { - throw new Error( - `The mode ${defaultCmsPublishMode} is invalid. Valid values are ${commaSeparatedValues( - ALL_CMS_PUBLISH_MODES - )}.` - ); - } - - const config = getAndLoadConfigIfNeeded(); - config.defaultCmsPublishMode = defaultCmsPublishMode; - - setDefaultConfigPathIfUnset(); - writeConfig(); -} - -/** - * @throws {Error} - */ -export function updateHttpTimeout(timeout: string): void { - const parsedTimeout = parseInt(timeout); - if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { - throw new Error( - `The value ${timeout} is invalid. The value must be a number greater than ${MIN_HTTP_TIMEOUT}.` - ); - } - - const config = getAndLoadConfigIfNeeded(); - config.httpTimeout = parsedTimeout; - - setDefaultConfigPathIfUnset(); - writeConfig(); -} - -/** - * @throws {Error} - */ -export function updateAllowUsageTracking(isEnabled: boolean): void { - if (typeof isEnabled !== 'boolean') { - throw new Error( - `Unable to update allowUsageTracking. The value ${isEnabled} is invalid. The value must be a boolean.` - ); - } - - const config = getAndLoadConfigIfNeeded(); - config.allowUsageTracking = isEnabled; - - setDefaultConfigPathIfUnset(); - writeConfig(); -} - -/** - * @throws {Error} - */ -export async function renameAccount( - currentName: string, - newName: string -): Promise { - const accountId = getAccountId(currentName); - const accountConfigToRename = getAccountConfig(accountId); - const defaultAccount = getConfigDefaultAccount(); - - if (!accountConfigToRename) { - throw new Error(`Cannot find account with identifier ${currentName}`); - } - - await updateAccountConfig({ - ...accountConfigToRename, - name: newName, - }); - - if (accountConfigToRename.name === defaultAccount) { - updateDefaultAccount(newName); - } - - return writeConfig(); -} - -/** - * @throws {Error} - */ -export async function deleteAccount(accountName: string): Promise { - const config = getAndLoadConfigIfNeeded() as CLIConfig_DEPRECATED; - const accounts = getConfigAccounts(config); - const accountIdToDelete = getAccountId(accountName); - - if (!accountIdToDelete || !accounts) { - throw new Error(`Cannot find account with identifier ${accountName}`); - } - - setConfig({ - ...config, - defaultPortal: - config.defaultPortal === accountName || - config.defaultPortal === accountIdToDelete - ? undefined - : config.defaultPortal, - portals: accounts.filter(account => account.portalId !== accountIdToDelete), - }); - - return writeConfig(); -} - -function setDefaultConfigPathIfUnset(): void { - if (!_configPath) { - setDefaultConfigPath(); - } -} - -function setDefaultConfigPath(): void { - setConfigPath(`${getCwd()}/${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}`); -} - -function configFileExists(): boolean { - return Boolean(_configPath && fs.existsSync(_configPath)); -} - -function configFileIsBlank(): boolean { - return Boolean(_configPath && fs.readFileSync(_configPath).length === 0); -} - -export function createEmptyConfigFile({ path }: { path?: string } = {}): void { - if (!path) { - setDefaultConfigPathIfUnset(); - - if (configFileExists()) { - return; - } - } else { - setConfigPath(path); - } - - writeConfig({ source: '', path }); -} - -export function deleteEmptyConfigFile(): void { - configFileExists() && configFileIsBlank() && fs.unlinkSync(_configPath || ''); -} - -export function deleteConfigFile(): void { - configFileExists() && fs.unlinkSync(_configPath || ''); -} - -function getConfigVariablesFromEnv() { - const env = process.env; - - return { - apiKey: env[ENVIRONMENT_VARIABLES.HUBSPOT_API_KEY], - clientId: env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_ID], - clientSecret: env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_SECRET], - personalAccessKey: env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY], - portalId: - parseInt(env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] || '', 10) || - parseInt(env[ENVIRONMENT_VARIABLES.HUBSPOT_PORTAL_ID] || '', 10), - refreshToken: env[ENVIRONMENT_VARIABLES.HUBSPOT_REFRESH_TOKEN], - httpTimeout: env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] - ? parseInt(env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT] as string) - : undefined, - env: getValidEnv( - env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] as Environment - ), - }; -} - -function generatePersonalAccessKeyConfig( - portalId: number, - personalAccessKey: string, - env: Environment, - httpTimeout?: number -): { portals: Array; httpTimeout?: number } { - return { - portals: [ - { - authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, - portalId, - personalAccessKey, - env, - }, - ], - httpTimeout, - }; -} - -function generateOauthConfig( - portalId: number, - clientId: string, - clientSecret: string, - refreshToken: string, - scopes: Array, - env: Environment, - httpTimeout?: number -): { portals: Array; httpTimeout?: number } { - return { - portals: [ - { - authType: OAUTH_AUTH_METHOD.value, - portalId, - auth: { - clientId, - clientSecret, - scopes, - tokenInfo: { - refreshToken, - }, - }, - env, - }, - ], - httpTimeout, - }; -} - -function generateApiKeyConfig( - portalId: number, - apiKey: string, - env: Environment -): { portals: Array } { - return { - portals: [ - { - authType: API_KEY_AUTH_METHOD.value, - portalId, - apiKey, - env, - }, - ], - }; -} - -export function loadConfigFromEnvironment({ - useEnv = false, -}: { useEnv?: boolean } = {}): - | { portals: Array } - | undefined { - const { - apiKey, - clientId, - clientSecret, - personalAccessKey, - portalId, - refreshToken, - env, - httpTimeout, - } = getConfigVariablesFromEnv(); - const unableToLoadEnvConfigError = - 'Unable to load config from environment variables.'; - - if (!portalId) { - useEnv && logger.error(unableToLoadEnvConfigError); - return; - } - - if (httpTimeout && httpTimeout < MIN_HTTP_TIMEOUT) { - throw new Error( - `The HTTP timeout value ${httpTimeout} is invalid. The value must be a number greater than ${MIN_HTTP_TIMEOUT}.` - ); - } - - if (personalAccessKey) { - return generatePersonalAccessKeyConfig( - portalId, - personalAccessKey, - env, - httpTimeout - ); - } else if (clientId && clientSecret && refreshToken) { - return generateOauthConfig( - portalId, - clientId, - clientSecret, - refreshToken, - OAUTH_SCOPES.map(scope => scope.value), - env, - httpTimeout - ); - } else if (apiKey) { - return generateApiKeyConfig(portalId, apiKey, env); - } else { - useEnv && logger.error(unableToLoadEnvConfigError); - return; - } -} - -function loadEnvironmentVariableConfig(options: { - useEnv?: boolean; -}): CLIConfig_DEPRECATED | null { - const envConfig = loadConfigFromEnvironment(options); - - if (!envConfig) { - return null; - } - const { portalId } = getConfigVariablesFromEnv(); - - logger.debug( - `Loaded config from environment variables for account ${portalId}` - ); - - return setConfig(handleLegacyCmsPublishMode(envConfig)); -} - -export function isConfigFlagEnabled(flag: keyof CLIConfig_DEPRECATED): boolean { - if (!configFileExists() || configFileIsBlank()) { - return false; - } - - const config = getAndLoadConfigIfNeeded(); - - return Boolean(config[flag] || false); -} - -function handleLegacyCmsPublishMode( - config: CLIConfig_DEPRECATED | undefined -): CLIConfig_DEPRECATED | undefined { - if (config?.defaultMode) { - config.defaultCmsPublishMode = config.defaultMode; - delete config.defaultMode; - } - return config; -} diff --git a/config/defaultAccountOverride.ts b/config/defaultAccountOverride.ts new file mode 100644 index 00000000..7ebe364d --- /dev/null +++ b/config/defaultAccountOverride.ts @@ -0,0 +1,72 @@ +import findup from 'findup-sync'; +import fs from 'fs-extra'; + +import { getCwd } from '../lib/path'; +import { + DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID, + DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND, + DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME, +} from '../constants/config'; +import { i18n } from '../utils/lang'; +import { FileSystemError } from '../models/FileSystemError'; +import { getAllConfigAccounts } from './index'; + +const i18nKey = 'config.defaultAccountOverride'; + +export function getDefaultAccountOverrideAccountId(): number | null { + const defaultAccountOverrideFilePath = getDefaultAccountOverrideFilePath(); + + if (!defaultAccountOverrideFilePath) { + return null; + } + + let source: string; + try { + source = fs.readFileSync(defaultAccountOverrideFilePath, 'utf8'); + } catch (e) { + throw new FileSystemError( + { cause: e }, + { + filepath: defaultAccountOverrideFilePath, + operation: 'read', + } + ); + } + + const accountId = parseInt(source); + + if (isNaN(accountId)) { + throw new Error( + i18n(`${i18nKey}.getDefaultAccountOverrideAccountId.errorHeader`, { + hsAccountFile: defaultAccountOverrideFilePath, + }), + { + // TODO: This is improper use of cause, we should create a custom error class + cause: DEFAULT_ACCOUNT_OVERRIDE_ERROR_INVALID_ID, + } + ); + } + + const accounts = getAllConfigAccounts(); + + const account = accounts?.find(account => account.accountId === accountId); + if (!account) { + throw new Error( + i18n(`${i18nKey}.getDefaultAccountOverrideAccountId.errorHeader`, { + hsAccountFile: defaultAccountOverrideFilePath, + }), + { + // TODO: This is improper use of cause, we should create a custom error class + cause: DEFAULT_ACCOUNT_OVERRIDE_ERROR_ACCOUNT_NOT_FOUND, + } + ); + } + + return account.accountId; +} + +export function getDefaultAccountOverrideFilePath(): string | null { + return findup([DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME], { + cwd: getCwd(), + }); +} diff --git a/config/environment.ts b/config/environment.ts deleted file mode 100644 index 6062261f..00000000 --- a/config/environment.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - CLIConfig_NEW, - Environment, - EnvironmentConfigVariables, -} from '../types/Config'; -import { logger } from '../lib/logger'; -import { ENVIRONMENT_VARIABLES } from '../constants/environments'; -import { - API_KEY_AUTH_METHOD, - OAUTH_AUTH_METHOD, - PERSONAL_ACCESS_KEY_AUTH_METHOD, - OAUTH_SCOPES, -} from '../constants/auth'; -import { generateConfig } from './configUtils'; -import { getValidEnv } from '../lib/environment'; -import { i18n } from '../utils/lang'; - -const i18nKey = 'config.environment'; - -function getConfigVariablesFromEnv(): EnvironmentConfigVariables { - const env = process.env; - - return { - apiKey: env[ENVIRONMENT_VARIABLES.HUBSPOT_API_KEY], - clientId: env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_ID], - clientSecret: env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_SECRET], - personalAccessKey: env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY], - accountId: parseInt(env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID]!, 10), - refreshToken: env[ENVIRONMENT_VARIABLES.HUBSPOT_REFRESH_TOKEN], - env: getValidEnv( - env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] as Environment - ), - }; -} - -export function loadConfigFromEnvironment(): CLIConfig_NEW | null { - const { - apiKey, - clientId, - clientSecret, - personalAccessKey, - accountId, - refreshToken, - env, - } = getConfigVariablesFromEnv(); - if (!accountId) { - logger.debug(i18n(`${i18nKey}.loadConfig.missingAccountId`)); - return null; - } - - if (!env) { - logger.debug(i18n(`${i18nKey}.loadConfig.missingEnv`)); - return null; - } - - if (personalAccessKey) { - return generateConfig(PERSONAL_ACCESS_KEY_AUTH_METHOD.value, { - accountId, - personalAccessKey, - env, - }); - } else if (clientId && clientSecret && refreshToken) { - return generateConfig(OAUTH_AUTH_METHOD.value, { - accountId, - clientId, - clientSecret, - refreshToken, - scopes: OAUTH_SCOPES.map((scope: { value: string }) => scope.value), - env, - }); - } else if (apiKey) { - return generateConfig(API_KEY_AUTH_METHOD.value, { - accountId, - apiKey, - env, - }); - } - - logger.debug(i18n(`${i18nKey}.loadConfig.unknownAuthType`)); - return null; -} diff --git a/config/getAccountIdentifier.ts b/config/getAccountIdentifier.ts deleted file mode 100644 index 56b8097b..00000000 --- a/config/getAccountIdentifier.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GenericAccount } from '../types/Accounts'; - -export function getAccountIdentifier( - account?: GenericAccount | null -): number | undefined { - if (!account) { - return undefined; - } else if (Object.hasOwn(account, 'portalId')) { - return account.portalId; - } else if (Object.hasOwn(account, 'accountId')) { - return account.accountId; - } -} diff --git a/config/index.ts b/config/index.ts index b148eb50..c7aedb2a 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,289 +1,451 @@ -import * as config_DEPRECATED from './config_DEPRECATED'; -import { CLIConfiguration } from './CLIConfiguration'; -import { - configFileExists as newConfigFileExists, - getConfigFilePath, - deleteConfigFile as newDeleteConfigFile, -} from './configFile'; -import { CLIConfig_NEW, CLIConfig } from '../types/Config'; -import { CLIOptions, WriteConfigOptions } from '../types/CLIOptions'; -import { - AccountType, - CLIAccount, - CLIAccount_NEW, - CLIAccount_DEPRECATED, - FlatAccountFields, -} from '../types/Accounts'; -import { getAccountIdentifier } from './getAccountIdentifier'; +import fs from 'fs-extra'; + +import { ACCOUNT_IDENTIFIERS, MIN_HTTP_TIMEOUT } from '../constants/config'; +import { HubSpotConfigAccount } from '../types/Accounts'; +import { HubSpotConfig, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; +import { logger } from '../lib/logger'; +import { + getGlobalConfigFilePath, + getLocalConfigFilePath, + readConfigFile, + parseConfig, + buildConfigFromEnvironment, + writeConfigFile, + getLocalConfigDefaultFilePath, + getConfigAccountByIdentifier, + isConfigAccountValid, + getConfigAccountIndexById, + getConfigPathEnvironmentVariables, + getConfigAccountByInferredIdentifier, +} from './utils'; +import { CMS_PUBLISH_MODE } from '../constants/files'; +import { Environment } from '../types/Config'; +import { i18n } from '../utils/lang'; +import { getDefaultAccountOverrideAccountId } from './defaultAccountOverride'; + +export function localConfigFileExists(): boolean { + return Boolean(getLocalConfigFilePath()); +} -// Use new config if it exists -export function loadConfig( - path: string, - options: CLIOptions = {} -): CLIConfig | null { - // Attempt to load the root config - if (newConfigFileExists()) { - return CLIConfiguration.init(options); - } - return config_DEPRECATED.loadConfig(path, options); +export function globalConfigFileExists(): boolean { + return fs.existsSync(getGlobalConfigFilePath()); } -export function getAndLoadConfigIfNeeded( - options?: CLIOptions -): Partial | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config; +function getConfigDefaultFilePath(): string { + const globalConfigFilePath = getGlobalConfigFilePath(); + + if (fs.existsSync(globalConfigFilePath)) { + return globalConfigFilePath; } - return config_DEPRECATED.getAndLoadConfigIfNeeded(options); -} -export function validateConfig(): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.validate(); + const localConfigFilePath = getLocalConfigFilePath(); + + if (!localConfigFilePath) { + throw new Error(i18n('config.getDefaultConfigFilePath.error')); } - return config_DEPRECATED.validateConfig(); + + return localConfigFilePath; } -export function loadConfigFromEnvironment(): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.useEnvConfig; - } - return Boolean(config_DEPRECATED.loadConfigFromEnvironment()); +export function getConfigFilePath(): string { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); + + return configFilePathFromEnvironment || getConfigDefaultFilePath(); } -export function createEmptyConfigFile( - options: { path?: string } = {}, - useHiddenConfig = false -): void { - if (useHiddenConfig) { - CLIConfiguration.write({ accounts: [] }); - } else { - return config_DEPRECATED.createEmptyConfigFile(options); +export function getConfig(): HubSpotConfig { + const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); + + if (useEnvironmentConfig) { + return buildConfigFromEnvironment(); } + + const pathToRead = getConfigFilePath(); + + logger.debug(i18n('config.getConfig', { path: pathToRead })); + const configFileSource = readConfigFile(pathToRead); + + return parseConfig(configFileSource); } -export function deleteEmptyConfigFile() { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.delete(); +export function isConfigValid(): boolean { + const config = getConfig(); + + if (config.accounts.length === 0) { + logger.debug(i18n('config.isConfigValid.missingAccounts')); + return false; } - return config_DEPRECATED.deleteEmptyConfigFile(); + + const accountIdsMap: { [key: number]: boolean } = {}; + const accountNamesMap: { [key: string]: boolean } = {}; + + return config.accounts.every(account => { + if (!isConfigAccountValid(account)) { + return false; + } + if (accountIdsMap[account.accountId]) { + logger.debug( + i18n('config.isConfigValid.duplicateAccountIds', { + accountId: account.accountId, + }) + ); + return false; + } + if (account.name) { + if (accountNamesMap[account.name.toLowerCase()]) { + logger.debug( + i18n('config.isConfigValid.duplicateAccountNames', { + accountName: account.name, + }) + ); + return false; + } + if (/\s+/.test(account.name)) { + logger.debug( + i18n('config.isConfigValid.invalidAccountName', { + accountName: account.name, + }) + ); + return false; + } + accountNamesMap[account.name] = true; + } + + accountIdsMap[account.accountId] = true; + return true; + }); } -export function getConfig(): CLIConfig | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config; - } - return config_DEPRECATED.getConfig(); +export function createEmptyConfigFile(useGlobalConfig = false): void { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); + const defaultPath = useGlobalConfig + ? getGlobalConfigFilePath() + : getLocalConfigDefaultFilePath(); + + const pathToWrite = configFilePathFromEnvironment || defaultPath; + + writeConfigFile({ accounts: [] }, pathToWrite); } -export function writeConfig(options: WriteConfigOptions = {}): void { - if (CLIConfiguration.isActive()) { - const config = options.source - ? (JSON.parse(options.source) as CLIConfig_NEW) - : undefined; - CLIConfiguration.write(config); - } else { - config_DEPRECATED.writeConfig(options); +export function deleteConfigFile(): void { + const pathToDelete = getConfigFilePath(); + fs.unlinkSync(pathToDelete); +} + +export function getConfigAccountById(accountId: number): HubSpotConfigAccount { + const { accounts } = getConfig(); + + const account = getConfigAccountByIdentifier( + accounts, + ACCOUNT_IDENTIFIERS.ACCOUNT_ID, + accountId + ); + + if (!account) { + throw new Error(i18n('config.getConfigAccountById.error', { accountId })); } + + return account; } -export function getConfigPath( - path?: string, - useHiddenConfig = false -): string | null { - if (useHiddenConfig || CLIConfiguration.isActive()) { - return getConfigFilePath(); +export function getConfigAccountByName( + accountName: string +): HubSpotConfigAccount { + const { accounts } = getConfig(); + + const account = getConfigAccountByIdentifier( + accounts, + ACCOUNT_IDENTIFIERS.NAME, + accountName + ); + + if (!account) { + throw new Error( + i18n('config.getConfigAccountByName.error', { accountName }) + ); } - return config_DEPRECATED.getConfigPath(path); + + return account; } -export function configFileExists(useHiddenConfig?: boolean): boolean { - return useHiddenConfig - ? newConfigFileExists() - : Boolean(config_DEPRECATED.getConfigPath()); +export function getConfigAccountIfExists( + identifier: number | string +): HubSpotConfigAccount | undefined { + const config = getConfig(); + return getConfigAccountByInferredIdentifier(config.accounts, identifier); } -export function getAccountConfig(accountId?: number): CLIAccount | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getConfigForAccount(accountId); +export function getConfigDefaultAccount(): HubSpotConfigAccount { + const { accounts, defaultAccount } = getConfig(); + + let defaultAccountToUse = defaultAccount; + + if (globalConfigFileExists()) { + const defaultAccountOverrideAccountId = + getDefaultAccountOverrideAccountId(); + defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; } - return config_DEPRECATED.getAccountConfig(accountId) || null; -} -export function accountNameExistsInConfig(name: string): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.isAccountInConfig(name); + if (!defaultAccountToUse) { + throw new Error(i18n('config.getConfigDefaultAccount.fieldMissingError')); } - return config_DEPRECATED.accountNameExistsInConfig(name); -} -export function updateAccountConfig( - configOptions: Partial -): FlatAccountFields | null { - const accountIdentifier = getAccountIdentifier(configOptions); - if (CLIConfiguration.isActive()) { - return CLIConfiguration.addOrUpdateAccount({ - ...configOptions, - accountId: accountIdentifier, - }); + const account = getConfigAccountByInferredIdentifier( + accounts, + defaultAccountToUse + ); + + if (!account) { + throw new Error( + i18n('config.getConfigDefaultAccount.accountMissingError', { + defaultAccountToUse, + }) + ); } - return config_DEPRECATED.updateAccountConfig({ - ...configOptions, - portalId: accountIdentifier, - }); + + return account; } -export function updateDefaultAccount(nameOrId: string | number): void { - if (CLIConfiguration.isActive()) { - CLIConfiguration.updateDefaultAccount(nameOrId); - } else { - config_DEPRECATED.updateDefaultAccount(nameOrId); +export function getConfigDefaultAccountIfExists(): + | HubSpotConfigAccount + | undefined { + const { accounts, defaultAccount } = getConfig(); + + let defaultAccountToUse = defaultAccount; + + if (globalConfigFileExists()) { + const defaultAccountOverrideAccountId = + getDefaultAccountOverrideAccountId(); + defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; } -} -export async function renameAccount( - currentName: string, - newName: string -): Promise { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.renameAccount(currentName, newName); - } else { - return config_DEPRECATED.renameAccount(currentName, newName); + if (!defaultAccountToUse) { + return; } + + const account = getConfigAccountByInferredIdentifier( + accounts, + defaultAccountToUse + ); + + return account; } -export function getAccountId(nameOrId?: string | number): number | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getAccountId(nameOrId); - } - return config_DEPRECATED.getAccountId(nameOrId) || null; +export function getAllConfigAccounts(): HubSpotConfigAccount[] { + const { accounts } = getConfig(); + + return accounts; } -export function removeSandboxAccountFromConfig( - nameOrId: string | number -): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.removeAccountFromConfig(nameOrId); +export function getConfigAccountEnvironment( + identifier?: number | string +): Environment { + if (identifier) { + const config = getConfig(); + + const account = getConfigAccountByInferredIdentifier( + config.accounts, + identifier + ); + + if (account) { + return account.env; + } } - return config_DEPRECATED.removeSandboxAccountFromConfig(nameOrId); + const defaultAccount = getConfigDefaultAccount(); + return defaultAccount.env; } -export async function deleteAccount( - accountName: string -): Promise { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.removeAccountFromConfig(accountName); - } else { - return config_DEPRECATED.deleteAccount(accountName); +export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { + if (!isConfigAccountValid(accountToAdd)) { + throw new Error(i18n('config.addConfigAccount.invalidAccount')); } -} -export function updateHttpTimeout(timeout: string): void { - if (CLIConfiguration.isActive()) { - CLIConfiguration.updateHttpTimeout(timeout); - } else { - config_DEPRECATED.updateHttpTimeout(timeout); + const config = getConfig(); + + const accountInConfig = getConfigAccountByIdentifier( + config.accounts, + ACCOUNT_IDENTIFIERS.ACCOUNT_ID, + accountToAdd.accountId + ); + + if (accountInConfig) { + throw new Error( + i18n('config.addConfigAccount.duplicateAccount', { + accountId: accountToAdd.accountId, + }) + ); } + + config.accounts.push(accountToAdd); + + writeConfigFile(config, getConfigFilePath()); } -export function updateAllowUsageTracking(isEnabled: boolean): void { - if (CLIConfiguration.isActive()) { - CLIConfiguration.updateAllowUsageTracking(isEnabled); - } else { - config_DEPRECATED.updateAllowUsageTracking(isEnabled); +export function updateConfigAccount( + updatedAccount: HubSpotConfigAccount +): void { + if (!isConfigAccountValid(updatedAccount)) { + throw new Error(i18n('config.updateConfigAccount.invalidAccount')); } -} -export function deleteConfigFile(): void { - if (CLIConfiguration.isActive()) { - newDeleteConfigFile(); - } else { - config_DEPRECATED.deleteConfigFile(); + const config = getConfig(); + + const accountIndex = getConfigAccountIndexById( + config.accounts, + updatedAccount.accountId + ); + + if (accountIndex < 0) { + throw new Error( + i18n('config.updateConfigAccount.accountNotFound', { + accountId: updatedAccount.accountId, + }) + ); } + + config.accounts[accountIndex] = updatedAccount; + + writeConfigFile(config, getConfigFilePath()); } -export function isConfigFlagEnabled(flag: keyof CLIConfig): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.isConfigFlagEnabled(flag); +export function setConfigAccountAsDefault(identifier: number | string): void { + const config = getConfig(); + + const account = getConfigAccountByInferredIdentifier( + config.accounts, + identifier + ); + + if (!account) { + throw new Error( + i18n('config.setConfigAccountAsDefault.accountNotFound', { + accountId: identifier, + }) + ); } - return config_DEPRECATED.isConfigFlagEnabled(flag); + + config.defaultAccount = account.accountId; + writeConfigFile(config, getConfigFilePath()); } -export function isTrackingAllowed() { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.isTrackingAllowed(); +export function renameConfigAccount( + currentName: string, + newName: string +): void { + const config = getConfig(); + + const account = getConfigAccountByIdentifier( + config.accounts, + ACCOUNT_IDENTIFIERS.NAME, + currentName + ); + + if (!account) { + throw new Error( + i18n('config.renameConfigAccount.accountNotFound', { + currentName, + }) + ); } - return config_DEPRECATED.isTrackingAllowed(); -} -export function getEnv(nameOrId?: string | number) { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getEnv(nameOrId); + const duplicateAccount = getConfigAccountByIdentifier( + config.accounts, + ACCOUNT_IDENTIFIERS.NAME, + newName + ); + + if (duplicateAccount) { + throw new Error( + i18n('config.renameConfigAccount.duplicateAccount', { + newName, + }) + ); } - return config_DEPRECATED.getEnv(nameOrId); + + account.name = newName; + + writeConfigFile(config, getConfigFilePath()); } -export function getAccountType( - accountType?: AccountType, - sandboxAccountType?: string | null -): AccountType { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getAccountType(accountType, sandboxAccountType); +export function removeAccountFromConfig(accountId: number): void { + const config = getConfig(); + + const index = getConfigAccountIndexById(config.accounts, accountId); + + if (index < 0) { + throw new Error( + i18n('config.removeAccountFromConfig.accountNotFound', { + accountId, + }) + ); } - return config_DEPRECATED.getAccountType(accountType, sandboxAccountType); -} -export function getConfigDefaultAccount(): string | number | null | undefined { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getDefaultAccount(); + config.accounts.splice(index, 1); + + if (config.defaultAccount === accountId) { + delete config.defaultAccount; } - return config_DEPRECATED.getConfigDefaultAccount(); + + writeConfigFile(config, getConfigFilePath()); } -export function getDisplayDefaultAccount(): string | number | null | undefined { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config?.defaultAccount; +export function updateHttpTimeout(timeout: string | number): void { + const parsedTimeout = + typeof timeout === 'string' ? parseInt(timeout) : timeout; + + if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { + throw new Error( + i18n('config.updateHttpTimeout.invalidTimeout', { + minTimeout: MIN_HTTP_TIMEOUT, + }) + ); } - return config_DEPRECATED.getConfigDefaultAccount(); + + const config = getConfig(); + + config.httpTimeout = parsedTimeout; + + writeConfigFile(config, getConfigFilePath()); } -export function getConfigAccounts(): - | Array - | Array - | null - | undefined { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getConfigAccounts(); - } - return config_DEPRECATED.getConfigAccounts(); +export function updateAllowUsageTracking(isAllowed: boolean): void { + const config = getConfig(); + + config.allowUsageTracking = isAllowed; + + writeConfigFile(config, getConfigFilePath()); } export function updateDefaultCmsPublishMode( cmsPublishMode: CmsPublishMode -): void | CLIConfig_NEW | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.updateDefaultCmsPublishMode(cmsPublishMode); +): void { + if ( + !cmsPublishMode || + !Object.values(CMS_PUBLISH_MODE).includes(cmsPublishMode) + ) { + throw new Error( + i18n('config.updateDefaultCmsPublishMode.invalidCmsPublishMode') + ); } - return config_DEPRECATED.updateDefaultCmsPublishMode(cmsPublishMode); -} -export function getCWDAccountOverride() { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getCWDAccountOverride(); - } + const config = getConfig(); + + config.defaultCmsPublishMode = cmsPublishMode; + + writeConfigFile(config, getConfigFilePath()); } -export function getDefaultAccountOverrideFilePath() { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getDefaultAccountOverrideFilePath(); +export function isConfigFlagEnabled( + flag: ConfigFlag, + defaultValue?: boolean +): boolean { + const config = getConfig(); + + if (typeof config[flag] === 'undefined') { + return defaultValue || false; } -} -// These functions are not supported with the new config setup -export const getConfigAccountId = config_DEPRECATED.getConfigAccountId; -export const getOrderedAccount = config_DEPRECATED.getOrderedAccount; -export const getOrderedConfig = config_DEPRECATED.getOrderedConfig; -export const setConfig = config_DEPRECATED.setConfig; -export const setConfigPath = config_DEPRECATED.setConfigPath; -export const findConfig = config_DEPRECATED.findConfig; + return Boolean(config[flag]); +} diff --git a/config/migrate.ts b/config/migrate.ts index 13fbec89..25d81338 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -1,218 +1,132 @@ -import * as config_DEPRECATED from './config_DEPRECATED'; -import { CLIConfiguration } from './CLIConfiguration'; -import { - CLIConfig, - CLIConfig_DEPRECATED, - CLIConfig_NEW, - Environment, -} from '../types/Config'; -import { CmsPublishMode } from '../types/Files'; -import { - writeConfig, - createEmptyConfigFile, - loadConfig, - deleteEmptyConfigFile, -} from './index'; -import { - getConfigFilePath, - configFileExists as newConfigFileExists, -} from './configFile'; +import fs from 'fs'; + +import { HubSpotConfig } from '../types/Config'; +import { createEmptyConfigFile } from './index'; import { - GLOBAL_CONFIG_PATH, DEFAULT_CMS_PUBLISH_MODE, HTTP_TIMEOUT, ENV, HTTP_USE_LOCALHOST, ALLOW_USAGE_TRACKING, DEFAULT_ACCOUNT, - DEFAULT_PORTAL, } from '../constants/config'; -import { i18n } from '../utils/lang'; - -const i18nKey = 'config.migrate'; - -export function getDeprecatedConfig( - configPath?: string -): CLIConfig_DEPRECATED | null { - return config_DEPRECATED.loadConfig(configPath); -} - -export function getGlobalConfig(): CLIConfig_NEW | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config; - } - return null; -} - -export function configFileExists( - useHiddenConfig = false, - configPath?: string -): boolean { - return useHiddenConfig - ? newConfigFileExists() - : Boolean(config_DEPRECATED.getConfigPath(configPath)); -} - -export function getConfigPath( - configPath?: string, - useHiddenConfig = false -): string | null { - if (useHiddenConfig) { - return getConfigFilePath(); - } - return config_DEPRECATED.getConfigPath(configPath); -} +import { + getGlobalConfigFilePath, + parseConfig, + readConfigFile, + writeConfigFile, +} from './utils'; +import { ValueOf } from '../types/Utils'; -function writeGlobalConfigFile( - updatedConfig: CLIConfig_NEW, - isMigrating = false -): void { - const updatedConfigJson = JSON.stringify(updatedConfig); - if (isMigrating) { - createEmptyConfigFile({}, true); - } - loadConfig(''); +export function getConfigAtPath(path: string): HubSpotConfig { + const configFileSource = readConfigFile(path); - try { - writeConfig({ source: updatedConfigJson }); - config_DEPRECATED.deleteConfigFile(); - } catch (error) { - deleteEmptyConfigFile(); - throw new Error( - i18n(`${i18nKey}.errors.writeConfig`, { configPath: GLOBAL_CONFIG_PATH }), - { cause: error } - ); - } + return parseConfig(configFileSource); } -export function migrateConfig( - deprecatedConfig: CLIConfig_DEPRECATED | null -): void { - if (!deprecatedConfig) { - throw new Error(i18n(`${i18nKey}.errors.noDeprecatedConfig`)); - } - const { defaultPortal, portals, ...rest } = deprecatedConfig; - const updatedConfig = { - ...rest, - defaultAccount: defaultPortal, - accounts: portals - .filter(({ portalId }) => portalId !== undefined) - .map(({ portalId, ...rest }) => ({ - ...rest, - accountId: portalId!, - })), - }; - writeGlobalConfigFile(updatedConfig, true); +export function migrateConfigAtPath(path: string): void { + createEmptyConfigFile(true); + const configToMigrate = getConfigAtPath(path); + writeConfigFile(configToMigrate, getGlobalConfigFilePath()); + fs.unlinkSync(path); } -type ConflictValue = boolean | string | number | CmsPublishMode | Environment; export type ConflictProperty = { - property: keyof CLIConfig_NEW; - oldValue: ConflictValue; - newValue: ConflictValue; + property: keyof HubSpotConfig; + oldValue: ValueOf; + newValue: ValueOf; }; export function mergeConfigProperties( - globalConfig: CLIConfig_NEW, - deprecatedConfig: CLIConfig_DEPRECATED, + toConfig: HubSpotConfig, + fromConfig: HubSpotConfig, force?: boolean ): { - initialConfig: CLIConfig_NEW; + configWithMergedProperties: HubSpotConfig; conflicts: Array; } { - const propertiesToCheck: Array> = [ - DEFAULT_CMS_PUBLISH_MODE, - HTTP_TIMEOUT, - ENV, - HTTP_USE_LOCALHOST, - ALLOW_USAGE_TRACKING, - ]; const conflicts: Array = []; - propertiesToCheck.forEach(prop => { - if (prop in globalConfig && prop in deprecatedConfig) { - if (force || globalConfig[prop] === deprecatedConfig[prop]) { - // @ts-expect-error Cannot reconcile CLIConfig_NEW and CLIConfig_DEPRECATED types - globalConfig[prop] = deprecatedConfig[prop]; - } else { + if (force) { + toConfig.defaultCmsPublishMode = fromConfig.defaultCmsPublishMode; + toConfig.httpTimeout = fromConfig.httpTimeout; + toConfig.env = fromConfig.env; + toConfig.httpUseLocalhost = fromConfig.httpUseLocalhost; + toConfig.allowUsageTracking = fromConfig.allowUsageTracking; + toConfig.defaultAccount = fromConfig.defaultAccount; + } else { + toConfig.defaultCmsPublishMode ||= fromConfig.defaultCmsPublishMode; + toConfig.httpTimeout ||= fromConfig.httpTimeout; + toConfig.env ||= fromConfig.env; + toConfig.httpUseLocalhost = + toConfig.httpUseLocalhost === undefined + ? fromConfig.httpUseLocalhost + : toConfig.httpUseLocalhost; + toConfig.allowUsageTracking = + toConfig.allowUsageTracking === undefined + ? fromConfig.allowUsageTracking + : toConfig.allowUsageTracking; + toConfig.defaultAccount ||= fromConfig.defaultAccount; + + const propertiesToCheck = [ + DEFAULT_CMS_PUBLISH_MODE, + HTTP_TIMEOUT, + ENV, + HTTP_USE_LOCALHOST, + ALLOW_USAGE_TRACKING, + DEFAULT_ACCOUNT, + ] as const; + + propertiesToCheck.forEach(prop => { + if (toConfig[prop] !== undefined && toConfig[prop] !== fromConfig[prop]) { conflicts.push({ property: prop, - oldValue: deprecatedConfig[prop]!, - newValue: globalConfig[prop]!, + oldValue: fromConfig[prop], + newValue: toConfig[prop], }); } - } - }); - - if ( - DEFAULT_ACCOUNT in globalConfig && - DEFAULT_PORTAL in deprecatedConfig && - globalConfig.defaultAccount !== deprecatedConfig.defaultPortal - ) { - if (force) { - globalConfig.defaultAccount = deprecatedConfig.defaultPortal; - } else { - conflicts.push({ - property: DEFAULT_ACCOUNT, - oldValue: deprecatedConfig.defaultPortal!, - newValue: globalConfig.defaultAccount!, - }); - } - } else if (DEFAULT_PORTAL in deprecatedConfig) { - globalConfig.defaultAccount = deprecatedConfig.defaultPortal; + }); } - return { initialConfig: globalConfig, conflicts }; + return { configWithMergedProperties: toConfig, conflicts }; } -function mergeAccounts( - globalConfig: CLIConfig_NEW, - deprecatedConfig: CLIConfig_DEPRECATED +function buildConfigWithMergedAccounts( + toConfig: HubSpotConfig, + fromConfig: HubSpotConfig ): { - finalConfig: CLIConfig_NEW; - skippedAccountIds: Array; + configWithMergedAccounts: HubSpotConfig; + skippedAccountIds: Array; } { - let existingAccountIds: Array = []; - const skippedAccountIds: Array = []; - - if (globalConfig.accounts && deprecatedConfig.portals) { - existingAccountIds = globalConfig.accounts.map( - account => account.accountId - ); - - const newAccounts = deprecatedConfig.portals - .filter(portal => { - const isExisting = existingAccountIds.includes(portal.portalId!); - if (isExisting) { - skippedAccountIds.push(portal.portalId!); - } - return !isExisting; - }) - .map(({ portalId, ...rest }) => ({ - ...rest, - accountId: portalId!, - })); + const existingAccountIds = toConfig.accounts.map( + ({ accountId }) => accountId + ); + const skippedAccountIds: Array = []; - if (newAccounts.length > 0) { - globalConfig.accounts.push(...newAccounts); + fromConfig.accounts.forEach(account => { + if (existingAccountIds.includes(account.accountId)) { + skippedAccountIds.push(account.accountId); + } else { + toConfig.accounts.push(account); } - } + }); return { - finalConfig: globalConfig, + configWithMergedAccounts: toConfig, skippedAccountIds, }; } -export function mergeExistingConfigs( - globalConfig: CLIConfig_NEW, - deprecatedConfig: CLIConfig_DEPRECATED -): { finalConfig: CLIConfig_NEW; skippedAccountIds: Array } { - const { finalConfig, skippedAccountIds } = mergeAccounts( - globalConfig, - deprecatedConfig - ); +export function mergeConfigAccounts( + toConfig: HubSpotConfig, + fromConfig: HubSpotConfig +): { + configWithMergedAccounts: HubSpotConfig; + skippedAccountIds: Array; +} { + const { configWithMergedAccounts, skippedAccountIds } = + buildConfigWithMergedAccounts(toConfig, fromConfig); - writeGlobalConfigFile(finalConfig); - return { finalConfig, skippedAccountIds }; + writeConfigFile(configWithMergedAccounts, getGlobalConfigFilePath()); + return { configWithMergedAccounts, skippedAccountIds }; } diff --git a/config/utils.ts b/config/utils.ts new file mode 100644 index 00000000..77db76c1 --- /dev/null +++ b/config/utils.ts @@ -0,0 +1,446 @@ +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import findup from 'findup-sync'; + +import { + HUBSPOT_ACCOUNT_TYPES, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + ENVIRONMENT_VARIABLES, + ACCOUNT_IDENTIFIERS, + GLOBAL_CONFIG_PATH, +} from '../constants/config'; +import { + PERSONAL_ACCESS_KEY_AUTH_METHOD, + API_KEY_AUTH_METHOD, + OAUTH_AUTH_METHOD, + OAUTH_SCOPES, +} from '../constants/auth'; +import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; +import { FileSystemError } from '../models/FileSystemError'; +import { logger } from '../lib/logger'; +import { + HubSpotConfigAccount, + OAuthConfigAccount, + AccountType, + TokenInfo, +} from '../types/Accounts'; +import { getValidEnv } from '../lib/environment'; +import { getCwd } from '../lib/path'; +import { CMS_PUBLISH_MODE } from '../constants/files'; +import { i18n } from '../utils/lang'; +import { ValueOf } from '../types/Utils'; + +export function getGlobalConfigFilePath(): string { + return GLOBAL_CONFIG_PATH; +} + +export function getLocalConfigFilePath(): string | null { + return findup([ + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), + ]); +} + +export function getLocalConfigDefaultFilePath(): string { + return `${getCwd()}/${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}`; +} + +export function getConfigPathEnvironmentVariables(): { + useEnvironmentConfig: boolean; + configFilePathFromEnvironment: string | undefined; +} { + const configFilePathFromEnvironment = + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH]; + const useEnvironmentConfig = + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] === + 'true'; + + if (configFilePathFromEnvironment && useEnvironmentConfig) { + throw new Error( + i18n( + 'config.utils.getConfigPathEnvironmentVariables.invalidEnvironmentVariables' + ) + ); + } + + return { + configFilePathFromEnvironment, + useEnvironmentConfig, + }; +} + +export function readConfigFile(configPath: string): string { + let source = ''; + + try { + source = fs.readFileSync(configPath).toString(); + } catch (err) { + throw new FileSystemError( + { cause: err }, + { + filepath: configPath, + operation: 'read', + } + ); + } + + return source; +} + +export function removeUndefinedFieldsFromConfigAccount< + T extends + | HubSpotConfigAccount + | Partial = HubSpotConfigAccount, +>(account: T): T { + Object.keys(account).forEach(k => { + const key = k as keyof T; + if (account[key] === undefined) { + delete account[key]; + } + }); + + if ('auth' in account && account.auth) { + if (account.authType === OAUTH_AUTH_METHOD.value) { + Object.keys(account.auth).forEach(k => { + const key = k as keyof OAuthConfigAccount['auth']; + if (account.auth?.[key] === undefined) { + delete account.auth?.[key]; + } + }); + } + + if ( + 'tokenInfo' in account.auth && + typeof account.auth.tokenInfo === 'object' + ) { + Object.keys(account.auth.tokenInfo).forEach(k => { + const key = k as keyof TokenInfo; + if (account.auth?.tokenInfo[key] === undefined) { + delete account.auth?.tokenInfo[key]; + } + }); + } + } + + return account; +} + +// Ensure written config files have fields in a consistent order +export function formatConfigForWrite(config: HubSpotConfig) { + const { + defaultAccount, + defaultCmsPublishMode, + httpTimeout, + allowUsageTracking, + accounts, + ...rest + } = config; + + const orderedConfig = { + ...(defaultAccount && { defaultAccount }), + defaultCmsPublishMode, + httpTimeout, + allowUsageTracking, + ...rest, + accounts: accounts.map(account => { + const { name, accountId, env, authType, ...rest } = account; + + return { + name, + accountId, + env, + authType, + ...rest, + }; + }), + }; + + return removeUndefinedFieldsFromConfigAccount(orderedConfig); +} + +export function writeConfigFile( + config: HubSpotConfig, + configPath: string +): void { + const source = yaml.dump( + JSON.parse(JSON.stringify(formatConfigForWrite(config), null, 2)) + ); + + try { + fs.ensureFileSync(configPath); + fs.writeFileSync(configPath, source); + } catch (err) { + throw new FileSystemError( + { cause: err }, + { + filepath: configPath, + operation: 'write', + } + ); + } +} + +function getAccountType(sandboxAccountType?: string): AccountType { + if (sandboxAccountType) { + if (sandboxAccountType.toUpperCase() === 'DEVELOPER') { + return HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX; + } + if (sandboxAccountType.toUpperCase() === 'STANDARD') { + return HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX; + } + } + return HUBSPOT_ACCOUNT_TYPES.STANDARD; +} + +export function normalizeParsedConfig( + parsedConfig: HubSpotConfig & DeprecatedHubSpotConfigFields +): HubSpotConfig { + if (parsedConfig.portals) { + parsedConfig.accounts = parsedConfig.portals.map(account => { + if (account.portalId) { + account.accountId = account.portalId; + delete account.portalId; + } + if (!account.accountType) { + account.accountType = getAccountType(account.sandboxAccountType); + delete account.sandboxAccountType; + } + return account; + }); + delete parsedConfig.portals; + } + + if (parsedConfig.defaultPortal) { + const defaultAccount = getConfigAccountByInferredIdentifier( + parsedConfig.accounts, + parsedConfig.defaultPortal + ); + + if (defaultAccount) { + parsedConfig.defaultAccount = defaultAccount.accountId; + } + delete parsedConfig.defaultPortal; + } + + if (parsedConfig.defaultMode) { + parsedConfig.defaultCmsPublishMode = parsedConfig.defaultMode; + delete parsedConfig.defaultMode; + } + + return parsedConfig; +} + +export function parseConfig(configSource: string): HubSpotConfig { + let parsedYaml: HubSpotConfig & DeprecatedHubSpotConfigFields; + + try { + parsedYaml = yaml.load(configSource) as HubSpotConfig & + DeprecatedHubSpotConfigFields; + } catch (err) { + throw new Error(i18n('config.utils.parseConfig.error'), { cause: err }); + } + + return normalizeParsedConfig(parsedYaml); +} + +export function buildConfigFromEnvironment(): HubSpotConfig { + const apiKey = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_API_KEY]; + const clientId = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_ID]; + const clientSecret = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_SECRET]; + const personalAccessKey = + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY]; + const accountIdVar = + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] || + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_PORTAL_ID]; + const refreshToken = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_REFRESH_TOKEN]; + const hubspotEnvironment = + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT]; + const httpTimeoutVar = process.env[ENVIRONMENT_VARIABLES.HTTP_TIMEOUT]; + const httpUseLocalhostVar = + process.env[ENVIRONMENT_VARIABLES.HTTP_USE_LOCALHOST]; + const allowUsageTrackingVar = + process.env[ENVIRONMENT_VARIABLES.ALLOW_USAGE_TRACKING]; + const defaultCmsPublishModeVar = + process.env[ENVIRONMENT_VARIABLES.DEFAULT_CMS_PUBLISH_MODE]; + + if (!accountIdVar) { + throw new Error( + i18n('config.utils.buildConfigFromEnvironment.missingAccountId') + ); + } + + const accountId = parseInt(accountIdVar); + const httpTimeout = httpTimeoutVar ? parseInt(httpTimeoutVar) : undefined; + const httpUseLocalhost = httpUseLocalhostVar + ? httpUseLocalhostVar === 'true' + : undefined; + const allowUsageTracking = allowUsageTrackingVar + ? allowUsageTrackingVar === 'true' + : undefined; + const defaultCmsPublishMode = + defaultCmsPublishModeVar === CMS_PUBLISH_MODE.draft || + defaultCmsPublishModeVar === CMS_PUBLISH_MODE.publish + ? defaultCmsPublishModeVar + : undefined; + + const env = getValidEnv(hubspotEnvironment); + + let account: HubSpotConfigAccount; + + if (personalAccessKey) { + account = { + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + accountId, + personalAccessKey, + env, + name: accountIdVar, + auth: { + tokenInfo: {}, + }, + }; + } else if (clientId && clientSecret && refreshToken) { + account = { + authType: OAUTH_AUTH_METHOD.value, + accountId, + auth: { + clientId, + clientSecret, + scopes: OAUTH_SCOPES.map((scope: { value: string }) => scope.value), + tokenInfo: { + refreshToken, + }, + }, + env, + name: accountIdVar, + }; + } else if (apiKey) { + account = { + authType: API_KEY_AUTH_METHOD.value, + accountId, + apiKey, + env, + name: accountIdVar, + }; + } else { + throw new Error( + i18n('config.utils.buildConfigFromEnvironment.invalidAuthType') + ); + } + + return { + accounts: [account], + defaultAccount: accountId, + httpTimeout, + httpUseLocalhost, + allowUsageTracking, + defaultCmsPublishMode, + }; +} + +export function getAccountIdentifierAndType( + accountIdentifier: string | number +): { + identifier: string | number; + identifierType: ValueOf; +} { + const identifierAsNumber = + typeof accountIdentifier === 'number' + ? accountIdentifier + : parseInt(accountIdentifier); + const isId = !isNaN(identifierAsNumber); + + return { + identifier: isId ? identifierAsNumber : accountIdentifier, + identifierType: isId + ? ACCOUNT_IDENTIFIERS.ACCOUNT_ID + : ACCOUNT_IDENTIFIERS.NAME, + }; +} + +export function getConfigAccountByIdentifier( + accounts: Array, + identifierFieldName: ValueOf, + identifier: string | number +): HubSpotConfigAccount | undefined { + return accounts.find(account => account[identifierFieldName] === identifier); +} + +export function getConfigAccountByInferredIdentifier( + accounts: Array, + accountIdentifier: string | number +): HubSpotConfigAccount | undefined { + const { identifier, identifierType } = + getAccountIdentifierAndType(accountIdentifier); + return accounts.find(account => account[identifierType] === identifier); +} + +export function getConfigAccountIndexById( + accounts: Array, + id: number +): number { + return accounts.findIndex(account => account.accountId === id); +} + +export function isConfigAccountValid( + account: Partial +): boolean { + if (!account || typeof account !== 'object') { + logger.debug(i18n('config.utils.isConfigAccountValid.missingAccount')); + return false; + } + + if (!account.accountId) { + logger.debug(i18n('config.utils.isConfigAccountValid.missingAccountId')); + return false; + } + + if (!account.authType) { + logger.debug( + i18n('config.utils.isConfigAccountValid.missingAuthType', { + accountId: account.accountId, + }) + ); + return false; + } + + let valid = false; + + if (account.authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { + valid = + 'personalAccessKey' in account && Boolean(account.personalAccessKey); + + if (!valid) { + logger.debug( + i18n('config.utils.isConfigAccountValid.missingPersonalAccessKey', { + accountId: account.accountId, + }) + ); + } + } + + if (account.authType === OAUTH_AUTH_METHOD.value) { + valid = 'auth' in account && Boolean(account.auth); + + if (!valid) { + logger.debug( + i18n('config.utils.isConfigAccountValid.missingAuth', { + accountId: account.accountId, + }) + ); + } + } + + if (account.authType === API_KEY_AUTH_METHOD.value) { + valid = 'apiKey' in account && Boolean(account.apiKey); + + if (!valid) { + logger.debug( + i18n('config.utils.isConfigAccountValid.missingApiKey', { + accountId: account.accountId, + }) + ); + } + } + + return valid; +} diff --git a/constants/config.ts b/constants/config.ts index 9a6f43dd..7158e0c5 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -44,3 +44,30 @@ export const HUBSPOT_ACCOUNT_TYPE_STRINGS = { APP_DEVELOPER: i18n('lib.accountTypes.appDeveloper'), STANDARD: i18n('lib.accountTypes.standard'), } as const; + +export const CONFIG_FLAGS = { + USE_CUSTOM_OBJECT_HUBFILE: 'useCustomObjectHubfile', + HTTP_USE_LOCALHOST: 'httpUseLocalhost', +} as const; + +export const ENVIRONMENT_VARIABLES = { + HUBSPOT_API_KEY: 'HUBSPOT_API_KEY', + HUBSPOT_CLIENT_ID: 'HUBSPOT_CLIENT_ID', + HUBSPOT_CLIENT_SECRET: 'HUBSPOT_CLIENT_SECRET', + HUBSPOT_PERSONAL_ACCESS_KEY: 'HUBSPOT_PERSONAL_ACCESS_KEY', + HUBSPOT_ACCOUNT_ID: 'HUBSPOT_ACCOUNT_ID', + HUBSPOT_PORTAL_ID: 'HUBSPOT_PORTAL_ID', + HUBSPOT_REFRESH_TOKEN: 'HUBSPOT_REFRESH_TOKEN', + HUBSPOT_ENVIRONMENT: 'HUBSPOT_ENVIRONMENT', + HTTP_TIMEOUT: 'HTTP_TIMEOUT', + HTTP_USE_LOCALHOST: 'HTTP_USE_LOCALHOST', + ALLOW_USAGE_TRACKING: 'ALLOW_USAGE_TRACKING', + DEFAULT_CMS_PUBLISH_MODE: 'DEFUALT_CMS_PUBLISH_MODE', + USE_ENVIRONMENT_HUBSPOT_CONFIG: 'USE_ENVIRONMENT_HUBSPOT_CONFIG', + HUBSPOT_CONFIG_PATH: 'HUBSPOT_CONFIG_PATH', +} as const; + +export const ACCOUNT_IDENTIFIERS = { + ACCOUNT_ID: 'accountId', + NAME: 'name', +} as const; diff --git a/constants/environments.ts b/constants/environments.ts index cb704fa2..a8bff37b 100644 --- a/constants/environments.ts +++ b/constants/environments.ts @@ -2,15 +2,3 @@ export const ENVIRONMENTS = { PROD: 'prod', QA: 'qa', } as const; - -export const ENVIRONMENT_VARIABLES = { - HUBSPOT_API_KEY: 'HUBSPOT_API_KEY', - HUBSPOT_CLIENT_ID: 'HUBSPOT_CLIENT_ID', - HUBSPOT_CLIENT_SECRET: 'HUBSPOT_CLIENT_SECRET', - HUBSPOT_PERSONAL_ACCESS_KEY: 'HUBSPOT_PERSONAL_ACCESS_KEY', - HUBSPOT_ACCOUNT_ID: 'HUBSPOT_ACCOUNT_ID', - HUBSPOT_PORTAL_ID: 'HUBSPOT_PORTAL_ID', - HUBSPOT_REFRESH_TOKEN: 'HUBSPOT_REFRESH_TOKEN', - HUBSPOT_ENVIRONMENT: 'HUBSPOT_ENVIRONMENT', - HTTP_TIMEOUT: 'HTTP_TIMEOUT', -} as const; diff --git a/http/__tests__/getAxiosConfig.test.ts b/http/__tests__/getAxiosConfig.test.ts index f3cbc1f1..6cddcddd 100644 --- a/http/__tests__/getAxiosConfig.test.ts +++ b/http/__tests__/getAxiosConfig.test.ts @@ -1,19 +1,16 @@ -import { getAndLoadConfigIfNeeded as __getAndLoadConfigIfNeeded } from '../../config'; +import { getConfig as __getConfig } from '../../config'; import { ENVIRONMENTS } from '../../constants/environments'; import { getAxiosConfig } from '../getAxiosConfig'; jest.mock('../../config'); -const getAndLoadConfigIfNeeded = - __getAndLoadConfigIfNeeded as jest.MockedFunction< - typeof __getAndLoadConfigIfNeeded - >; +const getConfig = __getConfig as jest.MockedFunction; const url = 'https://app.hubspot.com'; describe('http/getAxiosConfig', () => { it('constructs baseURL as expected based on environment', () => { - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [], }); @@ -25,7 +22,7 @@ describe('http/getAxiosConfig', () => { }); }); it('supports httpUseLocalhost config option to construct baseURL for local HTTP services', () => { - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ httpUseLocalhost: true, accounts: [], }); diff --git a/http/__tests__/index.test.ts b/http/__tests__/index.test.ts index f86fc84d..6015167a 100644 --- a/http/__tests__/index.test.ts +++ b/http/__tests__/index.test.ts @@ -2,13 +2,13 @@ import axios, { AxiosError } from 'axios'; import fs from 'fs-extra'; import moment from 'moment'; import { - getAndLoadConfigIfNeeded as __getAndLoadConfigIfNeeded, - getAccountConfig as __getAccountConfig, + getConfig as __getConfig, + getConfigAccountById as __getConfigAccountById, } from '../../config'; import { ENVIRONMENTS } from '../../constants/environments'; import { http } from '../'; import { version } from '../../package.json'; -import { AuthType } from '../../types/Accounts'; +import { HubSpotConfigAccount } from '../../types/Accounts'; jest.mock('fs-extra'); jest.mock('axios'); @@ -28,14 +28,19 @@ jest.mock('https', () => ({ })); const mockedAxios = jest.mocked(axios); -const getAndLoadConfigIfNeeded = - __getAndLoadConfigIfNeeded as jest.MockedFunction< - typeof __getAndLoadConfigIfNeeded - >; -const getAccountConfig = __getAccountConfig as jest.MockedFunction< - typeof __getAccountConfig +const getConfig = __getConfig as jest.MockedFunction; +const getConfigAccountById = __getConfigAccountById as jest.MockedFunction< + typeof __getConfigAccountById >; +const ACCOUNT: HubSpotConfigAccount = { + name: 'test-account', + accountId: 123, + apiKey: 'abc', + env: ENVIRONMENTS.QA, + authType: 'apikey', +}; + fs.createWriteStream = jest.fn().mockReturnValue({ on: jest.fn((event, callback) => { if (event === 'close') { @@ -46,27 +51,17 @@ fs.createWriteStream = jest.fn().mockReturnValue({ describe('http/index', () => { afterEach(() => { - getAndLoadConfigIfNeeded.mockReset(); - getAccountConfig.mockReset(); + getConfig.mockReset(); + getConfigAccountById.mockReset(); }); describe('http.getOctetStream()', () => { beforeEach(() => { - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ httpTimeout: 1000, - accounts: [ - { - accountId: 123, - apiKey: 'abc', - env: ENVIRONMENTS.QA, - }, - ], - }); - getAccountConfig.mockReturnValue({ - accountId: 123, - apiKey: 'abc', - env: ENVIRONMENTS.QA, + accounts: [ACCOUNT], }); + getConfigAccountById.mockReturnValue(ACCOUNT); }); it('makes a get request', async () => { @@ -126,10 +121,11 @@ describe('http/index', () => { describe('http.get()', () => { it('adds authorization header when using OAuth2 with valid access token', async () => { const accessToken = 'let-me-in'; - const account = { + const account: HubSpotConfigAccount = { + name: 'test-account', accountId: 123, env: ENVIRONMENTS.PROD, - authType: 'oauth2' as AuthType, + authType: 'oauth2', auth: { clientId: 'd996372f-2b53-30d3-9c3b-4fdde4bce3a2', clientSecret: 'f90a6248-fbc0-3b03-b0db-ec58c95e791', @@ -141,10 +137,10 @@ describe('http/index', () => { }, }, }; - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [account], }); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReturnValue(account); await http.get(123, { url: 'some/endpoint/path' }); @@ -172,10 +168,11 @@ describe('http/index', () => { }); it('adds authorization header when using a user token', async () => { const accessToken = 'let-me-in'; - const account = { + const account: HubSpotConfigAccount = { + name: 'test-account', accountId: 123, env: ENVIRONMENTS.PROD, - authType: 'personalaccesskey' as AuthType, + authType: 'personalaccesskey', personalAccessKey: 'some-secret', auth: { tokenInfo: { @@ -184,10 +181,10 @@ describe('http/index', () => { }, }, }; - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [account], }); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReturnValue(account); await http.get(123, { url: 'some/endpoint/path' }); @@ -215,26 +212,16 @@ describe('http/index', () => { }); it('supports setting a custom timeout', async () => { - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ httpTimeout: 1000, - accounts: [ - { - accountId: 123, - apiKey: 'abc', - env: ENVIRONMENTS.PROD, - }, - ], - }); - getAccountConfig.mockReturnValue({ - accountId: 123, - apiKey: 'abc', - env: ENVIRONMENTS.PROD, + accounts: [ACCOUNT], }); + getConfigAccountById.mockReturnValue(ACCOUNT); await http.get(123, { url: 'some/endpoint/path' }); expect(mockedAxios).toHaveBeenCalledWith({ - baseURL: `https://api.hubapi.com`, + baseURL: `https://api.hubapiqa.com`, url: 'some/endpoint/path', headers: { 'User-Agent': `HubSpot Local Dev Lib/${version}`, diff --git a/http/getAxiosConfig.ts b/http/getAxiosConfig.ts index 2f4ad454..856045c1 100644 --- a/http/getAxiosConfig.ts +++ b/http/getAxiosConfig.ts @@ -1,7 +1,8 @@ import { version } from '../package.json'; -import { getAndLoadConfigIfNeeded } from '../config'; +import { getConfig } from '../config'; import { getHubSpotApiOrigin } from '../lib/urls'; import { HttpOptions } from '../types/Http'; +import { HubSpotConfig } from '../types/Config'; import { AxiosRequestConfig } from 'axios'; import https from 'https'; import http from 'http'; @@ -48,7 +49,12 @@ const DEFAULT_TRANSITIONAL = { export function getAxiosConfig(options: HttpOptions): AxiosRequestConfig { const { env, localHostOverride, headers, ...rest } = options; - const config = getAndLoadConfigIfNeeded(); + let config: HubSpotConfig | null; + try { + config = getConfig(); + } catch (e) { + config = null; + } let httpTimeout = 15000; let httpUseLocalhost = false; diff --git a/http/index.ts b/http/index.ts index 6bebea8a..b28a714e 100644 --- a/http/index.ts +++ b/http/index.ts @@ -3,16 +3,21 @@ import fs from 'fs-extra'; import contentDisposition from 'content-disposition'; import axios, { AxiosRequestConfig, AxiosResponse, AxiosPromise } from 'axios'; -import { getAccountConfig } from '../config'; +import { getConfigAccountById } from '../config'; import { USER_AGENTS, getAxiosConfig } from './getAxiosConfig'; import { addQueryParams } from './addQueryParams'; import { accessTokenForPersonalAccessKey } from '../lib/personalAccessKey'; import { getOauthManager } from '../lib/oauth'; -import { FlatAccountFields } from '../types/Accounts'; import { HttpOptions, HubSpotPromise } from '../types/Http'; import { logger } from '../lib/logger'; import { i18n } from '../utils/lang'; import { HubSpotHttpError } from '../models/HubSpotHttpError'; +import { OAuthConfigAccount } from '../types/Accounts'; +import { + PERSONAL_ACCESS_KEY_AUTH_METHOD, + OAUTH_AUTH_METHOD, + API_KEY_AUTH_METHOD, +} from '../constants/auth'; const i18nKey = 'http.index'; @@ -27,15 +32,16 @@ export function addUserAgentHeader(key: string, value: string) { } async function withOauth( - accountId: number, - accountConfig: FlatAccountFields, + account: OAuthConfigAccount, axiosConfig: AxiosRequestConfig ): Promise { const { headers } = axiosConfig; - const oauth = getOauthManager(accountId, accountConfig); + const oauth = getOauthManager(account); if (!oauth) { - throw new Error(i18n(`${i18nKey}.errors.withOauth`, { accountId })); + throw new Error( + i18n(`${i18nKey}.errors.withOauth`, { accountId: account.accountId }) + ); } const accessToken = await oauth.accessToken(); @@ -82,34 +88,40 @@ async function withAuth( accountId: number, options: HttpOptions ): Promise { - const accountConfig = getAccountConfig(accountId); - - if (!accountConfig) { - throw new Error(i18n(`${i18nKey}.errors.withAuth`, { accountId })); - } + const account = getConfigAccountById(accountId); - const { env, authType, apiKey } = accountConfig; + const { env, authType } = account; const axiosConfig = withPortalId( accountId, getAxiosConfig({ env, ...options }) ); - if (authType === 'personalaccesskey') { + if (authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { return withPersonalAccessKey(accountId, axiosConfig); } - if (authType === 'oauth2') { - return withOauth(accountId, accountConfig, axiosConfig); + if (authType === OAUTH_AUTH_METHOD.value) { + return withOauth(account, axiosConfig); } - const { params } = axiosConfig; - return { - ...axiosConfig, - params: { - ...params, - hapikey: apiKey, - }, - }; + if (authType === API_KEY_AUTH_METHOD.value) { + const { params } = axiosConfig; + + return { + ...axiosConfig, + params: { + ...params, + hapikey: account.apiKey, + }, + }; + } + + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + authType, + }) + ); } async function getRequest( diff --git a/lang/en.json b/lang/en.json index a0ecd68d..f8a841af 100644 --- a/lang/en.json +++ b/lang/en.json @@ -74,7 +74,8 @@ "personalAccessKey": { "errors": { "accountNotFound": "Account with id {{ accountId }} does not exist.", - "invalidPersonalAccessKey": "Error while retrieving new access token: {{ errorMessage }}" + "invalidPersonalAccessKey": "Error while retrieving new access token: {{ errorMessage }}", + "invalidAuthType": "Error fetching access token: account {{ accountId }} uses an auth type other than personalaccesskey" } }, "cms": { @@ -232,90 +233,74 @@ } }, "config": { - "cliConfiguration": { - "errors": { - "noConfigLoaded": "No config loaded." - }, - "load": { - "configFromEnv": "Loaded config from environment variables for {{ accountId }}", - "configFromFile": "Loaded config from configuration file.", - "empty": "The config file was empty. Initializing an empty config." - }, - "validate": { - "noConfig": "Valiation failed: No config was found.", - "noConfigAccounts": "Valiation failed: config.accounts[] is not defined.", - "emptyAccountConfig": "Valiation failed: config.accounts[] has an empty entry.", - "noAccountId": "Valiation failed: config.accounts[] has an entry missing accountId.", - "duplicateAccountIds": "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}.", - "duplicateAccountNames": "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}.", - "nameContainsSpaces": "Valiation failed: config.name {{ accountName }} cannot contain spaces." - }, - "updateAccount": { - "noConfigToUpdate": "No config to update.", - "updating": "Updating account config for {{ accountId }}", - "addingConfigEntry": "Adding account config entry for {{ accountId }}", - "errors": { - "accountIdRequired": "An accountId is required to update the config" - } - }, - "updateDefaultAccount": { - "errors": { - "invalidInput": "A 'defaultAccount' with value of number or string is required to update the config." - } - }, - "getCWDAccountOverride": { - "errorHeader": "Error in {{ hsAccountFile }}", - "readFileError": "Error reading account override file." - }, - "renameAccount": { - "errors": { - "invalidName": "Cannot find account with identifier {{ currentName }}" - } - }, - "removeAccountFromConfig": { - "deleting": "Deleting config for {{ accountId }}", - "errors": { - "invalidId": "Unable to find account for {{ nameOrId }}." - } - }, - "updateDefaultCmsPublishMode": { - "errors": { - "invalidCmsPublishMode": "The CMS publish mode {{ defaultCmsPublishMode }} is invalid. Valid values are {{ validCmsPublishModes }}." - } - }, - "updateHttpTimeout": { - "errors": { - "invalidTimeout": "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." - } - }, - "updateAllowUsageTracking": { - "errors": { - "invalidInput": "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." - } - } + "getDefaultConfigFilePath": { + "error": "Error getting config file path: no config file found" }, - "configFile": { - "errorReading": "Config file could not be read: {{ configPath }}", - "writeSuccess": "Successfully wrote updated config data to {{ configPath }}", - "errorLoading": "A configuration file could not be found at {{ configPath }}.", - "errors": { - "parsing": "Config file could not be parsed" - } + "getConfig": "Reading config from {{ path }}", + "isConfigValid": { + "missingAccounts": "Invalid config: no accounts found", + "duplicateAccountIds": "Invalid config: multiple accounts with accountId: {{ accountId }}", + "duplicateAccountNames": "Invalid config: multiple accounts with name: {{ accountName }}", + "invalidAccountName": "Invalid config: account name {{ accountName }} contains spaces" + }, + "getConfigAccountById": { + "error": "Error getting config account: no account with id {{ accountId }} exists in config" + }, + "getConfigAccountByName": { + "error": "Error getting config account: no account with name {{ accountName }} exists in config" + }, + "getConfigDefaultAccount": { + "fieldMissingError": "Error getting config default account: no default account field found in config", + "accountMissingError": "Error getting config default account: default account is set to {{ defaultAccount }} but no account with that id exists in config" + }, + "addConfigAccount": { + "invalidAccount": "Error adding config account: account is invalid", + "duplicateAccount": "Error adding config account: account with id {{ accountId }} already exists in config" }, - "configUtils": { - "unknownType": "Unknown auth type {{ type }}" + "updateConfigAccount": { + "invalidAccount": "Error updating config account: account is invalid", + "accountNotFound": "Error updating config account: account with id {{ accountId }} not found in config" }, - "environment": { - "loadConfig": { - "missingAccountId": "Unable to load config from environment variables: Missing accountId", - "missingEnv": "Unable to load config from environment variables: Missing env", - "unknownAuthType": "Unable to load config from environment variables: Unknown auth type" + "setConfigAccountAsDefault": { + "accountNotFound": "Error setting config default account: account with id {{ accountId }} not found in config" + }, + "renameConfigAccount": { + "accountNotFound": "Error renaming config account: account with name {{ currentName }} not found in config", + "duplicateAccount": "Error renaming config account: account with name {{ newName}} already exists in config" + }, + "removeAccountFromConfig": { + "accountNotFound": "Error removing config account: account with id {{ accountId }} not found in config" + }, + "updateHttpTimeout": { + "invalidTimeout": "Error updating config http timeout: timeout must be greater than {{ minTimeout }}" + }, + "updateDefaultCmsPublishMode": { + "invalidCmsPublishMode": "Error updating config default CMS publish mode: CMS publish can only be set to 'draft' or 'publish'" + }, + "utils": { + "isConfigAccountValid": { + "missingAccount": "Invalid config: at least one account in config is missing data", + "missingAuthType": "Invalid config: account {{ accountId }} has no authType", + "missingAccountId": "Invalid config: at least one account in config is missing accountId", + "missingApiKey": "Invalid config: account {{ accountId }} has authType of apikey but is missing the apiKey field", + "missingAuth": "Invalid config: account {{ accountId }} has authtype of oauth2 but is missing auth data", + "missingPersonalAccessKey": "Invalid config: account {{ accountId }} has authType of personalAccessKey but is missing the personalAccessKey field" + }, + "getConfigPathEnvironmentVariables": { + "invalidEnvironmentVariables": "Error loading config: USE_ENVIRONMENT_HUBSPOT_CONFIG and HUBSPOT_CONFIG_PATH cannot both be set simultaneously" + }, + "parseConfig": { + "error": "An error occurred parsing the config file." + }, + "buildConfigFromEnvironment": { + "missingAccountId": "Error loading config from environment: HUBSPOT_ACCOUNT_ID not set", + "invalidAuthType": "Error loading config from environment: auth is invalid. Use HUBSPOT_CLIENT_ID, HUBSPOT_CLIENT_SECRET, and HUBSPOT_REFRESH_TOKEN to authenticate with Oauth2, PERSONAL_ACCESS_KEY to authenticate with Personal Access Key, or API_KEY to authenticate with API Key." } }, - "migrate": { - "errors": { - "writeConfig": "Unable to write global configuration file at {{ configPath }}.", - "noDeprecatedConfig": "No deprecated configuration file found. Skipping migration to global config." + "defaultAccountOverride": { + "getDefaultAccountOverrideAccountId": { + "errorHeader": "Error in {{ hsAccountFile }}", + "readFileError": "Error reading account override file." } } }, @@ -367,7 +352,8 @@ }, "errors": { "withOauth": "Oauth manager for account {{ accountId }} not found.", - "withAuth": "Account with id {{ accountId }} not found." + "withAuth": "Account with id {{ accountId }} not found.", + "invalidAuthType": "Error authenticating HTTP request: account {{ accountId }} has an invalid auth type {{ authType }}" } } }, diff --git a/lib/__tests__/environment.test.ts b/lib/__tests__/environment.test.ts index ebce2670..48fceeee 100644 --- a/lib/__tests__/environment.test.ts +++ b/lib/__tests__/environment.test.ts @@ -10,7 +10,6 @@ describe('lib/environment', () => { }); it('should return prod when the provided env is not equal to QA', () => { - // @ts-expect-error purposefully causing an error expect(getValidEnv('notQA')).toEqual(PROD); }); diff --git a/lib/__tests__/oauth.test.ts b/lib/__tests__/oauth.test.ts index 62a6f87c..820c985e 100644 --- a/lib/__tests__/oauth.test.ts +++ b/lib/__tests__/oauth.test.ts @@ -1,62 +1,62 @@ import { addOauthToAccountConfig, getOauthManager } from '../oauth'; -jest.mock('../../config/getAccountIdentifier'); jest.mock('../../config'); jest.mock('../logger'); jest.mock('../../errors'); +jest.mock('../../models/OAuth2Manager'); -import { updateAccountConfig, writeConfig } from '../../config'; -import { OAuth2Manager } from '../../models/OAuth2Manager'; -import { FlatAccountFields_NEW } from '../../types/Accounts'; +import { updateConfigAccount } from '../../config'; +import * as OAuth2ManagerModule from '../../models/OAuth2Manager'; import { ENVIRONMENTS } from '../../constants/environments'; import { AUTH_METHODS } from '../../constants/auth'; import { logger } from '../logger'; +import { HubSpotConfigAccount } from '../../types/Accounts'; -const OAuth2ManagerFromConfigMock = jest.spyOn(OAuth2Manager, 'fromConfig'); +const UnmockedOAuth2Manager = jest.requireActual('../../models/OAuth2Manager'); +const OAuth2Manager = UnmockedOAuth2Manager.OAuth2Manager; + +const OAuth2ManagerMock = jest.spyOn(OAuth2ManagerModule, 'OAuth2Manager'); describe('lib/oauth', () => { const accountId = 123; - const accountConfig: FlatAccountFields_NEW = { + const account: HubSpotConfigAccount = { + name: 'my-account', accountId, env: ENVIRONMENTS.QA, - clientId: 'my-client-id', - clientSecret: "shhhh, it's a secret", - scopes: [], - apiKey: '', - personalAccessKey: '', - }; - const account = { - name: 'my-account', + authType: AUTH_METHODS.oauth.value, + auth: { + clientId: 'my-client-id', + clientSecret: "shhhh, it's a secret", + scopes: [], + tokenInfo: {}, + }, }; + describe('getOauthManager', () => { it('should create a OAuth2Manager for accounts that are not cached', () => { - getOauthManager(accountId, accountConfig); - expect(OAuth2ManagerFromConfigMock).toHaveBeenCalledTimes(1); - expect(OAuth2ManagerFromConfigMock).toHaveBeenCalledWith( - accountConfig, + getOauthManager(account); + expect(OAuth2ManagerMock).toHaveBeenCalledTimes(1); + expect(OAuth2ManagerMock).toHaveBeenCalledWith( + account, expect.any(Function) ); }); it('should use the cached OAuth2Manager if it exists', () => { - getOauthManager(accountId, accountConfig); - expect(OAuth2ManagerFromConfigMock).not.toHaveBeenCalled(); + getOauthManager(account); + expect(OAuth2ManagerMock).not.toHaveBeenCalled(); }); + + jest.clearAllMocks(); }); describe('addOauthToAccountConfig', () => { it('should update the config', () => { - addOauthToAccountConfig(new OAuth2Manager(account)); - expect(updateAccountConfig).toHaveBeenCalledTimes(1); - expect(updateAccountConfig).toHaveBeenCalledWith({ - ...account, - authType: AUTH_METHODS.oauth.value, - }); - }); - - it('should write the updated config', () => { - addOauthToAccountConfig(new OAuth2Manager(account)); - expect(writeConfig).toHaveBeenCalledTimes(1); + const oauthManager = new OAuth2Manager(account, () => null); + console.log('oauthManager', oauthManager.account); + addOauthToAccountConfig(oauthManager); + expect(updateConfigAccount).toHaveBeenCalledTimes(1); + expect(updateConfigAccount).toHaveBeenCalledWith(account); }); it('should log messages letting the user know the status of the operation', () => { diff --git a/lib/__tests__/personalAccessKey.test.ts b/lib/__tests__/personalAccessKey.test.ts index 028c9838..364e12ed 100644 --- a/lib/__tests__/personalAccessKey.test.ts +++ b/lib/__tests__/personalAccessKey.test.ts @@ -1,8 +1,8 @@ import moment from 'moment'; import { - getAndLoadConfigIfNeeded as __getAndLoadConfigIfNeeded, - getAccountConfig as __getAccountConfig, - updateAccountConfig as __updateAccountConfig, + getConfig as __getConfig, + getConfigAccountById as __getConfigAccountById, + updateConfigAccount as __updateConfigAccount, } from '../../config'; import { fetchAccessToken as __fetchAccessToken } from '../../api/localDevAuth'; import { fetchSandboxHubData as __fetchSandboxHubData } from '../../api/sandboxHubs'; @@ -14,7 +14,7 @@ import { getAccessToken, updateConfigWithAccessToken, } from '../personalAccessKey'; -import { AuthType } from '../../types/Accounts'; +import { HubSpotConfigAccount } from '../../types/Accounts'; import { mockAxiosResponse } from './__utils__/mockAxiosResponse'; jest.mock('../../config'); @@ -23,16 +23,13 @@ jest.mock('../../api/localDevAuth'); jest.mock('../../api/sandboxHubs'); jest.mock('../../api/developerTestAccounts'); -const updateAccountConfig = __updateAccountConfig as jest.MockedFunction< - typeof __updateAccountConfig +const updateConfigAccount = __updateConfigAccount as jest.MockedFunction< + typeof __updateConfigAccount >; -const getAccountConfig = __getAccountConfig as jest.MockedFunction< - typeof __getAccountConfig +const getConfigAccountById = __getConfigAccountById as jest.MockedFunction< + typeof __getConfigAccountById >; -const getAndLoadConfigIfNeeded = - __getAndLoadConfigIfNeeded as jest.MockedFunction< - typeof __getAndLoadConfigIfNeeded - >; +const getConfig = __getConfig as jest.MockedFunction; const fetchAccessToken = __fetchAccessToken as jest.MockedFunction< typeof __fetchAccessToken >; @@ -48,16 +45,20 @@ describe('lib/personalAccessKey', () => { describe('accessTokenForPersonalAccessKey()', () => { it('refreshes access token when access token is missing', async () => { const accountId = 123; - const account = { + const account: HubSpotConfigAccount = { + name: 'test-account', accountId, - authType: 'personalaccesskey' as AuthType, + authType: 'personalaccesskey', personalAccessKey: 'let-me-in', env: ENVIRONMENTS.QA, + auth: { + tokenInfo: {}, + }, }; - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [account], }); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReturnValue(account); const freshAccessToken = 'fresh-token'; fetchAccessToken.mockResolvedValue( @@ -77,16 +78,20 @@ describe('lib/personalAccessKey', () => { }); it('uses accountId when refreshing token', async () => { const accountId = 123; - const account = { + const account: HubSpotConfigAccount = { accountId, - authType: 'personalaccesskey' as AuthType, + name: 'test-account', + authType: 'personalaccesskey', personalAccessKey: 'let-me-in-2', env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: {}, + }, }; - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [account], }); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReturnValue(account); await accessTokenForPersonalAccessKey(accountId); expect(fetchAccessToken).toHaveBeenCalledWith( @@ -97,9 +102,10 @@ describe('lib/personalAccessKey', () => { }); it('refreshes access token when the existing token is expired', async () => { const accountId = 123; - const account = { + const account: HubSpotConfigAccount = { + name: 'test-account', accountId, - authType: 'personalaccesskey' as AuthType, + authType: 'personalaccesskey', personalAccessKey: 'let-me-in-3', auth: { tokenInfo: { @@ -109,10 +115,10 @@ describe('lib/personalAccessKey', () => { }, env: ENVIRONMENTS.QA, }; - getAndLoadConfigIfNeeded.mockReturnValue({ + getConfig.mockReturnValue({ accounts: [account], }); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReturnValue(account); const freshAccessToken = 'fresh-token'; fetchAccessToken.mockResolvedValue( @@ -134,26 +140,32 @@ describe('lib/personalAccessKey', () => { const accountId = 123; const accessKey = 'let-me-in-4'; const userId = 456; - const mockAccount = (expiresAt: string, accessToken: string) => ({ - accountId, - authType: 'personalaccesskey' as AuthType, - personalAccessKey: accessKey, - auth: { - tokenInfo: { - expiresAt, - accessToken, + function mockAccount( + expiresAt: string, + accessToken: string + ): HubSpotConfigAccount { + return { + name: 'test-account', + accountId, + authType: 'personalaccesskey', + personalAccessKey: accessKey, + auth: { + tokenInfo: { + expiresAt, + accessToken, + }, }, - }, - env: ENVIRONMENTS.QA, - }); + env: ENVIRONMENTS.QA, + }; + } const initialAccountConfig = mockAccount( moment().subtract(2, 'hours').toISOString(), 'test-token' ); - getAndLoadConfigIfNeeded.mockReturnValueOnce({ + getConfig.mockReturnValueOnce({ accounts: [initialAccountConfig], }); - getAccountConfig.mockReturnValueOnce(initialAccountConfig); + getConfigAccountById.mockReturnValueOnce(initialAccountConfig); const firstAccessToken = 'fresh-token'; const expiresAtMillis = moment().subtract(1, 'hours').valueOf(); @@ -177,10 +189,10 @@ describe('lib/personalAccessKey', () => { moment(expiresAtMillis).toISOString(), firstAccessToken ); - getAndLoadConfigIfNeeded.mockReturnValueOnce({ + getConfig.mockReturnValueOnce({ accounts: [updatedAccountConfig], }); - getAccountConfig.mockReturnValueOnce(updatedAccountConfig); + getConfigAccountById.mockReturnValueOnce(updatedAccountConfig); const secondAccessToken = 'another-fresh-token'; fetchAccessToken.mockResolvedValue( @@ -227,7 +239,7 @@ describe('lib/personalAccessKey', () => { 'account-name' ); - expect(updateAccountConfig).toHaveBeenCalledWith( + expect(updateConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ accountId: 123, accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, @@ -269,7 +281,7 @@ describe('lib/personalAccessKey', () => { 'account-name' ); - expect(updateAccountConfig).toHaveBeenCalledWith( + expect(updateConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ accountId: 123, accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, @@ -317,7 +329,7 @@ describe('lib/personalAccessKey', () => { 'Dev test portal' ); - expect(updateAccountConfig).toHaveBeenCalledWith( + expect(updateConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ accountId: 123, accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST, diff --git a/lib/__tests__/themes.test.ts b/lib/__tests__/themes.test.ts index 391d536c..e222c48a 100644 --- a/lib/__tests__/themes.test.ts +++ b/lib/__tests__/themes.test.ts @@ -1,7 +1,7 @@ import findup from 'findup-sync'; import { getHubSpotWebsiteOrigin } from '../urls'; import { getThemeJSONPath, getThemePreviewUrl } from '../cms/themes'; -import { getEnv } from '../../config'; +import { getConfigAccountEnvironment } from '../../config'; import { ENVIRONMENTS } from '../../constants/environments'; jest.mock('findup-sync'); @@ -15,7 +15,10 @@ jest.mock('../../constants/environments', () => ({ })); const mockedFindup = findup as jest.MockedFunction; -const mockedGetEnv = getEnv as jest.MockedFunction; +const mockedGetConfigAccountEnvironment = + getConfigAccountEnvironment as jest.MockedFunction< + typeof getConfigAccountEnvironment + >; const mockedGetHubSpotWebsiteOrigin = getHubSpotWebsiteOrigin as jest.MockedFunction< typeof getHubSpotWebsiteOrigin @@ -51,12 +54,12 @@ describe('lib/cms/themes', () => { describe('getThemePreviewUrl', () => { it('should return the correct theme preview URL for PROD environment', () => { mockedFindup.mockReturnValue('/src/my-theme/theme.json'); - mockedGetEnv.mockReturnValue('prod'); + mockedGetConfigAccountEnvironment.mockReturnValue('prod'); mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://prod.hubspot.com'); const result = getThemePreviewUrl('/path/to/file', 12345); - expect(getEnv).toHaveBeenCalledWith(12345); + expect(getConfigAccountEnvironment).toHaveBeenCalledWith(12345); expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.PROD); expect(result).toBe( 'https://prod.hubspot.com/theme-previewer/12345/edit/my-theme' @@ -65,12 +68,12 @@ describe('lib/cms/themes', () => { it('should return the correct theme preview URL for QA environment', () => { mockedFindup.mockReturnValue('/src/my-theme/theme.json'); - mockedGetEnv.mockReturnValue('qa'); + mockedGetConfigAccountEnvironment.mockReturnValue('qa'); mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://qa.hubspot.com'); const result = getThemePreviewUrl('/path/to/file', 12345); - expect(getEnv).toHaveBeenCalledWith(12345); + expect(getConfigAccountEnvironment).toHaveBeenCalledWith(12345); expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.QA); expect(result).toBe( 'https://qa.hubspot.com/theme-previewer/12345/edit/my-theme' diff --git a/lib/__tests__/trackUsage.test.ts b/lib/__tests__/trackUsage.test.ts index 3f5557b5..24a54531 100644 --- a/lib/__tests__/trackUsage.test.ts +++ b/lib/__tests__/trackUsage.test.ts @@ -1,30 +1,28 @@ import axios from 'axios'; import { trackUsage } from '../trackUsage'; import { - getAccountConfig as __getAccountConfig, - getAndLoadConfigIfNeeded as __getAndLoadConfigIfNeeded, + getConfigAccountById as __getConfigAccountById, + getConfig as __getConfig, } from '../../config'; -import { AuthType } from '../../types/Accounts'; +import { HubSpotConfigAccount } from '../../types/Accounts'; import { ENVIRONMENTS } from '../../constants/environments'; jest.mock('axios'); jest.mock('../../config'); const mockedAxios = jest.mocked(axios); -const getAccountConfig = __getAccountConfig as jest.MockedFunction< - typeof __getAccountConfig +const getConfigAccountById = __getConfigAccountById as jest.MockedFunction< + typeof __getConfigAccountById >; -const getAndLoadConfigIfNeeded = - __getAndLoadConfigIfNeeded as jest.MockedFunction< - typeof __getAndLoadConfigIfNeeded - >; +const getConfig = __getConfig as jest.MockedFunction; mockedAxios.mockResolvedValue({}); -getAndLoadConfigIfNeeded.mockReturnValue({}); +getConfig.mockReturnValue({ accounts: [] }); -const account = { +const account: HubSpotConfigAccount = { + name: 'test-account', accountId: 12345, - authType: 'personalaccesskey' as AuthType, + authType: 'personalaccesskey', personalAccessKey: 'let-me-in-3', auth: { tokenInfo: { @@ -43,8 +41,8 @@ const usageTrackingMeta = { describe('lib/trackUsage', () => { describe('trackUsage()', () => { beforeEach(() => { - getAccountConfig.mockReset(); - getAccountConfig.mockReturnValue(account); + getConfigAccountById.mockReset(); + getConfigAccountById.mockReturnValue(account); }); it('tracks correctly for unauthenticated accounts', async () => { @@ -56,7 +54,7 @@ describe('lib/trackUsage', () => { expect(mockedAxios).toHaveBeenCalled(); expect(requestArgs!.data.eventName).toEqual('test-action'); expect(requestArgs!.url.includes('authenticated')).toBeFalsy(); - expect(getAccountConfig).not.toHaveBeenCalled(); + expect(getConfigAccountById).not.toHaveBeenCalled(); }); it('tracks correctly for authenticated accounts', async () => { @@ -68,7 +66,7 @@ describe('lib/trackUsage', () => { expect(mockedAxios).toHaveBeenCalled(); expect(requestArgs!.data.eventName).toEqual('test-action'); expect(requestArgs!.url.includes('authenticated')).toBeTruthy(); - expect(getAccountConfig).toHaveBeenCalled(); + expect(getConfigAccountById).toHaveBeenCalled(); }); }); }); diff --git a/lib/cms/themes.ts b/lib/cms/themes.ts index 9f03b479..3ecf08f1 100644 --- a/lib/cms/themes.ts +++ b/lib/cms/themes.ts @@ -1,7 +1,7 @@ import findup from 'findup-sync'; import { getHubSpotWebsiteOrigin } from '../urls'; import { ENVIRONMENTS } from '../../constants/environments'; -import { getEnv } from '../../config'; +import { getConfigAccountEnvironment } from '../../config'; export function getThemeJSONPath(path: string): string | null { return findup('theme.json', { @@ -26,7 +26,9 @@ export function getThemePreviewUrl( if (!themeName) return; const baseUrl = getHubSpotWebsiteOrigin( - getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD + getConfigAccountEnvironment(accountId) === 'qa' + ? ENVIRONMENTS.QA + : ENVIRONMENTS.PROD ); return `${baseUrl}/theme-previewer/${accountId}/edit/${encodeURIComponent( diff --git a/lib/environment.ts b/lib/environment.ts index 6654d944..b3b36f06 100644 --- a/lib/environment.ts +++ b/lib/environment.ts @@ -2,7 +2,7 @@ import { ENVIRONMENTS } from '../constants/environments'; import { Environment } from '../types/Config'; export function getValidEnv( - env?: Environment | null, + env?: string | null, maskedProductionValue?: Environment ): Environment { const prodValue = diff --git a/lib/oauth.ts b/lib/oauth.ts index f4ea4881..00823c76 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -1,47 +1,33 @@ import { OAuth2Manager } from '../models/OAuth2Manager'; -import { AUTH_METHODS } from '../constants/auth'; -import { FlatAccountFields } from '../types/Accounts'; +import { OAuthConfigAccount } from '../types/Accounts'; import { logger } from './logger'; -import { getAccountIdentifier } from '../config/getAccountIdentifier'; -import { updateAccountConfig, writeConfig } from '../config'; +import { updateConfigAccount } from '../config'; import { i18n } from '../utils/lang'; const i18nKey = 'lib.oauth'; const oauthManagers = new Map(); -function writeOauthTokenInfo(accountConfig: FlatAccountFields): void { - const accountId = getAccountIdentifier(accountConfig); - +function writeOauthTokenInfo(account: OAuthConfigAccount): void { logger.debug( - i18n(`${i18nKey}.writeTokenInfo`, { portalId: accountId || '' }) + i18n(`${i18nKey}.writeTokenInfo`, { portalId: account.accountId }) ); - updateAccountConfig(accountConfig); - writeConfig(); + updateConfigAccount(account); } -export function getOauthManager( - accountId: number, - accountConfig: FlatAccountFields -) { - if (!oauthManagers.has(accountId)) { +export function getOauthManager(account: OAuthConfigAccount) { + if (!oauthManagers.has(account.accountId)) { oauthManagers.set( - accountId, - OAuth2Manager.fromConfig(accountConfig, () => - writeOauthTokenInfo(accountConfig) - ) + account.accountId, + new OAuth2Manager(account, () => writeOauthTokenInfo(account)) ); } - return oauthManagers.get(accountId); + return oauthManagers.get(account.accountId); } export function addOauthToAccountConfig(oauth: OAuth2Manager) { logger.log(i18n(`${i18nKey}.addOauthToAccountConfig.init`)); - updateAccountConfig({ - ...oauth.account, - authType: AUTH_METHODS.oauth.value, - }); - writeConfig(); + updateConfigAccount(oauth.account); logger.success(i18n(`${i18nKey}.addOauthToAccountConfig.success`)); } diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index c982a6e2..45ef14cd 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -7,22 +7,20 @@ import { } from '../api/localDevAuth'; import { fetchSandboxHubData } from '../api/sandboxHubs'; import { - CLIAccount, - PersonalAccessKeyAccount, + PersonalAccessKeyConfigAccount, ScopeGroupAuthorization, } from '../types/Accounts'; import { Environment } from '../types/Config'; import { - getAccountConfig, - updateAccountConfig, - writeConfig, - getEnv, - updateDefaultAccount, + getConfigAccountById, + getConfigAccountByName, + updateConfigAccount, + setConfigAccountAsDefault, + getConfigDefaultAccount, } from '../config'; import { HUBSPOT_ACCOUNT_TYPES } from '../constants/config'; import { fetchDeveloperTestAccountData } from '../api/developerTestAccounts'; import { logger } from './logger'; -import { CLIConfiguration } from '../config/CLIConfiguration'; import { i18n } from '../utils/lang'; import { isHubSpotHttpError } from '../errors'; import { AccessToken } from '../types/Accounts'; @@ -60,49 +58,40 @@ export async function getAccessToken( } async function refreshAccessToken( - personalAccessKey: string, - env: Environment = ENVIRONMENTS.PROD, - accountId: number + account: PersonalAccessKeyConfigAccount ): Promise { + const { personalAccessKey, env, accountId } = account; const accessTokenResponse = await getAccessToken( personalAccessKey, env, accountId ); const { accessToken, expiresAt } = accessTokenResponse; - const config = getAccountConfig(accountId); - updateAccountConfig({ - env, - ...config, - accountId, - tokenInfo: { - accessToken, - expiresAt: expiresAt, + updateConfigAccount({ + ...account, + auth: { + tokenInfo: { + accessToken, + expiresAt: expiresAt, + }, }, }); - writeConfig(); return accessTokenResponse; } async function getNewAccessToken( - accountId: number, - personalAccessKey: string, - expiresAt: string | undefined, - env: Environment + account: PersonalAccessKeyConfigAccount ): Promise { - const key = getRefreshKey(personalAccessKey, expiresAt); + const { personalAccessKey, auth } = account; + const key = getRefreshKey(personalAccessKey, auth.tokenInfo.expiresAt); if (refreshRequests.has(key)) { return refreshRequests.get(key); } let accessTokenResponse: AccessToken; try { - const refreshAccessPromise = refreshAccessToken( - personalAccessKey, - env, - accountId - ); + const refreshAccessPromise = refreshAccessToken(account); if (key) { refreshRequests.set(key, refreshAccessPromise); } @@ -119,18 +108,19 @@ async function getNewAccessToken( async function getNewAccessTokenByAccountId( accountId: number ): Promise { - const account = getAccountConfig(accountId) as PersonalAccessKeyAccount; + const account = getConfigAccountById(accountId); if (!account) { throw new Error(i18n(`${i18nKey}.errors.accountNotFound`, { accountId })); } - const { auth, personalAccessKey, env } = account; + if (account.authType !== PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + }) + ); + } - const accessTokenResponse = await getNewAccessToken( - accountId, - personalAccessKey, - auth?.tokenInfo?.expiresAt, - env - ); + const accessTokenResponse = await getNewAccessToken(account); return accessTokenResponse; } @@ -138,11 +128,19 @@ export async function accessTokenForPersonalAccessKey( accountId: number, forceRefresh = false ): Promise { - const account = getAccountConfig(accountId) as PersonalAccessKeyAccount; + const account = getConfigAccountById(accountId); if (!account) { throw new Error(i18n(`${i18nKey}.errors.accountNotFound`, { accountId })); } - const { auth, personalAccessKey, env } = account; + if (account.authType !== PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + }) + ); + } + + const { auth } = account; const authTokenInfo = auth && auth.tokenInfo; const authDataExists = authTokenInfo && auth?.tokenInfo?.accessToken; @@ -151,15 +149,10 @@ export async function accessTokenForPersonalAccessKey( forceRefresh || moment().add(5, 'minutes').isAfter(moment(authTokenInfo.expiresAt)) ) { - return getNewAccessToken( - accountId, - personalAccessKey, - authTokenInfo && authTokenInfo.expiresAt, - env - ).then(tokenInfo => tokenInfo.accessToken); + return getNewAccessToken(account).then(tokenInfo => tokenInfo.accessToken); } - return auth?.tokenInfo?.accessToken; + return auth.tokenInfo?.accessToken; } export async function enabledFeaturesForPersonalAccessKey( @@ -187,9 +180,12 @@ export async function updateConfigWithAccessToken( env?: Environment, name?: string, makeDefault = false -): Promise { +): Promise { const { portalId, accessToken, expiresAt, accountType } = token; - const accountEnv = env || getEnv(name); + const account = name + ? getConfigAccountByName(name) + : getConfigDefaultAccount(); + const accountEnv = env || account.env; let parentAccountId; try { @@ -230,22 +226,21 @@ export async function updateConfigWithAccessToken( logger.debug(err); } - const updatedAccount = updateAccountConfig({ + const updatedAccount = { accountId: portalId, accountType, personalAccessKey, - name, + name: name || account.name, authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, - tokenInfo: { accessToken, expiresAt }, + auth: { tokenInfo: { accessToken, expiresAt } }, parentAccountId, env: accountEnv, - }); - if (!CLIConfiguration.isActive()) { - writeConfig(); - } + }; + + updateConfigAccount(updatedAccount); if (makeDefault && name) { - updateDefaultAccount(name); + setConfigAccountAsDefault(name); } return updatedAccount; diff --git a/lib/trackUsage.ts b/lib/trackUsage.ts index 9944b1c7..e61d58b2 100644 --- a/lib/trackUsage.ts +++ b/lib/trackUsage.ts @@ -2,9 +2,10 @@ import axios from 'axios'; import { getAxiosConfig } from '../http/getAxiosConfig'; import { logger } from './logger'; import { http } from '../http'; -import { getAccountConfig, getEnv } from '../config'; +import { getConfigAccountById, getConfigAccountEnvironment } from '../config'; import { FILE_MAPPER_API_PATH } from '../api/fileMapper'; import { i18n } from '../utils/lang'; +import { getValidEnv } from './environment'; const i18nKey = 'lib.trackUsage'; @@ -40,9 +41,9 @@ export async function trackUsage( const path = `${FILE_MAPPER_API_PATH}/${analyticsEndpoint}`; - const accountConfig = accountId && getAccountConfig(accountId); + const account = accountId && getConfigAccountById(accountId); - if (accountConfig && accountConfig.authType === 'personalaccesskey') { + if (account && account.authType === 'personalaccesskey') { logger.debug(i18n(`${i18nKey}.sendingEventAuthenticated`)); try { await http.post(accountId, { @@ -56,7 +57,9 @@ export async function trackUsage( } } - const env = getEnv(accountId); + const env = accountId + ? getConfigAccountEnvironment(accountId) + : getValidEnv(); const axiosConfig = getAxiosConfig({ env, url: path, diff --git a/models/OAuth2Manager.ts b/models/OAuth2Manager.ts index c32c6d17..78793c65 100644 --- a/models/OAuth2Manager.ts +++ b/models/OAuth2Manager.ts @@ -4,26 +4,23 @@ import moment from 'moment'; import { getHubSpotApiOrigin } from '../lib/urls'; import { getValidEnv } from '../lib/environment'; import { - FlatAccountFields, - OAuth2ManagerAccountConfig, + OAuthConfigAccount, WriteTokenInfoFunction, RefreshTokenResponse, ExchangeProof, } from '../types/Accounts'; import { logger } from '../lib/logger'; -import { getAccountIdentifier } from '../config/getAccountIdentifier'; -import { AUTH_METHODS } from '../constants/auth'; import { i18n } from '../utils/lang'; const i18nKey = 'models.OAuth2Manager'; export class OAuth2Manager { - account: OAuth2ManagerAccountConfig; + account: OAuthConfigAccount; writeTokenInfo?: WriteTokenInfoFunction; refreshTokenRequest: Promise | null; constructor( - account: OAuth2ManagerAccountConfig, + account: OAuthConfigAccount, writeTokenInfo?: WriteTokenInfoFunction ) { this.writeTokenInfo = writeTokenInfo; @@ -36,30 +33,30 @@ export class OAuth2Manager { } async accessToken(): Promise { - if (!this.account.tokenInfo?.refreshToken) { + if (!this.account.auth.tokenInfo.refreshToken) { throw new Error( i18n(`${i18nKey}.errors.missingRefreshToken`, { - accountId: getAccountIdentifier(this.account)!, + accountId: this.account.accountId, }) ); } if ( - !this.account.tokenInfo?.accessToken || + !this.account.auth.tokenInfo.accessToken || moment() .add(5, 'minutes') - .isAfter(moment(new Date(this.account.tokenInfo.expiresAt || ''))) + .isAfter(moment(new Date(this.account.auth.tokenInfo.expiresAt || ''))) ) { await this.refreshAccessToken(); } - return this.account.tokenInfo.accessToken; + return this.account.auth.tokenInfo.accessToken; } async fetchAccessToken(exchangeProof: ExchangeProof): Promise { logger.debug( i18n(`${i18nKey}.fetchingAccessToken`, { - accountId: getAccountIdentifier(this.account)!, - clientId: this.account.clientId || '', + accountId: this.account.accountId, + clientId: this.account.auth.clientId, }) ); @@ -79,22 +76,22 @@ export class OAuth2Manager { access_token: accessToken, expires_in: expiresIn, } = data; - if (!this.account.tokenInfo) { - this.account.tokenInfo = {}; + if (!this.account.auth.tokenInfo) { + this.account.auth.tokenInfo = {}; } - this.account.tokenInfo.refreshToken = refreshToken; - this.account.tokenInfo.accessToken = accessToken; - this.account.tokenInfo.expiresAt = moment() + this.account.auth.tokenInfo.refreshToken = refreshToken; + this.account.auth.tokenInfo.accessToken = accessToken; + this.account.auth.tokenInfo.expiresAt = moment() .add(Math.round(parseInt(expiresIn) * 0.75), 'seconds') .toString(); if (this.writeTokenInfo) { logger.debug( i18n(`${i18nKey}.updatingTokenInfo`, { - accountId: getAccountIdentifier(this.account)!, - clientId: this.account.clientId || '', + accountId: this.account.accountId, + clientId: this.account.auth.clientId, }) ); - this.writeTokenInfo(this.account.tokenInfo); + this.writeTokenInfo(this.account.auth.tokenInfo); } } finally { this.refreshTokenRequest = null; @@ -105,8 +102,8 @@ export class OAuth2Manager { if (this.refreshTokenRequest) { logger.debug( i18n(`${i18nKey}.refreshingAccessToken`, { - accountId: getAccountIdentifier(this.account)!, - clientId: this.account.clientId || '', + accountId: this.account.accountId, + clientId: this.account.auth.clientId, }) ); await this.refreshTokenRequest; @@ -118,24 +115,10 @@ export class OAuth2Manager { async refreshAccessToken(): Promise { const refreshTokenProof = { grant_type: 'refresh_token', - client_id: this.account.clientId, - client_secret: this.account.clientSecret, - refresh_token: this.account.tokenInfo?.refreshToken, + client_id: this.account.auth.clientId, + client_secret: this.account.auth.clientSecret, + refresh_token: this.account.auth.tokenInfo.refreshToken, }; await this.exchangeForTokens(refreshTokenProof); } - - static fromConfig( - accountConfig: FlatAccountFields, - writeTokenInfo: WriteTokenInfoFunction - ) { - return new OAuth2Manager( - { - ...accountConfig, - authType: AUTH_METHODS.oauth.value, - ...(accountConfig.auth || {}), - }, - writeTokenInfo - ); - } } diff --git a/models/__tests__/OAuth2Manager.test.ts b/models/__tests__/OAuth2Manager.test.ts index 7e6f5e08..8d539fa8 100644 --- a/models/__tests__/OAuth2Manager.test.ts +++ b/models/__tests__/OAuth2Manager.test.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import moment from 'moment'; import { OAuth2Manager } from '../OAuth2Manager'; import { ENVIRONMENTS } from '../../constants/environments'; +import { HubSpotConfigAccount } from '../../types/Accounts'; jest.mock('axios'); @@ -19,10 +20,11 @@ const axiosSpy = axiosMock.mockResolvedValue({ const initialRefreshToken = '84d22710-4cb7-5581-ba05-35f9945e5e8e'; -const oauthAccount = { +const oauthAccount: HubSpotConfigAccount = { + name: 'my-account', accountId: 123, env: ENVIRONMENTS.PROD, - authType: 'oauth2' as const, + authType: 'oauth2', auth: { clientId: 'd996372f-2b53-30d3-9c3b-4fdde4bce3a2', clientSecret: 'f90a6248-fbc0-3b03-b0db-ec58c95e791', @@ -46,10 +48,7 @@ describe('models/Oauth2Manager', () => { describe('fromConfig()', () => { it('initializes an oauth manager instance', async () => { - const oauthManager = OAuth2Manager.fromConfig( - oauthAccount, - () => undefined - ); + const oauthManager = new OAuth2Manager(oauthAccount, () => undefined); expect(oauthManager.refreshTokenRequest).toBe(null); expect(oauthManager.account).toMatchObject(oauthAccount); @@ -58,10 +57,7 @@ describe('models/Oauth2Manager', () => { describe('refreshAccessToken()', () => { it('refreshes the oauth access token', async () => { - const oauthManager = OAuth2Manager.fromConfig( - oauthAccount, - () => undefined - ); + const oauthManager = new OAuth2Manager(oauthAccount, () => undefined); await oauthManager.refreshAccessToken(); @@ -76,10 +72,10 @@ describe('models/Oauth2Manager', () => { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); - expect(oauthManager.account.tokenInfo?.refreshToken).toBe( + expect(oauthManager.account.auth.tokenInfo?.refreshToken).toBe( mockRefreshTokenResponse.refresh_token ); - expect(oauthManager.account.tokenInfo?.accessToken).toBe( + expect(oauthManager.account.auth.tokenInfo?.accessToken).toBe( mockRefreshTokenResponse.access_token ); }); diff --git a/types/Accounts.ts b/types/Accounts.ts index 4765eec4..9cf16c78 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -2,50 +2,26 @@ import { HUBSPOT_ACCOUNT_TYPES } from '../constants/config'; import { CmsPublishMode } from './Files'; import { Environment } from './Config'; import { ValueOf } from './Utils'; - +import { + PERSONAL_ACCESS_KEY_AUTH_METHOD, + OAUTH_AUTH_METHOD, + API_KEY_AUTH_METHOD, +} from '../constants/auth'; export type AuthType = 'personalaccesskey' | 'apikey' | 'oauth2'; -export interface CLIAccount_NEW { - name?: string; +interface BaseHubSpotConfigAccount { + name: string; accountId: number; accountType?: AccountType; defaultCmsPublishMode?: CmsPublishMode; env: Environment; - authType?: AuthType; - auth?: { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - }; - sandboxAccountType?: string | null; - parentAccountId?: number | null; - apiKey?: string; - personalAccessKey?: string; + authType: AuthType; + parentAccountId?: number; } -export interface CLIAccount_DEPRECATED { - name?: string; - portalId?: number; - defaultCmsPublishMode?: CmsPublishMode; - env: Environment; - accountType?: AccountType; - authType?: AuthType; - auth?: { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - }; - sandboxAccountType?: string | null; - parentAccountId?: number | null; - apiKey?: string; - personalAccessKey?: string; -} - -export type CLIAccount = CLIAccount_NEW | CLIAccount_DEPRECATED; - -export type GenericAccount = { +export type DeprecatedHubSpotConfigAccountFields = { portalId?: number; - accountId?: number; + sandboxAccountType?: string; }; export type AccountType = ValueOf; @@ -56,76 +32,34 @@ export type TokenInfo = { refreshToken?: string; }; -export interface PersonalAccessKeyAccount_NEW extends CLIAccount_NEW { - authType: 'personalaccesskey'; +export interface PersonalAccessKeyConfigAccount + extends BaseHubSpotConfigAccount { + authType: typeof PERSONAL_ACCESS_KEY_AUTH_METHOD.value; personalAccessKey: string; -} - -export interface PersonalAccessKeyAccount_DEPRECATED - extends CLIAccount_DEPRECATED { - authType: 'personalaccesskey'; - personalAccessKey: string; -} - -export type PersonalAccessKeyAccount = - | PersonalAccessKeyAccount_NEW - | PersonalAccessKeyAccount_DEPRECATED; - -export interface OAuthAccount_NEW extends CLIAccount_NEW { - authType: 'oauth2'; auth: { - clientId?: string; - clientSecret?: string; - scopes?: Array; - tokenInfo?: TokenInfo; + tokenInfo: TokenInfo; }; } -export interface OAuthAccount_DEPRECATED extends CLIAccount_DEPRECATED { - authType: 'oauth2'; +export interface OAuthConfigAccount extends BaseHubSpotConfigAccount { + authType: typeof OAUTH_AUTH_METHOD.value; auth: { - clientId?: string; - clientSecret?: string; - scopes?: Array; - tokenInfo?: TokenInfo; + clientId: string; + clientSecret: string; + scopes: Array; + tokenInfo: TokenInfo; }; } -export type OAuthAccount = OAuthAccount_NEW | OAuthAccount_DEPRECATED; - -export interface APIKeyAccount_NEW extends CLIAccount_NEW { - authType: 'apikey'; +export interface APIKeyConfigAccount extends BaseHubSpotConfigAccount { + authType: typeof API_KEY_AUTH_METHOD.value; apiKey: string; } -export interface APIKeyAccount_DEPRECATED extends CLIAccount_DEPRECATED { - authType: 'apikey'; - apiKey: string; -} - -export type APIKeyAccount = APIKeyAccount_NEW | APIKeyAccount_DEPRECATED; - -export interface FlatAccountFields_NEW extends CLIAccount_NEW { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - scopes?: Array; - apiKey?: string; - personalAccessKey?: string; -} - -export interface FlatAccountFields_DEPRECATED extends CLIAccount_DEPRECATED { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - scopes?: Array; - apiKey?: string; - personalAccessKey?: string; -} - -export type FlatAccountFields = - | FlatAccountFields_NEW - | FlatAccountFields_DEPRECATED; +export type HubSpotConfigAccount = + | PersonalAccessKeyConfigAccount + | OAuthConfigAccount + | APIKeyConfigAccount; export type ScopeData = { portalScopesInGroup: Array; @@ -164,32 +98,6 @@ export type EnabledFeaturesResponse = { enabledFeatures: { [key: string]: boolean }; }; -export type UpdateAccountConfigOptions = - Partial & { - environment?: Environment; - }; - -export type PersonalAccessKeyOptions = { - accountId: number; - personalAccessKey: string; - env: Environment; -}; - -export type OAuthOptions = { - accountId: number; - clientId: string; - clientSecret: string; - refreshToken: string; - scopes: Array; - env: Environment; -}; - -export type APIKeyOptions = { - accountId: number; - apiKey: string; - env: Environment; -}; - export type AccessToken = { portalId: number; accessToken: string; @@ -201,18 +109,6 @@ export type AccessToken = { accountType: ValueOf; }; -export type OAuth2ManagerAccountConfig = { - name?: string; - accountId?: number; - clientId?: string; - clientSecret?: string; - scopes?: Array; - env?: Environment; - environment?: Environment; - tokenInfo?: TokenInfo; - authType?: 'oauth2'; -}; - export type WriteTokenInfoFunction = (tokenInfo: TokenInfo) => void; export type RefreshTokenResponse = { diff --git a/types/Config.ts b/types/Config.ts index ea24b094..f55baf1d 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -1,46 +1,35 @@ +import { CONFIG_FLAGS } from '../constants/config'; import { ENVIRONMENTS } from '../constants/environments'; -import { CLIAccount_NEW, CLIAccount_DEPRECATED } from './Accounts'; +import { + DeprecatedHubSpotConfigAccountFields, + HubSpotConfigAccount, +} from './Accounts'; import { CmsPublishMode } from './Files'; import { ValueOf } from './Utils'; -export interface CLIConfig_NEW { - accounts: Array; +export interface HubSpotConfig { + accounts: Array; allowUsageTracking?: boolean; - defaultAccount?: string | number; - defaultMode?: CmsPublishMode; // Deprecated - left in to handle existing configs with this field + defaultAccount?: number; defaultCmsPublishMode?: CmsPublishMode; httpTimeout?: number; env?: Environment; httpUseLocalhost?: boolean; + useCustomObjectHubfile?: boolean; } -export interface CLIConfig_DEPRECATED { - portals: Array; - allowUsageTracking?: boolean; - defaultPortal?: string | number; - defaultMode?: CmsPublishMode; // Deprecated - left in to handle existing configs with this field - defaultCmsPublishMode?: CmsPublishMode; - httpTimeout?: number; - env?: Environment; - httpUseLocalhost?: boolean; -} - -export type CLIConfig = CLIConfig_NEW | CLIConfig_DEPRECATED; +export type DeprecatedHubSpotConfigFields = { + portals?: Array; + defaultPortal?: string; + defaultMode?: CmsPublishMode; +}; export type Environment = ValueOf | ''; -export type EnvironmentConfigVariables = { - apiKey?: string; - clientId?: string; - clientSecret?: string; - personalAccessKey?: string; - accountId?: number; - refreshToken?: string; - env?: Environment; -}; - export type GitInclusionResult = { inGit: boolean; configIgnored: boolean; gitignoreFiles: Array; }; + +export type ConfigFlag = ValueOf; diff --git a/utils/accounts.ts b/utils/accounts.ts deleted file mode 100644 index 4c2177b4..00000000 --- a/utils/accounts.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CLIAccount } from '../types/Accounts'; -import { - CLIConfig, - CLIConfig_DEPRECATED, - CLIConfig_NEW, -} from '../types/Config'; - -export function getAccounts(config?: CLIConfig | null): Array { - if (!config) { - return []; - } else if (Object.hasOwn(config, 'portals')) { - return (config as CLIConfig_DEPRECATED).portals; - } else if (Object.hasOwn(config, 'accounts')) { - return (config as CLIConfig_NEW).accounts; - } - return []; -} - -export function getDefaultAccount( - config?: CLIConfig | null -): string | number | undefined { - if (!config) { - return undefined; - } else if (Object.hasOwn(config, 'defaultPortal')) { - return (config as CLIConfig_DEPRECATED).defaultPortal; - } else if (Object.hasOwn(config, 'defaultAccount')) { - return (config as CLIConfig_NEW).defaultAccount; - } -}